线程同步的基础是java内存模型,如果对java内存模型还不了解,需要先了解一下
1. volatile
volatile的特性
Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量定义为volatile之后,它将具备两种特性。
保证此变量对所有线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
volatile 型变量实现原理
具体实现方式是在编译期生成字节码时,会在指令序列中增加内存屏障来保证,下面是基于保守策略的 JMM 内存屏障插入策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。该屏障除了保证了屏障之前的写操作和该屏障之后的写操作不能重排序,还会保证了 volatile 写操作之前,任何的读写操作都会先于 volatile 被提交。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。该屏障除了使 volatile 写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使 volatile 变量的写更新对其他线程可见。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。该屏障除了使 volatile 读操作不会与之前的写操作发生重排序外,还会刷新处理器缓存,使 volatile 变量读取的为最新值。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。该屏障除了禁止了 volatile 读操作与其之后的任何写操作进行重排序,还会刷新处理器缓存,使其他线程 volatile 变量的写更新对 volatile 读操作的线程可见。
volatile能保证原子性吗?
volatile变量在各个线程的工作内存中不存在一致性问题,但是Java里面的运算并非原子操作,并且volatile并不能保证原子性,导致volatile变量的运算在并发下一样是不安全的。
为什么volatile不能保证原子性
对于i=1这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于i++这种复合操作,即使使用volatile关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。
private volatile int i = 0;
i++;
如果启了500条线程并发地去执行i++这个操作 最后的结果i是小于500的.
解释:
原子性意味着一个操作是不可中断的,即使在多线程环境下,某个操作一旦开始,就会一直执行到结束,中间不会被其他线程的操作打断。volatile 确保的是变量的可见性,但对于复合操作(如 count++)并不能保证原子性。
示例:
count++ 不是原子操作,count++ 实际上由三步组成:
读取变量的当前值(从内存中读取 count 到寄存器或缓存中)。
将值加一(在寄存器或缓存中对 count 的值进行加法运算)。
将结果写回变量(将加一后的值存回内存)。
由于这些步骤是分开执行的,在多线程环境下,线程之间可能发生交错执行,导致竞态条件。例如,一个线程读取了 count 的值为 5,另一个线程在此时也读取了同样的值,然后两个线程都对 count 进行加一操作,最后两个线程分别将结果 6 写回内存,导致 count 的最终值是 6 而不是预期的 7。
假设某一时刻i=5,此时有两个线程同时从主存中读取了i的值,那么此时两个线程保存的i的值都是5, 此时A线程对i进行了自增计算,然后B也对i进行自增计算,此时两条线程最后刷新回主存的i的值都是6(本来两条线程计算完应当是7)所以说volatile保证不了原子性。
volatile的使用场景
1.状态标记量
使用volatile来修饰状态标记量,使得状态标记量对所有线程是实时可见的,从而保证所有线程都能实时获取到最新的状态标记量,进一步决定是否进行操作。
2.双重检测机制实现单例
普通的双重检测机制在极端情况,由于指令重排序会出现问题,通过使用volatile来修饰instance,禁止指令重排序,从而可以正确的实现单例。
2. CAS
常用的锁机制有两种:
1、悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁的实现,往往依靠底层提供的锁机制;悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
2、乐观锁:假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操作,只在提交操作时检查是否违反数据完整性。如果因为冲突失败就重试,直到成功为止。乐观锁大多是基于数据版本记录机制实现。为数据增加一个版本标识,比如在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
乐观锁的缺点是不能解决脏读的问题。
在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
2.1 CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
CAS使用的是乐观锁机制。
2.2 CAS的实现原理
CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
实现CAS最重要的一点,就是比较和交换操作的一致性,否则就会产生歧义。
比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。
要实现这个需求,java中提供了Unsafe类,它提供了三个函数,分别用来操作基本类型int和long,以及引用类型Object
public final native boolean compareAndSwapObject
(Object obj, long valueOffset, Object expect, Object update);
public final native boolean compareAndSwapInt
(Object obj, long valueOffset, int expect, int update);
public final native boolean compareAndSwapLong
(Object obj, long valueOffset, long expect, long update);
参数的意义:
- obj 和 valueOffset:表示这个共享变量的内存地址。这个共享变量是obj对象的一个成员属性,valueOffset表示这个共享变量在obj类中的内存偏移量。所以通过这两个参数就可以直接在内存中修改和读取共享变量值。
- expect: 表示预期原来的值。
- update: 表示期待更新的值。
接下来我们来看看java并发框架下的atomic包是如何使用CAS的。
2.3 JUC并发框架下的原子类(atomic)
调用JUC并发框架下原子类的方法时,不需要考虑多线程问题。那么我们分析它是怎么解决多线程问题的。以AtomicInteger类为例
成员变量
// 通过它来实现CAS操作的。因为是int类型,所以调用它的compareAndSwapInt方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value这个共享变量在AtomicInteger对象上内存偏移量,
// 通过它直接在内存中修改value的值,compareAndSwapInt方法中需要这个参数
private static final long valueOffset;
// 通过静态代码块,在AtomicInteger类加载时就会调用
static {
try {
// 通过unsafe类,获取value变量在AtomicInteger对象上内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 共享变量,AtomicInteger就保证了对它多线程操作的安全性。
// 使用volatile修饰,解决了可见性和有序性问题。
private volatile int value;
有三个重要的属性:
- unsafe: 通过它实现CAS操作,因为共享变量是int类型