目录
3.3tryLock(long time, TimeUnit unit)
3.4 lockInterruptibly() 可以响应中断
5.2 ConcurrentHashMap1.7和和1.8的区别
7.4.3 利用CompletionService快速实现 Forking 集群模式
1.volatile关键字
1.1 保证多个线程运行时的可见性问题
- 造成可见性问题是因为,所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据,当修改完毕后,再把修改后的结果放回到主内存中,线程间变量值的传递需要通过主内存来完成。
- 在单线程的环境下是没有问题的,但在多线程的环境下可能会因为没有第一时间把工作内存中更新过的共享变量刷新到主内存中而出现脏数据,volatile的作用就是解决这个问题
1.2 禁止指令重排
禁止指令重排就是代码书写的顺序与实际执行的顺序不同,指令重排序是编译器为了提供程序的性能而做的优化
典型的应用示例就是双重检查锁实现一个线程安全的单例模式。
用volatile关键字修饰 singleton 对象,禁止指令重排
在 singleton = new Singleton(); 创建对象的时候,实际执行了三步
- 给 singleton 分配内存空间;
- 调用 Singleton 的构造函数等,来初始化 singleton;
- 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
2和3之间,可能会被重排序,对象指向了分配的内存地址,但是还没有初始化,直接造成空指针异常
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
2 synchronized 关键字
能够保证在同一时刻最多只有一个线程执行该代码,以达到保证并发安全的效果
2.1用法
分别是对象锁和类锁
对象锁:
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
类锁:
指synchronizd修饰静态的方法或指定锁为Class对象,Java类可能有多个对象,但只有1个Class对象
类锁有两种形式:
形式一:synchronizd加在static方法上
形式二:synchronizd(*.class)代码块
2.2 实现原理
- Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的
- monitor 有两个指令monitorenter和monitorexit,可以理解为加锁和解锁,当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的所有者。
- 它是可重入的,如果你已经是这个monitor的所有者了,你再次进入,就会把进入数+1。同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
- 其实,就是看你能否获得monitor的所有权
2.3 锁升级过程
无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁
- 偏向锁的思想就是如果自始至终,这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
- JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
- 重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
- JDK 1.6中的synchronized 就是重量级的锁
3.Lock锁
Lock 接口加解锁相关的主要有 5 个方法
- lock()
- tryLock()
- tryLock(long time, TimeUnit unit)
- lockInterruptibly()
- unlock()
3.1Lock()
- Lock() 是最基础的获取锁的方法。在线程获取锁时如果锁已被其他线程获取,则进行等待。Lock获取锁和释放锁都是显式的,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中使用unlock()方法释放锁,以保证发生异常时锁一定被释放
- 如果我们不遵守在 finally 里释放锁的规范,这样就会有问题,因为你不知道未来什么时候由于异常的发生,导致跳过了 unlock() 语句,使得这个锁永远不能被释放了,其他线程也无法再获得这个锁。以前有个大哥就因为没释放,造成了线上问题
代码示例:
Lock lock = new ReentrantLock();
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
lock.unlock(); //释放锁
}
3.2tryLock()
- lock() 方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock() 就会陷入永久等待,所以一般我们用 tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败.
- tryLock() 方法可以解决死锁问题
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
} else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
如果代码中我们不用 tryLock() 方法,那么便可能会产生死锁,比如有两个线程同时调用这个方法,传入的 lock1 和 lock2 恰好是相反的,那么如果第一个线程获取了 lock1 的同时,第二个线程获取了 lock2,它们接下来便会尝试获取对方持有的那把锁,但是又获取不到,于是便会陷入死锁,但是有了 tryLock() 方法之后,我们便可以避免死锁的发生,首先会检测 lock1 是否能获取到,如果能获取到再尝试获取 lock2,但如果 lock1 获取不到也没有关系,我们会在下面进行随机时间的等待,这个等待的目标是争取让其他的线程在这段时间完成它的任务,以便释放其他线程所持有的锁,以便后续供我们使用,同理如果获取到了 lock1 但没有获取到 lock2,那么也会释放掉 lock1,随即进行随机的等待,只有当它同时获取到 lock1 和 lock2 的时候,才会进入到里面执行业务逻辑,比如在这里我们会打印出“获取到了两把锁,完成业务逻辑”,然后方法便会返回。
3.3tryLock(long time, TimeUnit unit)
- tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。
- 这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;在等待的期间,也可以随时中断线程,这就避免了死锁的发生。
3.4 lockInterruptibly() 可以响应中断
- 我们可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过lockInterruptibly() 永远不会超时。
- 这个方法本身是会抛出 InterruptedException 的,所以使用的时候,如果不在方法签名声明抛出该异常,那么就要写两个 try 块
public void lockInterruptibly() {
try {
lock.lockInterruptibly();
try {
System.out.println("操作资源");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.5unLock()
unlock() 方法,用于解锁,对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数
4.Java锁的分类
4.1悲观锁和乐观锁
- 悲观锁比较悲观,它认为如果不锁住这个资源,别的线程就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失。
- Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想。
- 乐观锁比较乐观,认为自己在操作资源的时候不会有其他线程来干扰,所以并不会锁住被操作对象,不会不让别的线程来接触它,同时,为了确保数据正确性,在更新之前,会去对比在我修改数据期间,数据有没有被其他线程修改过:如果没被修改过,就说明真的只有我自己在操作,那我就可以正常的修改数据;如果发现数据和我一开始拿到的不一样了,说明其他线程在这段时间内修改过数据,那说明我迟了一步,所以我会放弃这次修改,并选择报错、重试等策略。
- 乐观锁的实现一般都是利用 CAS 算法实现的
4.2公平锁和非公平锁
公平锁指的是按照线程请求的顺序,来分配锁;而非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队
4.2.1为什么要有非公平锁
让我们考虑一种情况,假设线程 A 持有一把锁,线程 B 请求这把锁,由于线程 A 已经持有这把锁了,所以线程 B 会陷入等待,在等待的时候线程 B 会被挂起,也就是进入阻塞状态,那么当线程 A 释放锁的时候,本该轮到线程 B 苏醒获取锁,但如果此时突然有一个线程 C 插队请求这把锁,那么根据非公平的策略,会把这把锁给线程 C,这是因为唤醒线程 B 是需要很大开销的,很有可能在唤醒之前,线程 C 已经拿到了这把锁并且执行完任务释放了这把锁。相比于等待唤醒线程 B 的漫长过程,插队的行为会让线程 C 本身跳过陷入阻塞的过程,如果在锁代码中执行的内容不多的话,线程 C 就可以很快完成任务,并且在线程 B 被完全唤醒之前,就把这个锁交出去,这样是一个双赢的局面,对于线程 C 而言,不需要等待提高了它的效率,而对于线程 B 而言,它获得锁的时间并没有推迟,因为等它被唤醒的时候,线程 C 早就释放锁了,因为线程 C 的执行速度相比于线程 B 的唤醒速度,是很快的,所以 Java 设计者设计非公平锁,是为了提高整体的运行效率。
4.2.2优缺点对比
公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点就在于整体执行速度更慢,吞吐量更小,相反非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。
4.2.3源码分析
在 ReentrantLock 类包含一个 Sync 类,这个类继承自AQS(AbstractQueuedSynchronizer),Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
Sync 类的代码:
abstract static class Sync extends AbstractQueuedSynchronizer {...}
Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类:
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
公平锁获取锁的源码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
非公平锁获取锁的源码:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { //这里没有判断 hasQueuedPredecessors()
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以明显的看出公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false,这个方法就是判断在等待队列中是否已经有线程在排队了。这也就是公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队
4.3读写锁
4.3.1 读写锁的规则
读写锁的规则是多读一写
- 允许多个线程同时读共享变量
- 只允许一个线程写共享变量
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量
代码演示:
/**
* 描述:演示可以多个一起读,只能一个写
*/
class CinemaReadWrite{
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了读锁");
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()-> read(),"Thrad1").start();
new Thread(()-> read(),"Thrad2").start();
new Thread(()-> write(),"Thrad3").start();
new Thread(()-> write(),"Thrad4").start();
}
}
执行结果:
Thrad1得到了读锁,正在读取
Thrad2得到了读锁,正在读取
Thrad2释放了读锁
Thrad1释放了读锁
Thrad3得到了写锁,正在写入
Thrad3释放了写锁
Thrad4得到了写锁,正在写入
Thrad4释放了写锁
4.3.2 比读写锁效率更高的StampedLock锁
ReadWriteLock可以解决多线程同时读,但只有一个线程能写的问题,但是它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获
取写锁,即读的过程中不允许写,这是一种悲观的读锁。
Java在1.8这个版本里,提供了一种叫StampedLock的锁,读的过程中也允许获取写锁后写入,是一种乐观读锁
但是这样一来,可能造成的问题就是我们读的数据就可能不一致,所以,StampedLock额外了判断读的过程中是否有写入,通过tryOptimisticRead()获
取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取
4.4自旋锁
4.4.1什么自旋锁
“自旋”就是获取不到锁的时候不会进入阻塞,而是不停地循环去尝试获取锁,直至成功。阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。自旋锁可恶意让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
4.4.2源码解析
java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。
AtomicLong 的实现,里面有一个 getAndIncrement 方法:
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
调用了一个 unsafe.getAndAddLong:
public final long getAndAddLong (Object var1,long var2, long var4){
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
在这个方法中,它用了一个 do while 循环
这里的 do-while 循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止。
4.4.3适用场景
- 自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。
- 如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。
4.5 CAS
4.5.1概述:
CAS 是一种思想,它的英文全称是 Compare-And-Swap,中文叫做“比较并交换”
CAS 有三个操作数:内存值 V、预期值 A、要修改的值 B。CAS 最核心的思路就是,仅当预期值 A 和当前的内存值 V 相同时,才将内存值修改为 B。
4.5.2代码演示:
/**
* 描述: 模拟CAS操作,等价代码
*/
public class SimulatedCAS {
private int value;
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
}
return oldValue;
}
}
4.5.3 缺点:ABA问题
- 决定 CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,如果一致,就认为在此期间这个数值没有发生过变动,这在大多数情况下是没有问题的。
- 但是在有的业务场景下,我们想确切知道从上一次看到这个值以来到现在,这个值是否发生过变化。例如,这个值假设从 A 变成了 B,再由 B 变回了 A
- 解决办法:添加一个版本号
4.6死锁
4.6.1概述
死锁就是两个或多个线程(或进程)被无限期地阻塞,相互等待对方手中资源的一种状态。
4.6.2代码演示
/**
* 描述: 必定死锁的情况
*/
public class MustDeadLock implements Runnable {
public int flag;
static Object o1 = new Object();
static Object o2 = new Object();
public void run() {
System.out.println("线程"+Thread.currentThread().getName() + "的flag为" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1获得了两把锁");
}
}
}
if (flag == 2) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2获得了两把锁");
}
}
}
}
public static void main(String[] argv) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 2;
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
t1.start();
t2.start();
}
}
5.线程安全的容器
5.1HashMap线程为啥是不安全的
- 多线程下执行put()方法和扩容的时候都会有问题
- 因为如果有多个线程同时调用 put() 方法的话,会进行modCount++操作,这相当于是典型的“i++”操作,从表面上看 i++ 只是一行代码,但实际上它并不是一个原子操作,它的执行步骤主要分为三步,读取,增加,保存,而且在每步操作之间都有可能被打断,就会把 modCount 的值计算错。
- 在扩容的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。
- 当年还有人把这个当成一个bug提给过Sun公司,但是官方给的回复是,这个没问题,因为HashMap本来就不保证线程安全,你的使用方式就不对,这个情况你应该用ConcurrentHashMap 。
5.2 ConcurrentHashMap1.7和和1.8的区别
- 在 ConcurrentHashMap 1.7使用 Segment 分段锁实现的,Segment 继承了 ReentrantLock,一共分成了16个Segment,各个Segment之间是
- 互不影响的,相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率
- 1.8是数组 + 链表 + 红黑树实现的, CAS + synchronized 保证线程安全
6.ThreadLocal
6.1两个主要的使用场景
场景一:
使用ThreadLocal创建匿名内部类,重写initialValue()方法, 保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,保证了线程的安全性
代码实例:
public class ThreadLocalDemo {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
场景二:
线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦,使用ThreadLocal 就不需要参数层层传递了
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
class Service1 {
public void service1() {
User user = new User("场景二使用示例");
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().service3();
}
}
class Service3 {
public void service3() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
public class ThreadLocalDemo02 {
public static void main(String[] args) {
new Service1().service1();
}
}
6.2注意事项
- 可能发生的问题: 造成内存泄漏
- 造成的原因: ThreadLocal内部维护了一个ThreadLocalMap的内部类,ThreadLocalMap的Entry包含了对value的强引用,正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。
- 解决办法:调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象
7.Future 接口
7.1 概述
Future 是一种异步的思想,最主要的作用是当我们进行一些耗时的操作时,如果我们一直在原地等待方法返回,显然是不明智的,整体程序的运行效率会大大降低。这时我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。这样一来就可以把整个程序的运行效率提高。
7.2 get() 获取结果
代码演示:
public class OneFuture {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
Future<Integer> future = service.submit(new CallableTask());
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
service.shutdown();
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return new Random().nextInt();
}
}
}
7.3 用 FutureTask 来创建 Future
典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。
代码演示:
public class FutureTaskDemo {
public static void main(String[] args) {
Task task = new Task();
FutureTask<Integer> integerFutureTask = new FutureTask<>(task);
new Thread(integerFutureTask).start();
try {
System.out.println("task运行结果:"+integerFutureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程正在计算");
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
7.4 CompletionService批量执行异步任务
7.4.1 概述
CompletionService是将线程池Executor和阻塞队列BlockingQueue的功能融合在了一起
能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,可以轻松实现后续处理的有序性,避免无谓的等待
7.4.2 常用方法
- submit() - 提交任务;
- take() - 获取任务结果;
- poll() - 获取任务结果
- take()、poll()方法都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值
7.4.3 利用CompletionService快速实现 Forking 集群模式
- Dubbo中有一种叫做Forking的集群模式,这种集群模式下,支持并行地调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了。
- 首先我们创建了一个线程池executor 、一个CompletionService对象cs和一个Future<Integer>类型的列表 futures,每次通过调用CompletionService的submit()方法提交一个异步任务,会返回一个Future对象,我们把这些Future对象保存在列表futures中。通过调用 cs.take().get(),我们能够拿到最快返回的任务执行结果,只要我们拿到一个正确返回的结果,就可以取消所有任务并且返回最终结果了。
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);
// 用于保存Future对象
List<Future<Integer>> futures = new ArrayList<>(3);
//提交异步任务,并保存future到futures
futures.add(
cs.submit(()->geocoderByS1()));
futures.add(
cs.submit(()->geocoderByS2()));
futures.add(
cs.submit(()->geocoderByS3()));
// 获取最快返回的任务执行结果
Integer r = 0;
try {
// 只要有一个成功返回,则break
for (int i = 0; i < 3; ++i) {
r = cs.take().get();
//简单地通过判空来检查是否成功返回
if (r != null) {
break;
}
}
} finally {
//取消所有任务
for(Future<Integer> f : futures)
f.cancel(true);
}
// 返回结果
return r;
8.线程池
8.1核心参数
- corePoolSize 核心线程数
- maxPoolSize 最大线程数
- keepAliveTime 空闲线程的存活时间
- ThreadFactory 线程工厂, 用来新建线程
- workQueye 用于存放任务的队列
- Handler 处理被拒绝的任务
8.2工作原理
- 当提交任务后,线程池首先会检查当前线程数,如果此时线程数小于核心线程数,比如最开始线程数量为 0,则新建线程并执行任务,随着任务的不断增加,线程数会逐渐增加并达到核心线程数,此时如果仍有任务被不断提交,就会被放入 workQueue 任务队列中,等待核心线程执行完当前任务后重新从 workQueue 中提取正在等待被执行的任务。
- 此时,假设我们的任务特别的多,已经达到了 workQueue 的容量上限,这时线程池就会启动后备力量,也就是 maximumPoolSize 最大线程数,线程池会在 corePoolSize 核心线程数的基础上继续创建线程来执行任务,假设任务被不断提交,线程池会持续创建线程直到线程数达到 maximumPoolSize 最大线程数,如果依然有任务被提交,这就超过了线程池的最大处理能力,这个时候线程池就会拒绝这些任务,我们可以看到实际上任务进来之后,线程池会逐一判断 corePoolSize、workQueue、maximumPoolSize,如果依然不能满足需求,则会拒绝任务。
8.3四种拒绝策略
- 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
- 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
- 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
- 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
- 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
- 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
9.并发容器工具类
9.1 Semaphore 信号量
9.1.1 概述
Semaphore翻译过来是信号量,可以允许多个线程访问一个临界区,信号量就是维护一个"许可证"的计数,线程可以使用acquire()方法用来"获取"许可证,那信号量剩余的许可证就减一,线程也可以使用release()方法"释放"一个许可证,那信号量剩余的许可证就加一,当信号量所拥有的许可证数量为0那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证
9.1.2 主要方法
Semaphore(int permits, boolean fair) 构造函数,这里可以设置是否使用公平策略;如果传入true,那么会把之前等待的线程放到FIFO的队列里,以便于当有了新的许可证,可以分发给之前等待了最长时间的线程
acquire();获取许可证,可以响应中断
acquireUninterruptibly();不允许被中断
release:释放许可证
tryAcquire():看看现在有没有空闲许可证,如果有的话就获取,如果没有的话也没有关系,我不必陷入阻塞,我可以去做别的事过一会再来查看许可证的空闲情
tryAcquire(timeout):和tryAcquire()一样,但是多了一个超时时间,比如"3秒内获取不到许可证,我就去做别的事"
9.1.3 代码示例
/**
* 描述:一次最多拿到3个许可证
*/
class SemaphoreDemo{
static Semaphore semaphore = new Semaphore(3,true);
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(50);
for (int i = 0; i < 100; i++) {
service.submit(new Task());
}
service.shutdown();
}
static class Task implements Runnable{
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了许可证");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放了许可证");
semaphore.release();
}
}
}
执行结果:
pool-1-thread-1拿到了许可证
pool-1-thread-2拿到了许可证
pool-1-thread-3拿到了许可证
pool-1-thread-1释放了许可证
pool-1-thread-2释放了许可证
pool-1-thread-3释放了许可证
pool-1-thread-4拿到了许可证
pool-1-thread-5拿到了许可证
pool-1-thread-6拿到了许可证
pool-1-thread-5释放了许可证
pool-1-thread-4释放了许可证
pool-1-thread-6释放了许可证
9.2 CountDownLatch 倒计时门闩
9.2.1 概述
- CountDownLatch允许一个线程或多个线程一直等待,直到这些线程完成它们的操作。
- 他是基于AQS实现的,构建CountDownLatch对象的时候,传入的值就会赋值给AQS的State变量,执行CountDown方法时,就是利用CAS将State减1,执行await方法的时候就是判断State是否为0,如果不为0的话,就加入到队列中,将这个线程阻塞掉,除了头结点,因为头节点会一直自旋等待State为0,当State为0时,头节点把剩下的在队列中阻塞的节点也一并唤醒,释放所有等待的线程
- CountDownLatch不可以重用
9.2.2主要方法:
- CountDownLatch(int count):仅有一个构造函数,参数count为需要倒数的数值
- await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
- countDown():将count值减1,直到为0时,等待的线程会被唤起
9.2.3两个典型用法:
用法一:一个线程等待多个线程都执行完毕,再继续自己的工作。
public class CountDownLatchDemo1 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
int no = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
// 随机睡眠
Thread.sleep((long) (Math.random() * 1000));
System.out.println("No." + no + "检查完了。。。");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每个线程执行完成后都会进行等待
countDownLatch.countDown();
}
}
};
service.submit(runnable);
}
System.out.println("等待5个人检查完。。。");
countDownLatch.await();
System.out.println("所有人都检查完了,进入下一个环节");
}
}
执行结果:
等待5个人检查完。。。
No.2检查完了。。。
No.1检查完了。。。
No.0检查完了。。。
No.4检查完了。。。
No.3检查完了。。。
所有人都检查完了,进入下一个环节
用法二:多个线程等待某一个线程的信号,同时开始执行
/**
* 描述:模拟100米跑步,5名选手都准备好了,等裁判号令,所有人同时开始跑步
*/
class CountDownLatchDemo2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch begin = new CountDownLatch(1);
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 1; i <=5 ; i++) {
final int no = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("NO." + no + "准备完毕,等待发令枪");
try {
begin.await();
System.out.println("NO." + no + "开始跑步了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.submit(runnable);
}
// 裁判员检查发令枪
Thread.sleep(5000);
System.out.println("枪响了,比赛开始");
begin.countDown();
}
}
执行结果:
NO.1准备完毕,等待发令枪
NO.2准备完毕,等待发令枪
NO.3准备完毕,等待发令枪
NO.4准备完毕,等待发令枪
NO.5准备完毕,等待发令枪
枪响了,比赛开始
NO.1开始跑步了
NO.2开始跑步了
NO.4开始跑步了
NO.3开始跑步了
NO.5开始跑步了
结合用法一和用法二:
/**
* 描述:模拟100米跑步,5名选手都准备好了,等裁判号令,所有人同时开始跑步.
* 当所有人都到终点后比赛结束
*/
class CountDownLatchDemo1And2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch begin = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(5);
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 1; i <=5 ; i++) {
final int no = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("NO." + no + "准备完毕,等待发令枪");
try {
begin.await();
System.out.println("NO." + no + "开始跑步了");
// 模拟随机跑步时长
Thread.sleep((long) (Math.random() * 10000));
System.out.println("NO." + no + "跑到终点了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
end.countDown();
}
}
};
service.submit(runnable);
}
// 裁判员检查发令枪
Thread.sleep(5000);
System.out.println("枪响了,比赛开始");
begin.countDown();
end.await();
System.out.println("所有人到达终点,比赛结束");
}
}
执行结果:
NO.1准备完毕,等待发令枪
NO.2准备完毕,等待发令枪
NO.3准备完毕,等待发令枪
NO.4准备完毕,等待发令枪
NO.5准备完毕,等待发令枪
枪响了,比赛开始
NO.1开始跑步了
NO.3开始跑步了
NO.5开始跑步了
NO.4开始跑步了
NO.2开始跑步了
NO.1跑到终点了
NO.4跑到终点了
NO.3跑到终点了
NO.2跑到终点了
NO.5跑到终点了
所有人到达终点,比赛结束
9.3 CyclicBarrier循环栅栏
9.3.1 概述
- CyclicBarrier循环栅栏和CountDownLatch很相似,都能阻塞一组线程
- 当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier。CyclicBarrier可以构造一个集结点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集结点,那么该栅栏就会被撤销,所有线程再统一出发,继续执行剩下的任务
- CountDownLatch不能重用,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为 0的时候,如果 再有线程调用去 await() 方法,该线程会直接通过,不会再起到等待其他线程执行结果起到同步的作用,CyclicBarrier解决了这个问题
- 它没有像countDownlatch那样使用AQS的State变量,在构建CyclicBarrier对象的时候,传入的值会赋给CyclicBarrier内部维护的count变量,同时赋值给parties变量,每次调用await时,把count减1,使用ReentrantLock保证线程安全性,如果count不为0,则添加到condition队列中,如果count等于0的时候,则把节点从condition队列添加到AQS的队列中进行全部唤醒,把arties变量的值重新赋值给count,实现循环使用
9.3.2 代码演示
/**
* 描述:演示CyclicBarrier,5个人相约出行,全部到场再出发
*/
class CyclicBarrierDemo{
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("所有人都到场了,大家统一出发");
}
});
for (int i = 0; i < 5; i++) {
new Thread(new Task(i,cyclicBarrier)).start();
}
}
static class Task implements Runnable{
private int id;
private CyclicBarrier cyclicBarrier;
public Task(int id){
this.id = id;
}
public Task(int id, CyclicBarrier cyclicBarrier) {
this.id = id;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+id+"现在前往集合地点");
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println("线程"+id+"到了集合地点,开始等待其他人到达");
cyclicBarrier.await();
System.out.println("线程"+id+"出发了");
}catch (InterruptedException e){
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
执行结果:
线程0现在前往集合地点
线程1现在前往集合地点
线程2现在前往集合地点
线程3现在前往集合地点
线程4现在前往集合地点
线程0到了集合地点,开始等待其他人到达
线程3到了集合地点,开始等待其他人到达
线程4到了集合地点,开始等待其他人到达
线程2到了集合地点,开始等待其他人到达
线程1到了集合地点,开始等待其他人到达
所有人都到场了,大家统一出发
线程1出发了
线程0出发了
线程4出发了
线程2出发了
线程3出发了
9.3.3和 CountDownLatch 的异同
相同点:都能阻塞一个或一组线程,直到某个预设的条件达成发生,再统一出发。
不同点:
- 作用对象不同:CyclicBarrier 要等固定数量的线程都到达了栅栏位置才能继续执行,而 CountDownLatch 只需等待数字倒数到 0,也就是说 CountDownLatch 作用于事件,但 CyclicBarrier 作用于线程;CountDownLatch 是在调用了 countDown 方法之后把数字倒数减 1,而 CyclicBarrier 是在某线程开始等待后把计数减 1。
- 可重用性不同:CountDownLatch 在倒数到 0 并且触发门闩打开后,就不能再次使用了,除非新建一个新的实例;而 CyclicBarrier 可以重复使用,在刚才的代码中也可以看出,每 3 个同学到了之后都能出发,并不需要重新新建实例。CyclicBarrier 还可以随时调用 reset 方法进行重置,如果重置时有线程已经调用了 await 方法并开始等待,那么这些线程则会抛出 BrokenBarrierException 异常。
10 AQS
10.1概述
AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的
10.2实现原理
- AQS中 维护了一个 state变量 和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
- state变量是用volatile关键字修饰的,能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,等待其他获取锁的线程释放锁才能够被唤醒。state的操作都是通过CAS来保证其并发修改的安全性。