【总结】并发编程(二:volatile&cas&AQS&JUC)
1.aqs
1.1 简介
aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件(eg:多个线程通过lock竞争锁时,当竞争失败的锁是如何实现等待以及被唤醒的呢?)
1.2 aqs两种功能
独占锁,每次只能有一个线程持有锁,比如ReentrantLock就是以独占方式实现的互斥锁
共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
1.3 aqs方法
自定义同步器实现时主要实现以下几种方法:
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
1.4 aqs流程
1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3.acquireQueued()使线程在等待队列中休息,当前驱节点是头节点,并且获取到共享资源,会对后继节点unpark(),会去尝试获取资源。获取到资源后才返回

2.cas&volatile
2.1 volatile
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
2.1.1.使用volatile关键字会强制将修改的值立即写入主存
使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效,由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。这其实就是MESI)
2.1.2.禁止进行指令重排序
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
//线程1:
context = loadContext(); //语句1
volatile inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
如果不用volatile,可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
2.1.3 volatile如何实现这两个特性的?
内存屏障
(1)确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面 ---有序性
(2)读屏障 写屏障 :读屏障加在读操作前可以让高速缓存中失效的数据。强制从主内存中加载。写屏障加在写操作后,写入缓存中最新数据到主内存,让其它线程课件
2.2 cas
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值相同时,将内存值V修改为B,否则什么都不做。
2.2.1.cas怎么保证拿到的内存中的值是最新的?
volatile修饰变量,可以保证其可见性
2.2.2.ABA问题?
就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A(别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律)
2.2.3.解决ABA问题
①AtomicMarkableReference,维护一个boolean类型的标记(可以知道是否被修改过)
②AtomicStampedReference,维护一个版本号,发生改动后版本号会改动(可以知道被修改了几次)
2.2.4.cas性能为什么比锁高?
cas不同的地方在于不是通过信号量机制(强制阻塞)而是通过自旋CAS实现互斥访问的(原地空转,执行一个无限循环判断对象是否已解锁),避免了强制阻塞时用户态与核心态之间切换带来的开销(系统调用),这里的开销主要是保存用户态的上下文信息
2.2.5.cas为什么能保证线程安全?
我的疑问:两个线程T1,T2同时对同一对象执行CAS操作加锁,V存储同一块内存地址,A当然也是同样旧的预期值,那么这种情况下T1和T2都可以进行更新。这样不就线程不安全了吗?
cas其实是计算机底层(缓存一致性)保证了线程安全。一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效
2.2.6.处理器实现原子操作的两种做法
一是总线锁,在多CPU 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据
二是缓存锁,如果共享内存已经被缓存,那么锁总线没有意义。缓存锁核心是使用了缓存一致性协议,如MESI协议。MSEI表示缓存行的四种状态:修改,独占,共享,无效
CPU在读数据时,如果缓存行状态是I,则需要从内存中读取,并把缓存行状态置为S;如果不是I,则可以直接读取缓存中的值,但在此之前必须要等待对其他CPU的监听结果,如果其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存后再读取
3.juc
JUC是 在Java 5.0添加的 java.util.concurrent包的简称,目的就是为了更好的支持高并发任务,
让开发者利用这个包进行的多线程编程时可以有效的减少竞争条件和死锁线程。

3.1 aotmic
这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入
3.1.1 传统问题
我们先来看一个例子:计数器(Counter),采用Java里比较方便的锁机制synchronized关键字,初步的代码如下:
class Counter {
private int value;
public synchronized int getValue() {
return value;
}
public synchronized int increment() {
return ++value;
}
public synchronized int decrement() {
return --value;
}
}
其实像这样的锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,这里会有些问题:锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。而且阻塞时用户态和核心态带来开销。 因此,对于这种需求我们期待一种更合适、更高效的线程安全机制
3.1.2 atomic实现类
atomic的实现依赖于cas

3.2.Lock
3.2.1 synchronized和Lock的区别?
synchronized是java内置的锁,不需要手动释放锁。lock必须手动释放锁。这其实既是优点也是缺点,优点是使用灵活,缺点就是使用不当容易发生死锁。
(1)synchronized 获取锁的线程被阻塞,其它线程就只能一直等待。lock可以实现阻塞一定时间后自动释放trylock(time),或可中断锁lockInterruptibly()
(2)读操作和读操作不发生冲突,用synchronized,两个读操作之间都需要相互等待
3.2.2 Lock接口的方法
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
(1)lock():用来获取锁,如果已被其它线程获取,则进行等待
(2)trylock():尝试获取锁。获取成功返回true,获取失败返回false。(无论如何都会立即返回,拿不到锁时不会一直等待)
(3)tryLock(long time, TimeUnit unit):和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
(4)lockInterruptibly():获取可中断锁。在阻塞的时候调用线程的interrupt()中断线程的等待。(注意:只有等待时可以响应中断)
(5)unlock():释放锁
(6)newCondition():线程之间的交互,synchronized主要通过两个函数完成,Object.wait()和Object.notify()。而Lock通过Condition的await()和signal()实现
3.2.3 ReentrantLock
ReentrantLock可重入的互斥锁,是实现Lock接口的一个类,支持公平锁和非公平锁两种方式
(1).互斥锁
同一时间一个线程能访问同步代码块
(2).公平锁和非公平锁
公平锁在进行lock时,首先会进行tryAcquire()操作。在tryAcquire中,会判断等待队列中是否已经有别的线程在等待了。如果队列中已经有别的线程了,则tryAcquire失败,则将自己加入队列。如果队列中没有别的线程,则进行获取锁的操作。
非公平锁,在进行lock时,会直接尝试进行加锁,如果成功,则获取到锁,如果失败,则进行和公平锁相同的动作。
(区别就是非公平锁在lock的时候直接尝试进行加锁的操作,如果成功,获取到锁,失败则和公平锁一样,放到队列等待)
3.2.4 ReentrantReadWriteLock
ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
1.读写锁怎么实现分别记录读写状态的?
重写AQS中的方法,将同步状态state的拆分为高16位和低16位。低16位写状态用来表示写锁获取的次数。高16位读状态用来表示读锁获取的次数
2.什么情况下能获得读锁?什么情况下获得写锁?
(1)当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态
(2)当写锁被其他线程获取后,读锁获取失败(本线程获取写锁后是可以直接获取读锁),否则获取成功利用CAS更新同步状态
3.锁降级
(1)锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
(2)为什么要把持住当前写锁?保证可见性,如果先释放写锁再获取读锁,中间可能有其它的线程获取写锁并更新数据再释放写锁。那么当前线程无法感知另一个线程的更新
3.3 并发工具类(4个)
3.3.1 countDownLatch
计数器,当当前调用countDownLatch.await()后阻塞,只有countdown的值减为0,该线程才继续执行
(1)和join的区别?
控制力度更细,比如可以在子线程执行一部分后coutdown,就不一定要等到线程执行完成
/**
* 三个子线程执行完后才执行主线程
*
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始执行");
ExecutorService executorService = Executors.newFixedThreadPool(3);
CountDownLatch countDownLatch = new CountDownLatch(3);
executorService.execute(inrunnable(countDownLatch));
executorService.execute(inrunnable(countDownLatch));
executorService.execute(inrunnable(countDownLatch));
countDownLatch.await();
System.out.println("主线程执行完");
}
public static Runnable inrunnable(CountDownLatch countDownLatch){
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"执行结束");
countDownLatch.countDown();
}
};
return runnable;
}
}
3.3.2 CyclicBarrier
1.概念
它有一个barrier的概念,主要用来等待所有的线程都执行完毕,然后再去执行特定的操作
2.示例
/**
* 王者荣耀 10个人全部加载完毕后才能开始游戏
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
int count = 10;
CyclicBarrier cb = new CyclicBarrier(count, new Runnable() {
@Override
public void run() {
System.out.println("全部加载完毕");
}
});
ExecutorService executorService = Executors.newFixedThreadPool(count);
for (int x = 0; x < count; x++) {
executorService.execute(new Worker(cb));
}
}
}
class Worker extends Thread {
CyclicBarrier cyclicBarrier;
public Worker(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 已加载完");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
2.CountDownLatch和CyclicBarrier区别:
(1)countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
(2)CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用
3.3.3 Semaphore
1.概念
Semaphore(信号量):是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。
使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数
2.重要的两个方法:
(1)void acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
(2)void release():释放一个许可,将其返回给信号量
3.实例
/**
* 40个线程读取文件数据入库
* 但是数据库连接只有10个,所以需要做限制
*/
public class SemaphoreDemo {
private static final int COUNT = 40;
private static Executor executor = Executors.newFixedThreadPool(COUNT);
private static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) {
for (int i=0; i< COUNT; i++) {
executor.execute(new Task(semaphore));
}
}
}
class Task implements Runnable {
Semaphore semaphore;
public Task(Semaphore semaphore){
this.semaphore = semaphore;
}
@Override
public void run() {
try {
//读取文件操作
semaphore.acquire();
// 存数据过程
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
3.3.4 Exechanger
- 概念
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的
2.实例 —— 交换零食和钱
/**
* 卖东西的人需要零食换钱
* 等到买东西的人来了才进行交易
*/
public class ExchangerDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final Exchanger exchanger = new Exchanger();
service.execute(new Runnable() {
@Override
public void run() {
try{
String data1 = "零食";
System.out.println("线程"+Thread.currentThread().getName()+
"正在把数据 "+data1+" 换出去");
Thread.sleep((long)Math.random()*10000);
String data2 = (String)exchanger.exchange(data1);
System.out.println("线程 "+Thread.currentThread().getName()+
"换回的数据为 "+data2);
}catch(Exception e){
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
try{
String data1 = "钱";
System.out.println("线程"+Thread.currentThread().getName()+
"正在把数据 "+data1+" 交换出去");
Thread.sleep((long)(Math.random()*10000));
String data2 =(String)exchanger.exchange(data1);
System.out.println("线程 "+Thread.currentThread().getName()+
"交换回来的数据是: "+data2);
}catch(Exception e){
e.printStackTrace();
}
}
});
}
}
3.4 线程池

3.4.1.线程状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
目的是将线程状态和线程个数合二为一,这样可以用一次cas原子操作进行赋值

3.4.2.构造参数
(1)corePoolSize:核心线程池的大小,如果核心线程池有空闲位置,新的任务就会被核心线程池新建一个线程执行,执行完毕后不会销毁线程,线程会进入缓存队列等待再次被运行。
(2)workQueue:缓存队列,用来存放等待被执行的任务。
(3)maximunPoolSize:线程池能创建最大的线程数量。如果核心线程池和缓存队列都已经满了,新的任务进来就会创建救急线程来执行。但是数量不能超过maximunPoolSize,否侧会采取拒绝接受任务策略,我们下面会具体分析。
(4)keepAliveTime:救急线程线程能够空闲的最长时间,超过时间,线程终止。这个参数默认只有在线程数量超过核心线程池大小时才会起作用。只要线程数量不超过核心线程大小,就不会起作用。
(5)threadFactory:线程工厂,用来创建线程
(6)handler:拒绝策略
①abortPolicy:抛出异常(默认)
②discardPolicy:放弃本次任务
③discardoldestPolicy:放弃队列中最早的任务,本任务取代
④callerrunPolicy:让调用者运行任务
3.4.3.实现方式
Executors.newFixedThreadPool Executors.newCachedThreadPool Executors.newSingleThreadExecutor
(1)newFixedThreadPool
核心线程数=最大线程数(没有救急线程,因此也无需超时时间)
阻塞队列是无界的,可以放任意数量任务
(2)newCachedThreadPool:corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大
队列采用SynchronousQueue。它没有容量(这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程)
(3)newSingleThreadExecutor
线程固定为1,其它任务来时放进无界队列等待
阿里巴巴推荐使用:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则(自己定义参数),规避资源耗尽的风险。
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
3.4.4.常用方法
//执行任务
void execute(Runnable command);
//提交任务(有返回值)
submit(Callable task);
//提交任务队列所有任务
invokeAll()
//提交任务队列所有任务,哪个先执行完毕,返回结果,其它任务取消
invokeAny();
3.4.5.线程池原理
如果当前线程池中正在执行的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中正在执行任务的的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理
3.5 并发集合
java包下的线程安全集合:Vector,stack,Hashtable(依靠synchronized,效率低)
collections工具类:eg:Collections.synchronizedList(依靠synchronized,效率低)
juc:并发性能更高的线程安全集合:
3.5.1 List
1.CopyOnWriteArrayList相当于线程安全的ArrayList
(1)原理:运用了一种“写时复制”的思想。通俗的理解就是当我们需要修改(增/删/改)列表中的元素时,不直接进行修改,而是先将列表Copy,然后在新的副本上进行修改,修改完成之后,再将引用从原列表指向新列表。
这样做的好处是读/写是不会冲突的,可以并发进行,读操作还是在原列表,写操作在新列表
(2)缺点:由于CopyOnWriteArrayList使用了“写时复制”,所以在进行写操作的时候,内存里会同时存在两个array数组,如果数组内存占用的太大,那么可能会造成频繁GC,所以CopyOnWriteArrayList并不适合大数据量的场景
3.5.2 Set
CopyOnWriteArraySet:相当于线程安全的 HashSet,但是性能优于 HashSet
ConcurrentSkipListSet:相当于线程安全的TreeSet基于 跳表实现
3.5.3 Map
ConcurrentHashMap是线程安全的哈希表,相当于线程安全的HashMap
ConcurrentSkipListMap是线程安全的有序的哈希表,相当于线程安全的TreeMap
(1)原理:1.8之前使用分段锁,1.8之后数组+链表+红黑树。cas+synchronized
3.5.4 Queue
ArrayBlockingQueue:是一个由基于数组的、线程安全的阻塞队列
LinkedBlockingQueue:是一个基于链表的阻塞队列。
LinkedBlockingDeque:是一个基于链表的双端阻塞队列。
ConcurrentLinkedQueue:是一个基于单向链表的、无界的队列。
ConcurrentLinkedDeque:是一个基于双向链表的、无界的队列。
4.synchronized
4.1作用
synchronized用对象锁保证了临界区代码的原子性
4.2三种应用方式
1.普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2.静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3.同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

浙公网安备 33010602011771号