《深入浅出Java多线程》阅读笔记

1、进程与线程的基本概念

  • 进程:就是应用程序在内存中分配的空间,也就是正在运行的程序。各个进程之间互不干扰。
  • 程序:用某种编程语言编写,能够完成一定任务或功能的代码集合,是指令和数据的有序集合,静态代码。
  • 时间片轮转:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。
    进程让操作系统的并发性成为了可能,线程让建成的内部并发成了可能。
  • 使用多线程的好处:进程的通信比较复杂,而线程间的通信比较简单;进程是重量级的,线程是轻量级的,多线程方式的系统开销更小
  • 进程和线程的区别:
    进程是一个独立的运行环境,线程是在进程中执行的一个任务。
    本质区别 是否单独占有内存地址空间及其他系统资源。【进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位】
    进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;线程共享所属进程占有的内存空间地址和资源,数据共享简单,但是同步复杂;
    进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性低。
    进程的创建和销毁需要保存寄存器和栈信息,需要资源的分配回收和页调度,开销比较大;线程只需要保存寄存器和栈信息 开销比较小。
  • 上下文切换:指CPU从一个进程或线程切换到另一进程或线程。上下文:某一时间点CPU寄存器和程序计数器的内容。
    寄存器是cpu内部的少量的速度很快的闪存;
  • 程序计数器是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存的值为正在执行的指令的位置或下一个将要被执行的指令的位置。
    CPU为每个线程分配CPU时间片来实现多线程机制。

2、Java多线程入门类和接口

  • 如何实现多线程:
    继承Thread类,并重写run方法
    程序中调用start()方法后,虚拟机会先创建一个线程,等这个线程得到时间片后再调用run()方法;
  • 注意:不可以多次调用start()方法,第一次调用后,在此调用会抛出异常
    实现Runable接口的run方法
    Thread类的构造方法-原理:
    片段1-私有init(…)方法
    参数g:线程组,这个线程是在那个线程组下;参数target:要执行的任务;参数name:线程的名字;参数acc:片段3使用;
    参数inheritThreadLocals:片段4使用
    片段2-构造函数中调用init方法
    片段3-使用在init方法里初始化AccessControlContext类型的私有属性
    片段4-两个用于支持ThreadLocal的私有属性
    Thread类的几个常用方法:
    currentThread():静态方法 返回对当前正在执行的线程对象的引用;
    start():开始执行线程,jvm会调用线程内的run方法;
    yield():当前线程愿意让出CPU的占用。就算让出了,程序在调度的时候,也还有可能继续运行中这个线程;
    sleep():
    join():使当前线程等待另一线程执行完毕之后再继续执行,内部调用的Object类的wait方法实现的。
    两者的比较:
    Java单继承 多实现,Runable接口使用起来比Thread类灵活;
    Runable接口更符合面向对象,将线程单独进行对象的封装,降低了线程对象和线程任务的耦合性。
    优先使用 实现Runable接口这种方法
    run方法没有返回值
    Callable接口:
    有返回值,支持泛型;一般配合线程池ExecutorService使用。
    ExecutorService可以使用submit方法来让一个Callable接口执行,它会返回一个Future,可以通过这个Future的get方法得到结果
    Future接口:
    cancel方法:试图取消一个线程的执行。并不一定能取消成功。
    FutureTask类:
    FutureTask实现RunableFuture接口,RunableFuture接口同时继承了Runable接口和Future接口。
    Future接口中的cancel get isDone方法自己实现起来比较复杂。JDK提供了FutureTask类供使用。
    几个状态:NEW, COMPLETING, NORMAL, EXCEPTIONAL, CANCELLED, INTERRUPTING, INTERRUPTED

3、线程组和线程优先级

ThreadGroup:如果在new Thread时没有显式指定线程组,那么默认将父线程的线程组设置为自己的线程组。
线程组管理它下面的Thread,是一个标准的向下引用的树状结构,这样设计的原因是防止上级线程被下级线程引用而无法有效的被GC回收。
线程优先级:默认5,setPriority()函数可以指定;只是一个建议,OS并不一定采纳。真正的调用顺序,是由OS决定的。
线程调度器来监视和控制RUNABLE状态的线程。抢占式策略;同优先级先到先得。每个java程序都一个默认的主线程(jvm启动的第一个线程main线程)。
守护线程:默认的优先级比较低。如果某线程是守护线程,那么所有的非守护线程结束,这个守护线程也会自动结束。
线程组的优先级和线程的优先级不一样的时候:如果某线程的优先级大于所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
线程组的常用方法:
Thread.currentThread().getThreadGroup().getName()//获取当前线程组的名字
//复制一个线程数组 到 一个线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
TheadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);
线程组是一个树状结构,每个线程组还可以包含其他的线程组,而不仅仅是线程。线程组可以起到统一控制线程的优先级和检查线程权限的作用。
线程组的成员变量:

线程组的构造函数:
两个public构造函数;两个私有构造函数。

4、Java线程的状态和主要转化方法

操作系统线程的状态:就绪-执行-等待
Java线程的6个状态:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
NEW:尚未启动,还没有调用start方法。
源码解析:threadStatus变量用来记录线程的状态;内部调用stat0方法。
在调用一次start方法后,threadStatus的值会改变(!=0),此时再次调用start方法会抛出异常信息。比如threadStatus=2表示当前线程为TERMINATED
RUNNABLE:可能在jvm中运行,也可能在等待其他系统资源。包含了操作系统线程的ready和running两个状态
BLOCKED:线程正在等待锁的释放以进入同步区。
WAITING:处于等待状态的线程编程RUNNABLE状态需要其他线程唤醒;
LockSupport.park();除非获得调用许可,否则禁用当前线程进行线程调度。
TIMED_WAITING:
TERMINATED:线程执行完毕
线程间的状态切换:

sleep不会释放锁,wait会释放锁。
线程中断:通过中断操作并不能直接终止一个线程,而是通知被中断的线程自行处理。
Thread.interrupt():中断线程,设置线程的中断状态为true 默认是false
Thread.interrupted():测试当前线程是否被中断。调用一袭使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新变为false;
Thread.isInterrupted():测试当前线程是否被中断。这个方法并不会影响线程的中断状态。

5、Java线程间的通信

线程同步是线程之间按照一定的顺序执行。一个锁同一时间只能被一个线程持有。
等待和通知机制:wait、notify、notifyALl
信号量:Semaphore类;基于volatile关键字自己实现的信号量通信。
volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程中改变这个变量的值,那其他线程是立刻可见更改后的值的。
管道:PiperWriter、PipeReader、PipedOutputStream、PipedInputStream。前两个基于字符的,后面两个基于字节流。
其他通信相关:join方法;sleep方法;ThreadLocal类
sleep方法和wait方法的区别:
wait方法可以指定时间,也可以不指定;sleep必须指定时间
wait释放cpu资源 同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
wait必须放在同步块或同步方法中,而sleep可以在任意位置。

6、Java内存模型基础知识

两个问题:线程之间如何通信?线程之间如何同步?
两种并发模型:消息传递模型、共享内存模型(Java中使用的)
运行时内存划分:方法区、堆;虚拟机栈(局部变量、方法参数、异常处理器参数)、本地方法栈、程序计数器。
问题:既然堆是共享的,为什么在堆中会有内存不可见问题?
现代计算机为了高效,往往会在高速缓存区中缓存共享变量,因为cpu访问缓存区比访问内存要快的多。线程之间的共享变量存在煮内存中,每个线程都有一个私有的本地内存,存储了该线程以读写共享变量的副本。
JMM定义了线程和主内存之间的抽象关系
线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。
JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序。,synchronized关键字不仅可以保证可见性,同时也保证了原子性。在底层,JMM通过内存屏障来时间内存的可见性和禁止指令重排序。
JMM和java运行时内存区域的划分?
区别:两者是不同层次的概念。JMM是抽象的,描述一组规则,控制各个变量的访问方式,围绕原子性,有序性,可见性等展开。而java运行时内存的划分是具体的,是JVM运行java程序时,必要的内存划分。
联系:都存在私有数据区域和共享数据区域。一般来说,JMM主内存属于共享数据区域,包含了堆和方法区。JMM的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。

7、重排序和hapeens-before

指令重排对于提高CPU处理性能十分必要,由此带来了乱序的问题。指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义一致。
指令重排序有:编译器优化重排;指令并行重排;内存系统重排。
数据竞争与顺序一致性。
顺序一致性模型
一个线程中的所有操作必须按照程序的顺序来执行;
不管程序是逗同步,所有线程只能看到一个单一的操作执行顺序。每个操作必须是原子性的,且立刻对所有线程可见。
JMM中同步程序的顺序一致性效果和JMM中未同步程序的顺序一致性效果。
未同步程序在JMM和顺序一致性内存模型中的执行特性有如下差异:
顺序一致性保证单线程内的操作会按照程序的顺序执行;JMM不保证单线程内操作会按程序的顺序执行。
顺序一致性模型保证所有线程只能看到一致性的操作执行顺序,而JMM不保证所有线程能看到一致性的操作执行顺序。
JMM不保证对64位的long和doube变量的写操作具有原子性,而顺序一致性模型保证对所有内存读写操作都具有原子性。
happen-before原则:
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在hapeens-before关系,并不意味着java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按照happend-before关系执行的结果一致,那么JMM也允许这样的重排序。
如果操作A happens-before操作B,那么操作A在内存上的所有的操作对操作B都是可见的,不管他们在不在一个线程。
天然的happens-before原则:
程序顺序规则;监视器锁规则;volatile变量规则;传递性;start规则;join规则

8、volatile关键字

内存可见性:主内存;工作内存;指的是线程之间的可见性,当一个线程修改了共享变量时,另一线程可以读取到这个修改后的值。
重排序:优化程序性能。编译重排序,CPU重排序。
happens-before规则:
volatile的内存语义:保证变量的内存可见性;禁止volatile变量和普通变量重排序。
内存可见性:指的是当一个线程对volatile修饰的变量进行写操作时,JMM会立即把该线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile修饰变量进行读操作时,JMM会把该线程对应的本地内存置为无效,从主内存中读取共享变量的值。
volatile变量的写和锁的释放相似;读和锁的获取相似。
JMM是如何限制处理器的重排序呢?
内存屏障:硬件层面上分读屏障和写屏障
作用:禁止屏障两侧的指令重排序;强制把写缓冲区/高速缓冲区中的脏数据等写回主内存,或者让缓存中相应的数据失效。
内存屏障插入策略:(Load代表读操作,Store代表写操作)
在每个volatile写操作前插入一个StoreStore屏障
在每个volatile写操作后插入一个StoreLoad屏障
在每个volatile读操作后插入一个LoadLoad屏障
在每个volatile读操作后插入一个LoadStore屏障

在保证内存可见性上,volatile与锁有相同的内存语义,所以可以作为以恶搞轻量级锁来使用。但由于volatile仅仅保证对单个volatile变量的读写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。所以在功能上 锁比volatile更强大;在性能上 volatile更有优势。

9、synchronized与锁

关键字在实例方法上,锁为当前实例;关键字在静态方法上,锁为当前class对象;关键字在代码上,锁为括号里面的对象。
临界区:指的是某一代码区域,它同时只能由一个线程执行。
几种锁:无锁状态;偏向锁状态;轻量级锁状态;重量级锁状态。
几种锁会随着竞争情况的逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在STW期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后降级。
Java的锁都是基于对象的。每个Java对象都有对象头,如果是非数组类型,则用2字节来存储对象头,如果是数组类型,则会用3字节来存储对象头。

Mark World:
锁状态;29bit 或 61bit;1bit是否是偏向锁?;2bit锁标志位
无锁;-;0;01;
偏向锁;线程ID;1;01;
轻量级锁;指向栈中锁记录的指针;此时这一位不用来标识偏向锁;00
重量级锁;指向互斥量的指针;同上;10
偏向锁:偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要出发同步。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做,提高程序的运行性能。(对锁置个变量,如果发现为true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为false,代表其他线程竞争资源,那么就会走后面的流程。)
实现原理:
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程ID。当下次该线程进入这个同步块时,会检查锁的Mark Word里面是不是放的自己的线程ID。
如果是,表明该线程已经获得了锁,以后该线程再进入和退出同步块时不需要花费CAS操作来加锁和解锁;
如果不是,就代表另一个xc来竞争这个偏向锁。这时候会尝试使用CAS来替换mark word里面的线程ID,分两种情况:
一,成功,表示之前的线程不存在了,Mark word里面的线程ID为新线程的ID,锁不会升级,仍为偏向锁;
二,失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标志为0,锁标志位为00,升级为轻量级锁。会按照轻量级锁的方式进行竞争锁。
偏向锁升级轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,大致过程如下:
在一个安全点停止拥有锁的线程;
遍历线程栈,如果存在锁记录的话,虚呀修复锁记录和mark word,使其变成无锁状态;
唤醒被停止的线程,将当前锁升级为轻量级锁。
轻量级锁-加锁:JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,称为displaced mark word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的mark word复制到自己的displaced mark word里面。然后尝试使用CAS将锁的mark word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示mark word已经被替换成了其他线程的锁记录,说明在与其他线程竞争锁,当前线程就尝试使用自旋来获取锁。自旋失败,那么这个线程会阻塞,同时这个锁会升级成重量级锁。
轻量级锁-释放:当前线程会尝试使用CAS操作将displaced mark word的内容复制回锁的mark word里面。如果没有发生竞争,那么这个复制的操作成功。如果有其他线程因为自旋多次导致轻量级锁升级成重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
重量级锁:依赖于操作系统的互斥量实现。
每个对象都可以当作一个锁。当多个线程同时请求某个对象锁时,对象锁会设置几种状态来区分请求的线程:
Contention List:所有请求锁的线程将被首先放置到该竞争队列;
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List;
Wait Set:那些调用wait方法被阻塞的线程将被放置到Wait set;
OnDeck:任何时候最锁只有一个线程正在竞争该锁,该线程成为OnDck;
Owner:获得锁的线程成为Owner;
!Owner:释放锁的线程
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到Contention List的队列的队首,然后调用park函数挂起当前线程。
当线程释放锁时,会从Contention List或Entry List中唤醒一个线程,被唤醒的线程叫做假定继承人,假定继承人被唤醒后尝试获得锁。因为synchronized是非公平的,所以假定继承人不一定能获得锁。因为重量级锁,线程先自旋尝试获得锁,减少执行操作系统同步操作带来的开销。
如果获得锁后调用object.wait方法,则会将线程加入到waitset中,当被notify唤醒后,会将线程从wait set移动到contention lsit或 entry list中。
总结锁的升级流程:
每个线程在准备获取共享资源时:
第一步:检查mark word里面是不是放的自己的threadID,如果是,表示当前线程处于偏向锁。
第二步:如果mark word不是自己的thread ID 锁升级。这时候使用CAS来执行切换,新的线程跟进mark word里面现有的thread iD,通知之前线程暂停,之前线程的mark word的内容置空。
第三步:两个线程都把对象的hashcode复制到自己新建的用于存储锁的记录空间,接着开始用过CAS操作,把锁对象的mark word的内容修改为自己新建的记录空间的地址的方式竞争markword。
第四步:第三步中成功执行CAS的获得资源,失败的则进入自旋。
第五步:自旋的线程在自旋过程中,成功获得资源,则整个状态依旧处于轻量级锁的状态。
第六步:进入重量级锁状态,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
各种锁的优缺点比较:
偏向锁:优点-加锁和解锁不要额外的消耗,和执行非同步方法比仅存在纳秒级的差距;缺点-如果线程间存在锁竞争,会带来额外的锁撤销的消耗;适用场景:只有一个线程访问同步块场景。
轻量级锁:优点-竞争的线程不会阻塞,提高了程序的响应速度;缺点-如果一直得不到锁,竞争的线程使用自旋会消耗CPU;适用场景-追求响应时间。同步块执行速度非常快。
重量级锁:优点-线程竞争不使用自旋,不会消耗CPU;缺点-线程阻塞,响应时间慢;适用场景-追求吞吐量,同步块执行速度较快。

10、乐观锁和悲观锁

悲观锁:总是认为每次访问共享资源时会发生冲突,所以必须对数据加上锁,以保证临界区的程序同一时间只有一个线程正在执行;
乐观锁:假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁也无需等待。一旦发生冲突,乐观锁通常使用CAS来保证线程执行的安全性。
乐观锁多用于读多写少,避免频繁加锁影响性能;悲观锁多用于写多读少的环境,避免频繁失败和重试影响性能。
CAS概念:比较并交换。V 要更新的变量;E 预期值;N 新值。判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其他线程更新了V,则当前线程放弃更新。什么都不做。
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
CAS原理:
Unsafe类:native方法
boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
原子操作类:java.util.concurrent.atomic
原子更新基本类型
原子更新数组
原子更新引用
原子更新字段(属性
AtomicInteger类的getAndAdd(int delta)方法是调用Unsafe类的方法来实现的
weakCompareAndSet操作仅保留了volatile自身变量的特性,而出去了happens-before规则带来的内存语义。也就是说,weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。这在一定程度上可以提高性能。
CAS实现原子操作的三大问题:
ABA问题:就是一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。解决思路是在变量前面追加上版本号或者时间戳。
循环时间长开销大:CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源。解决思路是让JVM支持处理器提供的pause指令。
只能保证一个共享变量的原子操作:使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作; 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

11、AQS

AbstractQueuedSynchronizer 抽象队列同步器:
抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
队列:使用先进先出(FIFO)队列存储数据;
同步:实现了同步的功能。
AQS有什么用呢?AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器,只要之类实现它的几个protected方法就可以了。
AQS内部数据结构:一个volatile的变量state来作为资源的标识;几个获取和改版state的protected方法,子类可以覆盖这些方法来实现自己的逻辑(getState,getState,compareAndSetState);内部使用了一个先进先出(FIFO)的双端队列,并使用了两个指针head和tail用于标识队列的头部和尾部(并不是直接储存线程,而是储存拥有线程的Node节点)。
资源有两种共享模式,或者说两种同步方式:
独占模式(Exclusive):资源是独占的,一次只能一个线程获取。如ReentrantLock。
共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。
Node数据结构:
SHARED:标记一个结点(对应的线程)在共享模式下等待
EXCLUSIVE:标记一个结点(对应的线程)在独占模式下等待
prev:前驱节点
next:后继节点
thread; // 结点对应的线程
waitStatus的值:CANCELLED = 1; SIGNAL = -1; CONDITION = -2; PROPAGATE = -3;
AQS的设计是基于模板方法模式的,它有一些方法必须要子类去实现的,它们主要有:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
源码分析
获取资源的入口是acquire(int arg)方法。arg是要获取的资源的个数,在独占模式下始终为1。
首先调用tryAcquire(arg)尝试去获取资源
如果获取资源失败,就通过addWaiter(Node.EXCLUSIVE)方法把这个线程插入到等待队列中。其中传入的参数代表要插入的Node是独占式的。

12、线程池原理

使用线程池的原因:创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)可以对线程做统一管理。
Java中的线程池顶层接口是Executor接口,ThreadPoolExecutor是这个接口的实现类。
ThreadPoolExecutor构造函数核心参数:
int corePoolSize:该线程池中核心线程数最大值
核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
int maximumPoolSize:该线程池中线程总数最大值 。
该值等于核心线程数量 + 非核心线程数量。
long keepAliveTime:非核心线程闲置超时时长。
非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
TimeUnit unit:keepAliveTime的单位。
BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
LinkedBlockingQueue链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
ArrayBlockingQueue数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
SynchronousQueue同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
DelayQueue 延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
ThreadFactory threadFactory 创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
RejectedExecutionHandler handler 拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略:
ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出
RejectedExecutionException异常。ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
ThreadPoolExecutor的状态:类中定义了一个volatile int变量runState来表示线程池的状态 ,分别为RUNNING、SHURDOWN、STOP、TIDYING 、TERMINATED。
线程池创建后处于RUNNING状态。
调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,会等待阻塞队列的任务完成。
调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。
线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。
线程池主要的任务处理流程:
线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 < corePoolSize时)。注意,这一步需要获得全局锁。
线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。
当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁。
缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。

为什么要二次检查线程池的状态?在多线程的环境下,线程池的状态是时刻发生变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将command加入workqueue是线程池之前的状态。倘若没有二次检查,万一线程池处于非RUNNING状态(在多线程环境下很有可能发生),那么command永远不会执行。
ThreadPoolExecutor如何做到线程复用的?
ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组中,然后这个worker反复从阻塞队列中拿任务去执行。
首先去执行创建这个worker时就有的任务,当执行完这个任务后,worker的生命周期并没有结束,在while循环中,worker会不断地调用getTask方法从阻塞队列中获取任务然后调用task.run()执行任务,从而达到复用线程的目的。只要getTask方法不返回null,此线程就不会退出。
四种常见的线程池:
newCachedThreadPool
提交任务进线程池。
因为corePoolSize为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。
尝试将任务添加到SynchronousQueue队列。
如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。
如果SynchronousQueue已有任务在等待,入列操作将会阻塞。
newFixedThreadPool
核心线程数量和总线程数量相等,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程。因为LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,故如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。
与CachedThreadPool的区别: 因为 corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。 在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool会在60s后收回。 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多。 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。
newSingleThreadExecutor
有且仅有一个核心线程( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。

13、阻塞队列

BlockingQueue一般用于生产者-消费者模式。BlockingQueue就是存放元素的容器。。你只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。
BlockingQueue的操作方法:

BlockingQueue的实现类:
LinkedBlockingQueue链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
ArrayBlockingQueue数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
SynchronousQueue同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
DelayQueue 延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
PriorityBlockingQueue:基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是公平锁。
阻塞队列的原理:利用了Lock锁的多条件(Condition)阻塞控制
首先是构造函数,除了初始化队列的大小和是否是公平锁之外,还对同一个锁(lock)初始化了两个监视器,分别是notEmpty和notFull。作用为标记分组,当该线程是put操作时,给他加上监视器notFull,标记这个线程是一个生产者;当线程是take操作时,给他加上监视器notEmpty,标记这个线程是消费者。
put函数流程:
所有执行put操作的线程竞争lock锁,拿到了lock锁的线程进入下一步,没有拿到lock锁的线程自旋竞争锁。
判断阻塞队列是否满了,如果满了,则调用await方法阻塞这个线程,并标记为notFull(生产者)线程,同时释放lock锁,等待被消费者线程唤醒。
如果没有满,则调用enqueue方法将元素put进阻塞队列。注意这一步的线程还有一种情况是第二步中阻塞的线程被唤醒且又拿到了lock锁的线程。
唤醒一个标记为notEmpty(消费者)的线程。
take操作的流程:
所有执行take操作的线程竞争lock锁,拿到了lock锁的线程进入下一步,没有拿到lock锁的线程自旋竞争锁。
判断阻塞队列是否为空,如果是空,则调用await方法阻塞这个线程,并标记为notEmpty(消费者)线程,同时释放lock锁,等待被生产者线程唤醒。
如果没有空,则调用dequeue方法。注意这一步的线程还有一种情况是第二步中阻塞的线程被唤醒且又拿到了lock锁的线程。
唤醒一个标记为notFull(生产者)的线程。

14、锁接口和类

synchronized的不足之处:如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。synchronized无法知道线程有没有成功获取到锁使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
锁的几种分类:
可重入锁和非可重入锁:前者支持一个线程对资源重复加锁。synchronized是重入锁。如果我们自己在继承AQS实现同步器的时候,没有考虑到占有锁的线程再次获取锁的场景,可能会导致线程阻塞,那这个就是一个非可重入锁。
公平锁和非公平锁:如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。
读写锁和排它锁:synchronized用的锁和ReentrantLock,其实都是“排它锁”。ReentrantReadWriteLock是读写锁,即使用读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。
java.util.concurrent.locks包下,还为我们提供了几个关于锁的类和接口:
抽象类AQS/AQLS/AOS:
接口Condition/Lock/ReadWriteLock
Condition和Object的wait/notify基本相似。其中,Condition的await方法对应的是Object的wait方法,而Condition的signal/signalAll方法则对应Object的notify/notifyAll()
ReentrantLock是一个非抽象类。从源码上看,它内部有一个抽象类Sync,是继承了AQS,自己实现的一个同步器。
ReentrantLock内部有两个非抽象类NonfairSync和FairSync,它们都继承了Sync。从名字上看得出,分别是”非公平同步器“和”公平同步器“的意思。这意味着ReentrantLock可以支持”公平锁“和”非公平锁“。通过看着两个同步器的源码可以发现,它们的实现都是”独占“的。都调用了AOS的setExclusiveOwnerThread方法,所以ReentrantLock的锁的”独占“的,也就是说,它的锁都是”排他锁“,不能共享。在ReentrantLock的构造方法里,可以传入一个boolean类型的参数,来指定它是否是一个公平锁,默认情况下是非公平的。这个参数一旦实例化后就不能修改,只能通过isFair()方法来查看。
ReentrantReadWriteLock这个类也是一个非抽象类,它是ReadWriteLock接口的JDK默认实现。它与ReentrantLock的功能类似,同样是可重入的,支持非公平锁和公平锁。不同的是,它还支持”读写锁“。
同样是内部维护了两个同步器。且维护了两个Lock的实现类ReadLock和WriteLock。从源码可以发现,这两个内部类用的是外部类的同步器。ReentrantReadWriteLock实现了读写锁,但它有一个小弊端,就是在“写”操作的时候,其它线程不能写也不能读。我们称这种现象为“写饥饿”,将在后文的StampedLock类继续讨论这个问题。
StampedLock:java 1-8之后才有的。
它没有实现Lock接口和ReadWriteLock接口,但它其实是实现了“读写锁”的功能,并且性能比ReentrantReadWriteLock更高。StampedLock还把读锁分为了“乐观读锁”和“悲观读锁”两种。前面提到了ReentrantReadWriteLock会发生“写饥饿”的现象,但StampedLock不会。它是怎么做到的呢?它的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。这种模式也就是典型的无锁编程思想,和CAS自旋的思想一样。这种操作方式决定了StampedLock在读线程非常多而写线程非常少的场景下非常适用,同时还避免了写饥饿情况的发生。
StampedLock用这个long类型的变量的前7位(LG_READERS)来表示读锁,每获取一个悲观读锁,就加1(RUNIT),每释放一个悲观读锁,就减1。而悲观读锁最多只能装128个(7位限制),很容易溢出,所以用一个int类型的变量来存储溢出的悲观读锁。
写锁用state变量剩下的位来表示,每次获取一个写锁,就加0000 1000 0000(WBIT)。需要注意的是,写锁在释放的时候,并不是减WBIT,而是再加WBIT。这是为了让每次写锁都留下痕迹,解决CAS中的ABA问题,也为乐观锁检查变化validate方法提供基础。

15、并发容器集合

ConcurrentHashMap类:
提供的优点是:在并发环境下将实现更高的吞吐量,而在单线程环境下只损失非常小的性能。
可以这样理解分段锁,就是将数据分段,对每一段数据分配一把锁。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,HashEntry则用于存储键值对数据。
一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构(同HashMap一样,它也会在长度达到8的时候转化为红黑树)的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

16、CopyOnWrite容器

17、通信工具类

链接可以看看,有代码的实例。详细的也可以看并发编程之美的笔记📖《Java并发编程之美》部分

18、Fork/Join框架

19、Java8 Stream并行计算原理

20、计划任务

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值