五、Java多线程并发
1、Volatile
当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主内存。volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
(1)、不保证原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。volatile不保证原子性,在Java中synchronized和在lock、unlock中操作保证原子性。
①、volatile不保证原子性原因
Java中只有对基本类型变量的赋值和读取是原子操作,例如:i = 1的赋值操作,但是j = i或者i++这样的操作都不是原子操作。i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写回到缓存中。
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行i++自增操作,线程A首先得到了i的初始值100,但是还没来得及修改就阻塞了。这时线程B也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作得到101,然后线程B将新值101写入到缓存中,再刷回主内存中。根据volatile可见性的原则,这个主内存的值可以被其他线程可见。但是线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷回主内存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
(2)、可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。在Java中volatile、synchronized和final实现可见性。
①、原理
CPU内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集填充到高速缓存,如果非volatile变量当CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中,普通变量的值在线程间传递均需要通过主内存来完成。如果发现操作的变量是volatile共享变量,即在其他CPU中也存在该变量的副本,基于MESI协议,当CPU写数据时会发出信号通知其他CPU将该变量设置为无效状态。当其他CPU使用这个变量时,会重新从内存中读取这个变量。
②、MESI(缓存一致性)
Modify、Exclusive、Shared、Invalid,当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,通过CPU多核之间的嗅探机制实现,会发出信号通知其他CPU将该变量的缓存行为置为无效状态。因此当其他CPU需要读取这个变量时,发现自己缓存中缓存的该变量的缓存行是无效的,那么它就会从内存中重新读取。
(3)、有序性:即程序执行的顺序按照代码的先后顺序执行。
①、禁止指令重排序优化,从而避免多线程环境下程序出现乱序执行的现象。编译器和处理器都能执行指令重排优化,但是有数据依赖关系的不会进行重排序。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障。内存屏障指令重排序时不能把屏障后面的指令重排序到内存屏障之前的位置,因此任何CPU的线程都能读到这些数据的最新版本。(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
②、内存屏障会提供3个功能:
a、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
b、它会强制将对缓存的修改操作立即写入主存;
c、如果是写操作,它会导致其他CPU中对应的缓存行无效。
2、CAS
(1)、概述
CAS全称为Compare-And-Swap,比较当前工作内存(各个线程的栈空间:修改某个变量,各个线程会从主内存copy一份数据到自己的线程的工作内存,修改完后再更新回主内存)中的值,如果相同则执行规定操作,否则继续比较知道主内存和工作内存中的值一致为止,比较替换的过程是原子的。CAS在Java语言中应用就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令。
CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。首先获取V的原值A,如果进行更新的时候获取V地址的值还是A则CAS操作成功把A修改为B,否则CAS操作失败,返回V地址的新值,重新执行比较交换流程。类似于CAS的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么CAS会检测它(并失败),算法可以对该操作重新计算。
(2)、问题及解决方案
①、ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决思路:使用版本号。比如AtomicStampedReference类,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。
②、循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
解决思路:利用pause指令效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
③、只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。
解决思路:可以用锁或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
3、原子类(AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference)
高并发的情况下,i++无法保证原子性,往往会出现问题,为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题,引入AtomicInteger类,AtomicInteger能够保证多线程并发操作的原子性。
(1)、常用方法
①、以原子方式将给定值与当前值相加
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
②、如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
③、以原子方式设置为给定值,并返回旧值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
④、以原子方式将当前值加 1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
⑤、以原子方式将当前值减 1
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
(2)、AtomicInteger源码解析
AtomicInteger通过使用volatile修饰值value,保证了其在多线程之间的内存可见性。
AtomicInteger通过使用Unsafe类的native方法,保证了数据操作的原子性。
①、AtomicInteger类
public class AtomicInteger extends Number implements java.io.Serializable {
//获取Unsafe类,Unsafe类的native原始方法保证原子性
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内存地址偏移量
private static final long valueOffset;
static {
try {
//通过Unsafe类的objectFieldOffset方法获得内存地址偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
//volatile使value具有可见性
private volatile int value;
/**
* 以原子方式将当前值加 1
*/
public final int getAndIncrement() {
//底层调用Unsafe类native原始方法执行 i++ ,
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
②、Unsafe类
Atomic原子类、锁、ConcurrentHashMap等实现都是依靠Unsafe类保证原子性。
在Java中Unsafe类是CAS的核心类,Java中的CAS操作的执行依赖于Unsafe类的native方法,Unsafe中的native方法可以直接操作特定的内存数据,该操作是原子性的。Unsafe类存在于rt.jar包中的sun.misc包中。
public final class Unsafe {
//根据对象和内存地址偏移量获取值
public native int getIntVolatile(Object var1, long var2);
//将获取的值var4跟当前内存地址的值进行比较,如果一样替换为新值var5
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
......
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
//根据var1(this)和var2(valueOffset)内存地址获取当前值var5
var5 = this.getIntVolatile(var1, var2);
//用之前获取到的var5去和var1、var2内存地址的值进行比较,如果一样则替换为var5 + var4,返回true。
//!true为false,跳出while循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//返回var5旧值
return var5;
}
}
(3)、AtomicReference原子引用的应用
AtomicReference还可以简单实现自旋锁。compareAndSet()底层仍然是调用Unsafe类的native方法完成比较替换操作。
public class AtomicReference<V> implements java.io.Serializable {
......
}
public static void main(String[] args) {
User zhang = new User("张三", 20);
User li = new User("李四", 26);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zhang);
//输出结果:比较替换结果:true****User(name=李四, age=26)
System.out.println("比较替换结果:" + atomicReference.compareAndSet(zhang, li) + "****" + atomicReference.get().toString());
//输出结果:比较替换结果:false****User(name=李四, age=26)
System.out.println("比较替换结果:" + atomicReference.compareAndSet(zhang, li) + "****" + atomicReference.get().toString());
}
(4)、AtomicStampedReference
利用AtomicStampedReference原子引用类解决CAS中的ABA问题。
//new AtomicStampedReference<>(初始值, 初始版本号)
static AtomicStampedReference<Integer> asf = new AtomicStampedReference<>(5, 1);
public static void main(String[] args) {
new Thread(() -> {
//线程 T1 第1次取得版本号:1
int stamp = asf.getStamp();
//阻塞1秒等待T2拿到最初始的版本号
try { TimeUnit.SECONDS.sleep(1); }catch (Exception e){e.printStackTrace();}
//线程 T1 第2次取得版本号:2 ;compareAndSet结果:true ;当前最新值:10
boolean upate1 = asf.compareAndSet(5, 10, asf.getStamp(), asf.getStamp() + 1);
//线程 T1 第3次取得版本号:3 ;compareAndSet结果:true ;当前最新值:5
boolean upate2 = asf.compareAndSet(10, 5, asf.getStamp(), asf.getStamp() + 1);
}, "T1").start();
new Thread(() -> {
//线程 T2 第1次取得版本号:1
int stamp = asf.getStamp();
//阻塞5秒等线程T1完成ABA
try { TimeUnit.SECONDS.sleep(5); }catch (Exception e){e.printStackTrace();}
//线程 T2 第2次取得版本号:3 ;compareAndSet结果:false ;当前最新值:5
boolean upate4 = asf.compareAndSet(5, 100, stamp, stamp + 1);
}, "T2").start();
}
4、CountDownLatch
(1)、概述
CountDownLatch作用当一个线程需要另外一个或多个线程完成后,再开始执行,是基于AQS机制实现的。在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,该变量只能设置一次,并且其操作都是原子操作,该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。
(2)、过程
①、主线程创建实例CountDownLatch countDownLatch = new CountDownLatch(10)
②、创建子线程异步执行完业务之后调用countDownLatch.countDown(),使count计数-1
③、调用countDownLatch.await()
a、如果主线程调用countDownLatch.await()方法,则表示当count值为0的时候才继续执行主线程。
b、如果主线程调用countDownLatch.await(5, TimeUnit.SECONDS)设置超时时间为5秒,则表示主线程会在5秒后继续执行,不管此时子线程有没有执行完毕
5、CyclicBarrier
(1)、概述
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。是让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续执行。CyclicBarrier内部是基于ReentrantLock和Condition来实现的,每当一个线程调用await()方法时,将拦截的线程数加1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁。这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点),这一点与CountDownLatch不同,CountDownLatch初始数量只可以设置一次。
(2)、核心方法
①、parties:初始化相互等待的线程数量
②、await():在CyclicBarrier上进行阻塞等待,直到发生以下情形之一
a、在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。
b、当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
c、其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
d、其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
e、其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
③、await(timeout,TimeUnit):在CyclicBarrier上进行限时的阻塞等待,直到发生以下情形之一
a、在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。
b、当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
c、当前线程等待超时,则抛出TimeoutException异常,并停止等待,继续执行。
d、其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
e、其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
f、其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
(3)、CountDownLatch和CyclicBarrier的区别
①、两者都是等待对应的一个或一组线程都完成工作之后再进行下一步操作。
②、CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。
6、Semaphore(信号量)
信号量(Semaphore),又被称为信号灯,作用是限制线程并发的数量。在多线程环境下用于协调各个线程,以保证它们能够正确、合理的使用公共资源。信号量维护了一个许可集,我们在初始化Semaphore时需要为这个许可集传入一个数量值,该数量值代表同一时间能访问共享资源的线程数量。
线程可以通过acquire()方法获取到一个许可,然后对共享资源进行操作,注意如果许可集已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可,线程释放一个许可通过release()方法完成,"许可"将被归还给Semaphore。
(1)、方法
①、void acquire()
每调用1次此方法,就消耗掉一个许可。在提供一个许可前一直将线程阻塞,否则线程被中断。
②、void release()
每调用1次此方法,就动态释放一个许可,将其添加一个许可给信号量。
③、int availablePermits()
返回Semaphore对象中当前可以用的许可数。
④、final boolean hasQueuedThreads()
判断有没有线程在等待获取许可。
⑤、final int getQueueLength()
获取等待的许可的线程个数。
⑥、int drainPermits()
获取并返回所有的许可个数,并且将可用的许可重置为0。
⑦、Semaphore semaphore = new Semaphore(1, true);
a、表示同一时间内最多只允许1个线程执行acquire()和release()之间的代码。
b、False:表示非公平信号量,即线程启动的顺序与调用semaphore.acquire()的顺序无关,也就是线程先启动了并不代表先获得许可。
c、True:公平信号量,即线程启动的顺序与调用semaphore.acquire() 的顺序有关,也就是先启动的线程优先获得许可。
公平和非公平信号量:
有些时候获取许可的的顺序与线程启动的顺序有关,这是的信号量就要分为公平和非公平的。所谓的公平信号量是获得锁的顺序与线程启动的顺序有关,但不代表100%获得信号量,仅仅是在概率上能保证,而非公平信号量就是无关的。