多线程基础

多线程基础

阿里巴巴2021版JDK源码笔记(2月第三版).pdf

链接:https://pan.baidu.com/s/1XhVcfbGTpU83snOZVu8AXg
提取码:l3gy

1. 线程的优雅关闭

1. 1运行中的线程能否强制杀死?

不能,如果强制 杀死线程,则线程中所使用的资源,例如文件描述符、网络连接等不能正常关闭。

一个线程一旦运行起来,就不要去强行打断它,合理的关 闭办法是让其运行完(也就是函数执行完毕),干净地释放掉所有资 源,然后退出。如果是一个不断循环运行的线程,就需要用到线程间的通信机制,让主线程通知其退出。

1.2 什么是守护进程?

当在一个JVM进程里面开多个线程时,这些线程被分成两类:守护 线程和非守护线程。默认开的都是非守护线程。在Java中有一个规 定:当所有的非守护线程退出后,整个JVM进程就会退出。意思就是守 护线程“不算作数”,守护线程不影响整个 JVM 进程的退出。例如, 垃圾回收线程就是守护线程,它们在后台默默工作,当开发者的所有前台线程(非守护线程)都退出之后,整个JVM进程就退出了。

1.3 如何优雅关闭线程?

设置关闭的标志位,

2. InterruptedException()与interrupt()函数

2.1 什么时候抛出Interrupted异常

主线程中调用一句t.interrupt(),只有那些声明了会抛出 InterruptedException 的函数才会抛出异常,

2.2 轻量级阻塞和重量级阻塞

能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或 者TIMED_WAITING;而像 synchronized 这种不能被中断的阻塞称为重 量级阻塞,对应的状态是 BLOCKED

2.3t.isInterrupted()与Thread.interrupted()的区别

t.interrupted()相当于给线程发送了一个唤醒的信号, 所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛 出一个InterruptedException,并且线程被唤醒。而如果线程此时并 没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是 否收到过其他线程发来的中断信号,然后做一些对应的处理.

这两个函数都是线程用来判断自己是否收到过中断信号的,前者是非静态函数,后者是静态函数。二者的区别在于,前者只是读取中 断状态,不修改状态;后者不仅读取中断状态,还会重置中断标志位。

3. synchronized

3.1 锁的对象是什么?

synchronized关键字其实 是“给某个对象加了把锁”,这个锁究竟加在了什么对象上面?对于非静态成员函数,锁其实是加在实例对象a上面的;对于静态成员函数,锁是加在A.class上面的。当然,class本身也是对象。

一个静态成员函 数和一个非静态成员函数,都加了synchronized关键字,分别被两个线程调用,它们是否互斥?很显然,因为是两把不同的锁,所以不会互斥

3.2 锁的本质是什么

多个线程要访问同一个资源。线程就是一段段运行 的代码;资源就是一个变量、一个对象或一个文件等;而锁就是要实 现线程对资源的访问控制,保证同一时间只能有一个线程去访问某一 个资源。

3.3 synchronized实现原理

在对象头里,有一块数据叫Mark Word。 在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字 段:锁标志位和占用该锁的thread ID。

在这个基本的思路之上,synchronized还会有偏向、自旋等优化策略,ReentrantLock同样会用到这些优化策略

4. wait()与notify()

4.1 生产者消费者模式

一个内存队列,多个生产者线程往内存队列中放数据;多个消费 者线程从内存队列中取数据。

  • 内存队列本身要加锁,才能实现线程安全。
  • 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
  • 双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。

4.2 如何阻塞?

  1. 线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。
  2. 用一个阻塞队列,当取不到或者放不进去数据的时候,入 队/出队函数本身就是阻塞的。这也就是BlockingQueue的实现,后面会详细讲述。

4.3 如何双向通知?

  1. wait()与notify()机制。
  2. Condition机制。

4.3 为什么必须和synchronized一起使用

在调用wait()、notify()之前,要先通过synchronized关键字同步给对象,也就是给该对象加锁。

synchronized关键字可以加在任何对象的成员函 数上面,任何对象都可能成为锁。那么,wait()和notify()要同样如此普及,也只能放在Object里面了。

4.4 为什么wait()的时候必须释放锁

只有如此,才能避免死锁问题

4.5 wait()与notify()的问题

生产者本来只想通知消费者,但它把其他的生产者也通知了;消 费者本来只想通知生产者,但它被其他的消费者通知了。原因就是wait()和notify()所作用的对象和synchronized所作用的对象是同一 个,只能有一个对象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题。

5. volatile关键字

5.1 64位写入的原子性(Half Write)

举一个简单的例子,对于一个long型变量的赋值和取值操作而 言,在多线程场景下,线程A调用set(100),线程B调用get(),在某些场景下,返回值可能不是100。

因为JVM的规范并没有要求64位的long或者double的写入是原子的。在32位的机器上,一个64位变量的写入可能被拆 分成两个32位的写操作来执行。这样一来,读取的线程就可能读到 “一半的值”。解决办法也很简单,在long前面加上volatile关键字。

5.2 内存可见性

不仅64位,32位或者位数更小的赋值和取值操作,其实也有问 题。

“内存可见性”,指的是“写完之后立即对其 他线程可见”,它的反面不是“不可见”,而是“稍后才能可见”。解决这个问题很容易,给变量加上volatile关键字即可。

5.3 重排序:DCL问题

DCL(Double Checking Locking)

volatile的三重功效:64位写入的 原子性、内存可见性和禁止重排序。

6. JMM与happen-before

6.1 内存可见性问题产生的原因

因为存在CPU缓存一致性协议,例如MESI,多个CPU之间的缓存不会出现不同步的问题,不会有“内存可见性”问题。

缓存一致性协议对性能有很大损耗,为了解决这个问题,CPU 的设计者们在这个基础上又进行了各种优化。

L1、L2、L3和主内存之间是同步的,有缓存一致性协议的保证, 但是Store Buffer、Load Buffer和L1之间却是异步的。也就是说,往 内存中写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入L1中,同时同步写入主内存中

多CPU,每个CPU多核,每个核上面可能还有多个硬件线程,对于 操作系统来讲,就相当于一个个的逻辑CPU。每个逻辑CPU都有自己的缓存,这些缓存和主内存之间不是完全同步的。

6.2 重排序和内存可见性的关系

Store Buffer的延迟写入是重排序的一种,称为内存重排序(Memory Ordering)。除此之外,还有编译器和CPU的指令重排序。下面对重排序做一个分类:

  • 编译器重排序。对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
  • CPU指令重排序。在指令级别,让没有依赖关系的多条指令并行。
  • CPU内存重排序。CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。

6.3 as-if-serial语义

对开发者而言,当然希望不要有任何的重排序,这样理解起来最 简单,指令执行顺序和代码顺序严格一致,写内存的顺序也严格地和 代码顺序一致。但是,从编译器和CPU的角度来看,希望尽最大可能进 行重排序,提升运行效率。

重排序的场景:

  • 单线程程序的重排序规则:站在编译器和CPU的角度来说,不管怎么重排序, 单线程程序的执行结果不能改变
  • 多线程程序的重排序规则:编译器和CPU只能保证每个线程的as-if-serial语义。但多个线程会互相读取和写入共享的变量,对于这种相互影响,编译器和 CPU 不会考虑。

6.4 happen-before是什么

JMM(Java Memory Model)Java内 存模型(单线程场景不用说明,有as-if-serial语义保证)对上,是JVM和开发者之间的协定;对下,是JVM和编译器、CPU之间的协定。

定义这套规范,其实是要在开发者写程序的方便性和系统运行的 效率之间找到一个平衡点。一方面,要让编译器和CPU可以灵活地重排 序;另一方面,要对开发者做一些承诺,明确告知开发者不需要感知 什么样的重排序,需要感知什么样的重排序。然后,根据需要决定这 种重排序对程序是否有影响。如果有影响,就需要开发者显示地通过v为了描述这个规范,JMM引入了happen-before,使用happen-befo

为了描述这个规范,JMM引入了happen-before,使用happen-before描述两个操作之间的内存可见性。那么,happen-before是什么呢?

happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见

JMM对开发者做出了一系列=承诺:

  • 单线程中的每个操作,happen-before 对应该线程中任意后续操作(也就是 as-if-serial语义保证)。
  • 对volatile变量的写入,happen-before对应后续对这个变量的读取
  • 对synchronized的解锁,happen-before对应后续对这个锁的加锁

6.5 happen-before的传递性

除了这些基本的happen-before规则,happen-before还具有传递 性,即若A happen-before B,B happen-before C,则A happen-before C。

7. 内存屏障

为了禁止编译器重排序和 CPU 重排序,在编译器和 CPU 层面都 有对应的指令,也就是内存屏障(Memory Barrier)。这也正是JMM和happen-before规则的底层实现原理。

编译器的内存屏障,只是为了告诉编译器不要对指令进行重排 序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。

而CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。下面主要讲CPU的内存屏障。

7.1 Linux中的内存屏障

这是一个RingBuffer,允许一个线程写入,一个线程读取(只能 一写一读),整个代码没加任何的锁,也没有CAS,但是线程是安全的,

7.2 JDK中的内存屏障

把基本的CPU内存屏障分成四种

  • LoadLoad:禁止读和读的重排序。
  • StoreStore:禁止写和写的重排序。
  • LoadStore:禁止读和写的重排序。
  • StoreLoad:禁止写和读的重排序

JDK内存屏障unsafe

  • loadFence=LoadLoad+LoadStore
  • storeFence=StoreStore+LoadStore
  • fullFence=loadFence+storeFence+StoreLoad

7.3 volatile实现原理

  • 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  • 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  • 在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。

8.final关键字

8.1 构造函数溢出问题

一个对象的构造并不是“原 子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。

8.2 final的happen-before语义

final关键字也有相应的happen-before语义

对final域的写(构造函数内部),happen-before于后续对final域所在对象的读。

对final域所在对象的读,happen-before于后续对final域的读

8.3 happen-before规则总结

  • 单线程中的每个操作,happen-before于该线程中任意后续操作
  • 对volatile变量的写,happen-before于后续对这个变量的读
  • 对synchronized的解锁,happen-before于后续对这个锁的加锁。
  • 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。

9. 综合应用:无锁编程

9.1 一写一读的无锁队列:内存屏障

一写一读的无锁队列即Linux内核的kfifo队列,一写一读两个线程,不需要锁,只需要内存屏障

9.2 一写多读的无锁队列:volatile关键字

9.3 多写多读的无锁队列:CAS

同内存屏障一样,CAS(Compare And Set)也是CPU提供的一种原子指令。

基于CAS和链表,可以实现一个多写多读的队列。具体来说,就是 链表有一个头指针head和尾指针tail。入队列,通过对tail进行CAS操作完成;出队列,对head进行CAS操作完成

9.4 无锁栈

无锁栈比无锁队列的实现更简单,只需要对 head 指针进行 CAS操纵,就能实现多线程的入栈和出栈。

9.5 无锁链表

相比无锁队列与无锁栈,无锁链表要复杂得多,因为无锁链表要在中间插入和删除元素。

原文地址:https://www.cnblogs.com/steven158/p/15012924.html