读书笔记 | Java并发编程实战
一、基础知识
1. 线程安全性
线程安全的代码,核心在于对状态访问操作的管理特别是共享和可变状态的管理
- 对象的状态:存储在状态变量(如实例或静态域)中的数据
- 共享意味着变量可以由多个线程同时访问
- 可变意味着变量的值会发生改变
当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问.
Java常见的同步机制:
* synchronized
* volatile
* 显示锁(Explicit Lock)
* 原子变量
2. 什么是线程的安全性
线程的安全性就是当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就是线程安全的.
3. 非原子的64位操作
- Java内存模型要求,变量的读取和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分为两个32位的操作.当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读到某个值的高32位和另一个值得低32位.因此,即使不考虑失效数据问题,在多线程中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来
- 如何复现问题,64位系统上也会有这种问题吗
- 64位系统中没有这个问题
4. volatile
- volatile变量用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方.因此在读取volatile变量时,总是返回最新写入的值.
- volatile变量对可见性的影响:
- 当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前所有对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的.
- volatile变量的正确使用方式包括
- 确保它们自身状态的可见性
- 确保它们所引用对象的状态的可见性
- 作为一些事件的开关.
5. 发布与逸出
- 发布:
- 将一个指向该对象的引用保存到其他代码能访问的地方
- 或者在一个非私有的方法中返回该引用
- 或者将一个引用传递到其他类的方法中
- 逸出:
- 当某个不该发布的对象被发布时,这种情况就是逸出
6. 并发容器
6-1. ConcurrentHashMap
- 内部结构
- add
- get
- size
6-2. CopyOnWriteArrayList
- 用途:在一些读操作远大于写操作的情况下,才可以使用写入时复制容器
- 在事件通知系统中,在分发通知时,需要迭代已注册的监听器链表,在大多数情况下,注册和注销事件监听器的操作远小于接收事件的操作.
6-3. 阻塞队列和生产者消费者模式
- 阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法.
6-4. 同步工具类
6-4-1. 闭锁
- 闭锁是一种同步工具类,可以延迟线程的进度知道其达到终止状态.
- 闭锁可以用来确保某些活动直到其他活动都完成后才继续执行
- 确保某个计算所需要的资源都初始化后才开始执行(资源初始化)
- 确保某个服务所依赖的其他服务都启动后才启动(服务依赖)
- 确保某个操作的所有操作者都就绪后再继续执行
- CountDownLatch
- CountDownLatch(int)
- await():void
- await(long,TimeUnit):boolean
- countDown():void
// 在计时测试中使用CountDownLatch来启动和停止线程
public long timeTasks(int nThreads, Runnable task) throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread(() -> {
try {
//线程启动后都在这里等待startGate变为0
startGate.wait();
try {
task.run();
} finally {
//任务运行完,endGate减一
endGate.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
long start = System.nanoTime();
startGate.countDown(); //所有线程开始任务
endGate.wait(); //等待所有线程执行完成
long end = System.nanoTime();
return end - start;
}
6-4-2. FutureTask
- FutureTask实现了Future语义,表示一种抽象的可生成结果的计算.FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于一下三种状态:等待运行,正在运行,运行完成.运行完成表示计算的所有可能结束方式,包括正常结束,由于取消而结束和由于异常而结束等.当FutureTask进入完成状态后,它会永远停止在这个状态上.
- FutureTask的用途:
- 在Executor框架中表示异步任务
- 还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动.通过提前启动计算,可以减少等待结果时需要的时间.
- FutureTask的问题
- Callable表示的任务可以抛出受检查的或不受检查的异常,这些异常被封装到
ExecutionException
中,并在Future.get中被重新抛出
,这将使得调用get的代码变得复杂,因为它要对不同的异常进行不同的处理.
- Callable表示的任务可以抛出受检查的或不受检查的异常,这些异常被封装到
// 使用FutureTask来提前加载稍后需要的数据
public class N5_5_12Proloader {
private final FutureTask<ProductInfo> future = new FutureTask<>(ProductInfo::new);
private final Thread thread = new Thread(future);
public void start() {
thread.start();
}
public ProductInfo get() throws InterruptedException {
try {
return future.get();
} catch (ExecutionException e) {
System.out.println("初始化ProductionInfo发生错误");
return null;
}
}
}
class ProductInfo {
}
6-4-3. 信号量Semaphore
- 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量.计数信号量还可以用来实现某种资源池,或者对容器施加边界.
- Semaphore
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
public void acquire() throws InterruptedException
public void release()
6-4-4. 栅栏CyclicBarrier
- CyclicBarrier
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
public int await()
- 等到设定的n个线程都到达了指定位置后在再同时继续往下执行,CyclicBarrier在初始化时还可以设置Runnable的action,最后一个到达指定位置的线程会去运行这个action
6-5. 构建高效且可伸缩的结果缓存
- 场景:假设有个函数 value = fun(key),这个计算过程需要消耗一定的时间和资源,现在想要将计算的结果缓存下来,下次再计算同一个key时可以从缓存中直接获取value.
- 思路:可以使用map类将key和value缓存起来,每次计算key的值时,先看map中有没有这个key对应的value,如果有,直接返回,如果没有,计算结果并存入map中.
- 其中的坑:
- 涉及到多线程,要使用ConcurrentHashMap,确保get和set时的线程安全
- 因为计算需要消耗一定时间,如果一个线程在计算key的时候,另一个线程也来请求计算key,这个时候因为第一个线程的计算结果没出来,所以map中是空的,这时候第二个线程会再去计算.
- 解决办法:map中不保存key和value的键值对,而是保存key和Future,其中Future中在计算value的值,通过future.get()方法,如果计算完成了直接返回value的值,如果计算还没结束,会阻塞一直等到它计算完成并返回.还需要注意的是,需要使用map.putIfAbsent(key,future)方法存入key和future,因为判断key是否存在和放入key不是原子操作.