5.LockSupport与线程中断
5.1 线程中断
- 蚂蚁金服面试题:如何中等一个线程,如何停止一个线程
- 什么是中断机制
- 首先:一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
- 其次:在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。
- 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
- 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
- 中断三大方法:interrupt() ,interrupted(),isinterrupted()
- public void interrupt() :
- 实例方法,Just to set the interrupt flag
- 实例方法interrupt0)仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
- public static boolean interrupted():
- 静态方法,Thread.interrupted();判断线程是否被中断并清除当前中断代态。这个方法做了两件事:
- 返回当前线程的中断状态,测试当前线程是否已被中断
- 将当前线程的中断状态清零并重新设为false,清除线程的中断状态
- 此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样
- 静态方法,Thread.interrupted();判断线程是否被中断并清除当前中断代态。这个方法做了两件事:
- public boolean islnterrupted():
- 实例方法,判断当前线程是否被中断(通过检查中断标志位)
- public void interrupt() :
- 大场面试题中断机制考点:
- 如何中断运行中的线程?
- 通过一个volatile变量实现
static volatile boolean isStop = false; new Thread(() -> { while(true){ if(isStop){ System.out.println("-----isStop = true,程序结束。"); break; } System.out.println("------hello isStop"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { isStop = true; },"t2").start();
- 通过AtomicBoolean
static AtomicBoolean atomicBoolean = new AtomicBoolean(false); new Thread(() -> { while(true) { if(atomicBoolean.get()) { System.out.println("-----atomicBoolean.get() = true,程序结束。"); break; } System.out.println("------hello atomicBoolean"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { atomicBoolean.set(true); },"t2").start();
- 通过Thread类自带中断api实现
- 思路:在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("-----isInterrupted() = true,程序结束。"); break; } System.out.println("------hello Interrupt"); } }, "t1"); t1.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { t1.interrupt();//修改t1线程的中断标志位为true },"t2").start();
- API:源码分析
- interrupt() : 代码调用了interrupt0(),是一个本地方法;
- isInterrupted():调用本地方法private native boolean isInterrupted(boolean ClearInterrupted);
- 小总结:具体来说,当对一个线程,调用 interrupt() 时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而己。被设置中断标,志的线程将继续正常运行,不受影响。所以,interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
- 思路:在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
- 通过一个volatile变量实现
- 当前线程的中断标识为true,是不是线程就立刻停止?不会
0. 实例方法interrupt()仅仅是设置线程的中断状态位设置true,不会停止线程- 后手案例:阻塞中的线程被中断会抛出异常:
Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("-----isInterrupted() = true,程序结束。"); break; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("------hello Interrupt"); } }, "t1"); t1.start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { t1.interrupt();//修改t1线程的中断标志位为true },"t2").start();
- 执行1中程序会发现,中断后抛出异常,但是并不会结束,需要在catch里面加入再次设置中断
Thread.currentThread().interrupt(); //线程的中断标志位为false,无法停下,需要再次掉interrupt()设置true
- 考点:为什么还需要在设置一次中断
- 线程处于阻塞状态时,进行中断会抛异常,并且清除阻塞标记状态
- 小结:中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断
- 后手案例:阻塞中的线程被中断会抛出异常:
- 静态方法Thread.interrupted()的理解:
- 对比和isInterrupted()区别:静态与实例;是否复位;均返回boolean
-
public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); }
- 源码:本质也是调用了当前线程的isInterrupted本地方法,只是在ClearInterrupted这个位置设置了true。底层逻辑和实例方法相同。
- 如何中断运行中的线程?
5.2 线程等待唤醒机制
- 三种线程阻塞和唤醒方法:
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
-
new Thread(() -> { //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectLock){ System.out.println(Thread.currentThread().getName()+"\t"+"---come in"); try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { synchronized (objectLock){ objectLock.notify(); System.out.println(Thread.currentThread().getName()+"\t"+"---发出通知"); } },"t2").start();
- 注意:
- wait和notify必须在synchronized块内使用,否则报异常
- wait必须执行在notify前面,否则notify无法将其唤醒
-
- 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
-
new Thread(() -> { //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t"+"---come in"); condition.await(); System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } },"t1").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName()+"\t"+"---发出通知"); }finally { lock.unlock(); } },"t2").start();
- 注意:
- await和signal都要在lock和unlock块内使用,否则会抛出异常
- 顺序也不能反,先await后singal
-
- 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
- 代码演示在5.3.4中
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 为什么有新方法诞生:为什么会从1到2再到3
0.- wait/notify和await/signal都需要先阻塞再唤醒,而lockSupport可以支持先发许可再检查许可
- lockSupport不需要锁块内去操作park和unpark
5.3 LockSupport
- 用于创建锁和其他同步类的基本线程阳塞原语。
- 没有构造方法,内置八个方法,全是静态方法:
- static object getBLocker(Thread t)返回提供给尚未解除阻塞的park方法的最新调用的阻止程序对象,如果未阻止,则返回null。
- static void park()除非许可证可用,否则禁用当前线程以进行线程调度。
- static void park(object blocker)除非许可证可用,否则禁用当前线程以进行线程调度。
- static void parkNanos ( Long nanos)除非许可证可用,否则禁用当前线程以进行线程调度,直到指定的等待时间。
- static void parkNanos (Object blocker, Long nanos)除非许可证可用,否则禁用当前线程以进行线程调度,直到指定的等待时间。
- static void parkUntil(long deadline)除非许可证可用,否则禁用当前线程以进行线程调度,直到指定的截止时间。
- static void parkUntil(object blocker, Long deadline)除非许可证可用,否则禁用当前线程以进行线程调度,直到指定的截止时间。
- static void unpark(Thread thread)如果给定线程尚不可用,则为其提供许可。
- LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程
- 原理
- 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
- LockSuppor类使用了一种名为Permit(许可)的概念来做到阳塞和唤醒线程的功能,每个线程都有一个许可(permit).但与 Semaphore 不同的是,许可的累加上限是1。
- 该类与使用它的每个线程关联一个许可证(在Semaphore类的意义上〉。如果许可证可用,将立即返回park,并在此过程中消费;否则可能会阳止。如果尚未提供许可,则致电unpark获得许可。(与Semaphores不同,许可证不会累积。最多只有一〉可靠的使用需要使用volatile(或原子)变量来控制何时停放或取消停放。对于易失性变量访问保持对这些方法的调用的顺序,但不一定是非易失性变量访问。
- API:8个静态方法
- park (Object blocker):
- 源码调用了UNSAFE.park()方法,UNSAFE是一个功能非常强大的C语言包,不过容易造成内存泄露。
- public native void park(boolean isAbsolute, Long time);
- permit许可证默认没有不能放行,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程的发放permit, park方法才会被唤醒。
- unpark (Thread thread)
- 线程不为null的时候调用UNSAFE.unpark(thread);
- 调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。
- 这就意味着,许可证可以提前发给他
- park (Object blocker):
- 案例
Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + "---come in"); LockSupport.park(); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t" + "---被唤醒"); }, "t1"); t1.start(); new Thread(() -> { LockSupport.unpark(t1); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.unpark(t1); System.out.println(Thread.currentThread().getName()+"\t"+"---发出通知"); },"t2").start();
- 小总结:
- 许可证不累加:许可证卡发多少个多不会累计。所以一定是一对一。也意味着lockSupport是不可重入锁。
- unpark发放许可证,park会消耗掉许可证,即使给你发放过,那也是之前的事情了。
6. Java内存模型JMM
- 大厂面试题
- 你知道什么是Java内存模型JMM吗?
- JMM与volatile它们两个之间的关系?(下一章详细讲解)
- JMM有哪些特性or它的三大特性是什么?
- 为什么要有JMM,它为什么出现?作用和功能是什么?
- happens-before先行发生原则你有了解过吗?
- 计算机硬件存储系统
- cpu寄存器 -> CPU一级緩存->CPU二级缓存->CPU三级缓存->主存->本地磁盘->网络
- 因为有这么多级的缓存(cpu和物理主内存的速度不一致的,CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题
- JM规范中试图定义一种Java内存模型 (java Memory Model,简称JMM) 来屏蔽掉各种便件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。所以,推导出我们需要知道JMM
- Java内存模型Java Memory Model(学术定义)
- JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
- 原则:JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
- 能干嘛?
- 通过JMM来实现线程和主内存之间的抽象关系。
- 屏蔽各个便件平合和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
- JMM规范下,三大特性
- 可见性:
- 是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。
- Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
- 不满足可见性可能会导致脏读,即其他线程读取了旧数据,没有读取到最新的数据。
- 原子性:指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰
- 有序性:
- 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只婴程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
- 优缺点:
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生"脏读"),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。 - 指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
- 从源码到最终执行示例图:
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
- 可见性:
- JMM规范下,多线程对变量的读写过程
- 读取过程:
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取喊值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
- 小总结:
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直按从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变品,线程问变量值的传逆需要通过主内存米进行(同级不能相互访问)
- 读取过程:
- JMM规范下,多线程先行发生原则之happens-before
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者 代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上的先后关系
- 案例:
1.x = 5 线程A执行 y = x 线程B执行 上述称之为:写后读 - 问题?
y是否等于5呢?
如果线程A的操作(x= 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;
如果他们不存在happens-before原则,那么y = 5 不一定成立。
这就是happens-before原则的威力。-------------------》包含可见性和有序性的约束
- 问题?
- 先行发生原则说明
- 如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰噤,但是我们在编写Java并发代码的时候并没有家觉到这一点。
- 我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下有一个“先行发生”(Happens-Before)的原则限制和规矩,给你立好了规矩!
- 这个原则非常重要:
它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
- happens-before总原则:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
- happens-before之8条:
- 次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;
- 加深说明:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量x赋值为1,那后面一个操作肯定能知道X已经变成了1。
- 锁定规则:一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。对象没有完成初始化之前,是不能调用finalized()方法的
- 次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;
- 小结:
- 在 Java 语言里面,Happens-Before 的语义本质上是一种可见性
- A Happens-Before B 意味着 A发生过的事情对B来说是可见的,无论 A事件和B事件是否发生在同一个线程里.
- JMM的设计分为两部分:
- 一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员闻述了一个强内存模型,我们只要理解
- happens-before规则,就可以编写并发安全的程序了。
- 另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码即可。
- 案例:对于下面代码进行分析
private int value = 0; public int getValue() { return value; } public int setValue() { return ++value; }
- 假设存在线程A和B,线程A先(时间上的先后)调用了setValue(),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
- 我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、§可以忽略,因为他们和这段代码毫无关系):
- 由于两个方法是由不同的线程调用,不在同一个线程中,所以背定不滿足程序次序规则,
- 两个方法都没有使用锁,所以不游足锁定规则:
- 变量不是用volatle修饰的,所以volatle变量规则不满足:
- 传逆规则背定不满足,
- 所以我们无法通过happens-before原则推导出线程A happens-before线程B,宝然可以确认在时间正线程A优死于线程B指定,但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢:
- 修复:
- 把getter/setter方法都定义为synchronized方法
- 把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景
7. volatile与JMM
- 被volatile修饰的变量有2大特点
- 可见性:
- 有序性:排序要求,有时需要禁重排
- volatile的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
- 简单说:写穿策略、每次都读主内存
- volatile凭什么可以保证可见性和有序性:内存屏障
7.1 内存屏障(面试重点必须拿下)
- 定义:
- 内存屏障〈也称内存棚栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。
- 内存屏障之前的所有写操作都要回写到主内存,
- 内存屏障之后的所有读染作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
- 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是
说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。 - 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保
证后面的读取数据指令一定能够读取到最新的数据。
- 分类:粗分两种,细分4种
- 粗分:
- 读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
- 写屏障(Store Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
- 细分:
0. static votd loadload();
static void storestore();
static void loadstore();
static void storeload():- C++源码分析 —— IDEA工具里面找Unsafe.class —— Unsafe. java——OrderAccess.hpp——orderAccess_ linux_x86.inline.hpp
- orderAccess_ linux_x86.inline.hpp
- 四大屏障分别是什么意思
屏障类型 指令示例 说明 LoadLoad Load1;LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前执行 StoreStore Store1; StoreStore; Store2 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 LoadStore Load1; LoadStore; Store2 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行
- 保证有序性:
- 重排序有可能影响程序的执行利实现、因此,我们有时候希望告诉JVM你別“自作聪明”给我重排序,我这里不需婴排序,听主人的。
- 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
- 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
- happens-before 之 volatile 变量规则
第一个操作 第二个操作:普通读写 第三个操作:volatile读 第二个操作:volatile写 普通读写 可以重排 可以重排 不可以重排 volatile读 不可以重排 不可以重排 不可以重排 volatile写 可以重排 不可以重排 不可以重排 - 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
- 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
- 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
- volatile读插入内存屏障后生成的指令序列示意图
- volatile写插入内存屏障后生成的指令序列示意图
- 保证可见性:
0. 保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见- 代码案例:
// 不使用volatile static boolean flag = true; public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t"+"---come in"); while(flag){ new Integer(308); } System.out.println("t1 over"); },"t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread. currentThread().getName()+"\t 修改完成f1ag:"+flag); } // 结果:flag被main线程成功修改,但是线程t1依然没有结束 // 修改为volatile,修改完flag,t1立刻结束
- 复盘:
- 线程t1中为何看不到被主线程main修改为false的flag的值?
- 因为main修改的是本地内存,没有写穿到主内存。
- 或者main写穿了,但是t1还是读本地内存数据
- 解决:用volatile修饰
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
- 线程t1中为何看不到被主线程main修改为false的flag的值?
- Java内存模型中定义的8种工作内存与主内存之间的原子操作:
- 顺序:read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
- read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
- 由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁 ,所以,JVM提供了另外两个原子指令:
- lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
- unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
- 代码案例:
- 没有原子性
- volatile不具备原子性,比如i++的操作,用synchronized就没问题,用volatile会产生并发读写问题
- 对于volatile设最具各可见任,JVM只是保证从主内存加我到线程工作内存的值是最新的,也仅是数据加教时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对子多线程修改主内存共享变量的场景必须使用加锁同步。
- 本质上:volatile修饰的1w次i++不能达到i=10000的效果,因为volatie的可见性,当其在执行++操作时,i值发生变化,则++操作失效。
- 结论:
- volatile不适合参与到依赖当前值的运算
- 那么依靠可见性的特点volatile可以用在哪些地方呢? 常volatile用做保存某个状态的boolean值or int值
- 由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、 java.util.concurrent中的锁或原子类)来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
- 指令禁重排 粗分4个,细分两个,总结起来就三句话:
- volatile读后的任何操作都禁和volatile读重排
- volatile写前的任何操作都禁和volatile写重排
- volatile读写直接禁止重排
- 如何正常使用volatile(使用场景)
- 单一赋值可以,but含复合运算赋值不可以(++之类)
- 状态标志,判断业务是否结束:见9中代码案例
- 开销较低的读,写锁策略:
- 当读远多于写,结合使用內部锁和 volatile 变量来减少同步的开销:写的时候用synchronized,读的时候用volatile修饰
public class Counter { private volatile int value; public int getValue() { return value;//利用volatile保证读茶作的可见姓 } public synchronized int increment () { return value++;//利/用synchronized保证复合额作的原子性 } }
- 当读远多于写,结合使用內部锁和 volatile 变量来减少同步的开销:写的时候用synchronized,读的时候用volatile修饰
- DCL双端锁的发布:进来之前判断一下,然后加锁,进锁之后又判断一下,饿汉式单例模式里面使用。
7.2 volatile八股小作文
volatile是为了解决多线程共享资源由于JMM结构造成的更新延时带来的问题,如脏读。JMM规定每个线程不允许直接修改主内存,而是拷贝一份到本地空间进行修改,然后再更新到主内存。这就导致了很多修改不能及时在线程间共享,于是产生了volatile关键字,他具有可见性、有序性,保证不同线程的修改可以及时被其他线程知道。可见性底层由内存屏障保证实施,禁止了volatile读与其后操作的重排、volatile写与其前面操作的重排和volatile读写之间的重排,同时屏障会强制刷新CPU缓存,使得其他线程可以读取到该变量的最新值。但是volatile操作不保证原子性,适合读操作较多的场景,配合synchronized使用,写操作加锁,读操作的对象用volatile修饰。在单例模式的饿汉式中,使用双端锁时,创建的对象使用volatile修饰。