并发编程
java并发编程

1.基础知识
同步VS异步
同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调
用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面
的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员
跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用
了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知
去取就好。
并发与并行
并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进
行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切
换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这
个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相
反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界
区资源被一个线程占有,那么其他线程必须等待
进程和线程
1.区别
进程之间共享信息可通过TCP/IP协议,线程间共享信息可通过共用内存
进程是资源分配的最小单位,线程是CPU调度的最小单位
线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
线程没有自己独立的地址空间,多进程的程序比多线程的程序健壮
进程的切换比线程的切换开销大
2.进程的通信方式
管道 FIFO(命名管道) 信号量 共享内存 消息对列
进程和线程的关系
Java对操作系统提供的功能进行封装,包括进程和线程
运行一个程序会产生一个进程,进程包含至少一个线程
每个进程对应一个JVM实例,多个线程共享JVM里的堆
Java采用单线程编程模型,程序会自动创建主线程
主线程可以创建子线程,原则上要后于子线程完成执行
多线程的六种状态

NEW 新建
Runnable (包括Running和Ready)
Blocked 阻塞
Waiting 等待(死等 不见不散)
Timed_Waiting (过时不候)
Terminated 终止
多线程的实现方法
1.继承THread
2.实现Runnable
3.callable
4.线程池
Callable具体实现

适配器模式
RunnableFuture是Runnable的子接口
RunnableFuture的实现类是FutureTask,所以FutureTask实现了Runnable
而FutureTask的参数可以是Callable
//callable 带参数 有返回值
class Mythread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("here!");
return 999;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask=new FutureTask<>(new Mythread());
new Thread(futureTask,"AA").start();
//此时BB不会运算 因为多个线程抢占一个futureTask 计算结果只会一次 除非有多个futureTask
new Thread(futureTask,"BB").start();
// while (!futureTask.isDone()){
//
// }
//get一般放在main线程后面 因为会造成阻塞 或者使用上面的while 当线程完成再进行后续操作
int res=futureTask.get();
System.out.println(res);
}
}
Callable和Runable区别
1.Callable有参数 返回值 Runable无
2.Callable 会抛异常 这样便于找出每个异常的错误
3.接口实现的方法不一样 Runable是run(),Callable是call()
4.Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程池
为啥使用 优点

基本使用demo
public static void ThreadPoolDemo1() {
// 以下三种常用线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);//固定量的线程池
//ExecutorService executorService = Executors.newSingleThreadPool();//一池处理一个线程
//ExecutorService executorService = Executors.newCachedThreadPool();//可缓存变线程池
//ExecutorService executorService =Executors.newScheduledThreadPool(int corePoolSize)//创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务
try{
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t do something!");
});
}
}catch(Exception e){
e.printStackTrace();
}finally{
executorService.shutdown();
}
}

线程池工作流程



七大参数详解
这里有疑惑的话先看上面的线程池工作流程

maximmumPoolSize合理配置
要根据程序而定:
IO密集型:
1.

2.

CPU密集型:

手写线程池
注意:大坑 因为线程池的workQueue默认使用的是LinkedBlockingQueue,下面是它的源码:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);//这个数是21亿多 实际上根本不可能 这样直接就崩了
}
如上,所以实际生产中,我们三个线程池都不用,自己手写,先看来看一下线程池的底层源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到底层真正实现是ThreadPoolExecutor,再看看它的源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
自己手写的demo:
//手写线程池
ExecutorService executorService=new ThreadPoolExecutor(
3,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
拒绝策略( Rejected Policy)

AbortPolicy()); 超过最大容量+阻塞队列数目就会报错
CallerRunsPolicy();超过就回退给父线程 大多为main
DiscardOldestPolicy();超过丢掉等待最久的
DiscardPolicy();超过直接丢弃
死锁
两个或两个以上进程间持有自己的资源却又请求对方的资源造成的,若无外力作用则无法推动下去
代码实现:
class ThreadLock implements Runnable{
private String lockA;
private String lockB;
public ThreadLock(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+":owned "+lockA +",but try to get "+lockB);
//sleep是为了两个线程能分别获得锁 免得一个线程全都获得了
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+":owned "+lockB +",but try to get "+lockA);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
//传参数顺序不一样 刚好改变两个锁的顺序 互相持有
new Thread(new ThreadLock(lockA, lockB),"ThreadAAA").start();
new Thread(new ThreadLock(lockB, lockA),"ThreadBBB").start();
}
}
死锁排查及的定位:
程序不停前提下,用jps -l查看 ,找到程序对应的进程号pid,然后用jstack +pid 就能看到问题所在

2.并发理论(JMM)
抽象模型

如图为JMM抽象示意图,线程A和线程B之间要完成通信的话,要经历如下两步:
- 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;
- 线程B从主存中读取最新的共享变量
特性:可见性、有序性、原子性
1.可见性
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程
的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证
a操作将对b操作可见)
2.有序性
计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种
编译器优化的重排
指令并行的重排
内存系统的重排
对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也
不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给
人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进
行指令重排,重排后的指令与原指令的顺序未必一致
3.原子性
指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
happens-before规则
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第
一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before
关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么
这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面的1)是JMM对程序员的承诺
上面的2)是JMM对编译器和处理器重排序的约束原则
哪些指令不能重排:Happen-Before 规则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的start()方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行,结束先于finalize() 方法
对上面的具体的六项规则描述:
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
JMM设计:

JMM是语言级的内存模型,在我的理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重
排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控
制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员能够迅速高效率的进行并发
编程。站在JMM设计者的角度,在设计JMM时需要考虑两个关键因素:
程序员对内存模型的使用
程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
编译器和处理器对内存模型的实现
编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编
译器和处理器希望实现一个弱内存模型。
另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序可以分为两类:
会改变程序执行结果的重排序。
不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略,如下。
对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种
重排序)
JMM的设计图为:
3.并发关键字
(1)Synchrnized关键字

高内聚低耦合
线程 操作 资源类
八锁理论总结:静态同步方法锁类,非静态锁对象
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
import java.util.concurrent.TimeUnit;
/**
* @author: lanvce$
* @date: 2020/3/27$ 下午3:50$
* Description:
**/
class Phone { //Phone.class
public synchronized void SendSMS() {
System.out.println("发了短信");
}
public static synchronized void SendMail() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发了邮件");
}
public void hello(){
System.out.println("hello");
}
}
//1.标准访问,发短信、发邮件的打印顺序 Email,SMS
//2.发邮件线程中执行时睡眠4秒,发短信、发邮件的打印顺序 Email,SMS
//3.新增hello普通方法,hello,发邮件的打印顺序 hello,Email
//4.两部手机,先短信还是先邮件 SMS,Email
//5.2个静态同步方法,一部手机,先短信还是先邮件 Email,SMS
//6.2个静态同步方法,2部手机,先短信还是先邮件 Email,SMS
//7.一个静态同步方法,一个普通同步方法,一手机,先短信还是先邮件 SMS,Email
//8.一个静态同步方法,一个普通同步方法,2手机,先短信还是先邮件 SMS,Email
/**
* 1和2说明:synchronized方法锁的是当前对象 this。多个同步方法被调用只能等待先被调用的同步方法
* 释放锁之后才能继续执行
* 2和3说明:普通方法不参与锁的竞争 普通方法相当于手机壳
* 4说明:不同对象的同步方法调用,不竞争(非同一把锁)
* 5、6说明:同步方法被静态关键字修饰时,对象锁变成了class字节码的锁,只要是这一个字节码的实例
* 对象,
* 不管是几个对象,全部参与竞争锁。锁的不是手机 而是手机模板
* new this 具体的一部手机
* static 静态 锁唯一的手机模板
*7说明:一个实例的非静态同步方法和静态同步方法获得的是不同的锁,不产生竞争
* 非静态锁的对象 静态锁的是class类,手机模板锁了 但是手机依然可用
* 8说明:一个实例的非静态同步方法获得锁之后,本实例的其他非静态同步方法必须等待,
* 但是其他实例的非静态同步方法与他不是同一把锁,所有没有竞争条件,不必等待获取锁。
*/
public class LockDemo04 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() ->{
try {
phone.SendMail();
// phone.SendSMS();;
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() ->{
try {
phone2.SendSMS();
// phone.SendSMS();
// phone.hello();
}catch (Exception e){
e.printStackTrace();
}
},"B").start();
// new Thread(() -> {
// try{
// phone.hello();
// }catch(Exception e){
// e.printStackTrace();
// }
// },"").start();
}
}
synchronized底层
① synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
用javap -v SynchronizedDemo.class查看字节码文件:

首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized
进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,
否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。
执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重
入性,即在同一锁程中,线程不需要再次获取同一把锁。
② synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
锁获取和锁释放的内存语义
基于java内存抽象模型的Synchronized的内存语义。

线程A写共享变量
从上图可以看出,线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到自己的本地内
存,进行加一操作后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相
对应的内存语义。

线程B读共享变量
线程B获取锁的时候同样会从主内存中共享变量a的值,这个时候就是最新的值1,然后将该值拷贝到线程B
的工作内存中去,释放锁的时候同样会重写到主内存中。
从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主
内存中,其他线程获取锁时会强制从主内存中获取最新的值。另外也验证了2 happens-before 5,2的执行
结果对5是可见的。
从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在
为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。
CAS操作
1.什么是CAS?
使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获
取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假
设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因
此,线程就不会出现阻塞停顿的状态。
那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出
现冲突,出现冲突就重试当前操作,直到没有冲突为止。
2.CAS的操作过程(Compare and Swap)
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预
期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被
其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即
可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会
重新尝试,当然也可以选择挂起线程.CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处
理器提供的CMPXCHG指令实现。
3.CAS与Synchronized区别
元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带
来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。
4.CAS的问题
1.ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变
成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。
2.自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如
果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一
定的提升。
3. 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子
性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这
个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。
Java对象锁
同步(Synchrized)的时候是获取对象的monitor,即获取到对象的锁
无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark
Word里默认的存放的对象的Hashcode,分代年龄和锁标记位
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量
级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁
后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

锁升级的图示过程:

偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
1.偏向锁的获取
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在
进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否
存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一
下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;
如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程
2.偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程
才会释放锁。
如图,偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥
有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无
锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头
的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停
的线程。
下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。

3.如何关闭偏向锁
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用
JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下
处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻
量级锁状态
轻量级锁
1.加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
2.解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有
竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,
就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁
的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
各种锁的比较

(2)volatile
1.简介
了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是
java虚拟机提供的最轻量级的同步机制
2.特性
1.可见性
2.禁止指令重排序
注意:不保证原子性
3.volatile实现原理
在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令(具体的大家可以
使用一些工具去看一下,这里我就只把结果说出来)。我们想这个Lock指令肯定有神奇的地方,那么Lock
前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:
1. 将当前处理器缓存行的数据写回系统内存;
2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
结论:
1. Lock前缀的指令会引起处理器缓存写回内存;
2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
4.volatile的内存语义
假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。

当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。

了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发
送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去取啦。
5.volatile的内存语义实现
JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。
JMM内存屏障分为四类见下图:

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了
实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile
重排序规则表:

"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存
屏障来禁止特定类型的**处理器重排序**。对于编译器来说,发现一个最优布置来最小化插入屏障的总数
几乎是不可能的,为此,JMM采取了保守策略:
1. 在每个volatile写操作的**前面**插入一个StoreStore屏障;
2. 在每个volatile写操作的**后面**插入一个StoreLoad屏障;
3. 在每个volatile读操作的**后面**插入一个LoadLoad屏障;
4. 在每个volatile读操作的**后面**插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面**分别插入内存屏障**,而volatile读操作是在**后面插入两个内
存屏障**
**StoreStore屏障**:禁止上面的普通写和下面的volatile写重排序;
**StoreLoad屏障**:防止上面的volatile写与下面可能有的volatile读/写重排序
**LoadLoad屏障**:禁止下面所有的普通读操作和上面的volatile读重排序
**LoadStore屏障**:禁止下面所有的普通写操作和上面的volatile读重排序
下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。

(3)多线程中final
final域重排序规则
1.final域为基本类型
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
假设线程A在执行writer()方法,线程B执行reader()方法。
写final域重排序规则
写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
JMM禁止编译器把final域的写重排序到构造函数之外;
编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看这篇文
章)。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情:
1.构造了一个FinalDemo对象;
2.把这个对象赋值给成员变量finalDemo。

由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读
到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会
禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后
的值。
因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始
化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对finalDemo。
读final域重排序规则
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这
两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个
LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这
两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
read()方法主要包含了三个操作:
1.初次读引用变量finalDemo;
2.初次读引用变量finalDemo的普通域a;
3.初次读引用变量finalDemo的final与b;

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通
域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引
用,从而就可以避免这种情况。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的
引用。
2.final域为引用类型
对final修饰的对象的成员域写操作
针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修
饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个
操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使
用。这句话是比较拗口的,下面结合实例来看。
public class FinalReferenceDemo {
final int[] arrays;
private FinalReferenceDemo finalReferenceDemo;
public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
}
public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
}
public void writerTwo() {
arrays[0] = 2; //4
}
public void reader() {
if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执
行reader方法。

写final修饰引用类型数据可能的执行时序
由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员
域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。
对final修饰的对象的成员域读操作
JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而
写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C
之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。
关于final重排序的总结
按照final修饰的数据类型分类:
基本数据类型:
1.final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象
对所有线程可见时,该对象的final域全部已经初始化过。
2.final域读:禁止初次读对象的引用与包含final域变量的读对象的重排序。
引用数据类型:
额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用
赋值给引用变量 重排序
3.final的实现原理
上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读
final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会
对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也
就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理
器
4.为什么final引用不能从构造函数中“溢出”
这里还有一个比较有意思的问题:上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该
对象的final域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:在构造函数,
不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“逸出”。以下面的例子
来说:
public class FinalReferenceEscapeDemo {
private final int a;
private FinalReferenceEscapeDemo referenceDemo;
public FinalReferenceEscapeDemo() {
a = 1; //1
referenceDemo = this; //2
}
public void writer() {
new FinalReferenceEscapeDemo();
}
public void reader() {
if (referenceDemo != null) { //3
int temp = referenceDemo.a; //4
}
}
}

假设一个线程A执行writer方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖
性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当
线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,
其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。
4.Lock体系
(1)初识Lock与AQS(AbstractQueuedSynchronizer)
队列同步器AbstractQueuedSynchronizer(简称同步器)
1.concurrent包的结构层次


2.Lock
API:
void lock(); //获取锁
void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断
boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未
中断的情况下能够获取锁
Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进
行等待时会先释放锁,当再次获取锁时才能从等待中返回
基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承
AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器
AbstractQueuedSynchronizer(简称同步器)的理解。
3.初识AQS
同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通
过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,
其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个
方法。
同步器和锁的区别:
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是
面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作

浙公网安备 33010602011771号