Java并发编程中锁的正确使用方法

在Java并发编程中,为了保证线程安全,我们需要使用锁机制进行控制。但是使用锁也会带来一些负面影响,比如死锁和性能损耗。这篇文章将介绍我们如何正确、合理地使用锁,以避免发生死锁,减少性能损耗。

一、避免死锁

有需要的可以先去看看这篇操作系统教材上关于死锁的知识:操作系统 关于死锁的面试题

死锁的必要条件有四个:

  • 互斥条件:在一段时间内,某资源只能被一个进程占用
  • 请求和保持条件:进程已保持了资源,又提出了新的资源请求,而该资源已被其他进程占有,此时该进程阻塞,但不释放已持有的资源
  • 不可抢占条件:进程已经获得的资源,在未使用完之前不能被抢占,只能等该进程使用完后自己释放
  • 循环等待条件:发生死锁时,存在一个进程-资源循环链,1等2,2等3,......

这四个必要条件,只要破坏其中一个,就可避免死锁。“互斥条件”是临界资源所必需的,无法破坏,因此主要是破坏其他的三个条件以避免死锁。那么具体到Java并发编程中,我们应该如何做呢?

1. 超时放弃

当使用synchronized关键字提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,而且无法放弃已经持有的锁。然而Lock接口的实现类ReentrantLock中,提供了 tryLock()和tryLock(long time, TimeUnit unit) 方法,该方法可以按照固定时长等待锁,如果获得了锁就万事大吉并返回true,如果在限制时间内没有获得锁,就会返回false。那么我们就可以根据其返回值来执行不同的操作,比如线程T获得了lock1,有用获取lock2,并用tryLock()获取lock2,这时我们可以在tryLock()返回false的时候手动释放lock1也就是说,当线程获取锁失败时候,要释放已经持有的锁。

2. 以一致的顺序加锁

我们在并发编程时,尽量一次只获取一个锁。如果必须获取多个锁,即获取了一个锁,还没有释放,又去获取另一个锁,这种情况下,要让每个线程都按照一致的顺序获取锁,那么死锁就可以避免。否则就可能会产生死锁。如图:

此方法保证“循环等待”的条件不会成立。

二、提高锁的性能

锁的竞争必然会导致程序的整体性能下降。为了将这种副作用降到最低,这里提出一些关于使用锁的建议,遵循这些建议可以写出性能更高的程序。

1. 减少锁持有的时间

能用无锁的数据结构,就不要用锁;能锁代码块,就不要锁整个方法体;能用对象锁,就不要用类锁。也就是要让加锁的代码块工作量尽可能少,不要扩大加锁的范围,把无需加锁的代码也放进同步块。这样可以减少线程持有锁的时间,减少线程间的锁冲突。

2. 减小锁的粒度

减小锁的粒度是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力。比较典型的使用场景是ConcurrentHashMap,在JDK1.7中,采用的方式是将HashMap分段,并为每个段独立加锁,不至于将整个HashMap锁住。而在JDK1.8中就更进了一步,干脆直接锁住桶中的链表头结点或红黑树根节点。个人觉得和MySql中的表锁,页锁和行锁的机制有点像。关于ConcurrentHashMap更详细的介绍,可以看这篇文章:ConcurrentHashMap!你居然不知道1.7和1.8可不一样?!

3、锁粗化

通常情况下,为了减少线程见的冲突,应该尽可能缩短锁的持有时间,使用锁之后立即释放。但是呢,凡事需要辩证地看待。因为加锁和释放锁是要陷入内核且会发生上下文切换,是有成本的,如果一个线程连续地对同一个锁不断地请求和释放,或者说释放锁和再次加锁之间有一些无锁操作,但是这些操作很快就完成了,那么应该将对锁的多次请求和释放整合成一次请求和释放。这个操作叫作锁的粗化,代码实例如下

public void demoMethod{
    synchronized(lock){
        do sth 1.
    }
    do sth 2. (不需要同步,但是很快完成)
    synchronized(lock){
        do sth 3.
    }
}

public void demoMethod{
    // 整合成一次锁请求
    synchronized(lock){
        do sth 1.
        do sth 2. (不需要同步,但是很快完成)
        do sth 3.
    }
}

另一个可能用到锁粗化进行锁优化的场景是,在循环体内请求锁时。不过要看具体情况,如果循环体内还有大量不需同步的代码,在循环体外面加锁就会使得锁的持有时间很长。具体看代码说明

for (int i = 0; i < MAX; i++){
    synchronized(lock){
        do sth...
    }
}

// 在循环的外层只请求一次锁
synchronized(lock){
    for (int i = 0; i < MAX; i++){
        do sth...
    }
}

4. 读写分离锁代替独占锁

如果说减小锁粒度通过分割数据结构实现的,那么读写分离锁则是对系统功能点的分割。所谓独占锁,就是无论对变量进行读操作还是写操作,都要加排他锁,使得本来不冲突的读和读之间也需要同步。Java的JUC包提供了一个读写分离锁,ReadWriteLock的ReentrantReadWriteLock实现类,其中的两个方法readLock()和writeLock()分别返回读锁和写锁。读写分离锁允许多线程同时读,尤其在读多写少的场合效果明显。

5. 锁分离

刚才说到的读写分离锁就是使用了锁分离的思想,但是这种思想可以延伸开来,即不同的功能尽量不要用同一个锁,要专锁专用。最典型的例子就是LinkedBlockingQueue(链表式的阻塞队列)。在这个队列中,take()方法从队头取数据,put()方法在队尾添加数据。当队列有大于1个元素时,两者是不冲突的,它们的功能不同,作用的空间也不同,大可不必在一端操作时,将整个队列锁住,以至于另一端也无法操作。而JDK中就是让take()方法和put()方法使用两把不同的锁,互不干扰。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值