Java并发编程(JUC)
锁的设计思路
- 需要用到一个标记来实现互斥,全局变量(值可以是0或1;值也可以是0或大于0的整数,可重入锁的计数)
- 需要用到一个变量记录当前持有锁的线程
- 某个线程抢占到了锁,如何处理:
- 记录当前持有锁的线程
- 某个线程没有抢到锁,如何处理:
- 需要阻塞等待(释放CPU资源)。如何实现:
- wait/notify,缺点是无法指定唤醒某个线程
- LockSupport.park()/unpark() ,可以阻塞/唤醒指定的线程
- Condition
- 需要排队:
- 将阻塞的线程存储到某个数据结构中排队
- 需要阻塞等待(释放CPU资源)。如何实现:
- 锁的释放过程,如何处理:
- notify/notifyAll,此方法无法指定唤醒某个线程
- LockSupport.unpark(),唤醒指定的一个线程
- 抢占锁的公平性(是否允许插队)
- 公平
- 非公平(性能较好)
ReentrantLock & AQS
- AbstractQueuedSynchronizer,简称为AQS,是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很方便的构造出来。
- 例如,可重入锁ReentrantLock的实现:
wait/notify
- wait和notify/notifyAll一定要配合synchronized使用
- Java中的“任何一个对象都有一个monitor与之关联”是指每个Java对象都有一个内置的监视器(也称为锁),它可以用来控制对对象的访问,以实现线程同步。
- 在HotSpot虚拟机中,monitor采用ObjectMonitor实现
Condition
-
Condition condition = lock.newCondition()
-
await 等待
-
signal/signalAll 通知唤醒
-
Condition的优势在于可以为多个线程建立不同的Condition
-
基于Condition实现生产者消费者模型:
public class ProducerConsumerTest {
private static final int BUFFER_SIZE = 10;
private static final Lock lock = new ReentrantLock();
private static final Condition notFull = lock.newCondition();
private static final Condition notEmpty = lock.newCondition();
private static final Product[] buffer = new Product[BUFFER_SIZE];
private static int tail;
private static int head;
private static int count;
public static void main(String[] args) {
new Thread(new Producer("P0")).start();
new Thread(new Consumer("C1")).start();
new Thread(new Consumer("C2")).start();
}
static class Producer implements Runnable {
private String name;
public Producer(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (count == BUFFER_SIZE) {
System.out.println("buffer is full, " + name + " is waiting");
notFull.await();
}
buffer[tail] = new Product(String.valueOf(i));
System.out.println(name + " produce one: " + buffer[tail].getName());
tail = (tail + 1) % BUFFER_SIZE;
count++;
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class Consumer implements Runnable {
private String name;
public Consumer(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (count == 0) {
System.out.println("buffer is empty, " + name + " is waiting");
notEmpty.await();
}
Product tmp = buffer[head];
buffer[head] = null;
head = (head + 1) % BUFFER_SIZE;
count--;
notFull.signal();
Thread.sleep(100);
System.out.println(name + " consume one: " + tmp.getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class Product {
public Product(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
}
}
阻塞队列
- ArrayBlockingQueue:基于数组实现的阻塞队列
- LinkedBlockingQueue:基于链表实现的阻塞队列
- PriorityBlockingQueue:带优先级的无界阻塞队列,每次出队返回优先级最高的元素
- DelayQueue:延时阻塞队列
- SynchronousQueue:一个没有数据缓冲的BlockingQueue
- Executors.newCachedThreadPool(),使用的就是SynchronousQueue
CountDownLatch
- CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,让一组线程等待另一组线程执行完后再执行。
- 共享锁的实现,使用计数器
- 处理流程:
- 假设有2组线程,需要实现让A组的线程等待B组的线程
- 设置计数器初始值,通常为B组的线程数量
- 让A组的线程等待计数器归零(调用await()方法)
- 当B组中的一个线程完成自己任务后,触发计数器的值减1(调用countDown()方法)
- 当计数器的值为0时,唤醒所有被阻塞的A组的线程
CyclicBarrier
- 循环屏障,可循环使用(Cyclic)的屏障(Barrier),或者叫栅栏
- 让一组线程到达一个同步点时被阻塞,直到最后一个线程到达同步点的时候,屏障会放开,所有被阻塞的线程恢复执行后续逻辑
- 处理流程:
- 设置计数器初始值,通常为线程数量
- 当一个线程到达同步点时(调用await()方法),阻塞线程数+1
- 当阻塞线程数达到计数器的设定值时,唤醒所有被阻塞的线程
- 计数可以重置后循环使用
Semaphore
- 信号量,可实现限流器,限制资源的访问
- Semaphore(int permits)/Semaphore(int permits, boolean fair) 初始化时指定许可个数
- acquire()/acquire(int permits) 获取1个/多个许可
- release()/release(int permits) 释放1个/多个许可
- 公平/非公平(是否允许插队)
ThreadLocal
线程间变量隔离,每个线程有各自独立的变量副本
ThreadLocal的内存泄漏问题:
- Entry中的key为弱引用,value是强引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
-
如果Thread没有被回收,则ThreadLocalMap不会被回收,则其中的Entry中的key不一定会被回收(key是弱引用,当没有其他强引用时会被回收),value不会被回收。
-
一般使用线程会使用池化技术,一个线程使用完后不会被销毁,而是会复用,所以会存在ThreadLocal的内存泄漏问题。(例如:tomcat的处理请求会从线程池中取出线程执行当前的请求)
-
如何解决内存泄漏问题:调用ThreadLocal的remove方法
-
实际解决内存泄漏的案例:
- tomcat请求线程:可以在请求返回的filter中调用ThreadLocal的remove方法
- com.github.pagehelper分页插件:基于ThreadLocal实现分页参数的传递,分页查询前调用startPage,使用threadLocal存储分页参数,PageInterceptor实现了Mybatis的拦截器接口执行分页查询逻辑,在PageInterceptor的拦截方法中的finally语句块中调用ThreadLocal的remove方法
Fork/Join
- 任务的拆分计算与结果合并
-
ForkJoinTask
- RecursiveAction
- RecursiveTask
- CountedCompleter
- 等等
-
fork-让task异步执行
-
join-让task同步执行,等待获取返回值
-
ForkJoinPool,专门运行ForkJoinTask,线程复用
双端队列
- 工作窃取算法
- 双端队列,可以从头和尾来获取任务
- 每个线程有各自的队列,一个线程在执行完毕自己队列中的任务之后,可以窃取其他线程队列中的任务来执行。
- 为了减少竞争,原本的线程从队列头部获取任务,窃取任务的线程从队列尾部获取任务。
ConcurrentHashMap
ConcurrentHashMap中的设计思想:
- 计算元素存放到数组下标:(n - 1) & hash
- put方法,当hashKey对应数组下标不存在node时,CAS操作在当前数组下标添加一个node
- put方法,当hashKey冲突,node存在时,synchronized(node),然后修改该node(链表或红黑树)上的节点的值。(锁的粒度仅为该node,这是jdk1.8之后的优化,jdk1.7锁的是segment)
- 扩容
- 支持多个线程协助并发扩容,每个线程负责数组的一段区间。
- 高低位迁移,需要迁移的数据放在高位链表,不需要迁移的放在低位链表,低位链表在扩容之后的索引与原哈希表中的索引位置一样,高位链表的索引位置是原索引位置+n。
- 元素个数的统计
- 在多线程情况下,如果每个线程都自旋CAS修改size的值,会有性能问题,所以没有用此方案。
- 线程竞争不激烈时,直接CAS(baseConunt)
- 线程竞争激烈时(CounterCell[]数组已初始化或CAS(baseConunt)失败),使用数组分片统计
- 数组的方式,分片计数。CounterCell[]数组长度初始为2,线程随机算法选择数组元素执行CAS修改value
//Contended注解,对齐填充到64字节,解决CPU缓存的伪共享问题 @sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } }
- 汇总数组的值+baseCount的值来完成数据累加。
- 当链表长度大于8,并数组长度大于等于64时,链表转换为红黑树。
红黑树
- 二叉搜索树
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
- 它的左右子树也分别为二叉搜索树。
- 查询时间复杂度介于O(logN)和O(N)之间
- 平衡二叉树(AVL树):严格平衡,左子树和右子树的高度差的绝对值不超过1
- 红黑树:一种接近平衡的二叉搜索树。
- 红黑树确保没有一条路径会比其他路径长两倍,因而是接近平衡的。
- 红黑树是接近平衡的,是因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,并没有严格的卡定某个平衡因子来维持绝对平衡,所以红黑树的整体性能比AVL树更好。
FutureTask
//futureTask.get()会阻塞当前线程
FutureTask<String> futureTask = new FutureTask<>(() -> {
Thread.sleep(5000);
return "Hello World!";
});
futureTask.run();
System.out.println(futureTask.get());
CompletableFuture
//非阻塞的方式异步获取返回值
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "Hello World!";
}).thenApply((x) -> x + " Now..").thenAccept(x -> System.out.println(x));
//处理异常情况1
CompletableFuture.supplyAsync(() -> {
int num = Integer.parseInt("abc");
return num;
}).exceptionally(th -> {
System.out.println("方法执行出错:" + th.getMessage());
return 0;
}).thenAccept((x) -> System.out.println(x + " Now.."));
//处理异常情况2
CompletableFuture.supplyAsync(() -> {
int num = Integer.parseInt("abc");
return num;
}).handle((x, th) -> {
if (th != null) {
System.out.println("方法执行出错:" + th.getMessage());
return 0;
}
return x;
}).thenAccept((x) -> System.out.println(x + " Now.."));
//组合处理多个异步任务
CompletableFuture.supplyAsync(() -> {
int num = Integer.parseInt("100");
return num;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
int num = Integer.parseInt("200");
return num;
}), (r1, r2) -> r1 + r2).thenAccept(x -> System.out.println(x));