多线程原理

多线程原理

1.线程状态

6个状态定义:java.lang.Thread.State

  1. New:尚未启动的线程的线程状态;
  2. Runnable:可运行线程的线程状态.等待CPU调度;
  3. Blocked:线程阻塞等待监视器锁定的线程状态.处于synchronized同步代码块中或方法中被阻塞;
  4. Waiting:等待线程的线程状态.下列不带超时的方式:Object.wait,Thread.join,LockSupport.park;
  5. Timed Waiting:具有指定等待时间的等待线程的线程状态.下列带超时的方式:Thread.sleep,Object.wait,Thread.join,LockSupport.parkNanos,LockSupport.parkUntil;
  6. Terminated:终止线程的线程状态.线程正常完成执行或者出现异常;

多线程原理

2.线程中止

2.1.不正确的线程中止-stop

stop:中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议使用;

estory:JDK未实现该方法;

2.2.正确的线程中止-interrupt

如果目标线程在调用Object class的wait(),wait(long)或wait(long,int)方法,join(),join(long,int)方法时被阻塞,那么interrupt会生效,该线程的中断状态将被清除,抛出InterrptedException异常;

如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被终端或者返回特殊异常值.达到终止线程的目的;

如果以上条件都不满足,则会设置此线程的中断状态;

2.3.正确的线程中止-标志位

代码逻辑中,增加一个判断,用来控制线程执行的中止;

多线程原理

3.内存屏障和CPU缓存

3.1.CPU性能优化手段-缓存

为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化;

例如:CPU高速缓存,尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能.

多线程原理

多级缓存

  • L1 Cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存.一般服务器的CPU的L1缓存容量通常在32-4096kb.
  • L2 由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存储器,即二级缓存.
  • L3 现在的都是内置的,而它的实际作用即是,L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能.具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度.一般是多核共享一个L3缓存.

CPU在读取数据时,现从L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外存储器.

3.1.1.缓存同步协议

多CPU读取同样的数据进行缓存,进行不同运算之后,最终写入主内存以哪个CPU为准?

在这种高速缓存回写的场景下,有一个缓存一致性协议多数CPU厂商对它进行了实现.

MESI协议,它规定每条缓存有个状态为,同时定义了下面四个状态:

修改态(Modified):此cache行已被修改过(脏行),内容已不同于主存,为此cache专有;

专有态(Exclusive):此cache行内容同于主存,但不出现于其它cache中;

共享态(Shared):此cache行内容同于主存,但也出现于其它cache中;

无效态(Invalid):此cache行内容无效(空行);

多处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU;也就意味着,CPU处理要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致;

3.2.CPU性能优化手段-运行时指令重排

多线程原理

指令重排的场景:当CPU写缓存时发现缓存区正在被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行.

并非随便重排,需要遵循as-if-serial语义.

as-if-serial语义的意思是指,不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变.编译器,runtime和处理器必须遵循as-if-serial语义,也就是说:编译器和处理器不会对存在数据依赖关系的操作做重排序.

3.3.引发的问题

1.CPU高速缓存下有一个问题:

缓存中的数据与主内存的数据并不是实时同步的,各个CPU核心之间缓存的数据也不是实时同步.在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的.

2.CPU执行指令重排序优化下有一个问题:

虽然遵守了as-if-serial语义,单仅在单CPU自己执行的情况下能保证结果正确.多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误.

3.4.内存屏障

处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述两个问题:

**写内存屏障(Store Memory Barrier)**在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见.强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排.

**读内存屏障(Load Memory Barrier)**在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存中加载数据.强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题.

4.线程通信

4.1.通信方式

要想实现多个线程之间的协同,如:线程执行先后顺序,获取某个线程执行的结果等等.涉及到线程之间相互通信,分为下面四类:

  1. 文件共享;
  2. 网络共享;
  3. 变量共享;
  4. jdk提供的线程协调API(wait/notify,park/unpark)

4.1.1.文件共享

多线程原理

4.1.2.变量共享

多线程原理

4.1.3.线程协作-JDK API

JDK对于需要多线程协作完成某一任务的场景,提供了对应API的支持.多线程协作的典型场景是;生产者 - 消费者模型(线程阻塞,线程唤醒);

多线程原理

4.1.3.1.API-被弃用的suspend和resume

调用suspend挂起目标线程,通过resume可以恢复线程执行.

多线程原理

被弃用的主要原因是,容易写出死锁代码.所以用wait/notify和park/unpark机制对它进行替代.

  • 在同步代码块中使用容易产生死锁(suspend不会释放锁);
  • suspend比resume后执行容易产生死锁
4.1.3.2.API-wait/notify机制

这些方法只能由同一对象锁的持有者线程调用,也就是写在同步块里面,否则会抛出illegalMonitorStateException异常;

wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁.wait/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程.

虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于waiting状态.

多线程原理

如上图标记,synchronized的锁对象和调用wait的对象必须是同一个对象

4.1.3.3.park/unpark机制

线程调用park则等待"许可",unpark则为指定线程提供"许可";

不要求park和unpark方法的调用顺序.多次调用unpark之后,再调用park,线程会直接运行.但不会叠加,也就是说,连续多次调用park方法,第一次会拿到许可直接运行,后续调用会进入等待.

多线程原理

需要注意的是LockSupport.unpark(XXXXThread)方法中要指定unpark的线程.

多线程原理

如上是死锁的park/unpark代码

4.1.3.4.伪唤醒

之前的包子店的代码中用if语句来判断,是否进入等待状态,是错误的.官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出.

伪唤醒是指线程并非因为notify,notifyAll,unpark等api调用而唤醒,是更底层原因导致的.

多线程原理

5.线程封闭

5.1.线程封闭概念

多线程访问共享可变数据时,涉及到线程间数据同步的问题.并不是所有时候,都要用到共享数据,所以线程封闭概念就提出来了.

数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭.

线程封闭具体的体现有:ThreadLocal,局部变量.

5.1.1.ThreadLocal

ThreadLocal是Java里一种特殊的变量.

它是一个线程级别的变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,在并发模式下是绝对安全的变量.

用法:ThreadLocal<T> var = new ThreadLocal<T>()会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响.可以用ThreadLocal存储一些参数,以便在线程中多个方法使用,用来代替方法传参的做法.

多线程原理

多线程原理

我们可以理解为:JVM维护了一个Map<Thread,T>,每个线程要用这个T的时候,用当前的线程去Map里面取.仅仅作为概念理解.

5.1.2.栈封闭

局部变量的固有属性之一就是封闭在线程中.它们位于执行线程的栈中,其他线程无法访问这个栈.

6.线程池原理

6.1.线程池原理-概念

  1. 线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
  2. 工作线程:线程池中线程,在没有任务时处于等待状态;
  3. 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列:用于存放没有处理的任务,提供一种缓冲机制;

多线程原理

6.2.线程池API-接口定义和实现类

多线程原理

6.3.线程池API-方法定义

  1. ExecutorService的API

多线程原理

  1. ScheduledExecutorService

多线程原理