1、synchronized
- 当多个线程同时访问一个对象的同步方法时,同一时间只能有一个线程得到执行,其他线程必须等待当前线程执行完之后再依次执行。
- 当有线程正在执行一个对象的同步方法时,其他线程仍可访问此对象的非同步方法。
- 当有线程正在执行一个对象的同步方法时,其他线程不能访问此对象的任意一个同步方法。因为当有线程开始执行一个对象的同步代码块或同步方法,它就获取到了此对象的对象锁,此时其他线程对该加锁对象的所有同步代码部分的访问都将被阻塞。
2、volatile
对于并发编程来说,线程间通信
和线程间同步
问题是重中之重。,JMM (Java Memory Model) 中可见性、原子性、有序性也是这两个问题带来的。volatile是java虚拟机提供的轻量级的同步机制。
通信问题
在命令式编程中,线程之间的通信包括共享内存和消息传递 而 java并发采用的是共享内存模型,线程之间共享程序的公共状态,通过读写内存总的公共状态来隐式通信
同步问题
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷回主内存
- 线程加锁前,必须读取共享内存的最新值到自己的本地内存
- 加锁解锁是同一把锁
2.1 volatile关键字简介
Java的内存分为主内存
和工作内存
两部分,规定程序中所有的变量都存储在主内存
中,每个线程还有自己的工作内存
,工作内存中保存了该线程使用到的变量的主内存的拷贝,线程对变量的所有操作(赋值、读取等)都必须在工作内存中进行,而不能直接读取主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递都必须经过主内存的传递来完成。
工作内存值改变后刷新到主内存是需要一定时间的,所以可能会出现多个线程操作同一个变量的时候出现取到的值还是未更新前的值。这样的情况我们通常称之为可见性,加上 volatile 关键字修饰的变量就可以保证对所有线程的可见性。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将会从主内存中读取共享变量。
2.2 为什么volatile关键字可以保证可见性
volatile应用内存屏障(Memory Barrier)机制实现可见性。
内存屏障可以保证内存可见性和特定操作的执行顺序。
volatile写操作之后会插入一个store屏障,将工作内存中的值刷回到主内存。在读操作之前都会插入一个load屏障,从主内存读取最新的数据,而无论是stroe还是load都会告诉编译器和cpu,屏障前后的指令都不要进行重排序优化(禁止指令重排序)。
2.3 volatile关键字能否保证线程安全
volatile 并不能保证线程安全,线程安全要满足3个条件:原子性,可见性,有序性。
而volatile只能保证可见性和有序性。
- volatile 只提供了一种弱的同步机制,用来确保将变量的更新操作通知到其他线程
- volatile 语义是禁用 CPU 缓存,直接从主内存读、写变量。语义表现为:更新(写) volatile 变量时,JMM 会把线程对应的本地内存中的共享变量值刷新到主内存中;读 volatile 变量时,JMM 会把线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 当把变量声明为 volatile 类型后,JVM 增加内存屏障,禁止 CPU 进行指令重排
2.4 什么是原子性操作
对基本数据类型的变量读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量)才是原子操作。(变量之间的相互赋值不是原子操作,比如 y = x,实际上是先读取 x 的值,再把读取到的值赋值给 y 写入工作内存)
i++
实际上包含了 3 个独立的操作:读取 i 的值,将值加 1,然后将计算结果赋值给 i。这是一个读取-修改-写入
的操作序列,并且其结果状态依赖于之前的状态,所以在多线程环境下存在问题。
2.5 指令重排
指令重排:处理器为了提高程序运行效率,可能对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,却会影响到多线程的执行结果。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。
2.6 使用场景
- 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
比如下面的场景,就很适合使用 volatile 来控制并发,当 shutdown() 方法调用的时候,就能保证所有线程中执行的 work() 立即停下来。
volatile boolean shutdownRequest;
private void shutdown(){
shutdownRequest = true;
}
private void work(){
while (!shutdownRequest){
// do something
}
}
3. 总结
volatile 特性:保证可见性、禁止指令重排、解决 long 和 double 的 8 字节赋值问题。它并不能保证并发安全(不能保证原子性),不要和 synchronized 混淆。
用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。
volatile和synchronized的区别与联系:
-
本质不同:volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,
主要用于解决变量在多个线程之间的可见性问题
;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞,主要用于解决多个线程访问资源的同步性问题
-
作用域不同:volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
-
是否原子性:volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;volatile不保证原子性的原因:线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为volatile 变量没上锁
-
是否加锁(阻塞):volatile不会造成线程的阻塞(没有上锁);synchronized可能会造成线程的阻塞
-
是否指令重排:volatile标记的变量不会被编译器优化(即禁止指令重排);synchronized标记的变量可以被编译器优化
缓存一致性问题举例: 同一个变量 i = 0,有两个线程执行 i++ 方法,线程1 把 i 从内存中读取进缓存,而现在线程2也把 i 读取进缓存,两个线程执行完 i++ 后,线程1写回内存,i = 1,线程2也写回内存i = 1,两次自加结果最终值为1
参考:
https://www.jianshu.com/p/bc7b6f14984e
https://www.jianshu.com/p/deeddb1a82b5