Java并发
牛客的课,整理一下笔记
基础
栈是线程私有的,有多少线程就有多少栈
这个意思就是说,操作系统执行指令是原子的,但是高级语言中的指令不一定是原子的,例如n++这个指令,在操作系统中会被拆成多条指令执行
如果不加锁的话,两个结果可能都是1,所以n++不是原子的
两个线程同步对变量X进行更改,因为一个线程更改以后另一个不会立刻看到,会造成可见性问题
CPU操作系统的机制会对程序的运行顺序进行重排,原则是保证单线程的正常运行,但是如这个例子,正常是先创建单例对象再赋值,但是这里CPU调整成先赋值,再创建单例对象,这样就造成了在并发执行时,线程B可能会返回一个空的instance的情况,导致后面的代码运行出错
JMM
通信是隐式的,同步是显式的,需要自己在代码中处理
JMM是JVM的一块,是一个抽象概念,它控制线程A什么时候写出主内存,线程B什么时候读入
主内存也是JVM的一块
JMM从内存可见性的角度,能够解决可见性问题;内存屏障,解决重排序的问题
这里的问题是,正常来讲先执行A1,再执行A2,但是这里先执行A1是将其写入缓存区A,然后执行A2,由于缓存区没有b这个变量,所以会直接到内存中读取,导致x变量为0,然后执行A3,是A1操作的后续,因此实际的执行顺序变成了A2,A1(A1+A3),因为缓存的存在,内存对操作顺序进行了重排序,导致出现了问题
在编译后的代码里偷偷插入内存屏障
内存可见性:
(这个地方勘误一下,这里应该是ThreadB.start(),即在A线程中启动B线程,所以这个操作happens-before于B线程中的操作,这很好理解了。)
volatile,例如,如果对一个变量加了volatile,对这个变量的写先于这个变量的读
如果是CPU导致的重排序,使用内存屏障解决
JMM解决了编译器的重排序,如果是编译器重排序,使用happens-before解决(也解决了内存可见性的问题)
JAVA线程间的通信由JMM控制,决定了一个线程对共享变量的写入何时对另一个线程可见,用内存屏障(4个操作系统的内存屏障,然后java对其进行组合)来禁止CPU的重排序,对于编译器的重排序,定义了happens-before规则
volatile会直接从内存中读写
锁的内存语义和volatile一致,也是写直接写到主内存中,读也是将本地内存置为无效
JMM是基于内存模型实现通信的,这是前提
锁这个后面说,解决了可见性和有序性,原子性也就自然解决了
原子类
用的比较少,但是需要熟悉
java.util.concurrent,简称JUC,并发包
atomic中的类都是基于这三个CAS方法实现的
这三个CAS方法是原子方法,被编译成了操作系统的原子指令,这个指令在执行的时候是不会被打断的
volatile保证了变量的读写具有原子性
atomic保证了变量的常见操作入,如加减具有原子性
要把n++变成原子性的,可以加锁,更简单的方法就是给变量添加atomic关键字,封装成atomic类型,调用其中的方法实现加的操作
compareAndSwapInt 比较和替换Int,下面类似
方法中第二个变量是long类型,代表的并不是成员变量类型,而是在内存中的偏移量,调用这个方法时,会自动将类中的属性转换成偏移量,var4代表旧值,var5是更新的值
改的时候,会去比较一下,看一看这个变量是不是旧值,不是旧值的话就不更改
适用于并发比较少的情况,失败了循环再来一次
看源码,idea中搜索,按两次shift,输入要搜索的内容,可以勾选include non-project items,把其他类也能搜索到;然后ctrl+f 搜索,或者左边有个structure,看类的整体结构
Unsafe类只提供了原子操作整数的方法,其他类型都是转换成整型进行处理
上面的VALUE是偏移量
addAndGet的意思是增加指定的值返回新值
unsafe类中getAndAdd是返回旧值,然后加上给的值
getAndAdd,返回旧值
getAndIncrement 加一,原子的++,返回旧值
IncrementAndGet,返回新值
compareAndSet,原子替换
unsafe类中的方法
getIntVolatile 是从内存中读取数据
compareAndSwap 比较并替换,
先读取旧值,然后更新,如果更新不成功就继续读取,知道更新成功跳出循环
返回的就是旧值
AtomicBoolean 中的这个方法,原子的替换boolean值,也是先转换成int再替换
什么是ABA问题,例如执行a++,应该读取到是1,然后实际读取到的也是1,但是有可能是先变成2,再变成的1,也就是说看到的是1,认为没有变,实际上是经过变化的
而解决方法就是加上一个版本号,或者加标记
整型的ABA问题没有必要解决,引用类型的ABA问题需要处理
偏移量在静态代码块中做
AtomicReference中常用的方法,就是替换
AtomicStampedReference 带有版本标记的原子更新引用类型
reference是引用
stamp是版本
要替换只有版本和引用都相同才替换
替换方法compareAndSet,返回值这里的意思是,首先要保证原引用和当前引用相同,版本号相同,其次如果新的引用和版本号也相同,就不需要替换了,否则执行casPair方法,对版本号和引用一起替换,也就是替换Pair
另一个带标记的原子更新引用类型,只不过标记是boolean,一次性
原子更新属性用的比较少,了解
AtomicIntegerFieldUpdater 类是抽象的,里面有一个内部类进行了实现
这个类的构造方法前面有protected关键字,说明我们无法用构造方法进行实例化,需要调用下面的方法进行实例化
Reflection.getCallerClass() 获取调用者类,因为传入的类可能是子类,需要取到父类
这里的注解只是一个标识,意思是当获取调用者类的时候,把当前类忽略
下面这个类中,如果传入的属性是protected的,那么就更新子类的属性,否则就更新父类
先检查这个对象的类型
AtomicReferenceFieldUpdater
tclass目标类型,field属性,vclass属性的类型,跟上面的AtomicIntegerFieldUpdater基本一样
应用场景举例,例如有一个Person类,改不了,有一个family的属性,它的类型是Family
如果要对这个属性进行替换,需要先new一个原子更新引用属性这样一个类,然后对这个属性进行替换
这个类用的更少
base数组位置的偏移量,相当于一个指针指向了数组起始位置
静态代码块中,scale是计算数组中某个元素的大小,如果不是2的n次方,报错
shift表示2的多少次方,也就是scale是2的shift次方
为什么要求shift,是为了进行位运算
验证i是否在范围内,如果在,换算成偏移量
数组中元素加
更改数组中的某一个元素
这个比较重要,高并发下CAS除了加锁,也可以用这个类Striped64类
Striped64类
这个解决方法是将Long拆成多份,然后对每一份进行处理,最后加起来,拆分成一个base(基本的值)和多个cell
transient 是不能序列化的意思,因为要将一个数据拆成多份,对每一份序列化没有意义
用cas的方式对base进行累加
狭义的理解为对cell进行原子的累加操作,也可以创建cell等等,细节不看了
子类LongAdder
add方法,用cas的方式,将x累加到了不同的部分
取完整的值 longValue(),将所有的值累加到一起
另一个子类,LongAccumulator
因为计算不一定只是加,可以是别的运算
其中有一个function变量,它的类是Long类型的二元操作,需要自定义操作逻辑,是一个接口
identity是初始值
自定义计算逻辑,二元操作符
这个方法用于变量的计算
锁
synchronized
字宽是指32位,64位
一个对象有对象头和对象体
对象体是存对象的具体业务数据的部分,例如年龄性别等;对象头是存储对象的描述信息,里面存储了很多信息,其中就包括锁
无锁状态标志是01,无偏向标志是0
锁会升级,先会升级成偏向锁,标记也是01,但是有偏向位1,偏向锁其实没有真正加锁,指的是没有竞争。当前只有一个线程在访问加锁的范围(临界区),只是简单记一下线程ID,打个标记,下一次访问的时候检查一下ID是不是这个ID,是的话继续访问。Epoch相当于版本号,代表时间
如果有两个线程并发访问的时候(此时偏向锁标记改变),就会升级为轻量锁,锁加在了线程的栈帧里,Mark Word这里有一个指针指向栈帧,即指向锁,标志位变成00。
轻量级锁加上以后,并不会阻塞,而是在自旋,以原子的方式CAS,去抢锁,如果多轮都抢不到就升级为重量级锁,然后阻塞;重量级锁加上以后就会阻塞
如果竞争加剧,就会升级为重量级锁,状态变为10,就是通常所说的锁
无锁和轻量级锁区别就是,是否是在访问临界区
如果锁要降级的话,需要阻塞当前线程,需要停止了,才能恢复数据
线程2参与竞争后,要原子替换标志位,但是不能替换,因为线程1占用了;但是线程2觉得这样偏向线程1不公平,所以需要撤销偏向锁
在升级为轻量锁前要将偏向锁撤销,撤销流程是由JVM调度的,会让线程1暂停,撤销以后,退回无锁状态,两个线程再抢锁
如果没有竞争的话,执行完直接解锁
要加锁,需要在栈中,创建一个记录锁的空间,将对象头中的mark word复制到锁记录中;
抢锁就是 将自己指向栈的指针写入对象头Mrak Word,代表抢到了锁,当前线程执行
如果线程1要释放锁的时候,因为现在mark word已经改成了重量级锁,所以cas修改mark word失败,会释放锁,然后唤醒等待线程
但是如果是还是轻量级锁,就会直接释放,不用唤醒,因为线程2在自旋抢锁,没有阻塞
AQS
Lock是基于AQS
模板方法模式,简单来说就是给出一个解决特定问题的流程,但是有些细节需要子类重写实现一下(因为当前无法确定)。例如下例中,b方法不确定,需要到具体业务场景中才能实现,把b设置成抽象类,实现的时候,只需要实现这个抽象方法就可以了。
AQS就是加锁的套路是固定的,不能改,但是某个环节不知道,在不同场景下不同,需要重写一下
所以使用AQS需要继承这个类,重写指定的方法
独占线程:当前抢到锁的线程,不在AQS类里面,而是在父类里面
使用双向链表的方式实现的同步队列
state 表示同步状态,在不同的场景之下不同数字代表的含义不同
模板方法,acquire 以独占的方式加锁,release以独占的方式解锁
acquireShared 以共享方式加锁,releaseShared以共享的方式解锁
共享锁何时用?例如读锁,可以共享;写锁不行,需要互斥
这里要求重写尝试加锁的方法,但是没有定义成抽象的,而是采用不重写抛异常的方法;为什么不弄成抽象的?
这是因为例如有时候只需要互斥锁,不需要重写共享锁的方法,就不要重写,如果定义成抽象的就必须重写
Node,同步队列,是双向链表
AQS中持有队列的头尾节点,同步状态state,互斥,可能是01表示无锁有锁,重入可能表示重入的次数,共享表示共享的次数
独占方式获取锁,可中断的加锁
共享方式加锁
这个方法直接抛异常,因此需要重写,如果不重写会抛异常
锁的内容:1.锁的状态state; 2.线程,表示当前哪个线程可以独占这个锁; 3. 同步队列,表示此时还有哪些线程在等待
AQS只是实现锁用到的一个工具,一个框架
同步队列使双向链表,同步器持有双向链表的头尾结点
改尾结点要CAS原子性的去改,因为可能有多个线程抢锁。改头节点不需要,因为是拿到锁的结点,只有一个,没有竞争
acquire,先尝试抢锁,如果抢成功了,就跳出;如果没有抢成功,先addWaiter加入队列,acquireQueued 处理队列状态,什么时候阻塞,什么时候唤醒
如果加入队列成功了,因为acquireQueued 没办法响应中断,也就是排队过程中阻塞终止不了,所以需要一个中断信号,当当前线程排到队首,可以获取到这个中断信号,进行处理
加入队列尾部,原子性的操作,如果CAS竞争失败,进入enq(),自旋,重复做这样的事情,直到加入队列成功
enq(),自旋,第一次创建头节点也要自旋,相当于加一个尾结点
处理队列状态acquireQueued
也是自旋,如果当前节点的前一个结点p是头节点,意味着前面的节点释放锁,第二个节点就能抢锁了;那么就尝试获取锁,如果获取到了,就把当前节点设置为头节点;
如果位于后面的位置,那么就阻塞
release()
如果解锁成功了,且如果头节点不为空,状态不为0(在等待状态),就唤醒(unpark)新的头节点,让新的头节点解除阻塞
加共享锁,如果尝试失败了(<0),去排队
尝试失败的原因一般是现在有独占锁(写锁)存在
先加到同步队列中,然后自旋
共享体现在,如果当前节点加锁成功,会执行setHeadAndPropagate,设置为头节点并且向后传播,如果后面也有共享锁,也可以加锁,然后状态+1(10个线程状态就为10);如果后面是写锁,传播停止
设置共享锁时独占线程是空的
setHeadAndPropagate,先设置头节点是自己
解共享锁
如果下一个节点是null或者是共享状态,就调用release方法,唤醒
doReleaseShared()中的逻辑:
SIGNAL表示唤醒还是不唤醒
重要的操作就是唤醒后继节点
Lock
加锁,可以中断,是获取到锁的线程可以被中断,synchronized不行
非阻塞,尝试加锁,如果获取到了就加锁,获取不到就放弃,而不是进入阻塞,比synchronized更灵活
可超时,在这个时间之内如果加不到锁就放弃
Lock更灵活,因此Lock可以应付更加复杂的业务场景
synchronized的等待通知是通过wait,notify,notifyAll实现的,底层是通过一个等待队列实现的,等待队列不是同步队列
synchronized有两个队列,一个同步队列,排队抢锁;一个等待队列,通知谁等待排队
但是有时候一个等待队列不够用,效果不好
等待通知组件Condition,一个Lock可以有多个Condition,可以有多个等待队列
为什么要有lock接口,很大程度是因为synchronized只有一个等待队列,很多业务场景下不方便
ReetrantLock
可重入锁,加了锁以后没有释放锁,可以再加锁
ReetrantLock是基于Sync同步器实现的,有非公平和公平的实现
默认创建一个不公平的锁,因为不公平的锁能插队,效率高
如果不能插队,调用这个构造器,传入true,创建一个公平锁
锁底层是由同步器实现的,加锁和解锁都是同步器实现的,所以直接看sync就行
sync
加锁lock留给子类实现(公平或者不公平),Sync继承AQS,AQS是采用模板方法模式设计的,所以要实现try…的方法。
Sync实现的是独占的同步器,是互斥锁
因为sync是独占锁,所以没有重写tryshare…()
Sync没有实现tryAcquire,因为加公平锁和不公平锁不一样,要留给子类实现,但是释放锁是一样的,所以只实现了tryRelease
releases表示释放锁的个数,因为锁是可重入的,所以可能有多个
getState()获得重入锁的个数(重入的次数),如果等于0表示此时没有锁
如果c等于0,把独占当前线程清空
NonfairSync
不公平锁
刚进入就抢锁,不管有没有排队,允许插队,抢到了就把当前线程设置为我自己
抢不到再排队
acquire会调用tryAcquire,tryAcquire返回父类实现的nonfairTryAcquire(Sync里的)
如果当前没有锁,就CAS的修改状态,把当前线程设置为独占的, 这里也体现了不公平,因为c等于0表示当前没有线程占用这把锁,但是并不代表没有人排队
所以不公平锁有两次试图插队的行为,体现了不公平性(第一次,加锁lock的时候,先插队抢锁,抢不到,在加入队列的时候,还会尝试加锁)
如果有锁,即c不等于0,并且当前锁的线程就是该线程,那么就重入
FairSync
公平锁,没有一进入就抢锁,而是直接去排队
如果当前没有锁,判断前面有没有前驱节点,如果没有才去抢锁
公平也体现在两点,1.lock的时候,要排队,2.排队的时候要去看同步队列前面有没有节点,如果没有才去尝试修改状态
重入逻辑和不公平锁是一样的
读写锁,写锁是独占式的,底层是acquire,读锁是acquireShare实现的
ReadWriteLock
写锁是独占式的, 底层是调用acquire()实现的
读锁是共享式的, 底层是调用acquireShare()实现的
ReadWriteLock是接口,一个典型实现类是ReentrantReadWriteLock
读写锁的实现:重入的读写锁
读锁写锁也是基于AQS实现的,读写锁都由Sync、公平锁和不公平锁三个类实现的
与重入锁区别主要体现在读锁上,写锁基本一样
接口,只有这两个方法
实现类:重入的读写锁
构造器,无参的默认也是不公平的,调用了有参的构造器
构造的时候,先创建同步器并且指定公平或者不公平,然后利用同步器创建读写锁
所以说这里可以看出,读写锁的公平性是一致的,要么都公平,要么都不公平
读锁,ReadLock构造器,用的是lock里面的sync
实现了lock(),unlock(),写锁结构也一致,只不过调用的是acquire和release,是独占的
写锁的逻辑:
所以 重点看是看Sync和公平不公平锁,读写锁只是调用了其中的方法
Sync类
sharedCount,读锁的数量
exclusiveCount,写锁的数量
Sync只有一个状态,是一个int变量c,32位,所以用高位存读锁,低位存写锁,这里用了位运算,好理解
取读锁就需要向右移16位
取写锁就是直接与16个11111111…相与
是否要阻塞,抢锁的时候要不要被阻塞,要调用这个方法,读写不一样
实现AQS模板所规定的方法
tryAcquire()方法,要加写锁
w是写锁
如果c不等于0,锁也不是写锁,也就是当前是读锁,或者当前独占线程是不是我自己,如果不是就不能加锁;如果锁是写锁,并且当前线程是自己,就重入
如果c等于0,判断是否要被阻塞,如果要被阻塞,就返回false;如果没有被阻塞,那么就尝试抢锁,抢锁失败,也返回fasle;如果抢到了,就设置当前线程为独占线程
解锁,如果独占锁数量为0,那么就清空独占线程
tryAcquireShared()
如果独占线程数量不等于0,就意味着有写锁,并且当前线程不是我自己,就返回-1
否则获得当前读锁的数量,如果读线程不被阻塞,就抢锁,更改状态,高位加1;
以共享方式解锁,关键是for循环里面,死循环,自旋
这里Sync把try方法都实现了,但是公平和不公平没有体现,而公平和不公平体现在了readerShouldBlock()和writerShouldBlock()上,公平锁和不公平锁实现了这两个方法
不公平的实现,写锁不阻塞,直接抢
读锁,要看一下队列头部有没有写锁,如果有就阻塞;如果是读锁,就抢
公平的,读和写都要看前驱节点,如果有就阻塞
锁降级
Condition
这里Monitor同步器就是sychronized后面跟的对象,调用当前对象的wait()方法,当前线程等待;其他线程调用notify()方法,就唤醒,或者notifyAll()唤醒全部
lock是用Condition实现等待通知的
有多个等待队列有什么好处吗?
阻塞队列
Sychronized()在生产者和消费者问题中,因为只有一个等待队列,生产者和消费者都需要等待,都会加到一个队列中去,如果唤醒的时候,就有可能生产者唤醒生产者,消费者唤醒消费者
ArrayBlockingQueue
items,存了队列中的数据,节点;有一个重入锁
两个队列,一个生产者队列,非满队列,notfull;一个消费者队列,非空队列notempty
构造方法,传入容量,调用构造器
放东西,加锁,加的是中断锁;如果满了,生产者通过notFull被阻塞,否则加入队列中
加完以后,notEmpty.signal()唤醒消费者
消费的方法
出队,唤醒生产者
从上面的生产消费过程可以看出,用两个Condition,可以实现精确通知到生产者或者消费者
Condition源码
看一下Condition,是一个接口
它的实现类,是AQS的内部类
下面是一个实现类
其中的节点Node和AQS同步队列是一样的,但等待队列是单向的,用的是其中的nextWaiter
node节点中有一个nextWaiter是为等待队列服务的
Condition中的等待方法,addConditionWaiter()把当前线程加到等待队列中,返回一个node
阻塞之前先解锁,将重入的锁都解除
如果当前节点不在同步队列中,就阻塞自己,唤醒以后再加入同步队列,再加锁
唤醒方法,唤醒等待队列中的第一个节点
先头节点后移,头节点的next置为空,代表唤醒了头节点
唤醒的具体实现,唤醒当前节点对应的线程。
将当前节点加入同步队列,并且把当前线程解锁
唤醒所有节点
线程池
实现机制:我们给线程池一个任务(线程池的execute()方法),它帮我们找一个线程执行这个任务
三个关键点:核心线程,队列,扩展线程
一般有以下流程:
先检查池里线程的数量,有没有达到核心线程的上限。假设核心线程数是8,最大线程数是16,首先判断是否达到核心线程数8,如果没有就创建一个新的线程,可以理解为是一个核心线程;
如果当前核心线程数已经达到8了,就把当前任务存入一个队列中,排队;
如果队列也满了,那么就判断最大线程数是否达到了,如果没有达到,再去创建线程,这时创建的线程是扩展线程;
如果最大线程也满了,那么就无法创建了,如果继续创建,超负荷了,服务就会关闭,是不允许的。所以就会拒绝这个用户任务;
虚线部分的逻辑:如果线程池中有线程空闲了,那么就会到队列中取任务来执行,不管是核心线程还是扩展线程;如果拿不到任务,就彻底空闲了,如果达到一定时间没有任务,当前线程就会被释放
拒绝有四个策略,第一个CallerRunsPolicy,谁提交的任务谁去执行,不创建新的线程去执行,调用者去执行
AbortPolicy,抛出异常
DiscardPolicy,放弃这个任务,忽视这个任务
DiscardOldestPolicy,放弃队列队头的任务放弃,也就是最早存入的任务,然后放入当前任务
核心线程数怎么确定?
建议:CPU密集型的业务,核心线程数定为CPU的核数+1;内存密集型的,可以定为两倍的CPU的核数;混合型,…(自己查一下)
keepAliveTime 空闲线程存活时间:如果刚才并发高,创建了很多个线程,如果现在并发降低了,很多线程空闲了,就可以通过这个参数来关掉这些线程
线程工厂用于创建线程,是具体创建线程的机制
拒绝策略这里是一个父类或者接口,四种策略是它的子类
线程池有五种状态,用五个数字来表示,变化只能由小到大,不可逆
shutdown 是要停止线程池,但是如果池中还有线程的话,要考虑线程的运行情况
stop 就是赶快停掉,也不用考虑现在要执行的线程
如果线程池、队列为空,那么就进入tidying状态,准备挂
最后经历了terminated()方法,即回调函数,就最终死亡了
这个方法现在是空的,如果有什么特殊的需要,可以在这个函数中加一个逻辑
execute()方法:
c是线程的状态
worker是一个工作者,封装了一个线程,一个worker就是一个线程
第一步:如果线程的数量小于核心线程数,就把当前任务加到线程池里,创建核心线程(标志为true)
第二步:如果达到核心线程数,并且有c个线程在运行,那么就把任务加到队列中去
然后得到任务的状态,如果不是运行态,就尝试将任务移除,如果不成功,就拒绝这个任务(是第二步中的一个细节处理)
如果核心线程数等于 0,创建一个线程
第三步:如果加入队列失败,队列也满了,那么就创建一个扩展线程,非核心线程(false)
第四步:如果创建一个扩展线程也失败了,就拒绝这个任务
addWorker方法:
这里retry是外层循环的名字,为了能在内层循环跳出外层循环
关键是内存循环的第一个判断,如果线程的数量,超过了核心线程或者最大线程(判断哪个看标志),就返回false
如果没有达到最大数量,那么就添加线程
先根据所给的任务创建一个线程,然后加锁、解锁
workers.add(w),加到线程池里面
if(woekerAdded)如果添加成功了,就运行这个线程;添加失败,也会有处理
如果添加失败,移除remove()
Worker是线程池的内部类,实现了Runnable接口,继承了AQS,所以本身就是一个同步器
构造器里面是传入任务,用一个成员变量来持有,然后调用了工厂创建一个线程
线程运行的方法,调用了runWorker,是线程池来实现的,所以还是跳出外面。
对于worker我们只需要知道基本结构就可以,核心逻辑还是线程池来做
如果有任务,就执行,如果没有,就从队列中取任务来执行;而且这个逻辑在while中,就是不停的取任务做
在run()方法之前,会有一个beforeExecute,之后有一个afterExecute,都被称钩子函数,回调函数
表示在关键的步骤之前要不要插入特殊的逻辑,这个函数里面什么也没有,需要的话创建一个子类重写这个方法
如果没有任务执行的话,就有一个退出的机制,这个后面看
在队列中获取任务的方法,
首先第一变量表示是否超时
timed,允许核心线程超时吗,或者线程数大于核心线程数吗;表示核心线程之后创建的线程,超出核心线程数的线程要考虑时间的问题,超时要释放
先看try里面的逻辑,如果要考虑时间,那么就从队列中取任务,并且考虑线程的超时时间,例如当前队列为空,并且在超时时间内队列中也没有任务加进来,那么就将 r 赋值为null,timeOut为true
如果不考虑时间,直接出队
如果线程数大于最大值,或者要考虑时间且超时了,就表示此时队列中自旋等待了很久但是还是没有任务加入,那么就原子的减少线程数量,返回空的任务
runWorker中,如果没有任务执行了,那么就退出
这里的核心代码是workers.remove(w),刚刚getTask中只是减少了线程的数量,这个方法在finally中,一定会执行,作用是移除这个线程
接下来,如果线程池的状态小于STOP,即没有停止
判断这个线程有没有猝死,
如有没有猝死,计算最小线程数是多少
如果当前线程个数超过最小线程数,那么是合理的,直接返回
如果低于最小线程数了,是不允许的,就创建一个线程,保证有任务来时可以及时执行
线程池永远会保留一定的线程数
线程池的状态:
这个方法主要作用是为了执行terminated()方法,因为线程即将死亡了,这是一个很严肃的事情, 这里给了一个机会去拯救,这个terminated()方法也是空的,需要自己实现
状态是存在ctl里的,是一个原子类。但是ctl不只是存了状态,高3位存线程池的状态,低29位存的线程个数
核心参数
拒绝策略。
第一个,调用者自己执行
直接抛出异常
直接放弃
将最先入队的任务出队,然后把当前任务加入
以上线程池使用Runnable做线程体,run是没有返回值的;还有一种是用Callable做线程体,是有返回值的
在ThreadPollExecutor的父类AbstractExecutorService中,定义了一个方法submit()
在这个方法中,可以看到,也是把task转成了Runnable,然后调用execute(),返回了ftask
这里把Callable变成了Runnable,这里用的是适配器模式(设计模式),这里很重要
RunnableFuture 既继承了Runnable,也继承了Future
两种适配器模式,一种适配类(继承),一种适配对象
newTaskFor方法
FutureTask继承了RunnableFuture,而这个父类刚刚看过又继承了Runnable,所以当前类需要实现run方法
构造器
核心就是c.call(),然后做记录,set(result),存储到outcome中,代表返回结果
run方法并没有实现具体逻辑,而是调用c.call()实现的
get()得到返回值,因为线程的执行时异步的,执行需要时间,所以如果没有执行完(s<=COMPLETING),就等待,算完再返回
get()其实是Future接口中的,这里是对它的实现
ScheduledThreadPoolExecutor
执行定时任务的线程池,实现延迟的核心是延迟队列
(如果在项目中要使用延迟队列的话,可以用这个线程池,也可以用redis)
这里第一个方法没有返回值
第二个方法有返回值,
第三个固定频率是指每两个任务开始时间的间隔,例如第一个任务开始了,那么就开始计时,达到period,就执行第二个任务
第四个方法中固定间隔是说第一个任务执行完了,过delay时间才执行下个任务
第三四个方法也能延迟执行(第二个参数)
内部类,继承了延迟队列
出队的方法,如果队头为空,阻塞;如果不为空,先取队首的延迟时间,如果延迟时间小于等于0了,那么就出队[finishPoll(first)]
如果时间没到,那么阻塞
构造方法,传入的参数中延迟队列必须是内部类的延迟队列,ThreadPoll传入的队列可以自己指定
延迟执行任务的方法,传入延迟时间和任务,封装成一个对象,构造出一个任务,然后去延迟的执行这个任务
把任务加入延迟队列
周期执行任务,固定频率的,也是时间任务,频率封装
固定间隔
可以看到,这两个方法逻辑基本一样,但是处理频率时,时间是正的,而固定周期,时间是负的
为什么这样处理呢,看一下构建的ScheduledFuture对象
其中的run()方法
首先对周期状态进行检查,如果当前任务没有到达执行的点,就取消执行
然后检查任务是否是周期的,如果不是,就调用父类普通的run()方法,不加周期的处理
然后开始执行,执行以后,计算下次执行时间,再加到队列中
怎么计算呢,
如果p大于0,那么就直接加上这个时间,即任务开始就开始计时;如果小于0,以延迟的形式加上时间,即任务结束以后,在加上时间
计算完时间,重写加入队列,实现周期性的执行
Executors new的是无界队列,就是会不断的加入队列,所以线程数不会超过核心线程数,很容易会造成内存溢出
例如Executors中一个新建固定数量线程池的方法
这里new的延迟队列,默认是无界的
并发工具
Semaphore
里面有一个同步器Sync
构造器,传入数量,默认采用非公平的实现
Sync把传入的资源数量转变成状态,即锁
公平的实现,如果有前置节点,直接返回,严格排队;如果没有,计算剩余数量,
不公平的实现,不排队,直接加锁
CountDownLatch
构造方法,传入要等待的线程数量
await()方法由主线程调用
子线程执行完,调用countDown,把线程数量减一,都执行完,主线程唤醒了
内部类Sync
获取锁,当线程数量为0的时候,主线程就会拿到锁,结束阻塞
释放锁,等于0,不用释放了,否则线程数量减1
CyclicBarrier
参数:
重入锁、等待队列
Parties参与等待的线程
Count当前已经就位的线程
构造器,
每个线程调用await等待
await()调用dowait()方法,每次count减一;
如果所有线程都阻塞了,就位了。就执行run(),唤醒所有线程nextGeneration()
如果大于0,那么就阻塞 trip.await()
唤醒所有线程,重置count,重新开始
补充:ThreadLocal
链接:https://www.jianshu.com/p/3c5d7f09dfbd
补充:sleep和wait的区别
Java中的多线程是一种抢占式的机制,而不是分时机制。抢占式的机制是有多个线程处于可运行状态,但是只有一个线程在运行。
共同点 :
- 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
- wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
不同点 :
1.每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
2.wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
3.sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
4.sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
5.wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。