1.避免死锁
场景模拟
有2个小孩,A和B,A拿着B的玩具,B拿着A的零食,A对B说,你先把零食还给我,我才把玩具还给你,B不干,于是对A说,你先把玩具给我,我才把零食还给你,于是陷入了僵持,这就是死锁,死锁的场景也有可能是多个小孩
出现的条件
- 互斥条件:一个资源每次只能被一个进程使用(B的玩具,被A拿来完,其他人就不能玩了)
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放(A拿着B的玩具,不还零食不放手)
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺(来个大人,强行抢了A手里的玩具,还给B)
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(A和B相互僵持着,谁也不肯让一步)
总结
只要破坏死锁4个必要条件中的任何一个,死锁问题就能得以解决,死锁是一个设计问题,我们程序设计的时候,应该尽量避免
2.减小锁持有的时间
如果一个线程锁住的代码过多,运行时间过长,其他线程如果需要这份资源,等待的时间就会越长,所以我们希望尽量减少锁住代码的范围主要有下面2中方式:
缩小范围
- 尽量缩小需要同步的代码的范围,下面是原代码
public synchronized void method(){
code1();
code2();
code3();
}
- 我们实际只需要同步代码2,然后修改成下面的
public void method(){
code1();
synchronized(this){
code2();
}
code3();
}
增加判断语句
- 如上面代码,有时候我们的code2()可能只需要在某些条件下才运行,所以我们还可以将判断语句提出来,于是,我们还可以优化成下面的样子:
public void method(){
code1();
if(!isRun){
synchronized(this){
if(!isRun){
code2();
}
}
}
code3();
}
3.减小锁粒度
和上面的有点相似,但不同的是,上面的针对代码,而这个是针对类。这里举例的是ConcurrentHashMap。
ConcurrentHashMap的加锁
使用多线程对ConcurrentHashMap类进行添加的时候,在默认条件下,会将ConcurrentHashMap类分为16段,然后对每段分别加锁,于是最优的情况下,可以允许16个线程同时对ConcurrentHashMap类进行修改,大大的提高了吞吐量
ConcurrentHashMap的不足
但是当需要获得ConcurrentHashMap全局信息时,比如size,就需要同时获得16个子段的锁,分别计算再求和
总结
因此,减小锁粒度适合在不频繁获取全局信息时,但有会频繁修改的情况下。
4.锁分离
场景模拟
这里举个例子LinkedBlockingQueue的 take() 和 put() 操作,正常的思维是,对LinkedBlockingQueue进行同步,但是这个LinkedBlockingQueue分别是在头部取数据,尾部添加数据,他俩出来当数据为空或者数据满了(数据太多),才会发生资源争夺的情况,那么相对于“正常思维”,还有很大的优化空间
优化方案
没错,就是锁分离,步骤如下:
添加数据时
- 如果数据满,就停止当前操作,并将唤醒权交给取除数据的操作
- 如果发现数据为空,先添加数据,并唤醒取数据操作
取数据操作
- 如果数据为空了,也一样就停止当前操作,并将唤醒权交给添加数据操作
- 如果数据为满了,先取出一个数据,并唤醒添加数据操作
代码就不用给大家了,思路都有了,如果还不明白的,可以在这篇博客的基础上稍加修改
5.重入锁和内部锁
- 内部锁的实现,重入锁都可以实现,但是内部锁使用更加简单、方便
- 重入锁有更加强大的功能,比如提供了锁的等待时间、支持锁中断、快速锁轮训
- 重入锁加入了Condition机制,可以实现更加复杂的线程控制
- 某些线程控制,内部锁也可以通过wait()和notify()实现,平时推荐使用内部锁
6.锁粗化
锁粗化是和第二个减小锁持有的时间相反的,因为过度频繁的同步和释放,同样会造成系统资源的浪费,不过幸运的是,虚拟机一般会优化这样的操作,但是我们平时也应该注意这一点,下面举2个例子:
- 优化前:
public void method(){
synchronized(this){
code1();
}
synchronized(this){
code2();
}
}
- 优化后
public void method(){
synchronized(this){
code1();
code2();
}
}
- for循环也一样,优化前
for(String str:list){
synchronized(lock){
code1();
}
}
- 优化后
synchronized(lock){
for(String str:list){
code1();
}
}
7. 通过JVM优化锁的方法
7.1 自旋锁
问题
在多线程并发时,频繁的挂起和恢复线程的操作会给系统带来极大的压力,特别是当访问共享资源仅需要花费很小一段CPU时间时,锁的等待可能只需要很短的时间,这段时间可能要比将线程挂起来并恢复的时间还要短,因此,为了这段时间去做重量级的线程切换不值得。
解决方案
JVM引入了自旋锁。自旋锁可以是线程在没有取得锁时,不被挂起,而转而去执行一个空循环,空循环后
- 如果获得锁,则继续执行
- 如果没有获得锁,才会被挂起
优缺点
- 优点:减少被挂起的几率,线程执行的连贯性加强
- 缺点:但是对于锁竞争激烈、单线程锁占用时间长,自旋转后还是会被挂起,这样就会系统和CPU浪费资源
开启方法
- XX:+UseSpinning : 开启自旋锁
- XX:PreBlockSpin :设置自旋锁的等待次数
7.2 锁消除
问题
在不可能存在共享资源的地方使用锁相关的工具类,比如StringBuffer、Vector等,浪费了无意义的请求锁的时间
解决方案
JVM会在运行时,通过逃逸分析技术(没听过,哈哈),捕获到这些不可能存在竞争却有申请锁的代码段,并将其消除,这就是锁消除
开启防范
- 开启逃逸分析: - XX:+DoEscapeAnalysis(加号变减号就是消除)
- 开启锁消除: -XX:+Eliminate Locks
7.3锁偏向
如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省了操作时间。如果在此之间其他线程进行了锁请求,则锁退出偏向模式
开启方案
只有当锁竞争不激烈的时候,才建议开启锁偏向,否则会加重系统的负担
- 开启方案:-XX:+UseBiasedLocking(同上,关闭为减号)
8. 读写分离锁
- 读写分离锁来替换独占锁,这个是为防止读取的数据是脏数据,但是又能提高读写的效率,这篇博客已经介绍过了