目录
1 线程的优雅关闭
1.1 stop()和destory函数不能关闭线程
1.2 线程分为守护线程和非守护线程
1.3 设置状态标志位
2 InterruptedException()函数与interrupt()函数
2.1 什么情况下会抛出Interrupted异常
2.2 轻量级阻塞与重量级阻塞
3 synchronized关键字
3.1 锁的对象是什么
3.2 锁的本质是什么
3.3 锁的实现原理
3.4 锁的升级过程
4 wait()与notify()
4.1 生产者-消费者模型是常见的多线程编程模型,要实现这个模型,需要做三件事:
4.2 为什么必须和synchronized一起使用
4.3 为什么wait()必须释放锁
4.4 wait()与notify()问题
5 volatile 关键字
5.1 64位写入的原子性(Half Write)
5.2 内存可见性
5.3 重排序
6 JMM与happen-before
6.1 为什么会存在“内存可见性”问题
6.2 重排序与内存可见性的关系
6.3 as-if-serial语义
6.4 happen-before 是什么
6.5 happen-before 具有传递性
6.6 C++中volatile关键字
7 内存屏障
7.1 Linux中的内存屏障
7.2 JDK中的内存屏障
7.3 volatile实现原理
8 final关键字
1.9 综合应用:无锁编程
1 线程的优雅关闭
1.1 stop()和destory函数不能关闭线程
1.2 线程分为守护线程和非守护线程
守护线程:①.在main()函数执行完成后守护线程自动退出;②.守护线程不影响jvm虚拟机的退出
需要在t.start()前面添加t.setDeamon(true)方式设置为守护线程
典型案例:gc回收线程就是守护线程。
非守护线程:在main()函数执行完成后不退出,只有自己线程执行完成才退出;②.非守护线程必须全部退出了jvm虚拟机才退出。
1.3 设置状态标志位
缺点:当线程阻塞在某个地方,可能一直调用不到状态标志位,即没办法退出线程。
2 InterruptedException()函数与interrupt()函数
2.1 什么情况下会抛出Interrupted异常
只有声明了会抛出InterruptedException的函数才会抛出异常:join(),wait(),sleep();注意:LockSupport.park()/unpark()不会抛出异常
public static native void sleep(long millis) throws InterruptedException(...)
public final void wait() throws InterruptedException(...)
public final void join() throws InterruptedException(...)
2.2 轻量级阻塞与重量级阻塞
轻量级阻塞:能被中断的阻塞
重量级阻塞:不能被中断的阻塞
一个线程完整的状态迁移过程:
t.interrupted()精确含义是“唤醒轻量级阻塞”
3 synchronized关键字
3.1 锁的对象是什么
synchronized关键字其实是“给某个对象加了把锁”
class A {
public void synchronized f1(){...}
public static void synchronized f2(){...}
}
相当于下面代码
class A {
public void f1(){
synchronized(this){...}
}
public void f2(){
synchronized(A.class){...}
}
}
间接地回答了静态锁和非静态锁肯定是不互斥的,因为不是一把锁。
3.2 锁的本质是什么
锁的本质其实是一个“对象”,这个对象要完成以下几件事:
①.这个对象要有一个状态标志位(state变量),记录自己有没有被某个线程占用。
②.如果被某个线程占用要记录这个线程的thread ID。
③.这个对象还得维护一个thread id list,记录其他阻塞的、等待拿这个锁的线程。
3.3 锁的实现原理
monitor对象和Mark word共同实现的
monitor对象:理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象,持有monitor对象则持有锁(指令级别的)
在java对象头中有一块数据较Mark Word,Mark Word中有两个重要的字段:锁的标志位和偏向锁占用该锁的thread id
3.4 锁的升级过程
java 6 之后版本:无锁/偏向锁--------->轻量级锁-------->重量级锁
synchronized实现原理详细见:Synchronized实现原理_lzcWHUT的博客
synchronized和ReentrantLock区别:ReentrantLock和Synchronized的区别和原理_大萝北的博客
4 wait()与notify()
4.1 生产者-消费者模型是常见的多线程编程模型,要实现这个模型,需要做三件事:
1.内存队列本身要加锁。
2.阻塞。当内存队列满了,生产者阻塞。当内存队列为空时,消费者阻塞。
3.双向通知。消费者被阻塞后,生产者放入数据要通知消费者。生产者被阻塞后,消费者拿出数据要通知生产者。
如何阻塞:①.线程自己阻塞自己。②.用阻塞队列。
如何双向通知:①.wait()与notify()机制。②.Condition机制
4.2 为什么必须和synchronized一起使用
两个线程之间要进行通信,对同一个对象来说,该对象本身就需要同步。
synchronized是作用于对象,wait()与notify()是通过对象调用的。
4.3 为什么wait()必须释放锁
因为不释放锁一直不能退出synchronized代码块,其他线程无法进入代码块,肯定不会收到notify通知,所以会死锁。
所以wait()的流程是先释放锁然后阻塞,等待其他线程notify()通知。然后重新拿锁。
4.4 wait()与notify()问题
生产者想通知消费者,但是他把其他的生产者也通知了,消费者想通知生产者,但是他把其他的消费者也通知了。原因是他们都作用于对象,没有按照线程区分。这也是后面Condition要解决的问题。
5 volatile 关键字
5.1 64位写入的原子性(Half Write)
在jvm的规范中没有要求64位long或者double的写入是原子的。所以在32位机器中写入可能是有问题的。解决方案是在long前面加上volatile。
5.2 内存可见性
写完之后立即对其他线程可见。加上volatile关键字的变量即具有内存可见性。
5.3 重排序
DCL问题
常见单例模式DCL写法如下:
public class Sington{
private static Sington instance;
private static Sington getInstance(){
if(instance==null){
synchronized(Sington.class){
if(instance==null)
instance=new Instance();//有问题代码
}
}
}
}
上述的instance = new Instance();分成三个步骤:
①.分配一块内存。
②.在内存上初始化对象。
③.把instance引用指向当前对象。
在上面三个步骤中:②③可能重排序。解决办法就是为instance加上volatile修饰符。
综上所述:volatile三大作用:64位写入原子性,内存可见性,禁止重排序。
6 JMM与happen-before
6.1 为什么会存在“内存可见性”问题
1.以下是一个x86架构的cpu缓存的布局,由于缓存一致性协议L1,L2和L3不会出现内存不一致现象
2.由于缓存一致性对性能有很大损耗,又加了很多优化,例如在计算单元和L1之间加Store Buffer、Load Buffer,如下:
3.如上图Store Buffer、Load Buffer和L1之间是异步的。通俗的说,在往内存写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入L1中,同时写入主内存中。站在操作系统内核的角度,可以统一看成如下所示的操作系统内核视角下的cpu缓存模型。本地缓存和主缓存不完全一致。
4.对应到java里,就有了如下所示的java抽象模型:
6.2 重排序与内存可见性的关系
重排序的一个分类:
①.编译器重排序。对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序
②.cpu指令重排序。在指令级别,让没有依赖关系的多条指令并行。
③.cpu内存重排序。cpu有自己的缓存,指令执行的顺序与写入主内存的顺序不完全一致。
第三种是造成内存可见性的主要原因。
6.3 as-if-serial语义
1.单线程重排序不会改变执行结果,这就是as-if-serial语义
2.多线程编译器和cpu无法完全理解数据之间的依赖性,所以多线程不能保证as-if-serial语义
6.4 happen-before 是什么
为了明确定义在多线程场景下,什么时候可以重排序,什么时候不能重排序,Java引入了JMM(Java Memory Model),也就是Java内存模型。这个模型就是一个规范,为了描述这个规范,JMM引入了happen-before。
happen-before:如果A happen-before B,意味着A的执行结果必须对B可见。
例如:①.单线程每个操作,happen-before 对应线程中任意后续操作。
②.对volatile变量的写入,happen-before对应后续对这个变量的读取。
③.对synchronized的解锁,happen-before对应后续对这个锁的加锁。
6.5 happen-before 具有传递性
6.6 C++中volatile关键字
C++中volatile关键字不遵循happen-before,会重排序,与Java不同。
7 内存屏障
为了禁止编译器重排序和cpu重排序,在编译器和cpu层面都有对应的指令,也就是内存屏障,这也正是JMM和happen-before规则的底层实现原理。
7.1 Linux中的内存屏障
见22页
7.2 JDK中的内存屏障
在理论上可以把基本的cpu内存屏障分成四种:
①.LoadLoad:禁止读与读的重排序。
②.StoreStore:禁止写与写的重排序。
③.LoadStore:禁止读与写的重排序。
④.StoreLoad:禁止写与读的重排序。
java 8 在Unsafe类提供三个内存屏障函数。
①.loadFence:LoadLoad+LoadStore
②.storeFence:StoreStore+LoadStore
③.fullFence:loadFence+storeFence+StoreStore
7.3 volatile实现原理
在x86平台上,在volatile写操作后面加上StoreLoad屏障,其他cpu架构做法不一。
volatile更多介绍:android 进阶之光 第四章 多线程编程_龚礼鹏的博客
volatile实现原理剖析:volatile底层原理详解 - 知乎
8 final关键字
对于只初始化一次的变量,可以用final关键字,因为final也遵循happen-before协议
对于final变量的写或者读,happen-before于final域对象的读。
1.9 综合应用:无锁编程
1.9.1 一写一读的无锁队列:内存屏障
1.9.2 一写多读的无锁队列:volatile关键字
1.9.3 多写多读的无锁队列:CAS
1.9.4 无锁栈:比无锁队列更简单,只需要对head指针进行CAS操作。
1.9.5 无锁链表:无锁链表复杂很多,因为无锁链表需要在中间插入和删除元素。ConcurrentSkipListMap实现就是基于无锁链表。