JAVA多线程基础:锁的优化方式总结

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.重入锁和内部锁

重入锁内部锁,这2篇博客已经介绍了,我这里就总结一下:

  • 内部锁的实现,重入锁都可以实现,但是内部锁使用更加简单、方便
  • 重入锁有更加强大的功能,比如提供了锁的等待时间支持锁中断快速锁轮训
  • 重入锁加入了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. 读写分离锁

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值