跳出来看全局就是3部分:分工、协作、互斥
钻进去看本质:技术的本质就是背后的理论模型
方案:按需禁用缓存以及编译优化
方法:volatile,synchronzied,final关键字,六项Happens-Before规则
源头:线程切换
解决方案:锁,互斥锁,一把锁可以保护多个资源
加锁本质就是在锁对象的对象头中写入当前线程id
用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁,会有死锁问题
锁的资源:无关联资源可用各自对象,有关联资源可用class对象,class对象是Java虚拟机加载类时创建的,具有唯一性
产生的条件:互斥、占有资源A等待资源B且不释放资源A、其他资源不能强行抢占资源、循环等待
解决办法:破坏其中一个条件
破坏占有且等待条件:由一个单例类来分配资源
破坏不可抢占条件:主动释放资源可用Lock,synchroized做不到
破坏循环等待条件:资源进行排序,等待-通知机制
原因:线程永久阻塞,类似两个人过同一个门,都走左边,然后互相谦让又都走右边,一直循环下去
方案:添加一个随机的等待时间
原因:优先级低的线程获取锁的机会小,持有锁的线程执行时间长
方案:公平锁,线程等待有先来后到,获取锁的机会平等
阿姆达尔定律:
公式里的 n 可以理解为 CPU 的核数,p 可以理解为并行百分比,那(1-p)就是串行百分比了,也就是我们假设的 5%。我们再假设 CPU 的核数(也就是 n)无穷大,那加速比 S 的极限就是 20。
方案:
解决互斥问题思路:将共享变量及其对共享变量的操作封装起来。
解决同步问题思路:条件变量等待队列
可通过jstack或Java VisualVm可视化工具查看线程栈信息
I/O密集型:最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
CPU密集型:线程的数量 =CPU 核数
Lock和synchoronized的区别:
sync申请资源如果申请不到,线程直接阻塞,无法释放
Lock能够响应中断、支持超时、非阻塞的获取锁
// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
Lock如何保证可见性:内部维护了用volatile的state字段
可重入锁:线程可以重复获取同一把锁
公平锁和非公平锁:ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。公平锁从等待队列里唤醒等待时间最长的线程,非公平锁则不保证。
用锁最佳实践
方案:Semaphore 信号量,像生活中的红绿灯
关键方法:acquire(),release(),new Semaphore(size)
方案:读写锁 ReadWriteLock
什么是读写锁?1、允许多个线程同时读共享变量;2、只允许一个线程写共享变量;3、执行写操作时禁止读。
按需加载:先加读锁,读取,释放读锁,加写锁,写值,释放写锁。
注意:不允许锁升级(读锁变为写锁,导致写锁永久等待,线程阻塞),允许降级(写锁变为读锁)
StampedLock:写锁、悲观读锁、乐观读(性能好的原因,无锁算法CAS,不是所有写操作都被阻塞)
注意:使用时一定不要调用中断操作,只有使用悲观读锁和写锁时可以中断
StampedLock读模版
final StampedLock sl =
new StampedLock();
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验stamp
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 读入方法局部变量
.....
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
//使用方法局部变量执行业务操作
......
StampedLock写模版
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
CountDownLatch:设置要同步的线程数量,线程执行完后可调用latch.countDown()来给计数器减一
CyclicBarrier:可以传入回调函数,线程都执行完后可通知,计数器清零后可重置,可循环利用
CompletableFuture:默认使用ForkJoinPool线程池,建议根据不同业务创建线程池,避免阻塞
CompletionStage接口:40个方法,AND聚合关系,OR聚合关系
原理:CAS(比较并交换,3个变量,共享变量A,比较值B,新值C)
线程池是生产者-消费者模型
注意事项:尽量使用有界队列,默认拒绝策略慎用,若处理任务非常重要建议自定义拒绝策略。
创建线程池参数:
将Executor和BlockingQueue的功能融合,让异步任务的执行结果有序化。快速实现Forking Cluster这样的需求。
该计算框架核心组件是ForkJoinPool,支持任务窃取,让所有线程工作量基本均衡。Java8的Stream API 的并行流也是以此为基础。需要注意,所有程序共享一个线程池,所以建议使用不同的ForkJoinPool执行不同类型的计算任务。