sleep和wait方法的区别
最重要的区别是sleep方法不会释放锁,而wait方法会释放锁。 wait:wait方法需要在同步块或同步方法中才能被调用,否则会抛出异常,调用wait方法的线程需要被notify或notifyAll来唤醒,又或者是到了超时时间(如果设置了超时)自动唤醒,当调用wait的线程被唤醒之后,需要先获取到锁才能继续往下执行。 sleep:sleep方法仅仅是让当前线程停止运行一段时间,当时间过了之后可以立即继续往下执行。 wait-notify通知机制可以用作线程间的通信,比如用于生产者消费者模式中。wait-notify都必须在同步块中使用,也就是需要先获取锁。之所以需要在同步块中才能被调用,是为了避免通知丢失。 对于中断的响应:sleep与wait都会响应中断,且都会抛出InterruptedException异常!!
park/unpark与wait/notify的区别
- wait和notify方法必须和同步锁 synchronized一块儿使用。而park/unpark使用就比较灵活了,没有这个限制,可以在任何地方使用。
- park/unpark 使用时没有先后顺序,先对一个线程调用unpark,然后在该线程内再调用park,则线程不会阻塞。而wait必须在notify前先使用,如果先notify,再wait,则线程会一直等待。
- notify只能随机释放一个线程,并不能指定某个特定线程,notifyAll是释放锁对象中的所有线程。而unpark方法是唤醒指定的线程。
- 调用wait方法会使当前线程释放锁资源,但使用的前提是必须已经获得了锁。 而park不会释放锁资源。
- 响应中断:park可以被中断,但不会抛异常,会设置中断位,而wait会抛出中断异常。
Thread的start方法与run方法
start方法会创建一个线程并使线程进入就绪状态,等待cpu调度;run方法不会创建线程,如果直接调用run方法,只会在当前线程执行。start在启动一个线程之后,会运行run方法。
volatile关键字(final关键字)
两个作用:
保证线程的可见性和禁止指令重排序。需要注意的是,volatile关键字并不保证原子性。 保证线程可见性是指volatile修饰的变量,当被修改时,所有线程中该变量的副本都会立即失效,并且需要从内存中重新load进线程本地变量。 在编译器做优化以及机器运行指令时可能会将没有依赖关系的指令进行重新排序,但是volatile禁止了这个行为。
volatile的线程可见性的实现:
当一个线程对volatile变量进行写操作之后会立即将volatile变量刷新到主内存,这样其他线程本地内存存储的volatile变量就会失效(也就是缓存失效),需要重新从主内存读取。
volatile禁止重排序的实现:
1、在每个volatile写之前插入一个StoreStore内存屏障 2、在每个volatile写之后插入一个StoreLoad内存屏障 3、在每个volatile读之后插入一个LoadLoad内存屏障 4、在每个volatile读之后插入一个LoadStore内存屏障
内存屏障:
一组处理器指令,用来禁止处理器对某类内存访问的重排序,JMM把内存屏障分为四类:
- LoadLoad:load1;LoadLoad;load2,load1的装载先于load2及后续所有装载指令的装载
- StoreStore:store1;StoreStore;store2, 确保store1对其他处理器可见先于store2及之后所有存储指令的存储
- LoadStore:load1;LoadStore;store2,确保load1数据的装载先于store2及后续所有存储指令写入内存
- StoreLoad:store1;StoreLoad;load1, 确保store1数据对其他处理器变得可见(指刷新到内存)先于load2及后续所有装载指令的装载。StoreLoad会使该屏障之前的所有访问内存访问指令完成之后,才执行该屏障之后的内存访问指令。StoreLoad是一个全能型的屏障,它同时具备其他3个屏障的效果。
final域的重排序规则:
对于final域,编译器和处理器需要遵守两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象赋值给一个引用,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能进行重排序。
写final域的重排序规则:编译器在final域的写之后,构造函数的return之前,插入一个StoreStore屏障,用来禁止将final域的写重排序到构造函数之外。 写final域的重排序规则确保在对象引用为任意线程可见之前,该对象的final域已经被正确的初始化了。 读final域的重排序规则:编译器会在final域的读之前插入一个LoadLoad屏障。实际上读对象引用与读对象的final域之间存在依赖关系,大部分编译器都不会重排序这两个操作,但是也有会重排序的,所以这个规则就是针对那些可能会对存在依赖关系进行重排序的处理器。
JMM
happens-before原则
- 程序顺序规则:在一个线程内,按照程序编写的先后顺序,书写在前面的先行发生于书写后面的。
- 监视器规则:对一个锁的解锁先行发生于对这个锁的加锁。
- volatile变量规则:对于volatile变量的写先行发生于对volatile的读。
- 传递性:如果A先行发生于B,B先行发生于C,则A先行发生于C。
- start()规则:在线程A中执行threadB.start(),则执行start()先行发生于threadB中的任意操作。
- join()规则:如果线程A中执行threadB.join(),则线程B中的任意操作先行发生于线程A从threadB.join()操作成功返回。
synchronized关键字
语法上
可以修饰方法,也可以修饰语句块。线程进入同步块或者同步方法前需要先获取对象的锁:对于普通同步方法,锁的是当前实例;对于静态同步方法,锁的是当前类对象;对于同步块,锁的是Synchronized括号里的对象。 同一时刻只有一个线程能够进入同步方法或者同步块。synchronized锁时存在对象头里的,每一个对象的对象头都有一个2bit的锁标志位,用来标识当前对象的锁状态。另外,在修饰方法和同步块时,编译的结果是不一样的:对于同步块,编译器会在代码块开始处加一个monitorenter指令,在代码块结束处和异常处加一个monitorexit指令;而修饰方法时,只是显式地将该方法标记为ACC_SYNCHRONIZED。synchronized是非公平的可重入锁。
锁优化
synchronized锁在jdk1.6做了优化,引入了自旋锁和自适应自旋锁、偏向锁、轻量级锁、重量级锁(synchronized之前都是重量级锁)。
偏向锁
锁标志位为01
偏向锁的获取
当一个线程尝试获取对象的锁时,首先在自己的栈中找到一个可用的Lock Record(内存地址从低到高,找到地址最高的可用的Lock Record,可用的Lock Record是obj字段为null的),将Lock Record的obj字段指向锁对象,然后判断锁对象是否是可偏向的,以及对象的类是否是可偏向的,如果都为True,则判断该对象有没有偏向线程,如果有且偏向线程就是当前线程且对象的epoch与class对象中的epoch一致,则说明该线程已经获取了偏向锁,直接进入同步块就行,如果epoch已过期,则需要进行重偏向。如果无偏向线程,则尝试用CAS将该线程的threadId设置到对象的Mark Word中,如果成功,则获取偏向锁,如果失败则进行锁撤销和尝试获取轻量级锁。如果有偏向线程,但是不是当前线程,且epoch已过期,则进行重偏向,如果未过期,则进行锁撤销和升级。
偏向锁的重入
找到一个新的Lock Record(在第一个Lock Record的低位处),并将Lock Record的obj指向锁对象即可。释放的时候也是依次删除obj里的指针。
偏向锁的释放
将线程中的Lock Record释放掉就可以了,不会去修改对象的Mark Word
偏向锁的撤销
一旦偏向锁出现争用现象,即A获得了偏向锁,这时候B也去争夺偏向锁,就会撤销对象的偏向锁,并升级为轻量级锁。偏向锁的撤销要等到全局safepoint,VM thread会找到当前对象偏向的线程,如果线程已死或不在同步块中,直接撤销偏向锁,判断线程是否在同步块中只需要遍历线程的栈,如果栈中仍存在指向锁对象的Lock Record,则说明在同步块中。如果线程在同步块中,则将指向锁对象的最高位置的Lock Record的displaced mark word设为对象的Mark word,其他所有低位的Lock Record的displaced Mark word 设为null,将锁对象的Mark word指向高位的Lock Record。至此,偏向锁升级为轻量级锁。对象的class对象会维护一个偏向撤销计数器,每当有一个对象偏向锁被撤销,都会将对应的class对象的偏向撤销计数器加1,然后检查该计数器,当计数器达到批量重偏向的阈值(默认为20)时,进行批量重偏向的操作,当达到批量撤销的阈值(默认为40)时,进行批量撤销。每个class对象也有一个epoch,每当发生一次批量重偏向,就将class的epoch自增,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次有线程尝试获取偏向锁时会比较对象的epoch与class的epoch是否相同,如果不相同,不管该对象是否有偏向线程,都可以通过CAS修改对象的Mark Word来获取偏向锁。
批量重偏向
将class的epoch自增1,处理正在被使用的锁对象,通过遍历存活线程的栈,找到正在使用的所有偏向锁的对象,将它们的epoch更新为新值。
批量撤销
将类的偏向标记关闭,之后所有该类的实例的锁都会升级为轻量级锁,该类新分配的对象的偏向标记也会关闭,不再可偏向。处理正在使用的锁对象,通过遍历所有存活线程的栈,找到正在使用的偏向锁对象,然后撤销偏向锁(撤销过程与上述一样)。
轻量级锁
锁标志位为00
轻量级锁的获取
首先依旧是找到可用的Lock Record,与偏向锁中逻辑相同, 然后构建一个无锁状态的Displaced Mark Word,构建的方法时拷贝对象当前的Mark Word的值,然后将锁标志位置设为为无锁状态(01),之后将Displaced Mark Word设置到Lock Record中,然后尝试用CAS把对象的Mark Word设为指向Lock Record的指针,CAS中的参数为:旧值: 刚刚构建的无锁状态的Displaced Mark Word,新值为Lock Record地址,操作对象为锁对象。如果CAS成功,则表示获取锁成功。实际上,判断该对象是否已被锁定是通过CAS来做的,如果对象的Mark Word的锁标志位本身是有锁状态的,那么CAS肯定会失败,因为用无锁状态的Displaced Mark Word作为CAS中的旧值。不直接去检查对象的锁标志位的原因应该是这一步不会是原子操作,不如直接用CAS来判断方便。
轻量级锁的重入
创造一个新 Lock Record,将obj指向锁对象,Displaced Mark Word设为null即可。因为对象原来的Mark Word已经保存在第一个Lock Record中了,设置为null就能知道这个锁时重入的。
轻量级锁的释放
释放锁的时候依次清除线程栈中Lock Record中的obj(从栈的地位到高位的顺序)即可,当释放最后一个锁时,就会用CAS将Lock Record中的Displaced Mark Word设置到对象的Mark Word中。
轻量级锁的膨胀
如果一个线程尝试获取轻量级锁失败,并且自旋一段时间后再次尝试获取锁依然失败,会将轻量级锁膨胀为重量级锁,该线程也会进入重量级锁的争用过程中。
重量级锁
锁标志位为10,锁模型与ReentrantLock中的差不多,不过是由系统层面的锁实现
synchronized与ReentrantLock的区别
- synchronized是JVM层面的锁实现,ReentrantLock是JDK层面锁实现;
- synchronized的锁状态无法在代码中判断,而ReentrantLock的锁状态可以通过ReentrantLock#isLocked来判断;
- synchronized是非公平锁,而ReentrantLock可以是公平锁也可以是非公平锁;
- synchronized不可被中断,而ReentrantLock#lockInterruptibly方法是可以被中断的;
- 在发生异常时,synchronized会自动释放锁,这是由编译器自动实现的,而ReentrantLock需要显式地在finally块中释放锁,才能保证发生异常时也能释放锁;
- ReentrantLock获取锁的方式有很多种:比如立即返回的tryLock,带有超时时间的锁等待,更加灵活;
- ReentrantLock能够实现更复杂或者粒度更合适的锁,比如: lockA.lock()->lockB.lock()->lockA.release()->doSomething()->lockB.release(),而synchronized则不行;
- synchronized在某些情况下可能会出现对于 已经在等待的线程 后来的线程先获取锁,而ReentrantLock对于已经在等待的线程,一定是先来的线程先获取锁;
- synchronized的条件队列只有一个,而一个ReentrantLock可以创建多个condition,使线程可以在不同的条件上等待。
AQS
通过维护一个volatile int state(代表资源)字段和一个FIFO的线程等待队列来实现锁。整体流程为,当获取锁时,尝试用CAS修改state的值,如果成功则获取到锁,如果失败则进入队列里等待,先进入队列里的线程会先获取锁;在释放锁时,也是通过修改state的值完成。
自定义锁需要实现的方法
- boolean isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- boolean tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- boolean tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- int tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- boolean tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
其中tryAcquire和tryRelease为一组,用于实现独占锁的,tryAcquireShared与tryReleaseShared为一组,用于实现共享锁。
acquire(int)方法
该方法是独占模式下获取锁时实际调用的方法。方法体如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
主要涉及到三个关键的方法,首先是调用tryAcquire尝试快速获取锁,如果成功就直接返回了,不会再有后面的步骤,如果失败,也就是返回false,则会将当前线程作入队等待操作。
addWaiter这个方法,是将当前线程封装成一个内部类的对象Node,然后尝试用CAS的方法将Node添加到队列尾部,如果尝试失败,则采用自旋+CAS的方法来将Node添加到队尾。传入的参数表示这个Node的类型是独占的还是共享的,acquire方法是获取独占锁,所以传入的是独占类型。返回的结果为封装了当前线程的这个Node。
acquireQueued方法中通过自旋来为线程获取锁。首先判断传入的节点的前驱节点是不是头结点,如果是的话,调用tryAcquire获取锁,获取成功,则将head指向当前节点,并返回是否被中断过。如果这两个条件有一个不满足,则尝试让该节点里的线程进入等待状态。具体调用的方法如下
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
shouldParkAfterFailedAcquire(Node pred, Node node)是帮助当前节点node找到一个合适的位置进行等待。每个节点都有一个状态waitStatus:
- CANCLED = 1 表示节点已取消调度,当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SINGAL = -1 表示后继节点在等待当前节点唤醒,后继结点入队时,会将前驱节点的状态更新为SIGNAL。
- CONDITION = -2 表示节点在condition上等待,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE = -3 共享模式下,不仅会唤醒后继节点,还会唤醒后继节点的后继节点
- 0 新节点的默认状态
由上面的状态代表的含义可知,当waitStatus<=0时是等待调度的正常状态。shouldParkAfterFailedAcquire帮当前节点找到合适的位置的意思是,找到一个没有被CANCLED的节点,并将当前节点连接到该节点后面。如果传入的参数中的pred本身就是SINGAL的,直接返回true,否则从前驱节点出发往前找,直到找到一个节点A的waitStatus<=0,然后将当前节点的prev指针指向节点A,将节点A的next指针指向当前节点,最后用CAS将节点A的waitStatus设置为SINGAL,并返回false,继续走自旋逻辑。
当shouldParkAfterFailedAcquire返回true时,会进入parkAndCheckInterrupt()方法,该方法调用LockSupport.park(this);使当前线程陷入等待状态,直到被unpark或者被中断唤醒,返回该线程是否已被中断。
在acquire的过程中,如果线程被中断过并不会直接响应中断,而是将中断记录下来,不管被中断过几次,都将在acquire成功返回之后,补上一次中断。
总结
- 节点入队尾后检查状态,找到安全休息点;
- 调用park进入waiting状态,等待被unpark或中断唤醒;
- 被唤醒后看自己是否有资格获取锁,如果拿到,将head指向当前节点,并返回从入队到获取锁的过程中是否被中断过;如果没拿到,继续流程1.
release(int arg)
独占模式下释放锁的顶层逻辑
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
首先调用tryRelease方法释放锁,只有获取锁的线程才能release,不然可能抛异常,具体的做法需要用户在tryRelease中实现。如果tryRelease成功了,就将头节点摘掉,然后唤醒后继 没有放弃等待锁的 节点。相对于acquire来说,release比较简单。
acquireShared(int arg)
共享模式下获取锁的顶层设计
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared根据返回结果(int)来判断是否成功获取锁,定义如下:
- 返回结果为负,获取失败;
- 返回结果为0,获取成功,但是没有多余的资源了;
- 返回结果为正,获取成功,且还有多余资源,其他锁还可以就获取。
如果获取失败,会进入到doAcquireShared这个方法中。 该方法与doAcquire几乎是一样的逻辑,有一点不同的是,如果在该方法中获取成功之后,如果还有多余的资源,会唤醒后继节点,而doAcquire是不会唤醒后继节点的。
releaseShared(int arg)
共享模式下释放锁的顶层设计
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
如果tryReleaseShared成功,则通过doReleaseShared方法唤醒后继节点。
acquireInterruptibly(int arg)
独占模式下可中断地获取锁的顶层设计。 该方法与acquire几乎一样,只是在等待过程中,如果线程被中断,会直接抛出中断异常,而不是返回是否被中断过。
acquireShareInterruptibly(int arg)
共享模式下可中断地获取锁的顶层设计,也是与acquireShared差不多,也是被中断后直接抛异常。
公平锁与非公平锁
- 公平锁:按照线程申请锁的顺序依次获得。在AQS的实现中,就是先判断队列中有没有等待锁的线程,如果有,就直接入队等待,如果没有才尝试获取锁。
- 非公平锁:当线程第一次申请锁时,就直接尝试获取锁,而不管当前有没有正在等待的线程。如果获取失败也会入队等待,这时候就是谁先入队,谁先得到了。
独占锁与共享锁
- 独占锁:同一时刻,只有一个线程能获得锁
- 共享锁:同一时刻,可以有多个线程获得锁
读写锁
ReentrantReadWriteLock是可重入读写锁的实现,读写锁的本质就是独占锁和共享锁。写锁是独占式的,读锁时共享式的。当有写锁时,如果再获得写锁和读锁,当有读锁时,其他线程还可以获得读锁,但不能获得写锁。ReentrantReadWriteLock将state按位划分为两个部分:高16位为读锁资源,低16位为写锁资源。
锁优化(自旋锁、偏向锁、轻量级锁、重量级锁)
在synchronized实现中讲了偏向锁、轻量级锁和重量级锁,自旋锁是对于多核CPU的优化,如果一个线程没有获取到锁,以前的做法是进入等待队列,而现在有可能是先自旋一定的周期,然后再次尝试获取锁,如果依然失败,再进入等待队列,这样可以降低上下文切换的开销,因为线程进入waiting状态是需要内核帮助的,上下文切换的开销很大。
LockSupport
在AQS中被广泛使用,可以在非同步块中实现等待/通知。常用的方法如下:
阻塞线程
public static void park()
public static void park(Object blocker)
这两个方法都能实现线程的等待,带有一个参数的方法的唯一作用是可以记录线程具体在哪个对象上等待,在查找问题的时候很有用,其他方面与无参的方法完全一样。在dump一个线程可以发现:
// 调用park()的线程
"main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
at learn.LockSupportDemo.main(LockSupportDemo.java:7)
// 调用park("aaa")的线程
"main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x048c2d18> (a java.lang.String) //这里多出一条记录
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at learn.LockSupportDemo.main(LockSupportDemo.java:7)
所以,推荐使用带有参数的park方法。另外,从上面的线程信息也可以看出,park会使线程处于waiting状态,而不是blocked状态。而等待进入synchronized同步块的线程是blocked状态。
除此之外,还有带有超时时间的方法:
public static void parkNanos(Object blocker, long nanos)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(Object blocker, long deadline)
public static void parkNanos(long nanos)
public static void parkNanos(long nanos)
public static void parkUntil(long deadline) //deadline为绝对时间的时间戳
唤醒线程
public static void unpark(Thread t)
唤醒线程,无论是调用无参还是有参的park方法,都可以用unpark唤醒。
响应中断
park可以从中断中醒来,会将中断位设为true,不会抛出中断异常,而是继续往下执行。
并发工具类
CountdownLatch
利用AQS的共享模式实现的工具类,可以用于主线程等待多个其他线程完成任务之后再继续往下走,否则就处于阻塞状态,类似于wait-notify通知机制,但是主线程需要等待多个线程通知。
构造方法CountDownLatch(int count)
初始化的时候需要传入一个非负整数,表示有多少个资源可用。然后每当调用一次countDown()就会将资源state减1,直到state为0,tryReleaseShared才会返回true,然后才会唤醒等待获取锁的线程,也就是调用await方法的线程。一个CountDownLatch对象只能使用一次,当state为0之后,将不再可用。
await(), await(long timeout, TimeUnit unit)
当主线程将任务分配给多个线程之后,想要等待多个任务完成之后再往下走时,则调用await方法,这两个方法分别调用acquireSharedInterruptibly()和tryAcquireSharedNanos(long timeout, TimeUnit unit)方法来实现等待。
countDown()
该方法实际调用releaseShared(int arg)方法实现将state减1,如果state为0了就会唤醒等待的线程,否则直接返回false,调用countDownLatch可以继续做其他事。
CyclicBarrier
由ReentrantLock和ReentrantLock的ConditionObject实现。只有当所有线程都调用过await方法之后,这些线程才能继续运行下去,否则在ConditionObject上等待。与CountDownLatch不同的是,CyclicBarrier对象可以重复使用。
构造方法CyclicBarrier(int parties), CyclicBarrier(int parties, Runnable barrierAction)
parties表示需要多少个线程都走到栅栏处(调用await方法)时才继续往下走,如果传入barrierAction,则在最后一个线程到达栅栏处时会先执行barrierAction,然后再唤醒其他线程,让其他线程继续走自己的逻辑,如果不传barrierAction,则直接唤醒其他线程。
await(), await(long timeOut, TimeUnit timeUnit)
如构造方法中所说。利用了ReentrantLock锁,和ConditionObject,来实现等待通知机制。
Semaphore
利用AQS的共享模式实现并发数的控制,本质上就是一个共享锁。提供公平和非公平两种模式。保证同一时刻最多有指定的并发数。
构造方法Semaphore(int permits), Semaphore(int permits, boolean fair)
permits表示最大并发数,fair表示是否是公平锁,默认情况下是非公平的。
acquire(), acquire(int permits)
获取通行证,也就是获取共享锁。一个线程可以申请一个通行证,也可以申请多个,等待的过程中是可以被中断的。
release(), release(int permits)
归还通行证,也就是释放共享锁。也可以被中断。
小结
从ReentrantLock和Semaphore的实现,我们可以看到,独占锁的实现中,tryAcquire和tryRelease都只用到了CAS,而共享锁的实现中,tryAcquireShared与tryReleaseShared都是用自旋+CAS。所以在实现自己的锁的时候,可以参考这两个类。
阻塞队列
BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
阻塞队列的接口——BlockingQueue
Java中的阻塞队列是都实现BlockingQueue这个接口,其中提供了几个重要方法
| 方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
|---|---|---|---|---|
| 插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除方法 | remove() | poll() | take() | poll(time, unit) |
| 检查方法 | element() | peek() | 不可用 | 不可用 |
队列的插入都是插入到队尾,移除都是移除队首元素,检查是获取队首元素,但并不会移除队列。
ArrayBlockingQueue
使用循环数组实现的有界阻塞队列,采用一个ReentrantLock实例lock,和该lock生成的两个Condition: notFull(队列不满),notEmpty(队列不空)来实现线程安全和阻塞,插入和移除都需要先获取同一把锁。用户可以在构造ArrayBlockingQueue的时候指定锁的公平性。
使用阻塞方法put进行插入的时候,如果队列已满,则当前线程需要在条件notFull上等待,直到有线程从队列里取出来元素,并唤醒阻塞线程,然后插入,插入成功之后调用notEmpty.singal()来唤醒在notEmpty条件上等待的线程
使用阻塞方法take从队列中取元素时,如果队列是空的,则当钱线程会在条件队列notEmpty上等待,直到有线程往队列里添加了元素,并唤醒前线程,取走元素之后,会调用notFull.singal()来唤醒在notFull条件上等待的线程。
因为插入和移除都是使用的同一个锁,生产者线程和消费者线程都需要竞争这把锁,所以同一时刻只能有一个线程操作队列。
LinkedBlockingQueue
使用单链表实现的阻塞队列,如果使用无参构造方法,则会生成一个无界队列(最大长度为int的最大值),也可以传入一个容量值,生成一个有界队列。
LinkedBlockingQueue使用两个ReentrantLock和两个Condition来实现线程安全和阻塞:
ReentrantLock putLock = new ReentrantLock();
ReentrantLock takeLock = new ReentrantLock();
Condition notFull=puLock.newCondition();
Condition notEmpty=takeLock.newCondition()
由此可以看出,与ArrayBlockingQueue的区别在于,LinkedBlockingQueue支持同一时刻生产者线程和消费者线程同时操作队列,并发度更大,效率也就更高;另外LinkedBlockingQueue里的锁都是非公平的,而ArrayBlockingQueue里的锁可以指定公平还是非公平。
SynchronousQueue(实现方式很特殊!)
SynchronousQueue不仅保存了元素,而且保存了操作线程和对应的操作,然后每当有一个线程执行对应的操作,都会去队列或栈里取匹配这个操作,插入和移除一对匹配的操作,如果匹配成功,则移除的线程直接获取到插入线程里的item,然后两个线程返回;如果匹配失败,即当前线程的操作与队列里的线程的操作是一样的,则将当前线程入队阻塞。
SynchronousQueue的实现中没有用到锁,吞吐量很高,但是也可能导致线程阻塞过多,由它实现逻辑可知,如果生产者与消费者线程不平衡,就会造成很多线程阻塞。
SynchronousQueue有公平和非公平两种模式,公平模式采用TransferQueue实现,非公平模式使用TransferStack实现。
PriorityBlockingQueue
底层是数组实现的无界优先级阻塞队列。优先级通过最小堆实现。通过ReentrantLock实现线程安全,通过一个Condition: notEmpty来实现线程的等待通知,之所有只有一个Condition,是因为这是无界队列!可以无限制地插入(实际上最大容量为int最大值)。
DelayQueue
使用PriorityQueue实现的延迟队列,无界。实际上是一个优先级队列,只是优先级是延迟时间,也是用最小堆实现的。延迟时间最小的在堆顶。
变量
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private final Condition available = lock.newCondition();
private Thread leader = null;
DelayQueue中只有一个ReentrantLock,和一个available条件,因为DelayQueue是无界队列。
leader的作用是减少其他线程不必要的等待时间。当调用take阻塞式地获取元素时,先看队首的元素是否已经过了超时时间,如果是的话,直接返回,否则看一下leader是否为null,如果leader不为null,则当前线程会进入available条件的等待队列,如果leader为null,则将leader设置为当前线程,并且自旋队首元素剩余的时间,之后尝试获取队首元素。
在队首元素不变且没有更多线程来竞争队首元素的情况下,最先等待队首元素的线程就是leader,会先获取到元素,然后通知其他线程。如果队首元素修改了(有新元素插入进来,且delay时间更小),则将leader置为null。
LinkedTransferQueue(重要!)
无界队列,LinkedTransferQueue的结构与SynchronousQueue一样,但是不同的是,向LinkedTransferQueue中插入元素的线程不会被阻塞,而是将元素插入到队列里后,直接返回了,而SynchronousQueue的插入线程可能会阻塞。
private E xfer(E e, boolean haveData, int how, long nanos)
该方法是LinkedTransferQueue中最重要的方法,插入和移除操作都是通过调用这个方法实现的!!参数的意义如下
e: 元素,可以为null,移除数据的时候一定是null;
haveData: 是否有数据,true表示这是插入操作,false表示这是接收操作。当为true时,e不能为null,否则会报NullPoniterException;
how: 这是一个怎样操作,有如下4中情况:
NOW,立即返回,没有匹配到立即返回,不做入队操作
ASYNC,异步,元素入队但当前线程不会阻塞(相当于无界LinkedBlockingQueue的元素入队)
SYNC,同步,元素入队后当前线程阻塞,等待被匹配到
TIMED,有超时,元素入队后等待一段时间被匹配,时间到了还没匹配到就返回元素本身
nanos: 超时时间
所有的阻塞队列的插入操作(add, offer, put)传入该方法的参数都是一样的:
xfer(e, true, ASYNC, 0);
同时,LinkedTransferQueue还提供了一个立即返回的插入方法tryTransfer, 表示如果队列里没有在等待接收数据的节点,则直接返回,并不会阻塞线程,也不会把元素入队。调用xfer方法时传入的参数为:
xfer(e, true, NOW, 0)
LinkedTransferQueue融合了SynchronousQueue和LinkedBlockingQueue的优点: 既有高吞吐,又支持数组存储。
LinkedBlockingDeque
双端链表实现的有界双向阻塞队列,与LinkedBlockingQueue的实现差不多,只不过是双端进出的,所以只能用一个ReentrantLock来实现线程安全,这一点与ArrayBlockingQueue一样。
线程池
execute运行原理(线程池原理)
- 工作线程数小于核心线程数时,直接新建核心线程执行任务;
- 大于核心线程数时,将任务添加进等待队列;
- 队列满时,创建非核心线程执行任务;
- 工作线程数大于最大线程数时,拒绝任务
ThreadPoolExecutor
构建ThreadPoolExecutor所需要的参数:
int corePoolSize //核心线程数
int maximumPoolSize // 最大线程数
long keepAliveTime // 当线程数超过核心线程数之后,如果线程的空闲时间超过该值,将会被销毁;为0时表示不限制时间
TimeUnit unit // keepAliveTime的单位
BlockingQueue<Runnable> workQueue // 阻塞队列,用来保存提交的没来得及执行的任务
ThreadFactory threadFactory // 线程工厂,创建线程时使用,通常用于给线程命名
RejectedExecutionHandler handler // 拒绝策略,当阻塞队列满了,且线程数达到最大线程数之后,如果依然有任务提交,则会执行该策略。
上面的参数都很容易理解,workQueue可以从上面所讲的阻塞队列里挑选合适的,ThreadPoolExecutor提供了几种拒绝策略如下
- AbortPolicy,抛出RejectedExecutionException异常
- CallerRunsPolicy,在提交任务的线程中执行该任务
- DiscardPolicy,直接丢掉该任务
- DiscardOldestPolicy,丢掉最老的任务,也就是阻塞队列队首的任务。
我们也可以通过实现RejectedExecutionHandler接口来自定义自己的拒绝策略。
ScheduledExecutorService
可以执行定时任务的ExecutorService,其中一个用线程池实现的类是ScheduledThreadPoolExecutor,该类继承了ThreadPoolExecutor,通过将DelayQueue设为阻塞的工作队列来实现定时或延期执行。常用的方法如下:
/**
* 在指定的delay时间之后执行command,只执行一次
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 在initalDelay之后开始执行command,之后每隔period时间再次创建任务并执行,
* 任务的创建不会等待上一个任务完成,完全以上一个任务的开始时间为节点。
* 一直运行下去,直到抛出异常,或者被取消或终止。
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 在initialDelay开始执行第一个任务,执行完成之后,会再等待delay时间,
* 然后再创建并执行第二个,以此类推,直到抛出异常,或被取消或终止。
* 与scheduleAtFixedRate不同的是,该方法会等待上一个任务执行完成。
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
scheduleAtFixedRate与scheduleWithFixedDelay区别的实现,是在执行完之后如何设置下一个任务执行时间,其中scheduleAtFixedRate将下一个任务的时间设置为当前任务的执行时间time加上period,而scheduleWithFixedDelay是将下一个任务的执行时间设置为当前时间now()加上delay。都是在任务执行完之后为设置下一个任务的,只是下一个任务的时间不一样。所以,这也实现了上面所说的,如果一个任务抛出异常,将不会再有下一个任务。
Future与FutureTask
Future是一个接口,用来实现异步获取任务的执行结果,FutureTask是其中的一个具体实现。而FutureTask实现也比较好理解,封装了一些必要的属性来实现异步获取结果。
/* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
/** The underlying callable; nulled out after running */
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
ThreadLocal
为线程保存本地变量,通过ThreadLocalMap实现。每个Thread对象都有一个ThreadLocal.ThreadLocalMap类型的数据成员threadLocals,当用一个ThreadLocal对象调用set方法时,首先判断当前线程的threadLocals是否为null,如果为null,则创建一个ThreadLocalMap对象并赋值给threadLocals。然后以ThreadLocal对象为key,set方法的参数为值,将其保存到threadLocals里。
初始化
有两种方法可以用来创建一个ThreadLocal对象:
ThreadLocal<Integer> tl1 = new ThreadLocal<>();
ThreadLocal<Integer> tl2 = ThreadLocal.withInitial(()->randomInt());
这两种方法的区别在于一个是不带初始值的,另一个可以生成初始值。也就是在调用get方法的时候,如果当前线程还没有set一个值,那么ThreadLocal会检查有没有初始值,如果有的话就返回初始值,没有的话就返回null。但是withInitial这个静态方法,需要传入一个Supplier对象,那么当get不到一个值的时候,会执行一次Supplier的get方法,也就是执行我们传入的方法体,并将初始值put作为value添加到线程的threadLocals里,那么下次再通过该ThreadLocal对象get,就依然能得到这个初始值。也就是在同一个线程里,初始值不会变,但是不同的线程里设置的初始值依赖于传入的方法返回的值是不是固定的。
实际上上withInitial方法返回了ThreadLocal的一个子类SuppliedThreadLocal,这个类重写initialValue方法,在方法体里调用了Supplier的get方法。
public void set(T value)
将value保存到当前线程的threadLocals中,以当前的ThreadLocal对象为key,value为值。如果当前线程的threadLocals为null,则先创建一个ThreadLocalMap对象,并赋值给threadLocals,然后再调用map的set方法。
public T get()
从当前线程的threadLocals中获取当前的ThreadLocal对象对应的value,如果当前线程的threadLocals为null,或者找不到当前ThreadLocal对象为key的Entry,则会创建一个新的threadLocals,然后调用initialValue()方法获取一个初始值,将其保存到threadLocals里,并返回这个初始值。
ThreadLocal对象的hashCode
ThreadLocal用一个静态方法来为每一个ThreadLocal对象生成hashCode,保存在变量threadLocalHashCode中,在往map中set的时候并不是调用hashCode()方法,而是直接取变量值。
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocalMap
真正存储数据的就是这个数据结构,每个Thread对象里的threadLocals变量的类型。ThreadLocalMap对hash冲突的解决方案采用的开放地址法,而不是像HashMap中采用拉链法的形式。个人觉得原因是,设计者觉得用户不会用线程保存大量的变量,开放地址法更能节省内存,而且由于数组的capacity并不会很大,所以时间开销也很小。
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,将k转化为一个弱引用保存,便于垃圾回收,也能在一定程度上防止内存泄漏;而v依然是强引用,原因是v不可丢失,如果v是弱引用,在k未被回收之前,v先被回收了,那线程就获取不到之前保存的变量了,导致ThreadLocal是不可信任的,根本没法用。
内存泄漏问题
经常会有ThreadLocal是否会导致内存泄漏的问题,尤其是采用线程池的时候。因为ThreadLocal本质是将变量存储到Thread对象中,如果采用线程池,某些线程的生命周期可能会非常长(甚至贯穿服务的真个生命周期),这样的话,可能这个线程的threadLocals里保存了大量的数据,我们知道ThreadLocalMap中的key是弱引用,很容易被回收,但是value是强引用,除非手动释放,否则不会被回收,这样就有内存泄漏的风险。
实际上,ThreadLocalMap在防止内存泄漏上做了很多事,在get,set方法被调用的时候,都有可能去清除key已经被回收的entry,并且也可以通过ThreadLocal显式调用remove方法去remove当前这个ThreadLocal对象的entry,同时也会像get,set一样清除无效的entry。这就使得内存泄漏的几率大大降低,所以除非是那种我们往线程里set了一个很大的对象,但是从此再也没有对该线程用过ThreadLocal,这种情况下确实会导致内存泄漏,但这是编码的问题!!
可继承的ThreadLocal
Thread类中有两个保存ThreadLocalMap类型的变量
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在创建一个Thread对象childThread的时候,会将当前线程的inheritableThreadLocals的k,v复制给childThread的inheritableThreadLocals。也就是说这个inheritableThreadLocals变量是世代传承的,并不是传承自上一个线程的threadLocals!
InheritableThreadLoal
这个类继承自ThreadLocal,与ThreadLocal的不同是,ThreadLocal是操作线程的threadLocals变量,而InheritableThreadLocal操作线程的inheritableThreadLocals。所以,如果一个ThreadLocal对象对应的值是引用类型,那么通过InheritableThreadLocal修改这个值,父线程中的变量也会变。如下:
public void testInheritableThreadLocal() throws InterruptedException {
InheritableThreadLocal<String[]> itl = new InheritableThreadLocal<>();
String[] localValue = {"a", "b"};
itl.set(localValue);
// out: 1. parent thread:[a, b]
System.out.println("1. parent thread:" + Arrays.toString(itl.get()));
new Thread(() -> {
String[] childLocalValue = itl.get();
// out: 1. child1 thread:[a, b]
System.out.println("1. child1 thread:" + Arrays.toString(childLocalValue));
childLocalValue[0] = "c";
// out: 2. child1 thread:[c, b]
System.out.println("2. child1 thread:" + Arrays.toString(itl.get()));
}).start();
TimeUnit.SECONDS.sleep(1);
// out: 2. parent thread:[c, b]
System.out.println("2. parent thread:" + Arrays.toString(itl.get()));
new Thread(() -> {
itl.set(new String[]{"d", "e"});
// out: 1. child2 thread:[d, e]
System.out.println("1. child2 thread:" + Arrays.toString(itl.get()));
}).start();
TimeUnit.SECONDS.sleep(1);
// out: 3. parent thread:[c, b]
System.out.println("3. parent thread:" + Arrays.toString(itl.get()));
}
原子类
concurrent包中提供了一些对于基本类型封装的可以进行原子操作的类型,如下:
AtomicInteger: 提供对int类型的原子操作,是对基本类型封装的一个代表;
AtomicReference: 提供对引用类型的原子操作,但是并不是说可以原子地操作引用的对象里的字段,可以将引用原子地指向两一个对象;
AtomicIntegerArray: 提供对int数组的原子操作,可以原子地更新数组里的某个index上的值;
AtomicIntegerFieldUpdater: 原子地更新一个对象的int类型的field,这个int类型的field必须是被volatile修饰的;
AtomicMarkableReference: 需要传入一个reference和一个boolean类型的标志位,可以原子地更新reference和标志位;
AtomicStampedReference: 与AtomicMarkableReference类似,不过把标志位换成了一个int值,原子的更新reference和int值;
AtomicReferenceArray: 原子地更新一个reference类型的数组。
原子类都是通过Unsafe的方法实现的,用自旋+CAS的方式。
reference
- 强引用:Object o = new Object(), o就是强引用。
- 软引用:SoftReference,如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用:“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,不能通过get方法获取引用的对象(get始终返回null)。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。