锁是用来控制多个线程访问共享资源的方式,一般来说一个锁可以防止多个线程访问共享资源(但有些锁可以允许多个线程访问共享资源,如读写锁)。在JavaSE5之前,Java使用synchronized关键字实现锁的功能,但是在JavaSE5之后,并发包中提供了Lock接口(及其实现类),用来实现锁的功能。

java.util.concurrent.locks包下常用的类与接口

java并发11-细说Lock

一、Lock VS synchronized

(1)JDK版本不同

  • synchronized关键字产生于JKD1.5之前,是低版本保证共享资源同步访问的主要技术。
  • Lock接口产生于JDK1.5版本,位于著名的java.util.concurrent并发包中,是Java提供的一种比synchronized关键字更加灵活与丰富的共享资源同步访问技术。

(2)读写锁

  • synchronized关键字只提供了一种锁,即独占锁。
  • Lock接口不仅提供了与前者类似的独占锁,而且还通过ReadWriteLock接口提供了读锁和写锁。
    读写锁最大的优势在于读锁与写锁并不独占,提高了共享资源的使用效率。

(3)块锁与链锁

  • synchronized关键字以代码块或者说是作用域机制实现了加锁与解锁,我简称为块锁。synchronized关键字的作用域机制导致同步块必须包含在同一方法中,且多个锁的加锁与解锁顺序正好相反。
  • Lock接口并不限制锁的作用域和加解锁次序,可以提供类似于链表样式的锁,所以我简称为链锁。Lock接口并不需要把加锁和解锁方法放在同一方法中,且加锁和解锁顺序完全随意

(4)解锁方式

  • synchronized关键字:随着同步块/方法执行完毕,自动解锁。
  • Lock接口:需要手动通过lock.unlock()方法解锁,一般此操作位于finally{}中

(5)阻塞锁与非阻塞锁:

  • synchronized关键字提供的锁是阻塞的,它会一直尝试通过轮询去获取对象的监视锁。
  • Lock接口通过lock.tryLock()方法提供了一种非阻塞的锁,它会尝试去获取锁,如果没有获取锁,则不再尝试。

(6)可中断锁

  • synchronized关键字提供的锁是不可中断的,它会一直尝试去获取锁,我们无法手动的中断它。
  • Lock接口通过lock.lockInterruptibly()提供了一种可中断的锁,我们可以主动的去中断这个锁,避免死锁的发生。

(7)可超时锁

  • synchronized关键字提供的锁是不可超时的,它会一直尝试去获取锁,直至获取锁。
  • Lock接口通过{tryLock(long, TimeUnit)方法}方法提供了一种可超时的锁,它会在一段时间内尝试去获取锁,如果限定时间超时,则不再尝试去获取锁,避免死锁的发生。

(8)公平锁

我们都知道,如果高并发环境下多个线程尝试去访问同一共享资源,同一时刻只有一个线程拥有访问这个共享资源的锁,其他的线程都在等待。

  • synchronized关键字提供的锁是非公平锁,如果持有锁的线程释放了锁,则新进入的线程与早就等待的线程拥有同样的机会获取这个锁,简单来说就是不讲究:先来后到,反而讲究:来得早不如来得巧。非公平锁可能导致某些线程永远都不会获取锁。
  • Lock接口默认也是非公平锁,但是他还可以通过fair参数指定为公平锁。在公平锁机制下,等待的线程都会被标记等待次数,等待次数越多的锁获取锁的优先级越高,也就是常说的:先到先得。

(9)互不干扰,可以共用

  • synchronized关键字是通过关键字实现对对象的加锁与解锁的。
  • Lock接口是通过Lock接口的实现类的实例对象的lock()和unlock()方法实现加锁与解锁的。
  • 我们也可以通过synchronized关键字对Lock接口的实现类的实例对象进行监视器锁的加锁与解锁。而且对监视器锁的加锁与解锁与Lock接口的实现类的实例对象的lock()和unlock()方法并不冲突。
  • 也就是说我们可以同时使用Lock接口和synchronized关键字实现同步访问控制。
  • 但是,原则上是极其不建议这种混搭的同步访问控制方式的,不仅代码难以阅读,而且容易出错。

二、Lock提供的接口

2.1 void lock() & unlock()

获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();//获取锁
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}

2.2 tryLock() & tryLock(long time, TimeUnit unit)

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

一般情况下,通过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

2.3 lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者,原因稍后阐述。因此,lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

2.4 Condition newCondition()

  • 获取一个绑定到当前Lock对象的Condition对象。
  • 获取Condition对象的前提是当前线程持有Lock对象

关于Condition的相关内容会在后续文章进行学习。

参考资料

https://www.cnblogs.com/myseries/p/10784076.html

https://blog.csdn.net/hanchao5272/article/details/79680547

https://www.cnblogs.com/haimishasha/p/11198419.html#autoid-1-0-0