目录

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不会出现内存不一致现象

第一章 多线程基础_java_02

2.由于缓存一致性对性能有很大损耗,又加了很多优化,例如在计算单元和L1之间加Store Buffer、Load Buffer,如下:

第一章 多线程基础_java_03

3.如上图Store Buffer、Load Buffer和L1之间是异步的。通俗的说,在往内存写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入L1中,同时写入主内存中。站在操作系统内核的角度,可以统一看成如下所示的操作系统内核视角下的cpu缓存模型。本地缓存和主缓存不完全一致。

第一章 多线程基础_守护线程_04

4.对应到java里,就有了如下所示的java抽象模型:

第一章 多线程基础_jvm_05

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 具有传递性

第一章 多线程基础_java_06

第一章 多线程基础_jvm_07

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实现就是基于无锁链表。