JUC并发编程原理精讲(源码分析)

打印 上一主题 下一主题

主题 873|帖子 873|积分 2619

1. JUC前言知识

JUC即 java.util.concurrent
涉及三个包:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

普通的线程代码:

  • Thread
  • Runnable 没有返回值、效率相比入 Callable 相对较低!
  • Callable 有返回值!【工作常用】

1.1 进程和线程

进程:是指一个内存中运行的程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程。进程是资源分配的单位。
记忆:进程的英文为Process,Process也为过程,所以进程可以大概理解为程序执行的过程。
(进程也是程序的一次执行过程,是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建、运行到消亡的过程)
线程:进程中的一个执行单元,负责当前进程中程序的执行。一个进程中是可以有多个线程的。线程是CPU调度和执行的单位。
【java默认有两个线程:main、GC】

举例:打开word使用是一个进程,word会检查你的拼写,两个线程:容灾备份,语法检查

进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

1.2 并发与并行

并行 :指两个或多个事件在同一时刻发生(同时发生)【多个CPU同时执行多个线程】
并发 :指两个或多个事件在同一个时间段内发生。(交替执行) 【一个CPU交替执行线程】

拓展

  • 并发编程的本质是充分利用cpu资源
    java代码查询cpu核数:
    1. //查询cpu核数
    2. //CPU 密集型,IO密集型
    3. System.out.println(Runtime.getRuntime().availableProcessors());
    复制代码
  • java真的可以开启线程吗?不能,通过源码可知底层开启线程的start()方法是native修饰的,意思是调用操作系统C++的代码
    1. //本地方法,底层的C++ java无法直接操作硬件
    2. private native void start0();
    复制代码

1.3 线程六种状态


  • NEW(新建)
    线程刚被创建,但是并未启动。还没调用start方法
  • Runnable(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器
  • Blocked(锁阻塞)
    当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
  • Waiting(无限等待)
    一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。
    进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
  • Timed Waiting(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。
    这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait
  • Teminated(被终止)
    因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

上源码:
  1. public enum State {
  2.     /**
  3.      * 新建
  4.      */
  5.     NEW,
  6.     /**
  7.      * 运行
  8.      */
  9.     RUNNABLE,
  10.     /**
  11.      * 阻塞
  12.      */
  13.     BLOCKED,
  14.     /**
  15.      * 等待,死死的等
  16.      */
  17.     WAITING,
  18.     /**
  19.      * 超时等待
  20.      */
  21.     TIMED_WAITING,
  22.     /**
  23.      * 停止
  24.      */
  25.     TERMINATED;
  26. }
复制代码
1.4 sleep与wait区别

只要是等待都需要抛出异常,中断异常

  • 来自不同的类

    • wait -> Object
    • sleep -> Thread

  • 关于锁的释放

    • wait会释放锁
    • sleep睡觉了,抱着锁睡觉,不会释放!

  • 使用的范围是不同的

    • wait必须在同步代码块中
    • sleep可以在任何地方睡


1.5 解耦写线程

学生写法(多耦):
  1. public class SaleTickerDemo01 {
  2.     public static void main(String[] args) {
  3.         Ticket ticket = new Ticket();
  4.         new Thread(ticket, "A").start();
  5.         new Thread(ticket, "B").start();
  6.                 new Thread(ticket, "C").start();
  7.     }
  8. }
  9. class Ticket implements Runnable{
  10.     private int number = 50;
  11.     public void run(){
  12.         if (number > 0) {
  13.             System.out.println(Thread.currentThread().getName() + "买了第" + (number--) + "张票");
  14.         }
  15.     }
  16. }
复制代码
工作写法(解耦):
  1. public class SaleTickerDemo01 {
  2.     public static void main(String[] args) {
  3.         Ticket ticket = new Ticket();
  4.         new Thread(() -> {
  5.             for (int i = 0; i < 50; i++) {
  6.                 ticket.sale();
  7.             }
  8.         }, "A").start();
  9.         new Thread(() -> {
  10.             for (int i = 0; i < 50; i++) {
  11.                 ticket.sale();
  12.             }
  13.         }, "B").start();
  14.         new Thread(() -> {
  15.             for (int i = 0; i < 50; i++) {
  16.                 ticket.sale();
  17.             }
  18.         }, "C").start();
  19.     }
  20. }
  21. class Ticket {
  22.     private int number = 50;
  23.     public synchronized void sale() {
  24.         if (number > 0) {
  25.             System.out.println(Thread.currentThread().getName() + "买了第" + (number--) + "张票");
  26.         }
  27.     }
  28. }
复制代码
1.6 锁基础

较难,能理解就理解
1.6.1 锁机制

通过使用synchronized关键字来实现锁,这样就能够很好地解决线程之间争抢资源的情况。那么,synchronized底层到是如何实现的呢?
我们知道,使用synchronized,一定是和某个对象相关联的,比如我们要对某一段代码加锁,那么我们就需要提供一个对象来作为锁本身:
  1. public static void main(String[] args) {
  2.     synchronized (Main.class) {
  3.         //这里使用的是Main类的Class对象作为锁
  4.     }
  5. }
复制代码
我们来看看,它变成字节码之后会用到哪些指令:

其中最关键的就是monitorenter指令了,可以看到之后也有monitorexit与之进行匹配(注意这里有2个),monitorenter和monitorexit分别对应加锁和释放锁,在执行monitorenter之前需要尝试获取锁,每个对象都有一个monitor监视器与之对应,而这里正是去获取对象监视器的所有权,一旦monitor所有权被某个线程持有,那么其他线程将无法获得(管程模型的一种实现)。
在代码执行完成之后,我们可以看到,一共有两个monitorexit在等着我们,那么为什么这里会有两个呢,按理说monitorenter和monitorexit不应该一一对应吗,这里为什么要释放锁两次呢?
首先我们来看第一个,这里在释放锁之后,会马上进入到一个goto指令,跳转到15行,而我们的15行对应的指令就是方法的返回指令,其实正常情况下只会执行第一个monitorexit释放锁,在释放锁之后就接着同步代码块后面的内容继续向下执行了。而第二个,其实是用来处理异常的,可以看到,它的位置是在12行,如果程序运行发生异常,那么就会执行第二个monitorexit,并且会继续向下通过athrow指令抛出异常,而不是直接跳转到15行正常运行下去。

实际上synchronized使用的锁就是存储在Java对象头中的,我们知道,对象是存放在堆内存中的,而每个对象内部,都有一部分空间用于存储对象头信息,而对象头信息中,则包含了Mark Word用于存放hashCode和对象的锁信息,在不同状态下,它存储的数据结构有一些不同。

1.6.2 重量级锁

在JDK6之前,synchronized一直被称为重量级锁,monitor依赖于底层操作系统的Lock实现,Java的线程是映射到操作系统的原生线程上,切换成本较高。而在JDK6之后,锁的实现得到了改进。我们先从最原始的重量级锁开始:
我们说了,每个对象都有一个monitor与之关联,在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的:
  1. ObjectMonitor() {
  2.     _header       = NULL;
  3.     _count        = 0; //记录个数
  4.     _waiters      = 0,
  5.     _recursions   = 0;
  6.     _object       = NULL;
  7.     _owner        = NULL;
  8.     _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
  9.     _WaitSetLock  = 0 ;
  10.     _Responsible  = NULL ;
  11.     _succ         = NULL ;
  12.     _cxq          = NULL ;
  13.     FreeNext      = NULL ;
  14.     _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  15.     _SpinFreq     = 0 ;
  16.     _SpinClock    = 0 ;
  17.     OwnerIsThread = 0 ;
  18. }
复制代码
每个等待锁的线程都会被封装成ObjectWaiter对象,进入到如下机制:

ObjectWaiter首先会进入 Entry Set等着,当线程获取到对象的monitor后进入 The Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取对象的monitor。
虽然这样的设计思路非常合理,但是在大多数应用上,每一个线程占用同步代码块的时间并不是很长,我们完全没有必要将竞争中的线程挂起然后又唤醒,并且现代CPU基本都是多核心运行的,我们可以采用一种新的思路来实现锁。
在JDK1.4.2时,引入了自旋锁(JDK6之后默认开启),它不会将处于等待状态的线程挂起,而是通过无限循环的方式,不断检测是否能够获取锁,由于单个线程占用锁的时间非常短,所以说循环次数不会太多,可能很快就能够拿到锁并运行,这就是自旋锁。当然,仅仅是在等待时间非常短的情况下,自旋锁的表现会很好,但是如果等待时间太长,由于循环是需要处理器继续运算的,所以这样只会浪费处理器资源,因此自旋锁的等待时间是有限制的,默认情况下为10次,如果失败,那么会进而采用重量级锁机制。

在JDK6之后,自旋锁得到了一次优化,自旋的次数限制不再是固定的,而是自适应变化的,比如在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么这次自旋也是有可能成功的,所以会允许自旋更多次。当然,如果某个锁经常都自旋失败,那么有可能会不再采用自旋策略,而是直接使用重量级锁。

1.6.3 轻量级锁

从JDK 1.6开始,为了减少获得锁和释放锁带来的性能消耗,就引入了轻量级锁。
轻量级锁的目标是,在无竞争情况下,减少重量级锁产生的性能消耗(并不是为了代替重量级锁,实际上就是赌同一时间只有一个线程在占用资源),包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。它不像是重量级锁那样,需要向操作系统申请互斥量。它的运作机制如下:
在即将开始执行同步代码块中的内容时,会首先检查对象的Mark Word,查看锁对象是否被其他线程占用,如果没有任何线程占用,那么会在当前线程中所处的栈帧中建立一个名为锁记录(Lock Record)的空间,用于复制并存储对象目前的Mark Word信息(官方称为Displaced Mark Word)。
接着,虚拟机将使用CAS操作将对象的Mark Word更新为轻量级锁状态(数据结构变为指向Lock Record的指针,指向的是当前的栈帧)
CAS(Compare And Swap)是一种无锁算法(我们之前在Springboot阶段已经讲解过了),它并不会为对象加锁,而是在执行的时候,看看当前数据的值是不是我们预期的那样,如果是,那就正常进行替换,如果不是,那么就替换失败。比如有两个线程都需要修改变量i的值,默认为10,现在一个线程要将其修改为20,另一个要修改为30,如果他们都使用CAS算法,那么并不会加锁访问i,而是直接尝试修改i的值,但是在修改时,需要确认i是不是10,如果是,表示其他线程还没对其进行修改,如果不是,那么说明其他线程已经将其修改,此时不能完成修改任务,修改失败。
在CPU中,CAS操作使用的是cmpxchg指令,能够从最底层硬件层面得到效率的提升。
如果CAS操作失败了的话,那么说明可能这时有线程已经进入这个同步代码块了,这时虚拟机会再次检查对象的Mark Word,是否指向当前线程的栈帧,如果是,说明不是其他线程,而是当前线程已经有了这个对象的锁,直接放心大胆进同步代码块即可。如果不是,那确实是被其他线程占用了。
这时,轻量级锁一开始的想法就是错的(这时有对象在竞争资源,已经赌输了),所以说只能将锁膨胀为重量级锁,按照重量级锁的操作执行(注意锁的膨胀是不可逆的)
所以,轻量级锁 -> 失败 -> 自适应自旋锁 -> 失败 -> 重量级锁
解锁过程同样采用CAS算法,如果对象的MarkWord仍然指向线程的锁记录,那么就用CAS操作把对象的MarkWord和复制到栈帧中的Displaced Mark Word进行交换。如果替换失败,说明其他线程尝试过获取该锁,在释放锁的同时,需要唤醒被挂起的线程。

轻量级锁的加锁过程:
(1)当线程执行代码进入同步块时,若Mark Word为无锁状态,虚拟机先在当前线程的栈帧中建立一个名为Lock Record的空间,用于存储当前对象的Mark Word的拷贝,官方称之为“Dispalced Mark Word”
(2)复制对象头中的Mark Word到锁记录中。
(3)复制成功后,虚拟机将用CAS操作将对象的Mark Word更新为执行Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果更新成功,则执行4,否则执行5。;
(4)如果更新成功,则这个线程拥有了这个锁,并将锁标志设为00,表示处于轻量级锁状态
(5)如果更新失败,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经拥有这个锁,可进入执行同步代码。否则说明多个线程竞争,轻量级锁就会膨胀为重量级锁,Mark Word中存储重量级锁(互斥锁)的指针,后面等待锁的线程也要进入阻塞状态。

1.6.4 偏向锁

偏向锁相比轻量级锁更纯粹,干脆就把整个同步都消除掉,不需要再进行CAS操作了。它的出现主要是得益于人们发现某些情况下某个锁频繁地被同一个线程获取,这种情况下,我们可以对轻量级锁进一步优化:偏向锁实际上就是专门为单个线程而生的,当某个线程第一次获得锁时,如果接下来都没有其他线程获取此锁,那么持有锁的线程将不再需要进行同步操作。
通俗的讲,偏向锁就是在运行过程中,对象的锁偏向某个线程。即在开启偏向锁机制的情况下,某个线程获得锁,当该线程下次再想要获得锁时,不需要再获得锁(即忽略synchronized关键词),直接就可以执行同步代码,比较适合竞争较少的情况。
可以从之前的MarkWord结构中看到,偏向锁也会通过CAS操作记录线程的ID,如果一直都是同一个线程获取此锁,那么完全没有必要在进行额外的CAS操作。当然,如果有其他线程来抢了,那么偏向锁会根据当前状态,决定是否要恢复到未锁定或是膨胀为轻量级锁。
如果我们需要使用偏向锁,可以添加-XX:+UseBiased参数来开启
所以,最终的锁等级为:未锁定 < 偏向锁 < 轻量级锁 < 重量级锁
值得注意的是,如果对象通过调用hashCode()方法计算过对象的一致性哈希值,那么它是不支持偏向锁的,会直接进入到轻量级锁状态,因为Hash是需要被保存的,而偏向锁的Mark Word数据结构,无法保存Hash值;如果对象已经是偏向锁状态,再去调用hashCode()方法,那么会直接将锁升级为重量级锁,并将哈希值存放在monitor(有预留位置保存)中。

偏向锁的获取流程:
(1)查看Mark Word中偏向锁的标识以及锁标志位,若是否偏向锁为1且锁标志位为01,则该锁为可偏向状态。
(2)若为可偏向状态,则测试Mark Word中的线程ID是否与当前线程相同,若相同,则直接执行同步代码,否则进入下一步。
(3)当前线程通过CAS操作竞争锁,若竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码,若竞争失败,进入下一步。
(4)当前线程通过CAS竞争锁失败的情况下,说明有竞争。当到达全局安全点时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

偏向锁的释放流程:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。

1.6.5 锁消除和锁粗化

锁消除和锁粗化都是在运行时的一些优化方案。

  • 锁消除是比如我们某段代码虽然加了锁,但是在运行时根本不可能出现各个线程之间资源争夺的情况,这种情况下,完全不需要任何加锁机制,所以锁会被消除。
  • 锁粗化则是我们代码中频繁地出现互斥同步操作,比如在一个循环内部加锁,这样明显是非常消耗性能的,所以虚拟机一旦检测到这种操作,会将整个同步范围进行扩展。

2. Lock锁

2.0 Lock锁和synchronized的区别


  • Synchronized是内置Java关键字;Lock是一个Java类。
  • Synchronized无法判断获取锁的状态;Lock可以判断是否获取到了锁。(boolean b = lock.tryLock();)
  • Synchronized会自动释放锁;Lock必须要手动释放锁,如果不释放锁,死锁。
  • Synchronized线程1获得锁阻塞时,线程2会一直等待下去;Lock锁线程1获得锁阻塞时,线程2等待足够长的时间后中断等待,去做其他的事。
  • Synchronized可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置)。
    lock.lockInterruptibly();方法:当两个线程同时通过该方法想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
  • Synchronized适合锁少量的代码同步问题;Lock适合锁大量的同步代码。

2.1 Lock接口的三个实现类


由jdk查询可知,有三种类

2.2 ReentrantLock类

ReentrantLock锁的对象是调用lock方法的实例对象
使用创建ReentrantLock对象代替传统的Synchronized锁
2.2.1 构造方法——公平锁and非公平锁

公平锁:十分公平,不能插队。
非公平锁:十分不公平,可以插队。(默认非公平锁)

需要更改默认的非公平锁为公平锁,需要在创建对象的时候参数设置为true(默认是false)

2.2.2 ReentrantLock类的使用
  1. class X {
  2.     private final ReentrantLock lock = new ReentrantLock();
  3.     // ...
  4.     public void m() {
  5.         lock.lock();  // block until condition holds
  6.         try {
  7.             //业务代码 ... method body
  8.         } finally {
  9.             lock.unlock();
  10.         }
  11.     }
  12. }
复制代码
2.3 Condition接口

使用await和signal方法代替传统的wait和notify方法



2.3.1 await() signal() 方法基本使用

就是在最原始的多线程synchronized写法上修改了使用的方法
使用while的缘故还是:可能会出现虚假唤醒



2.3.2 Condition实现精准通知唤醒

使用ReentrantLock创建的对象lock来创建多个condition对象,每次等待和唤醒都可以指定 如:conditionA.await(); conditionB.signal();
举例:
  1. public class C {
  2.     public static void main(String[] args) {
  3.         Data3 data3 = new Data3();
  4.         //A执行完,调用B,B执行完,调用C,C执行完,调用A
  5.         new Thread(() -> {
  6.             for (int i = 0; i < 10; i++) {
  7.                 data3.printA();
  8.             }
  9.         }, "A").start();
  10.         new Thread(() -> {
  11.             for (int i = 0; i < 10; i++) {
  12.                 data3.printB();
  13.             }
  14.         }, "B").start();
  15.         new Thread(() -> {
  16.             for (int i = 0; i < 10; i++) {
  17.                 data3.printC();
  18.             }
  19.         }, "C").start();
  20.     }
  21. }
  22. class Data3 {
  23.     private Lock lock = new ReentrantLock();
  24.     Condition conditionA = lock.newCondition();
  25.     Condition conditionB = lock.newCondition();
  26.     Condition conditionC = lock.newCondition();
  27.     private char ch = 'A';
  28.     public void printA() {
  29.         lock.lock();
  30.         try {
  31.             while (ch != 'A') {
  32.                 //等待
  33.                 conditionA.await();
  34.             }
  35.             System.out.println(Thread.currentThread().getName() + "--->A");
  36.             //唤醒
  37.             ch = 'B';
  38.             conditionB.signal();
  39.         } catch (InterruptedException e) {
  40.             e.printStackTrace();
  41.         } finally {
  42.             lock.unlock();
  43.         }
  44.     }
  45.     public void printB() {
  46.         lock.lock();
  47.         try {
  48.             while (ch != 'B') {
  49.                 //等待
  50.                 conditionB.await();
  51.             }
  52.             System.out.println(Thread.currentThread().getName() + "--->B");
  53.             //唤醒
  54.             ch = 'C';
  55.             conditionC.signal();
  56.         } catch (InterruptedException e) {
  57.             e.printStackTrace();
  58.         } finally {
  59.             lock.unlock();
  60.         }
  61.     }
  62.     public void printC() {
  63.         lock.lock();
  64.         try {
  65.             while (ch != 'C') {
  66.                 //等待
  67.                 conditionC.await();
  68.             }
  69.             System.out.println(Thread.currentThread().getName() + "--->C");
  70.             //唤醒
  71.             ch = 'A';
  72.             conditionA.signal();
  73.         } catch (InterruptedException e) {
  74.             e.printStackTrace();
  75.         } finally {
  76.             lock.unlock();
  77.         }
  78.     }
  79. }
复制代码
结果:执行顺序变成以此执行
  1. A--->A
  2. B--->B
  3. C--->C
  4. A--->A
  5. B--->B
  6. C--->C
  7. A--->A
  8. B--->B
  9. C--->C
  10. A--->A
  11. B--->B
  12. C--->C
  13. A--->A
  14. B--->B
  15. C--->C
  16. A--->A
  17. B--->B
  18. C--->C
  19. A--->A
  20. B--->B
  21. C--->C
  22. A--->A
  23. B--->B
  24. C--->C
  25. A--->A
  26. B--->B
  27. C--->C
  28. A--->A
  29. B--->B
  30. C--->C
复制代码
3. 生产者与消费者

3.1 传统的synchronized写法

synchronized+wait+notifyall
  1. public class A {
  2.     public static void main(String[] args) {
  3.         Data data = new Data();
  4.         new Thread(() -> {
  5.             for (int i = 0; i < 10; i++) {
  6.                 try {
  7.                     data.increment();
  8.                 } catch (InterruptedException e) {
  9.                     e.printStackTrace();
  10.                 }
  11.             }
  12.         }, "A").start();
  13.         new Thread(() -> {
  14.             for (int i = 0; i < 10; i++) {
  15.                 try {
  16.                     data.increment();
  17.                 } catch (InterruptedException e) {
  18.                     e.printStackTrace();
  19.                 }
  20.             }
  21.         }, "B").start();
  22.         new Thread(() -> {
  23.             for (int i = 0; i < 10; i++) {
  24.                 try {
  25.                     data.decrement();
  26.                 } catch (InterruptedException e) {
  27.                     e.printStackTrace();
  28.                 }
  29.             }
  30.         }, "C").start();
  31.         new Thread(() -> {
  32.             for (int i = 0; i < 10; i++) {
  33.                 try {
  34.                     data.decrement();
  35.                 } catch (InterruptedException e) {
  36.                     e.printStackTrace();
  37.                 }
  38.             }
  39.         }, "D").start();
  40.     }
  41. }
  42. class Data {
  43.     private int number = 0;
  44.     public synchronized void increment() throws InterruptedException {
  45.         if (number != 0) {
  46.             //等待
  47.             this.wait();
  48.         }
  49.         number++;
  50.         System.out.println(Thread.currentThread().getName() + "==>" + number);
  51.         //通知其他线程,我+1完毕了
  52.         this.notifyAll();
  53.     }
  54.     public synchronized void decrement() throws InterruptedException {
  55.         if (number == 0) {
  56.             //等待
  57.             this.wait();
  58.         }
  59.         number--;
  60.         System.out.println(Thread.currentThread().getName() + "==>" + number);
  61.         //通知其他线程,我-1完毕了
  62.         this.notifyAll();
  63.     }
  64. }
复制代码
输出:发现有问题,出现了负数和大于1的数,这就是虚假唤醒问题(看下面)
  1. A==>1
  2. C==>0
  3. B==>1
  4. A==>2
  5. B==>3
  6. C==>2
  7. C==>1
  8. C==>0
  9. B==>1
  10. A==>2
  11. B==>3
  12. C==>2
  13. C==>1
  14. C==>0
  15. B==>1
  16. A==>2
  17. B==>3
  18. D==>2
  19. D==>1
  20. D==>0
  21. C==>-1
  22. C==>-2
  23. C==>-3
  24. D==>-4
  25. D==>-5
  26. D==>-6
  27. D==>-7
  28. D==>-8
  29. D==>-9
  30. D==>-10
  31. B==>-9
  32. A==>-8
  33. B==>-7
  34. A==>-6
  35. B==>-5
  36. A==>-4
  37. B==>-3
  38. A==>-2
复制代码
3.2 Lock写法

ReentrantLock类 和 Condition接口:lock+await+signal
  1. public class PC {
  2.     public static void main(String[] args) {
  3.         Data data = new Data();
  4.         new Thread(()->{
  5.             for (int i = 0; i < 10; i++) {
  6.                 data.decrement();
  7.             }
  8.         },"A").start();
  9.         new Thread(()->{
  10.             for (int i = 0; i < 10; i++) {
  11.                 data.increment();
  12.             }
  13.         },"B").start();
  14.         new Thread(()->{
  15.             for (int i = 0; i < 10; i++) {
  16.                 data.decrement();
  17.             }
  18.         },"C").start();
  19.         new Thread(()->{
  20.             for (int i = 0; i < 10; i++) {
  21.                 data.increment();
  22.             }
  23.         },"D").start();
  24.     }
  25. }
  26. class Data {
  27.     private int number = 0;
  28.     Lock lock = new ReentrantLock();
  29.     Condition condition = lock.newCondition();
  30.     public void increment() {
  31.         lock.lock();
  32.         try {
  33.             if (number > 0) {
  34.                 try {
  35.                     condition.await();
  36.                 } catch (InterruptedException e) {
  37.                     e.printStackTrace();
  38.                 }
  39.             }
  40.             number++;
  41.             System.out.println(Thread.currentThread().getName() + "=>" + number);
  42.             condition.signalAll();
  43.         } finally {
  44.             lock.unlock();
  45.         }
  46.     }
  47.     public void decrement() {
  48.         lock.lock();
  49.         try {
  50.             if (number <= 0) {
  51.                 try {
  52.                     condition.await();
  53.                 } catch (InterruptedException e) {
  54.                     e.printStackTrace();
  55.                 }
  56.             }
  57.             number--;
  58.             System.out.println(Thread.currentThread().getName() + "=>" + number);
  59.             condition.signalAll();
  60.         } finally {
  61.             lock.unlock();
  62.         }
  63.     }
  64. }
复制代码
3.2 虚假唤醒问题

3.2.1 问题描述

白话:一个if条件里有等待语句,两个消费者都进入这个等待;当生产者生产了1数量的产品并唤醒消费者,此时之前处于等待的两个消费者会被唤醒,然后进行消费;导致最后消费的产品出现负数,因为产品只有一个,而消费者消费了两次。
虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。
举例:
  1. public class A {
  2.     public static void main(String[] args) {
  3.         Data data = new Data();
  4.         new Thread(() -> {
  5.             for (int i = 0; i < 10; i++) {
  6.                 try {
  7.                     data.increment();
  8.                 } catch (InterruptedException e) {
  9.                     e.printStackTrace();
  10.                 }
  11.             }
  12.         }, "A").start();
  13.         new Thread(() -> {
  14.             for (int i = 0; i < 10; i++) {
  15.                 try {
  16.                     data.increment();
  17.                 } catch (InterruptedException e) {
  18.                     e.printStackTrace();
  19.                 }
  20.             }
  21.         }, "B").start();
  22.         new Thread(() -> {
  23.             for (int i = 0; i < 10; i++) {
  24.                 try {
  25.                     data.decrement();
  26.                 } catch (InterruptedException e) {
  27.                     e.printStackTrace();
  28.                 }
  29.             }
  30.         }, "C").start();
  31.         new Thread(() -> {
  32.             for (int i = 0; i < 10; i++) {
  33.                 try {
  34.                     data.decrement();
  35.                 } catch (InterruptedException e) {
  36.                     e.printStackTrace();
  37.                 }
  38.             }
  39.         }, "D").start();
  40.     }
  41. }
  42. class Data {
  43.     private int number = 0;
  44.     public synchronized void increment() throws InterruptedException {
  45.         if (number != 0) {
  46.             //等待
  47.             this.wait();
  48.         }
  49.         number++;
  50.         System.out.println(Thread.currentThread().getName() + "==>" + number);
  51.         //通知其他线程,我+1完毕了
  52.         this.notifyAll();
  53.     }
  54.     public synchronized void decrement() throws InterruptedException {
  55.         if (number == 0) {
  56.             //等待
  57.             this.wait();
  58.         }
  59.         number--;
  60.         System.out.println(Thread.currentThread().getName() + "==>" + number);
  61.         //通知其他线程,我-1完毕了
  62.         this.notifyAll();
  63.     }
  64. }
复制代码
结果:出现了大于1的情况,以及小于0的情况
  1. A==>1
  2. C==>0
  3. B==>1
  4. A==>2
  5. B==>3
  6. C==>2
  7. C==>1
  8. C==>0
  9. B==>1
  10. A==>2
  11. B==>3
  12. C==>2
  13. C==>1
  14. C==>0
  15. B==>1
  16. A==>2
  17. B==>3
  18. D==>2
  19. D==>1
  20. D==>0
  21. C==>-1
  22. C==>-2
  23. C==>-3
  24. D==>-4
  25. D==>-5
  26. D==>-6
  27. D==>-7
  28. D==>-8
  29. D==>-9
  30. D==>-10
  31. B==>-9
  32. A==>-8
  33. B==>-7
  34. A==>-6
  35. B==>-5
  36. A==>-4
  37. B==>-3
  38. A==>-2
复制代码
3.2.2 解决方法




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

欢乐狗

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表