1. 同步控制
这部分介绍多线程控制方法.
1.1 重入锁
重入锁是synchronized的替代品,但是JDK6.0开始,synchronized做了大量的优化,两者性能差距不大.
1 | import java.util.concurrent.locks.ReentrantLock; |
重入锁特性
重入锁之所以叫这个名字,是因为这种锁可以反复进入,就是说,一个线程可以连续两次获得同一把锁,但是在释放锁的时候,也必须释放相同次数.
1 | reentrantLock.lock(); |
中断响应
就是让锁可以响应中断,使用lock1.lockInterruptibly()来进行上锁即可收到中断请求.
1 | import java.util.concurrent.locks.ReentrantLock; |
锁申请等待限时
会不停地去获取锁,但是最大等待时间不会超过给定的值.
通过lock.tryLock()实现,带参用法如下实例,不带参可以理解为时长为0.
获取锁成功返回值为true,否则返回值为false.
1 | import java.util.concurrent.TimeUnit; |
公平锁
公平锁保证先到者先得,后到者后得.不会产生饥饿现象.
可重入锁默认是非公平锁.公平锁性能相对非常低下,因为要求系统维护一个有序队列.
synchronized锁是非公平锁.
通过new ReentrantLock(true)设置参数指定开启公平锁,true表示开启.
1 | import java.util.concurrent.locks.ReentrantLock; |
1.2 Condition条件
与 重入锁 配合使用, 达到synchronized锁中的wait notify的效果.
condition.await()会使当前线程等待并释放锁.
使用condition.signal()之前一定要先获得锁,用完之后一定要记得释放锁.
1 | import java.util.concurrent.locks.Condition; |
1.3 信号量Semaphore
可以指定多个线程同时访问某一个资源,并指定准入数量.
有很多用法,跟ReentrantLock类似,下面给一个典型的demo.
1 | import java.util.concurrent.ExecutorService; |
1.4 读写锁ReadWriteLock
读写分离锁可以减少锁竞争,因为如果用重入锁,所有的读之间,读写之间,写写之间都是串行的.但是从日常需求上看,读读应该是允许并行的.适用于读多写少.
1 | import java.util.Random; |
1.5 倒计时器CountDownLatch
让线程等待,直到倒计时结束(满足停止等待的条件),再开始执行.
1 | import java.util.Random; |
1.6 循环栅栏CyclicBarrier
功能和 倒计时器CountDownLatch 类似, 但是 循环栅栏CyclicBarrier 功能更加强大. 表现在 可以反复使用.
用法:new CyclicBarrier(N, new BarrierRun(flag, N)),N表示计数器,new BarrierRun(flag, N)表示每次计数器满足之后所执行的操作.
1 | import java.util.Random; |
1.7 线程阻塞工具类LockSupport
和suspend+resume相比, 它不会因为resume而造成线程无法继续执行.
和wait+notify相比, 它不需要先获取某个对象的锁.
我觉得这个类本质上应该是用来替换suspend+resume的,因为线程挂起后并不会释放锁.
1 | import java.util.concurrent.locks.LockSupport; |
2. 线程池
为什么需要线程池?
线程的创建和关闭需要花费时间.
线程本身也是要占用内存空间.
线程的回收会给GC带来压力,延长GC时间.
在实际生产环境中,线程的数量必须得到控制,盲目的大量创建线程对系统性能是有伤害的.
使用线程池后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向池子归还线程.
2.1 使用线程池
2.1.1 固定大小的线程池
1 | import java.util.concurrent.ExecutorService; |
2.1.2 计划任务
周期性的执行任务.
scheduleAtFixedRate和scheduleWithFixedDelay的区别.
如果任务遇到异常,那么后续的所有子任务都会停止调度,因此,必须保证异常被及时处理,为周期性任务的稳定调度提供条件.
1 | import java.util.concurrent.Executors; |
2.1.3 核心线程池内部实现
都是基于下面这个.
1 | public ThreadPoolExecutor(int corePoolSize, |
2.1.4 拒绝策略
当任务数量超过系统实际承载能力时(或者线程池规定的能力),该如何处理.
2.1.5 自定义线程创建ThreadFactory
1 | import java.util.concurrent.*; |
2.1.6 扩展线程池
1 | import java.util.concurrent.*; |
2.1.7 优化线程池线程数量
最优的池的大小等于:
Nthreads = Ncpu * Ucpu * (1 + W/C);
Ncpu: CPU数量
Ucpu: 目标CPU的使用率, 0~1.
W/C: 等待时间与计算时间的比率
2.1.8 Fork/Join框架
这是一个分而治之的框架.直接看个demo.
1 | import java.util.ArrayList; |
3. JDK的并发容器
3.1 线程安全的HashMap
1 | // 方法一: 性能低,线程安全 |
3.2 List的线程安全
1 | // 线程不安全, 底层数组 |
3.3 高效读写的队列
1 | // 高并发环境中性能最好的队列,线程安全. |
3.4 高效读取CopyOnWriteArrayList
适用于读远大于写的场景.
只有写入与写入之间需要同步等待,其他情况都不用加锁.
1 | CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList(); |
3.5 数据共享通道BlockingQueue
用于多线程之间的数据共享. 这是一个接口, 可以选择很多实现.
1 | BlockingQueue blockingQueue = new LinkedBlockingQueue(); |
3.6 跳表SkipList
用来快速查找的数据结构,类似于平衡树.
与平衡树的区别在于: 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整.而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可.
这样做的好处在于: 在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全.而对于跳表,你只需要部分锁即可,从而拥有更好的性能.
就查询性能来说, 跳表的时间复杂度也是O(log n),所以在并发数据结构中,使用跳表来实现Map.
1 | ConcurrentSkipListMap concurrentSkipListMap = new ConcurrentSkipListMap(); |
4. 参考文献
<< Java高并发程序设计 >>(葛一鸣 郭超)