Java并发相关知识点梳理和研究
1. 知识点思维导图
(图比较大,可以右键在新窗口打开)
2. 经典的wait()/notify()/notifyAll()实现生产者/消费者编程范式深入分析 & synchronized
注:本节代码和部分分析参考了你真的懂wait、notify和notifyAll吗。
看下面一段典型的wait()/notify()/notifyAll()代码,对于值得注意的细节,用注释标出。
import java.util.ArrayList;
import java.util.List;
public class Something {
private Buffer mBuf = new Buffer(); // 共享的池子
public void produce() {
synchronized (this) { // 注1、注2
while (mBuf.isFull()) { // 注3
try {
wait(); // 注4
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mBuf.add();
notifyAll(); // 注5、注6
}
}
public void consume() {
synchronized (this) { // 见注1、注2
while (mBuf.isEmpty()) { // 注3
try {
wait(); // 注4
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mBuf.remove();
notifyAll(); // 注5、注6
}
}
private class Buffer {
private static final int MAX_CAPACITY = 1;
private List innerList = new ArrayList<>(MAX_CAPACITY);
void add() {
if (isFull()) {
throw new IndexOutOfBoundsException();
} else {
innerList.add(new Object());
}
System.out.println(Thread.currentThread().toString() + " add");
}
void remove() {
if (isEmpty()) {
throw new IndexOutOfBoundsException();
} else {
innerList.remove(MAX_CAPACITY - 1);
}
System.out.println(Thread.currentThread().toString() + " remove");
}
boolean isEmpty() {
return innerList.isEmpty();
}
boolean isFull() {
return innerList.size() == MAX_CAPACITY;
}
}
public static void main(String[] args) {
Something sth = new Something();
Runnable runProduce = new Runnable() {
int count = 4;
@Override
public void run() {
while (count-- > 0) {
sth.produce();
}
}
};
Runnable runConsume = new Runnable() {
int count = 4;
@Override
public void run() {
while (count-- > 0) {
sth.consume();
}
}
};
for (int i = 0; i < 2; i++) {
new Thread(runConsume).start();
}
for (int i = 0; i < 2; i++) {
new Thread(runProduce).start();
}
}
}
- 注1:wait()/notify()/notifyAll()必须在synchronized块中使用
- 注2:使用synchronized(this)的原因是,这段代码的main(),是通过实例化Something的对象,并使用它的方法来进行生产/消费的,因此是一个指向this的对象锁。不同的场景,需要注意同步的对象的选择。
- 注3:必须使用while循环来包裹wait()。设想一种场景:存在多个生产者或多个消费者消费者,以多个生成者为例,在缓冲区满的情况下,如果生产者通过notify()唤醒的线程仍是生产者,如果不使用while,那么获取锁的线程无法重新进入睡眠,锁也不能释放,造成死锁。
- 注4:wait()会释放锁
- 注5:notfiy()、notifyAll()会通知其他在wait的线程来获取锁,但是获取锁的真正时机是锁的原先持有者退出synchronized块的时候。
- 注6:使用notifyAll()而不是notfiy()的原因是,仍考虑注3的场景,假如生产者唤醒的也是生产者,后者发现缓冲区满重新进入阻塞,此时没有办法再唤醒在等待的消费者线程了,也会造成死锁。
- 注7:notfy()在JVM规范里是随机唤醒一个,但是在Hotspot里的实现是先进先出的 13.1 大佬问我: notify()是随机唤醒线程么?
扩展知识点1:synchronized块的两个队列
synchronized入口是将线程放入同步队列,wait()是将线程放入阻塞队列。notify()/notifyAll()实际上是把线程从阻塞队列放入同步队列。wait/notify/notifyAll方法需不需要被包含在synchronized块中,为什么?
扩展知识点2:synchronized重入原理
synchronized是可重入的,原理是它内部包含了一个计数器,进入时+1,退出时-1。 Java多线程:synchronized的可重入性
扩展知识点3:作用范围
synchronized支持三种用法:修饰静态方法、修饰实例方法、修饰代码块,前两种分别锁类对象、锁对象实例,最后一种根据传入的值来决定锁什么。
synchronized是基于java的对象头实现的,从字节码可以看出包括了一对进入&退出的监视器。
深入理解Java并发之synchronized实现原理
扩展知识点4:分布式环境synchronized的意义
单看应用所运行的的单个宿主机,仍然可能有多线程的处理模式,在这个前提下使用并发相关技术是必须的。
扩展知识点5:哪些方法释放资源,释放锁
所谓资源,指的是系统资源。
wait(): 线程进入阻塞状态,释放资源,释放锁,Object类final方法(notify/notifyAll一样,不可改写)。
sleep(): 线程进入阻塞态,释放资源,(如果在synchronized中)不释放锁,进入阻塞状态,唤醒随机线程,Thread类静态native方法。
yield(): 线程进入就绪态,释放资源,(如果在synchronized中)不释放锁,进入可执行状态,选择优先级高的线程执行,Thread类静态native方法。
如果线程产生的异常没有被捕获,会释放锁。
sleep和yield的比较
可以进一步地将阻塞划分为同步阻塞——进入synchronized时没获取到锁、等待阻塞——wait()、其他阻塞——sleep()/join(),可以参考线程的状态及sleep、wait等方法的区别
再进一步地,Java线程状态转移可以用下图表示(图源《Java 并发编程艺术》4.1.4 节)
WAITING状态的线程是不会消耗CPU资源的。
3. 线程数调优
理论篇
本节参考了《Java并发编程实战》8.2节,也可以结合面试问我,创建多少个线程合适?我该怎么说帮助理解,其中的计算题比较有价值。
前置知识
I/O密集型任务:I/O任务执行时CPU空闲。
CPU密集型任务:进行计算
有的任务是二者兼备的。为了便于分析,不考虑。
定性分析
场景:单核单线程/单核多线程/多核多线程。单核多线程+CPU密集型不能提升执行效率,多核+CPU密集型任务可以;单核多线程+I/O密集型可以提升执行效率。
因此,I/O耗时越多,线程也倾向于变多来充分利用IO等待时间。
定量分析
对于CPU密集型,线程数量=CPU 核数(逻辑)
即可。特别的,为了防止线程在程序运行异常时不空转,额外多设一个线程线程数量 = CPU 核数(逻辑)+ 1
对于I/O密集型,最佳线程数 = CPU核数 * (1/CPU利用率) = CPU核数 * (1 + I/O耗时/CPU耗时)
为什么CPU利用率=1/(1+ I/O耗时/CPU耗时)
?简单推导一下:
1/(1+ I/O耗时/CPU耗时) = 1/((CPU耗时+I/O耗时)/ CPU耗时) = CPU耗时/总耗时 = CPU利用率
如何获取参数——CPU利用率?
因为利用率不是一成不变的,需要通过全面的系统监控工具(如SkyWalking、CAT、zipkin),并长期进行调整观测。
可以先取2N即2倍核数,此时即假设I/O耗时/CPU耗时=1:1,再进行调优。
阿姆达尔定律
CPU并发处理时性能提升上限。
S=1/(1-a+a/n)
其中,a为并行计算部分所占比例,n为并行处理结点个数。
简单粗暴理解【阿姆达尔定律】
Java线程池篇
基本属性
/**
* 使用给定的初始参数和默认线程工厂创建一个新的ThreadPoolExecutor ,并拒绝执行处理程序。 使用Executors工厂方法之一可能更方便,而不是这种通用构造函数。
参数
* corePoolSize - 即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut
* maximumPoolSize - 池中允许的最大线程数
* keepAliveTime - 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最大时间。
* unit - keepAliveTime参数的时间单位
* workQueue - 在执行任务之前用于保存任务的队列。 该队列将仅保存execute方法提交的Runnable任务。
* threadFactory - 执行程序创建新线程时使用的工厂
*/
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
常见线程池
由java.util.concurrent.Executors创建的线程池比较常用,而不是使用ThreadPoolExecutor的构造方法。
注意:有的不带参数的方法,默认最大大小是Integer.MAX_VALUE,可能导致资源耗尽,尽量不要用。
名称 | 特性 | corePoolSize | maximumPoolSize | keepAliveTime + unit | workQueue | threadFactory |
---|---|---|---|---|---|---|
newFixedThreadPool | 线程池大小为固定值 | nThreads | nThreads | 0 ms | LinkedBlockingQueue | DefaultThreadFactory |
newSingleThreadExecutor | 线程池大小固定为1 | 1 | 1 | 0 ms | LinkedBlockingQueue | DefaultThreadFactory |
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60L s | SynchronousQueue | DefaultThreadFactory | |
newScheduledThreadPool | 延迟执行任务或按周期重复执行任务 | 0 | Integer.MAX_VALUE | 0 ns | DelayedWorkQueue | DefaultThreadFactory |
workQueue分析:
-
SynchronousQueue 不“直接”持有元素,而是持有线程,让生产者线程和消费者线程配对,无法配对的就会阻塞
- 支持先进先出和先进后出(栈)两种模式
-
LinkedBlockingQueue 缓冲队列
- 不设置容量则为Integer.MAX_VALUE
- put和get各有一个非公平ReentrantLock和对应的Condition
-
DelayedWorkQueue
- ScheduledThreadPoolExecutor的静态内部类,Worker会不断地从这里取任务执行
- 堆,如果未到时间,poll()返回null,这样Worker就不会执行了
- 新插入的元素是堆头时会通过Condition avaliable唤醒等待的线程。这个avaliable保证线程不会无限循环,而是阻塞在锁这里。
-
ArrayBlockingQueue 缓冲数组队列
- 只有一个ReentrantLock和两个Condition,因此不能put和get并行
- 可设置公平和非公平
- 必须指定大小
其他:
ArrayBlockingQueue
线程工厂的作用
用来创建线程,统一在创建线程时设置一些参数,如是否守护线程。线程一些特性等,如优先级。
可参考004-多线程-JUC线程池-ThreadFactory线程工厂
多线程的异常处理
子线程的异常,主线程不会处理。默认只会将子线程的堆栈打印到console上,这一步也不是主线程做的。
如果想要处理,可以通过以下方式:
- 设置线程的异常处理器即Thread.setDefaultUncaughtExceptionHandler()。线程池下可以用线程工厂批量设置。
- extends线程池,重写afterExecuteafterExecute(Runnable r, Throwable t)这个方法。默认是空的
- 使用future,在get()时处理。但是get()只会抛出InterruptedException、ExecutionException这两种异常。
4. 并发容器相关
并发容器可以说是一个面试时的高频问题了,网络上也有很多介绍,这里就不重复解读,将相关的知识整理一下,边看源码边读文章效果会很好。
先提一句,Vector是线程安全的,为啥现在不推荐用呢?看源码可以知道,它将大部分方法都加了synchronized,牺牲了性能换取线程安全,是不可取的。如果真的有需要线程安全的容器,可以用Collections.synchronizedList()来手动给list加synchronized。
再补充一句,其实Vector和Collections.synchronizedList()使用复合操作或迭代器Iterator时也不是线程安全的,具体解释会在下一篇博客Java容器中介绍。
ConcurrentHashMap
先重点介绍Map的两个实现类HashMap和ConcurrentHashMap
- HashMap和ConcurrentHashMap HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!
- HashMap扩容原理:HashMap的扩容机制---resize()
- 多线程下HashMap扩容resize可能导致链表循环
- 这两个数据结构在JDK1.7到1.8时,当数目达到一个阈值时,都从链表改用了红黑树
- HashMap的node重写了equals方法来比较节点。Objects.equals会调用Object的equals,对于Object实现类则是实现类自己的equals。
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
ConcurrentLinkedQueue
ConcurrentLinkedQueue使用CAS无锁操作,保证入队出队的线程安全,但不保证遍历时的线程安全。遍历要想线程安全需要单独加锁。
由于算法的特性,这个容器的尾结点是有延迟的,tail不一定是尾节点,但p.next == null的节点一定是尾结点。
入队出队操作很抽象,需要画图帮助理解源码,对应的源码分析可参考并发容器-ConcurrentLinkedQueue详解。
CopyOnWriteArrayList和CopyOnWriteArraySet
简单来说,CopyOnWriteArrayList底层是一个volatile的数组加一个ReentrantLock,所有写操作(add、set等)都会先锁,处理时copy一个新数组出来,处理完用新数组替换掉原来持有的老数组,这样确保读是可以同时进行的(读的是老数据)。
这样做确保写操作是线程安全的,而读操作是不需要锁的。
相比普通的ArrayList,读性能差不多,写的性能自然会差一点。
(其实之前的项目中,热更新配置里我的做法是类似的,监听到配置中心的热更新事件时,先将配置组装好,再替换掉原来配置对象的引用。)
CopyOnWriteArraySet底层是一个CopyOnWriteArrayList,只有在元素不存在时才会插入。
可以参考:
https://www.cnblogs.com/skywang12345/p/3498483.html#p1
https://www.cnblogs.com/skywang12345/p/3498497.html
5. AQS解读
抽象队列同步器AbstractQueuedSynchronizer(AQS)是JUC中很多并发工具类的基础,用来抽象各种并发控制行为,如ReentranLock、Semaphore。顺便一提,线程池的工作线程模型Worker也实现了AQS。
之前试着直接读源码,效果不太好,还是建议结合质量较高的文章来读,这里推荐一篇:Java并发之AQS详解,并且作者还在不断更新。
这里简单记录一下总结的点。
结构特点
- volatile int state标记位,标识当前的同步状态。具体的用法和使用AQS的工具类有关。同时,在做CAS的时候,state的状态变更是通过计算该变量在对象的偏移量来设置的。
- CLH队列。CLH锁(Craig,Landin andHagersten)是一种在SMP(Symmetric Multi-Processor对称多处理器)架构下基于单链表的高性能的自旋锁,队列中每个节点代表一个自旋的线程,每个线程只需在代表前一个线程的节点上的布尔值locked自旋即可,如图
图源和CLH的详解见算法:CLH锁的原理及实现 - exclusiveOwnerThread独占模式的拥有者,记录现在是哪个线程占用这个AQS。
操作特点
- 对state使用>0和<0的判断,初看代码很难看懂,这么写的原因是
负值表示结点处于有效等待状态,而正值表示结点已被取消
。 - 大量的CAS:无论是获取锁、入队、获取锁失败后的自旋,全部是依赖CAS实现的。
- 没有使用synchronized:不难理解,如果使用了同步块,那么其实现ReentranLock就没有和synchronized比较的价值了。不过这一点很少有文章专门提到。
- LockSupport类的unpark()/park()方法的使用:回忆上文提到的线程状态,如果线程获取不到AQS控制的资源,需要将线程置于waiting,对应可选的方法是wait()/join()/park()。在AQS这个场景下,显然一没有synchronized,二没有显式的在同一个代码块中用join处理多线程(借助队列来处理线程,线程相互之间不感知),那么只有park()才能达到目的。
处理流程
获取资源acquire(int)
- 尝试获取资源(改写state),成功则返回
- CAS(失败则自旋)加入等待队列队尾
- 在队列中自旋,尝试获取一次资源(前提:队头+ tryAcquire()成功),每次失败都会更改线程状态为waiting。自旋时会看看前驱有没有失效的节点(即不再请求资源的),如果有就插队到最前面并把前面无效节点清理掉便于gc
- waiting状态中不响应中断,获取资源后才会补一个自我中断selfInterrupt (调用Thread.currentThread().interrupt())
释放资源release(int)
- 尝试释放,成功则处理后续动作,失败直接返回false
- 唤醒(unpark)等待队列的下一个线程。如果当前节点没找到后继,则从队尾tail从后往前找。
共享模式获取资源acquireShared(int)
除了抽象方法tryAcquireShared()以外,基本和acquire(int)一致。
在等待队列中获取资源后,会调用独有的setHeadAndPropagate()方法,将这个节点设为头结点的同时,检查后续节点是否可以获取资源。
共享模式释放资源releaseShared()
和release(int)区别在于,唤醒后继时,不要求当前线程节点状态为0。举例:当前线程A原先拥有5个资源,释放1个,后继的等待线程B刚好需要1个,那么此时A、B就可以并行了。
未实现的方法
为了便于使用AQS的类更加个性化,AQS有一下方法直接抛UnsupportedOperationException。
- isHeldExclusively()
- tryAcquire()
- tryRelease()
- tryAcquireShared()
- tryReleaseShared()
不写成abstract方法的原因是,避免强迫不需要对应方法的类实现这些方法。比如要写一个独占的锁,那么就不需要实现共享模式的方法。
AQS小结
读完源码总结一下,AQS是一个维护资源和请求资源的线程之间的关系的队列。对于资源(有序或无序的)获取和释放已经提取成了线程的出入队方法,这个队列同时维护上线程的自旋状态和管理线程间的睡眠唤醒。
应用
本节可以看作为《JAVA并发变成实战》14.6的引申。
ReentrantLock
用内部类Sync实现AQS,Sync实现ReentrantLock的行为。Sync又有FairSync和UnfairSync两种实现。FairSync,lock对应aquire(1);UnfairSync,lock先CAS试着获取一次,不行再aquire(1)。
实际上,ReentrantLock的公平/非公平锁只在首次lock时有区别,入队后唤醒仍是按顺序的。可以参考reentrantLock公平锁和非公平锁源码解析
Sync只实现了独占模式。
注意:CyclicBarrier直接用了ReentrantLock,没有直接用AQS。
Semaphore
和ReentrantLock类似,Semaphore也有一个内部类Sync,但相反的是这个Sync只实现了共享模式的acquire()/release()。
Semaphore在acquire()/release()时会计算资源余量并设置,其中unfair模式下的acquire会无条件自旋CAS,fair模式下只有在AQS里不存在排队中的后继的情况下才会CAS,否则自旋。
CountDownLatch
同样有一个内部类Sync,但是不再区分fair/unfair,并且是共享模式的。
await()调用的是acquireSharedInterruptibly(),自然也存在自旋的可能,只是编程时一般不这么用。countDown()时释放一个资源继续在releaseShared()里自旋直到全部释放。
FutureTask
新版的FutureTask已经重写,不再使用AQS,这里就不再提了。
ReentrantReadWriteLock
可重入读写锁,涉及到锁升级,这里没有研究的很透彻,有兴趣可以自行了解。
注意到读锁和写锁是共用同一个Sync的。
线程池的Worker
Worker本身也是Runable,同时也利用AQS获取锁的逻辑。
6 JMM到底是个啥?
(这部分202408进行了重写)
即Java内存模型(Java Memory Model),是Java语言规范的一部分,它定义了Java程序中各种变量(线程共享变量)的访问规则,以及在并发环境下,这些变量的读写操作如何保证原子性、可见性和有序性。JMM主要解决以下几个关键问题:
- 原子性:确保复合操作(如自增操作)在多线程环境中作为一个不可分割的单元执行,要么全部执行,要么全部不执行。
- 可见性:确保当一个线程修改了线程共享变量的值,其他线程能够立即看到这个修改。
- 有序性:确保在单线程环境中,代码的执行顺序与编写顺序一致;在多线程环境中,通过happens-before原则来保证操作的相对顺序。
- happens-before原则:定义了两个操作之间的因果关系,如果操作A happens-before 操作B,那么A的结果对B可见。
- 内存屏障:通过内存屏障来控制指令的执行顺序,防止编译器和处理器对内存操作进行不合理的重排序。实现方式在不同平台不同,比如Java的volatile、某些CPU指令等。内存屏障又分为:
- 读屏障:确保所有在读屏障之前的读操作完成后,才能执行读屏障之后的读操作。
- 写屏障:确保所有在写屏障之前的写操作完成后,才能执行写屏障之后的写操作。
- 全屏障:同时具有读屏障和写屏障的功能,确保所有在全屏障之前的读写操作完成后,才能执行全屏障之后的读写操作。
happens-before
happens-before原则是Java内存模型(JMM)中的一个核心概念,它定义了在多线程环境中,一个操作何时对另一个操作可见。简单来说,如果一个操作A happens-before 另一个操作B,那么A的结果对B必须是可见的。这意味着:
-
数据一致性:在多线程环境中,当一个线程修改了共享变量的值,并且这个修改对其他线程可见,那么其他线程在执行后续操作时必须看到这个修改后的值。
-
操作顺序:happens-before原则确保了在程序的执行中,某些操作的顺序性。例如,在一个线程中,对共享变量的写操作必须在对该变量的读操作之前完成。
happens-before原则的常见情况包括:
- 程序顺序规则:在单个线程内,按照代码的书写顺序执行操作。
- 监视器锁规则:对同一个监视器锁的解锁操作happens-before于随后对这个监视器锁的加锁操作。
- volatile变量规则:对volatile变量的写操作happens-before于后续对这个变量的读操作。
- 线程启动规则:线程的start()方法happens-before于线程中的任何动作。
- 线程终止规则:线程中的任何动作都happens-before于其他线程检测到该线程的终止。
- 中断规则:线程的interrupt()方法happens-before于检测到中断的代码。
- 对象final字段规则:被声明为final的字段,在构造函数完成后,其值对于其他线程是可见的。
happens-before原则是JMM用来防止编译器和处理器对代码进行重排序的一种机制,它确保了在多线程环境中,即使代码被重排序,也不会破坏程序的内存一致性。理解并正确应用happens-before原则对于编写正确、高效的并发程序至关重要。
既然有JMM,为什么volatile的一项功能是禁止指令重排序?
volatile除了禁止指令重排序外还有其它功能:多线程环境下的可见性、防止内存屏障优化等,具有更强的功能。
7. 短篇话题整理总结
7.1 ThreadLocal的用法总结
应用场景:在多线程下替代类的静态变量(static),在多线程环境进行单个 类 的数据隔离。
为什么推荐使用static修饰ThreadLocal?
这时才能保证"一个线程,一个ThreadLocal",否则便成了“一个线程,(多个对象实例时)多个ThreadLocal”。
可能会有内存泄漏:ThreadLocalMap的Entry是弱引用,但指向的对象value不是,如果Entry被回收,value还在。解法是手动remove掉。
(本节参考了《Java并发编程实战》)
引申话题:gc时ThreadLocal的弱引用被回收,是否会造成程序bug?
答案是可以避免。
先回顾下各种引用 Java:强引用,软引用,弱引用和虚引用
然后再结合场景分析JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
7.2 CountDownLatch和CyclicBarrier区别
https://blog.csdn.net/tolcf/article/details/50925145
CountDownLatch的子任务调用countDown后会继续执行直至该线程结束。
CyclicBarrier的子任务await时会暂停执行;可重复使用,即await的数目达到设置的值时,唤醒所有await的线程进行下一轮。
7.3 ReentrantLock用了CAS但为什么不是乐观锁?
https://blog.csdn.net/qq_35688140/article/details/101223701
我的看法:因为仍有可能造成阻塞,而乐观锁更新失败则会直接返回(CAS允许自旋)。
换一个角度,悲观锁是预先做最坏的设想——一定会有其他任务并发,那么就先占好坑再更新;乐观锁则是认为不一定有并发,更新时判断再是否有问题。这样看来ReentrantLock从使用方式上来说是悲观锁。
7.4 双重检查加锁
public classDoubleCheckedLocking{ //1
private static Instance instance; //2
public staticI nstance getInstance(){ //3
if(instance==null){ //4:第一次检查
synchronized(DoubleCheckedLocking.class){ //5:加锁
if(instance==null) //6:第二次检查
instance=newInstance(); //7:问题的根源出在这里
} //8
}//9
return instance;
}
}
问题
一个线程看到另一个线程初始化该类的部分构造的对象,即以上代码注释第4处这里读到非null但未完全初始化
原因
注释第7处,创建对象实例的三步指令1.分配内存空间2.初始化3.引用指向分配的地址,2和3可能重排序
解决
方案1,给instance加violatile,确保其他线程看到的是初始化完成的对象
方案2,使用占位类,在类初始化时初始化对象,如下(实际上饿汉模式都可以解决)
public class InstanceFactory {
private static class InstanceHolder{
public static Instance instance= newInstance();
}
public static Instance getInstance() {
return InstanceHolder.instance; //这里将导致InstanceHolder类被初始化
}
}
7.5 FutureTask
FutureTask是Future的实现类,可以使用Future来接收线程池的submit()方法,也可以直接用FutureTask封装任务,作为submit()的参数。具体的用法可以参考Java并发编程:Callable、Future和FutureTask 。
新版的FutureTask不再使用AQS。
FutureTask设置了当前工作线程,对于其任务维护了一个内部状态转换状态机,通过CAS做状态判断和转换。
当其他线程来get()时,如果任务未完成则放入等待队列,自旋直到取到结果(for循环+LockSupport.park()),否则直接取结果。
具体实现原理可以参考《线程池系列一》-FutureTask原理讲解与源码剖析。
7.6 JDK1.6锁优化之轻量级锁和偏向锁
实际上二者是有联系的,都是基于mark word实现。这个转换关系可以用《深入理解Java虚拟机》第十三章的插图表现
但是这个图没有体现轻量级锁释放后,仍可恢复为可偏向的。
7.7 问题排查三板斧
- top查看内存占用率,-H可以看线程(不会完整展示),-p [pid]看指定进程的线程
注意:linux线程和进程id都是在pid这一列展示的。 - pstack跟踪进程栈,strace查看进程的系统操作。多次执行pstack来观察进程是不是总是处于某种上下文中。
- jps直接获取java进程id,jstat看java进程情况。jstate可用不同的参数来查看不同纬度的信息:类加载情况、gc统计、堆内存统计、新生代/老年代内存统计等,具体可以参考【JVM】jstat命令详解---JVM的统计监测工具
- jstack打印java线程堆栈,和pstack展示方式很像,是java纬度的
- jmap打印java内存情况,-dump可以生成dump文件
- 分析dump文件,如MAT
7.8 Thread.join()原理
根据源码可以发现,join()实质是判断对应线程是否还在存活,如果还在存活会一直空转(指定了超时时间则只空转这么久)。
再加上注释:线程终结时会notifyAll(),唤醒在join()中调用wait()的等待中的线程。
同时,这里是需要synchronized的。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) { // isAlive是native方法
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
只看源码,是有可能在调用join()前线程就完结的。
7.9 Future和FutureTask
- Future是一个(将来的)执行结果的抽象封装,可以通过接口判断是否执行完成、取得执行结果。持有Future的引用,在编码过程中会清晰一些,不需要关心具体执行的对象(如线程池)的状态,也不需要使用具体执行对象的API来获取结果。
- FutureTask是RunnableFuture的实现,RunnableFuture继承了Future和Runnable,封装了执行内容(一个Callable)、执行状态、执行结果,需要手动启动。
- CompletableFuture是JDK1.8新增的,用以支持函数式编程下stream并发处理流式数据的场景,内部默认使用ForkJoinPool。可以阅读实践:使用了CompletableFuture之后,程序性能提升了三倍的示例代码及CompletableFuture基本用法。
关于ForkJoinPool,又可以单独写文章了,请自行了解。
7.10 Runable和Callable的区别
(本节由kimi生成)
Callable
和 Runnable
都是 Java 中用于创建线程的接口,但它们之间存在一些关键的区别:
-
返回值:
Callable
接口的实现可以返回一个类型为V
的泛型返回值,通过Future
接口获取。Runnable
接口不接受返回值。
-
异常:
Callable
可以抛出 checked 或 unchecked 异常。Runnable
只能抛出 unchecked 异常(即RuntimeException
或其子类)。
-
方法签名:
Callable
接口的call()
方法声明了返回类型V
。Runnable
接口的run()
方法没有返回类型,即void
。
-
Future
接口:Callable
任务可以通过ExecutorService
提交,返回一个实现了Future
接口的对象,允许调用者获取任务的结果或取消任务。Runnable
任务没有返回Future
对象,因此不能获取任务的结果或取消任务。
-
用法:
Callable
适用于需要执行后返回结果的任务。Runnable
适用于不需要返回结果的后台任务。
-
实现方式:
- 实现
Callable
接口的类必须实现call()
方法。 - 实现
Runnable
接口的类必须实现run()
方法。
- 实现
-
线程执行模型:
Callable
任务可以与Future
结合使用,提供了一种异步执行任务的方式,可以在任务执行完毕后获取结果。Runnable
任务通常用于简单的线程执行,不提供结果获取或任务取消的功能。
以下是 Callable
和 Runnable
的简单示例:
// Runnable 示例
Runnable runnable = new Runnable() {
@Override
public void run() {
// 执行一些操作
}
};
// Callable 示例
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 执行一些操作并返回结果
return 123;
}
};
在实际应用中,选择 Callable
还是 Runnable
取决于任务是否需要返回结果以及是否需要异常处理。如果任务需要返回结果或抛出异常,Callable
是更好的选择;如果任务不需要返回结果,可以使用 Runnable
。
在Java中,Callable
和Runnable
接口都是通过线程池(ExecutorService
)来管理任务的提交和执行的。尽管它们在功能上有所不同,但线程池对它们的管理方式有相似之处。以下是Callable
和Runnable
在线程池中的管理方式:
-
任务提交:
Callable
任务通过ExecutorService
的submit(Callable<T> task)
方法提交,返回一个Future<T>
对象。Runnable
任务通过ExecutorService
的submit(Runnable task)
或execute(Runnable command)
方法提交,返回一个Future<?>
对象。
-
任务调度:
- 线程池接收到任务提交请求后,会根据其内部的调度算法将任务分配给线程池中的一个线程。
-
任务执行:
- 对于
Callable
任务,线程池中的线程会调用Callable
对象的call()
方法,并处理其返回结果或抛出的异常。 - 对于
Runnable
任务,线程会调用Runnable
对象的run()
方法执行任务。
- 对于
-
任务结果处理:
Callable
任务的结果可以通过返回的Future
对象获取,支持取消任务、查询任务状态、等待任务完成等操作。Runnable
任务由于没有返回值,其Future
对象主要用于任务取消和查询状态。
-
异常处理:
Callable
允许捕获和抛出异常,包括受检异常和非受检异常。异常可以在Future
对象上通过get()
方法被捕获。Runnable
只能抛出非受检异常(RuntimeException
及其子类)。如果需要处理异常,必须在Runnable
内部捕获并处理。
-
生命周期管理:
- 线程池负责管理任务的生命周期,包括任务的排队、执行、完成或取消。
-
线程重用:
- 线程池中的线程在执行完一个任务后,不会立即销毁,而是可以被重用来执行其他任务。
-
关闭线程池:
- 当不再需要线程池时,可以调用
shutdown()
或shutdownNow()
方法来关闭线程池,等待任务完成或尝试停止正在执行的任务。
- 当不再需要线程池时,可以调用
以下是使用线程池提交Callable
和Runnable
任务的示例:
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 提交Callable任务
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 执行一些操作并返回结果
return 123;
}
});
// 提交Runnable任务
executorService.submit(new Runnable() {
@Override
public void run() {
// 执行一些操作
}
});
// 关闭线程池
executorService.shutdown();
8. LeetCode多线程习题
原题目和详解参考Concurrency - 力扣
1114.按序打印
按照指定次序完成一系列动作,可以看做是buffer为1的1对1生产者消费者模型。
1115.交替打印FooBar
交替执行(不完全是生产者-消费者模型)某些动作。
可用的解法:
- synchronized
- Semaphore
- CountDownLatch
- CyclicBarrier
- Lock
1116.打印零与奇偶数:0102...
和1114类似
1188. 设计有限阻塞队列
注意: 使用synchronize解法时,wait()应置于while中循环判断.
如果只用if,唤醒后不再次判断dequeue可能NPE
本题可以加深理解为什么要用while
1195. 交替打印字符串
根据AC的解法推断, 每个线程只调用对应方法一次,因此需要在方法内部循环
不推荐只用synchronized,四个线程按顺序打印, 如果使用单一的锁很容易饥饿导致超时
推荐解法:
AtomicInteger无锁解法
CylicBarrier高效解法
Semaphore加锁
1279. 红绿灯路口
题目难懂,暗含条件:车来时红绿灯不是绿的,则强制变绿通过。红绿灯本身的时间没有严格控制
延伸阅读
什么是分布式锁
一文了解分布式锁
ThreadPoolExcutor 原理探究
9. 未展开的话题
并发研究之CPU缓存一致性协议(MESI)
线程池原理(四):ScheduledThreadPoolExecutor
一半是天使一半是魔鬼的Unsafe类详解 —— unsafe类都有什么?用偏移量直接访问、线程操作、内存管理和内存屏障、CAS
10. 其他参考
作者:五岳
出处:http://www.cnblogs.com/wuyuegb2312
对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。