当前位置: 首页 > news >正文

java SE -- 线程 asset

一.进程

进程,是正在运行的程序实例,是操作系统进行资源分配的最小单位。每个进程都有它自己的地址空间和系统资源(比如CPU时间,内存空间,磁盘IO等)。多个进程可以同时执行,每个进程在运行时都不会影响其他进程的运行,资源不共享;

程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

二.线程

2.1线程的简介

2.1.1线程和进程的区别

线程是进程的一部分,是CPU能够进行运算调度的最小单位。线程不能独立存在,必须依赖于进程。线程是一个进程中的顺序执行流(执行单元)。一个进程中可以有一个线程,也可能有多个线程。每个线程都有自己的指令指针、堆栈和局部变量等,但他们共享进程的代码,数据和全局变量等资源

多线程可以实现并发执行,提高程序的效率。

线程和进程的区别:

1.进程是操作系统运行的一个任务,线程是进程中的一个任务

2.进程是资源分配的最小单元,线程是程序执行的最小单元

3.线程是轻量级的进程,一个进程中包含多个线程,多线程共享进程中的数据,使用相同的地址空间,因此,线程间的通信更加方便,CPU切换一个线程的开销比进程小很多

4.一个进程结束,其内部的所有线程都会结束,但是不会对另外的进程造成影响。多线程程序,一个线程结束,可能会造成其他的线程结束

2.1.2  CPU时间片

CPU时间片在单核处理器上时,一次只能运行某一个进程的某一个线程,如何公平处理,一种方法就是引入时间片的概念,让每个程序轮流执行。

CPU调度机制算法:会将时间划分成一个个的时间片,时间片的大小从几ms到几百ms;

线程调度:线程调度是计算机多线程操作系统中分配CPU时间给各个线程的过程。每个线程代表程序中的一个执行路径,操作系统通过线程调度器分配处理器时间,决定哪个线程将获得执行的机会,以及获得的时间长短。

进程调度:进程调度是操作系统中分配CPU时间给各个进程的过程。进程是系统进行资源分配和调度的独立单位,它包含代码、数据以及分配的系统资源。与线程调度不同,进程调度涉及到的上下文切换成本更高,因为进程间共享的资源更少。

串行和并发:

串行指同步运行,并行指异步运行

操作系统将时间划分成很多时间片段,尽可能的均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程处于等待状态。所以这种微观上是走走停停,断断续续的,宏观上都在运行的现象叫并发。

2.2线程的调度机制

2.2.1 Java线程的状态简介

在java中,线程可以处于一下几种状态;

新建状态(New):线程对象已经创建,但还没有调用start()方法。

就绪状态(Runnable):线程已经调用start()方法,等待CPU调度执行。

运行状态(Running):线程获得CPU时间片,开始执行run()方法里的代码。

阻塞状态(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,直到进入就绪状态。

等待状态(Waiting):线程因为某些条件而进入等待状态,此时不会被分配CPU时间片,直到其他线程显式地唤醒。

超时等待状态(Timed Waiting):线程在指定的时间内等待,时间到后会自动返回到就绪状态。

终止状态(Terminated):线程的run()方法执行完毕或者因异常退出而结束线程的生命周期。

2.2.2 抢占式调度与协同式调度

java线程的调度基本上是抢占式的,在这种模式下,每个java线程都有机会获得时间片,操作系统基于线程的优先级来决定哪个线程会优先运行。高级的线程会获得更多的运行机会。

而协同式调度需要线程主动释放控制权,当前线程必须要主动让出CPU时间,其他的线程才能有执行的机会,导致协同式调度在java中出现得较少。

2.2.3 线程的优先级

线程的切换是由线程调度来控制的,我们无权通过代码来干涉,但是可以通过设置线程的优先级来提高线程获取时间片段的概率。通过setPriority来设置优先级,优先级较高的线程有更大概率获得CPU时间片。

线程的优先级分为1-10,1最低,10最高,线程内部提供了3个关键字来表示最低,最高,默认优先级。

Thread.MIN_PRIORITY  表示最低优先级

Thread.MAX_PRIORITY  表示最高优先级

Thread.NORM_PRIORITY  表示默认优先级

2.2.4 线程生命周期管理

java虚拟机可以通过Thread类的方法来管理线程的生命周期,比如start()、sleep()、yield()、join()、wait()等,让线程在恰当的位置运行或者暂停。

start(  )方法可以使线程处于就绪状态

yield(  )方法可以使当前运行的线程让出自己的时间片,但不会阻塞线程,只是将线程从运行状态转移至就绪状态。

join(  )方法可以让一个线程等待另一个线程完成后再继续执行。

sleep(  )方法可以使当前线程暂停执行指定时间。

wait(  )方法是当前线程释放锁,释放cpu等资源,进入等待状态

3.线程的创建及其常用的API

3.1 线程的三种创建方式

3.1.1第一种

使用Thread的实现类或者匿名内部类的方式创建一个线程;

1.实现Thread

class MyThread extends Thread{//重写run方法:  run方法就是用来编写线程的任务代码的@Overridepublic void run(){for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}
}

我们自定义一个类,继承Thread类,并重写里面的run方法, run方法就是用来编写线程的任务代码的,我们需要实现1-100的打印,所以设置一个for循环打印即可,在sout中,需要传入Thread.currentThread( ).getName( )来获取到当前线程的名字,后面加上 i 即可

 public static void main(String[] args) {//创建一个线程对象,处于新建状态MyThread mt = new MyThread();//启动线程,使其处于就绪状态mt.start();

之后我们就可以在main方法中创建一个对象;然后通过对象来调用start方法,让线程启动。

2.使用匿名内部类的方式:

Thread t2 = new Thread(){public void run(){for(int i=0; i<100; i++){System.out.println("hello world");}}};//启动线程t2,使其处于就绪状态t2.start();System.out.println("-----main结束-----");

我们可以直接在main方法中创建一个匿名内部类对象来创建线程,直接在花括号中重写run方法,确定线程任务行为;打印100次hello world;之后同样需要启动线程。

3.1.2 第二种

使用Runnable的实现类或者匿名内部类的方式创建一个线程;

1.实现Runnable接口

class MyTask implements Runnable {//重写run方法:线程的任务代码public void run() {int sum = 0;for (int i = 0; i < 101; i++) {if (i % 2 == 0) {sum += i;}}System.out.println("sum: "+sum);}
}

我们在自定义的类实现Runnable接口,重写里面的run方法,也就是任务所需的代码,我们希望打印出100以内的偶数和,

 public static void main(String[] args) {//获取Runnable的实例对象Runnable task = new MyTask();//创建线程对象,调用构造器Thread(Runnable runnable),传入任务Thread t1 = new Thread(task);//启动线程,进入就绪状态t1.start();

之后在main方法中获取Runnable的实例化对象,然后创建一个线程对象,将Runnable的实例化对象传入,然后用线程对象启动线程就可以了;

2.使用匿名内部类的方式

Runnable r = () ->{int sum = 0;for(int i=0; i<100; i++){if(i%2==1){sum+=i;}}System.out.println("sum="+sum);};Thread t2 = new Thread(r);t2.start();}
}

我们可以用Lambda的表达式()用于传递形参,{ }中写run方法的内容,我们希望打印100以内所有的奇数和,然后实例化一个线程对象,形参传入Runnable对象,并调用启动方法。

3.1.3 第三种

先获取一个Callable对象,重写里面的call方法,call相当于run,但是call方法有返回值,

之后获取一个FutureTask对象,将上面的Callable对象传入构造器,

之后获取Thread对象,将上面的FutureTask对象传入构造器;

public static void main(String[] args) throws ExecutionException, InterruptedException {//Callable 是函数式接口,里面的 V call () 相当于Thread或者Runnable的run方法,即任务代码书写的位置Callable c1 = ()->{int sum = 0;for (int i = 2; i <=100 ; i+=2) {sum+=i;Thread.sleep(100);}return sum;};//调用FutureTask的构造器,传入Callable对象FutureTask<Integer> ft = new FutureTask<>(c1);//创建Thread线程对象,调用start方法进入就绪状态Thread thread = new Thread(ft);thread.start();//获取线程结束之后的结果   注意:get方法有阻塞所在线程的效果Integer result = ft.get();System.out.println("result: "+result);System.out.println("=====main方法结束======");}

Callable的返回值返回后,我们把对象传入FutureTask构造器,创建出的对象传入Thread构造器;让对象调用启动方法,获取Callable返回的结果;并将结果打印出来。

3.2Thread的常用构造器

3.2.1  Thread(Runnable runnable)

我们在形参中传入一个Runnable的任务对象;

Runnable r =  ()-> {int a1 = (int)(Math.random()*100);int a2 = (int)(Math.random()*100);int a3 = (int)(Math.random()*100);int a4 = (int)(Math.random()*100);int a5 = (int)(Math.random()*100);int[] arr = {a1,a2,a3,a4,a5};//冒泡排序for (int i = 0; i < arr.length-1; i++) {for (int j = 0; j < arr.length-1-i; j++) {if (arr[j] > arr[j+1]) {int temp = arr[j];arr[j] = arr[j+1];arr[j+1] = temp;}}}System.out.println(Arrays.toString(arr));};new Thread(r).start();System.out.println("==========单参结束==========");

首先可以通过Lambda表达式创建好一个任务对象,先创建一个有随机数的数组,然后对这个数组进行冒泡排序,然后打印该数组;之后可以调用new关键字创建线程对象,传入任务对象,之后调用start启动该线程。

3.2.2  Thread(Runnable target,String name)

传入的第一个形参是任务接口的对象,第二个形参是创建出的线程名字;

Thread abc = new Thread(r, "abc");abc.start();System.out.println("=========双参结束=========");

任务接口就传入我们刚才创建的任务r就可以,命名是线程对象为abc,然后通过线程对象abc来调用start方法来启动线程。

就会得到第二个冒泡排序的数组。

3.2.3  Thread(String name)

形参是创建线程对象的名字,

Thread xiaohua = new Thread("xiaohua");

3.3常用的属性和方法

//1.获取当前线程的对象Thread current = Thread.currentThread();//2.获取当前线程的名字String name = current.getName();//3.获取当前线程的唯一标识符long id = current.getId();//4.获取当前线程的优先级int priority = current.getPriority();//5.获取当前线程的状态Thread.State state = current.getState();//6.查看当前线程是否存活boolean alive = current.isAlive();//7.查看当前线程是否被打断boolean interrupted = current.isInterrupted();//8.查看当前线程是否为守护进程boolean daemon = current.isDaemon();System.out.println("Current thread name: " + name);System.out.println("Current thread id: " + id);System.out.println("Current thread priority: " + priority);System.out.println("Current thread state: " + state.name());System.out.println("Current thread alive: " + alive);System.out.println("Current thread interrupted: " + interrupted);System.out.println("Current thread daemon: " + daemon);

main方法的本质上就是一个线程;我们可以调用main线程来查看相关的属性和方法,可以获取当前线程的对象并获取当前线程的名字,与当前线程的标识符,优先级。调用getState来获取当前线程的状态,判断当前线程是否存货,是否被打断,以及查看当前线程是否为守护线程。

3.4守护线程的说明

一个线程要么是守护线程,要么是前台线程:

前台线程:在表面运行的,能看到的,或者daemon的值为false的;

后台线程:就是守护线程,daemon的值为true的。

注意:当所有的前台线程都结束了,后台线程即使有任务,也要立即结束。

案例演示:

//第一个线程:rose喊10次 i jump  10次后,真跳了Thread rose = new Thread("rose"){public void run(){for(int i=1;i<=11;i++){System.out.println(getName()+"说:I jump");try{Thread.sleep(500);}catch(InterruptedException e){throw new RuntimeException(e);}}System.out.println("------正在落水中------");}};

首先定义一个前台线程,rose在喊了10次 i jump 后,跳入大海,首先创建一个进程名字是rose,使用匿名内部类的方式创建进程,重写里面的run方法,循环打印10次 i jump,之后让程序休眠0.5秒再执行,需要使用try - catch 来捕获异常,在循环结束后,打印正在落水。

Thread jack = new Thread("jack"){public void run(){for(int i=1;i<=100;i++){System.out.println(getName()+"说: you jump, I jump");try{Thread.sleep(500);}catch(InterruptedException e){throw new RuntimeException(e);}}}};

之后设计第二个线程,名字是Jack,在循环打印100次 you jump , i jump 也让程序休眠0.5秒,此时我们应该判断哪个线程是守护线程,由于rose在喊完10次后会直接落水,此时jack就不应该继续喊了,所以jack应该是守护线程,当rose线程执行完后,jack应该立即停止执行。

//jack应该是守护进程,即rose跳水后,jack应该停止喊叫jack.setDaemon(true);rose.start();jack.start();

3.5生命周期相关方法

3.5.1 sleep()方法

线程的睡眠方法     static void sleep(long time),可以传入一个参数时间,单位是毫秒,让线程里的当前代码休眠,进入阻塞状态;不再占用CPU的时间片,休眠时间一过,就会立即进入就绪状态,等待调度器分配时间片段

注意:在休眠期间可能会被打断,因此要处理异常    InterruptedException

还有一个重载方法     static void sleep(long time,int nanos),该方法的第二个参数是指单位是纳秒

public static void main(String[] args) {Thread t1 = new Thread("download"){public void run() {for (int i = 1; i <= 10; i++) {System.out.println("正在下载视频中......"+(i*10)+"%");//使用休眠方法来假装模拟正在下载中try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("========视频下载完成========");}};//测试t1.start();}

我们首先定义了一个线程名字是download,并使用匿名内部类的方式重写run方法循环打印10次下载过程来模拟现实生活中下载视频的过程。在每次打印下载过程中让程序休眠一段时间,需要捕获异常;

3.5.2 yield( )方法

线程的礼让方法    static void yield(): 表示让出CPU的时间片段,进入就绪状态,不过,下一个时间片段还有可能是该线程的

Thread t1 = new Thread("Thread-A"){public void run(){for(int i=0;i<=9;i++){System.out.println(getName()+":"+i);try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};

首先我们定义了一个Thread-A,重写run方法,循环打印10次数字,并调用休眠方法每隔0.1秒打印一次,

Thread t2 = new Thread("Thread-B"){public void run(){for(int i=0;i<=9;i++){//打印5之前,让一下时间片段if(i==5){//执行到28行,CPU正在被Thread-B使用Thread.yield(); //这一行代码表示Thread-B让出CPU,进入就绪状态。下一个时间片段可能还会被线程调度器分配给Thread-B}System.out.println(getName()+":"+i);try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};

之后我们创建线程b,同样是打印10次数字,但是当i = 5时,让出时间片,进入就绪状态,但是下一个时间片段可能还会被线程调度器分配给线程B,同样让程序每隔0.1秒打印一次。

3.5.3 join( )方法

另一个线程加入方法  void join():此时当前线程会进入阻塞状态,等待另一个线程结束之后 ,当前线程会进入到就绪状态;

案例演示:模拟图片的下载和显示过程,应该先下载图片,再显示图片

Thread download = new Thread("download"){public void run() {for (int i = 1; i < 11; i++) {System.out.println(getName() + "图片正在加载中... 进度条:" + i*10+"%");try {Thread.sleep(300);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("图片下载完成");}};

首先我们定义下载线程,循环打印10次进度条,并让程序休眠0.3s执行

Thread show = new Thread("show"){public void run() {try {//当前线程是显示线程,显示应该该下载线程执行完毕之后,再执行,因此要让download加入进来download.join();} catch (InterruptedException e) {throw new RuntimeException(e);}for (int i = 1; i < 11; i++) {System.out.println(getName() + "图片正在显示中... 进度条:" + i*10+"%");try {Thread.sleep(300);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("图片显示完成...");}};

之后我们定义显示线程,重写任务代码中,我们需要考虑如果时间片段先分给了显示线程,需要让下载线程的代码都执行完毕才能继续执行显示线程,所以需要通过下载线程调用join方法,让下载线程加入,同样需要捕获异常,

3.5.4 interrupt()方法

线程中断,打断方法    void interrupt( );表示当前线程需要去打断另一个线程;

需要注意:是在当前线程中调用另一个线程的打断方法

public static void main(String[] args) {Thread lin = new Thread("林永健"){public void run() {System.out.println(getName()+"说:开始睡觉了");try {//线程休眠100s,模拟林永健睡着了Thread.sleep(100000);} catch (InterruptedException e) {
//                    e.printStackTrace();System.out.println(getName()+"说:干嘛呢?都破了相了");}}};

首先我们定义林永健让他开始睡觉,并设置休眠时间,

Thread huang = new Thread("黄宏"){public void run() {for (int i = 0; i < 10; i++) {if (i == 0) {Thread.yield();}System.out.println(getName() + "说:"+(i+1)+"个80" );try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(getName()+"说:搞定了");//打断lin的睡觉lin.interrupt();}};

之后我们定义黄宏的线程,通过循环来打印几个八十,在打印完最后一个80时,需要通过lin的线程调用interrupt方法叫醒lin,

4.临界资源问题

4.1什么是临界资源

在一个进程之中,多个线程之间资源是共享的,如果一个资源同时被多个线程访问,那么这个资源就是临界资源,当多个线程同时并发读写一个临界资源问题时,就会发生线程并发安全问题。

常见的临界资源:

多线程共享实例变量        多线程共享静态公共变量

如果想解决线程安全问题,就需要将异步的操作变为同步的操作,

异步操作:相当于各干各的,多个线程并发

同步操作:操作有先后的顺序,相当于你干完我再干。

4.2锁机制

4.2.1锁机制的简介

针对于临界资源安全隐患问题的解决方式,引入了锁机制

1.锁机制的作用:将异步的代码块变成同步的代码块

2.语法:

synchronized(锁对象的地址){

             //需要同步的代码块(如果不同步,会出现安全隐患问题)

}

3.任何的java对象都可以作为锁,只有一个要求:所有的线程都是同一个对象即可。

4.同步的代码块尽量缩小范围,提高替他代码块的执行效率。

5.运行逻辑:

   当一个线程A执行到{ },说明该线程已经获取到了锁对象,其他的线程都必须等待,直到线程A执行完了同步代码块,会自动释放锁对象,其他的线程才有机会获取到锁对象,谁获取到锁对象,谁就能执行同步代码块,没获取到锁对象的线程继续等待。

4.2.2案例演示:

class Desk1 implements Runnable{//静态属性:豆子的数量,初始值10个private static int beanCount = 10;//拿走豆子,一次只能拿一个public void take(){beanCount--;}

首先我们定义一个类Desk1,有私有的静态变量10个豆子,并提供一个拿豆子的方法,

public void run() {while(beanCount!=0){  //桌子上只剩下一个豆子时,两个线程恰巧执行到条件判断。 1>0成立//那么两个线程都进入循环体了,各自拿走一个豆子,此时豆子就是-1个了,明显不合理//即出现了临界资源的安全隐患问题try {//增加出现安全隐患的概率Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}//将具有安全隐患问题的代码放入{};因为两个线程操作的是同一个桌子,因此this可以作为锁对象synchronized(this){if(beanCount>0){take();}}System.out.println(Thread.currentThread().getName()+"拿走了一个豆子,剩下的个数是"+beanCount);if(beanCount<=0){break;}}}

之后我们重写任务代码,当桌子上的豆子只剩下一个时,如果有两个线程同时去拿豆子,那么最后的豆子数量会变成-1,这明显不合理,就是临界资源安全隐患问题,

我们需要将会出现安全隐患的代码块编程同步的,即给这部分代码块上锁,使用synchronized关键字,()中传入对象,在这里我们使用this,如果豆子的数量大于0,拿走豆子,当拿到最后一个豆子时,会有一个线程获取到锁对象,那么他会把豆子取走,等到下一个线程获取到锁对象时,豆子的数量是0,那么就不会进入循环。

public static void main(String[] args) {Desk1 desk1 = new Desk1();Thread xiaoming = new Thread(desk1,"小明");Thread xiaohong = new Thread(desk1,"小红");//启动线程xiaoming.start();xiaohong.start();}
}

在main方法中进行测试,首先创建一个Desk1对象,然后创建两个线程小明和小红,调用start方法进行测试。

class BlackBoard implements Runnable{private int apple = 10;public void run() {while(true){take();System.out.println(Thread.currentThread().getName()+"拿走了一个苹果,剩下的个数"+apple);if(apple == 0){break;}}}/*** 可以对非静态方法上锁,锁对象就是this*/public synchronized void take(){if (apple >0){apple--;}}}

我们可以设计另一个类,在黑板上拿走苹果,实现Runnable接口,先定义实例变量10个苹果,然后再定义一个非静态方法take(),并对非静态方法上锁,在void前面加上synchronized即可,之后重写run方法,并通过循环拿走苹果,如果苹果的个数变为0就跳出循环。

public static void main(String[] args) {BlackBoard bb = new BlackBoard();Thread xiaoming = new Thread(bb,"小明");Thread xiaohong = new Thread(bb,"小红");xiaohong.start();xiaoming.start();}

在main方法中进行测试,新建两个线程小明和小红,并启动线程;

这样就可以有效的避免出现临界资源隐患问题。

4.3  synchronized 的作用域

上锁的范围是需要同步的代码块范围,如果要给非静态方法上锁,需要在方法前添加修饰词synchronized,当有一个线程访问了该方法时,就相当于获得了锁对象,其他线程如果像访问该方法,就去要处于等待状态,有一个前提,多个线程访问的实例对象必须是同一个,当某一个线程正在访问一个同步方法时,this这个锁对象即被他占用,其他线程想要执行该实例对象的其他方法也需要等待,因为该方法的所对象已经被占用了

class Desk implements Runnable {//添加一个非静态属性,充当锁对象Object lock = new Object();private int BeanCount = 10;//如果给方法中的所有代码上锁,不如直接在方法上添加synchronized关键字,给方法上锁//此时不需要特意制定锁对象,因为锁对象是thispublic synchronized void take() {System.out.println("开始取豆子");//给部分代码上锁// synchronized (lock) { //lock作为锁if (BeanCount > 0) {BeanCount--;}//  }System.out.println("豆子被拿走了一个");}public void run() {while(BeanCount > 0){take();}}public synchronized void sport() {//...}
}

在上述代码中,我们添加一个非静态属性,作为锁对象,并提供私有的属性10个豆子,如果需要给方法中的所有代码都上锁,不如直接给方法上添加关键字来上锁,此时不需要指定锁对象,因为锁对象是this,之后就可以重写run方法,

public static void main(String[] args) {Desk desk = new Desk();Thread t1 = new Thread(desk,"小明");Thread t2 = new Thread(desk,"小红");t1.start();t2.start();}

我们在main方法中测试,先创建一个desk对象,然后创建两个线程,并启动线程。

4.4单例模式的改进

懒汉式单例,在多线程的环境下,会出现问题。由于临界资源问题的存在,单例对象可能会被实例化多次。解决方案,就是将对象的 null值判断和实例化上锁,作为同步代码块执行。

class Boss{//第一步:提供一个该类的私有的静态的该类的变量private static Boss boss;//第二步:将构造器私有化private Boss(){}//第三步:提供一个公有的静态的返回该类型的方法//给静态方法上锁,就是给方法添加修饰词synchronized//锁对象时类名.class,这个类对象在整个项目下都是唯一的public synchronized static Boss getInstance(){
//        synchronized (Boss.class){if(boss == null){boss = new Boss();}
//        }return boss;}}

第一步先私有化静态变量,然后私有化构造器,提供返回值是该类型的公有方法,并对其上锁,如果对象的地址值是空,就新创建一个对象,返回该对象。这就是单例模式的懒汉模式。

4.5 死锁

死锁的产生原因:线程1先获取锁A,但是还想获取锁B,线程2获取了锁B,但还想获取锁A。两个线程都占用了对方想用的锁,而且对方还都占着锁不释放,因此都出现了等待现象,无法继续向下执行。

public static void main(String[] args) {Thread t1=new Thread("小明"){public void run(){synchronized("A"){for (int i = 0; i <50 ; i++) {System.out.println(getName()+":"+i);}synchronized("B"){for (int i = 50; i < 100; i++) {System.out.println(getName()+":"+i);}}}}};Thread t2=new Thread("小红"){public void run(){synchronized("B"){for (int i = 0; i <50 ; i++) {System.out.println(getName()+":"+i);}synchronized("A"){for (int i = 50; i < 100; i++) {System.out.println(getName()+":"+i);}}}}};t1.start();t2.start();}

上述代码的执行结果只能是执行到49,无法执行到后迷案的,因为出现了死锁。

避免死锁的方式:按照顺序加锁,或者设置一个超时等候,如果在一定范围内没有获取到锁,那么就不执行锁中的代码块。

public static void main(String[] args) {Thread t1=new Thread("小明"){public void run(){synchronized("A"){for (int i = 0; i <50 ; i++) {System.out.println(getName()+":"+i);}synchronized("B"){for (int i = 50; i < 100; i++) {System.out.println(getName()+":"+i);}}}}};Thread t2=new Thread("小红"){public void run(){synchronized("A"){for (int i = 0; i <50 ; i++) {System.out.println(getName()+":"+i);}synchronized("B"){for (int i = 50; i < 100; i++) {System.out.println(getName()+":"+i);}}}}};t1.start();t2.start();}

我们按照顺序给代码进行上锁,这样就不会出现死锁的情况了,因为都是同步代码,没有并行的代码。这样小明和小红都能执行到99.

4.6与锁相关的API

1.wait( )  释放已经占有的锁对象,进入到等待队列中,不参与时间片的争抢,也不参与锁的争抢,需要等待其他线程调用通知方法。

有两个重载方法 wait(long timeout ): 指定一个等待的时间,超过这个时间会被自动唤醒

wait(long timeout , int nanos):指定等待的时间更为精确。

2.notify( )  通知,唤醒等待队列的某一个线程,被唤醒的线程是随机的,开始参与锁对象的争抢

3.notifyAll( )  通知,唤醒等待队列中的所有线程,都开始争抢锁对象。

我们可以通过上述的下载图片的案例来更好的理解这些方法:

 public static void main(String[] args) {//定义一个对象,充当锁Object lock = new Object();//定义一个下载线程Thread down = new Thread(()->{System.out.println("---开始下载图片---");for (int i = 0; i < 11; i++) {System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("---图片下载完成---");synchronized (lock) {//通知等待队列中的某一个线程//如果想要调用锁的通知方法,那么也必须先获取锁对象lock.notifyAll();}},"下载图片");

首先我们在main方法中定义了一个锁对象,之后开始定义下载线程,使用for循环,并设置一个休眠时间是200ms,在循环结束之后打印图片下载完成,之后通过上锁的方式通知所有线程开始抢锁

//定义一个显示线程Thread show = new Thread(()->{//该显示线程,纲要执行任务时,就应该进入等待队列,等待下载队列try {synchronized (lock) {//如果想要进入等待队列,必须先获取锁对象,然后在调用锁对象的wait方法lock.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}//当下载线程结束后,该线程得到通知了,然后才有机会获取锁对象,然后获取时间片段,才能继续向下执行System.out.println("---开始显示图片---");for (int i = 0; i < 11; i++) {System.out.println(Thread.currentThread().getName()+"百分比:"+i*10+"%");try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("---图片显示完成---");},"显示图片");down.start();show.start();

之后在显示线程中,我们需要先对线程进行休眠操作,让下载线程先执行,当下载结束之后,再执行显示线程。

4.7  ReentrantLock

4.7.1 可重入锁的简介

ReentrantLock指的是可以让一个线程多次获取锁的类型;里面内置了一个计数器用于记录当前线程获取了该锁的次数,可以有效地避免出现死锁的情况。

该类提供了两个子类型,非公平锁和公平锁,非公平锁执行的顺序不是按照线程的请求顺序,而是可能发生插队现象,这种锁执行效率更高,但是可能会增加线程的饥饿情况;

公平锁:多个线程获取锁的方式是按照线程的请求顺序,谁都能获取到,减少了线程的饥饿情况,但是会降低系统的吞吐量,在构造器种传入true则表示公平锁,传入false则表示非公平锁。

  该类比使用synchronized关键字更加灵活,但是需要手动上锁或者解锁,

lock(): 上锁方法 锁对象没有被其他线程占用时,就会上锁成功,否则当前线程处于阻塞状态。unlock(): 解锁方法,必须在占有锁的时候,才能进行解锁,否则报异常;

tryLock(): 尝试获取锁,如果获取不到,并不阻塞,而是执行其他代码 

                获取不到锁返回false,获取到锁返回true

tryLock(long time , TimeUnit unit):  

可以指定一定时间内获取锁对象,如果超过这个时间还没有获取到锁对象,就返回false,

指定时间内获取到锁,就返回true

class MyCounter implements Runnable {private  int count = 0;private  String name;ReentrantLock lock = new ReentrantLock(true);public MyCounter(String name) {this.name = name;}public void run() {//先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平//调用lock方法,进行上锁lock.lock();for (int i = 0; i < 10; i++) {count++;System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}//同步代码块执行完后,需要解锁,给别的线程获取锁的机会lock.unlock();}

我们首先定义一个MyCounter类,提供私有的成员变量;之后创建一个可重入锁对象lock,是一个公平锁,重写run任务方法,调用锁对象对需要同步的代码块进行上锁,代码执行完后,需要调用锁对象解锁。

public static void main(String[] args) {//使用两个线程来模拟两个人来使用计数器MyCounter counter = new MyCounter("秒表");Thread t1 = new Thread(counter,"小明");Thread t2 = new Thread(counter,"小红");t1.start();t2.start();}

在main方法中,我们创建了一个该类的对象,并实例化两个线程,先启动小明,再启动小红。

4.7.2  可重入锁的标准写法

把释放锁的操作写在finally模块中

class MyCounter1 implements Runnable {private  int count = 0;private  String name;ReentrantLock lock = new ReentrantLock(true);public MyCounter1(String name) {this.name = name;}public void run() {//先创建锁对象,构造器中,可以传入true或者false,来表示公平和非公平,默认就是非公平//调用lock方法,进行上锁lock.lock();for (int i = 0; i < 10; i++) {count++;System.out.println(Thread.currentThread().getName() + " 使用了秒表进行计数 "+count);try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}/*** 当在释放锁之前,发生了异常,那么锁就无法释放了,那么别的线程想要获取锁对象,变成了不可能,所以都会处于阻塞状态。** 因此为了不阻塞其他线程对于锁的获取,当前线程不管是否有无异常,那都应该正确的释放锁* 所以锁的释放应该放在try的finally模块里*/try {String str = null;System.out.println(str.length());} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}//同步代码块执行完后,需要解锁,给别的线程获取锁的机会

再释放锁之前,如果代码片段发生了异常,那么此时锁就会无法释放,别的线程就不可能获取到锁对象,我们需要将解锁的方法放入到finally中实现.

public class TicketCenterDemo {public static void main(String[] args) {TicketCenter ticket = new TicketCenter(100);Thread t1 = new Thread(()->{try {ticket.buyOne();} catch (Exception e) {throw new RuntimeException(e);}},"小明");Thread t2 = new Thread(()->{try {ticket.buyOne();} catch (Exception e) {throw new RuntimeException(e);}},"小花");Thread t3 = new Thread(()->{try {ticket.buyBatch(20);} catch (Exception e) {throw new RuntimeException(e);}},"小强");Thread t4 = new Thread(()->{try {ticket.buyBatch(20);} catch (Exception e) {throw new RuntimeException(e);}},"小丽");t1.start();t2.start();t3.start();t4.start();}
}/*** 购票中心*/
class TicketCenter extends Thread{public static ReentrantLock lock = new ReentrantLock();int ticket ;public TicketCenter(int ticket) {this.ticket = ticket;}//购买一张高铁票public void buyOne(){boolean success = false;try {success = lock.tryLock(5, TimeUnit.SECONDS);} catch (InterruptedException e) {throw new RuntimeException(e);}if(success){if(ticket > 0){System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);try {Thread.sleep(1000);ticket--;System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {lock.unlock();}}}else{System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");}}//黄牛买多张票public void buyBatch(int num){boolean success ;try {success = lock.tryLock(5, TimeUnit.SECONDS);} catch (InterruptedException e) {throw new RuntimeException(e);}if(success){if(ticket > 0&&num < ticket){System.out.println(Thread.currentThread().getName()+"开始买票,剩余"+ticket);try {Thread.sleep(10000);ticket-=num;System.out.println(Thread.currentThread().getName()+"买完票了,剩余"+ticket);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {lock.unlock();}}}else{System.out.println(Thread.currentThread().getName()+"不等了,转身就走了");}}}

上述代码是tryLock的案例演示,有助于增加理解.

相关文章:

  • 基于yolov8的游戏人物自动锁定功能
  • 排序--堆排序【图文详解】
  • Vert.x 和 Spring Boot 是两种流行的 Java 框架的比较
  • Java AI 编程助手
  • 探索图像生成大模型Imagen:原理、比较与应用
  • Nginx的核心架构和设计原理
  • 大语言模型技术点总结
  • 二、词法分析,《编译原理》(本科教学版),第2版
  • 【C#】内存的使用和释放
  • AWS 管理控制台
  • 打造高质量软件架构 - 9大质量属性
  • [Linux]磁盘分区指令
  • 网络安全全方略
  • Python 爬虫 根据ID获得UP视频信息
  • Linux驱动开发(速记版)--并发与竞争
  • -------------------- 第二讲-------- 第一节------在此给出链表的基本操作
  • 230. Kth Smallest Element in a BST
  • canvas 五子棋游戏
  • Date型的使用
  • es6(二):字符串的扩展
  • ES6核心特性
  • JavaScript 基础知识 - 入门篇(一)
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • passportjs 源码分析
  • React-Native - 收藏集 - 掘金
  • scala基础语法(二)
  • SpringCloud集成分布式事务LCN (一)
  • Travix是如何部署应用程序到Kubernetes上的
  • underscore源码剖析之整体架构
  • 爱情 北京女病人
  • 前端js -- this指向总结。
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 微信小程序设置上一页数据
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • 小程序测试方案初探
  • 走向全栈之MongoDB的使用
  • PostgreSQL之连接数修改
  • ######## golang各章节终篇索引 ########
  • #经典论文 异质山坡的物理模型 2 有效导水率
  • (2)Java 简介
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (CPU/GPU)粒子继承贴图颜色发射
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (zt)最盛行的警世狂言(爆笑)
  • (初研) Sentence-embedding fine-tune notebook
  • (二十三)Flask之高频面试点
  • (介绍与使用)物联网NodeMCUESP8266(ESP-12F)连接新版onenet mqtt协议实现上传数据(温湿度)和下发指令(控制LED灯)
  • (七)Java对象在Hibernate持久化层的状态
  • (深入.Net平台的软件系统分层开发).第一章.上机练习.20170424
  • (一)Mocha源码阅读: 项目结构及命令行启动
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • (转)mysql使用Navicat 导出和导入数据库
  • ****** 二 ******、软设笔记【数据结构】-KMP算法、树、二叉树
  • .ai域名是什么后缀?