Volatile
内容摘自 哔哩哔哩 尚硅谷视频: https://space.bilibili.com/302417610/channel/seriesdetail?sid=457613
java.util.concurrent包下的类
谈谈对Volatile的理解
Volatile 是java虚拟机提供的轻量级的同步机制
三大特性:保证可见性,不保证原子性、禁止指令重排
谈谈JMM(Java内存模型 Java Memory Model)
本身是一种抽象的概念,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
同步的规定:
l 线程解锁前,必须把共享变量的值刷新回主内存。
l 线程加锁前,必须读取主内存的最新值到自己的工作内存
l 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都为其创建一个工作内存(有的地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,
首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成后再将变量写回主内存,
不能直接操作主内存的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
三大特性:可见性、原子性、有序性
Volatile可见性代码验证
public class MyTest { private static boolean flag = true; // private static volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "start"); try { TimeUnit.SECONDS.sleep(3); // 等价于 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; System.out.println(Thread.currentThread().getName() + "end"); } },"thread_A").start(); while (flag) { // Thread.sleep(1); /* 让while循环里 线程休眠时,不加 volatile,主内存的flag也会刷新到该线程的工作内存,但不是 实时刷新的 加了 volatile 会实时监听主内存的flag值的改变,只要改变就会立即将值刷新到该线程的工作内存(该线程不再从工作内存获取flag值,而是从主内存获取,然后更新到工作内存) */ } System.out.println("结束"); } }
Volatile不保证原子性代码验证
public class MyTest { private static volatile int num = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { num++; //分三步 拿到num 、进行num+1 、将num+1 赋值给num } } }).start(); } while (Thread.activeCount() > 2) {//保证前面的线程都执行完毕 Thread.yield(); } System.out.println(Thread.currentThread().getName() + num); } }
如何解决原子性问题
使用synchronized
synchronized ("A") { num++; //分三步 拿到num 、进行num+1 、将num+1 赋值给num }
使用JUC下atomic(原子的)包下的类
public class MyTest { private static volatile int num = 0; private static AtomicInteger num1 = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { num++; //分三步 拿到num 、进行num+1 、将num+1 赋值给num num1.getAndIncrement(); // 获取并加一 } } }).start(); } while (Thread.activeCount() > 2) {//保证前面的线程都执行完毕 Thread.yield(); } System.out.println(Thread.currentThread().getName() + num); // 结果:小于 20000 (不是一定小于) System.out.println(Thread.currentThread().getName() + num1);// 结果:等于 20000 } }
有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分一下三种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致 (不存在指令重排问题)
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
指令重排在多线程中存在的问题
1、
2、
变量通过volatile修饰,来禁止指令重排,解决 指令重排在多线程中的问题。
内存屏障
线程安全性获得保证:
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排序优化。
单例模式在多线程环境下存在的问题
DCL(Double Check Lock 双重校验锁)机制不一定安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
public class MySingleton { private static volatile MySingleton singletonInstance = null; private MySingleton() { System.out.println(Thread.currentThread().getName() + "构造方法"); } public static MySingleton getMySingletonObj() { if (singletonInstance == null) { synchronized (MySingleton.class) { if (singletonInstance == null) { singletonInstance = new MySingleton(); } } } return singletonInstance; } }
MESI(缓存一致性协议) & 总线风暴
【引用:CSDN博主「敖 丙」的原创文章 :原文链接:https://blog.csdn.net/qq_35190492/article/details/105837982
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。
MESI(缓存一致性协议)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
嗅探
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
总线风暴
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
volatile与synchronized的区别
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。 volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。
】
在 Java 中,线程之间的通信是指多个线程之间进行信息交换或协调操作的过程。线程之间的通信可以通过共享内存或消息传递来实现。
以下是几种常见的线程间通信方式:
-
共享内存:多个线程共享同一块内存区域,通过在内存中读写数据来实现线程间的通信。常见的共享内存方式包括使用共享变量、使用队列、使用共享对象等。例如,通过在共享变量中设置标志位来实现线程的同步与通知。
-
管程(Monitor):使用管程来实现线程间的互斥与同步。管程是一种用于保护共享资源的数据结构,提供了对共享资源的访问控制。Java 中的
synchronized
关键字和wait()
、notify()
、notifyAll()
方法就是基于管程实现线程间通信的机制。 -
信号量(Semaphore):信号量是一种用于控制对共享资源的访问的同步机制。它可以控制同时访问某一资源的线程数量,并提供了
acquire()
和release()
方法来获取和释放资源。 -
条件变量(Condition):条件变量是一种线程同步的高级机制,它可以让线程在某一条件满足时等待,而在条件发生变化时被通知。Java 中的
java.util.concurrent.locks.Condition
接口提供了条件变量的实现。 -
信号量屏障(CountDownLatch 和 CyclicBarrier):这两种屏障机制可以让一组线程在某一条件满足时同时执行。
CountDownLatch
是一次性的屏障,而CyclicBarrier
是可重复使用的屏障。
无论使用哪种方式,线程之间的通信都是为了实现多个线程之间的协调与同步,确保线程能够安全地访问共享资源,并在需要时进行合适的等待和唤醒操作。