dcLunatic's blog

java Lock包实现锁

字数统计: 2.7k阅读时长: 10 min
2018/09/21 Share

Java并发中lock的使用

简介

在java并发编程中,我们常常要考虑多个线程之间的同步问题,而这时候,synchronized关键字是一个不错的选择,使用synchronized可以实现对临界资源的同步互斥访问,但是synchronized粒度比较大,在使用它时,可能会造成一些性能上的问题,如响应中断等局限性问题。

相比之下,java中的Lock提供了比synchronized更为通用的、更为广泛的锁操作,它能更好的处理线程的同步问题。

synchronized的缺点

synchronized是java中的关键字,是java的内置特性之一,基于JVM层面实现。

当一个代码块被synchronized关键字修饰,然后当一个线程首先获取到了对应的锁之后,并执行该代码块时,其他线程只能一直等待知道占有线程的资源释放锁为止。也就是说,当占有锁的线程执行完这个代码块时,线程会释放对锁的占用;或者占有锁的线程发生异常,然后JVM自动释放对锁的占用。

仔细分析一下这个过程,会存在这样的问题:

当占用锁的线程由于IO或者其他的原因(sleep方法),使得该线程在某些阻塞了,但是此时线程又不会释放锁,所以其他需要得到锁的线程就会一直等待,从而影响了整个程序的运行效率,甚至会出现死锁的情况。(A线程在A1同步代码块中试图去执行B1同步代码块,而B线程在B1同步代码块中试图去执行A1同步代码块,此时,A线程和B线程就会互相等待,从而出现死锁。)

同时,使用synchronized关键字,在阻塞等待过程中的线程,是无法被中断的。

举个实际点的例子,当多个线程进行读写操作时,使用synchronized同步嘛,读和写会发生冲突,写和写也会发生冲突,这时候使用synchronized同步加锁是没有问题的,但是读跟读呢,好像两者并不相互影响。使用synchronized同步的话,就会导致性能问题了。

Lock的优点

相比于synchronized关键字,当我们使用Lock来解决这些同步问题时,可以实现更广泛的锁操作,实现更灵活的锁结构。

比如说会有以下优点:

  1. 对于多个线程进行读写操作时,Lock提供了一个特殊的锁,ReentrantReadWriteLock锁,来使得多个线程可以同时进行读操作。
  2. 当一个线程要获得一个同步锁时,可以即时(或者延时)返回取得锁的结果,成功或者不成功,而不是想synchronized一样,在那里一直阻塞等待(使用tryLock()或者tryLock(long time, TimeUnit unit))。
  3. 在等待获得锁的时候,我们可以直接中断这个等待,也就是说,能够响应中断(lockInterruptibly())。

但是相比synchronized关键字,使用Lock的同步锁,并不会自动释放该锁,该锁必须手动释放。当没有主动释放锁时,就有可能会出现死锁的情况。一般建议将加锁放在try里面,然后在finally中释放锁。

Lock的注意事项

三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock 类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。

由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。

Lock接口

在java.util.concurrent.locks包中,Lock接口是一个最基本的接口之一。

方法

  • void lock() //获取锁
  • void lockInterruptibly() //如果当前线程没有被中断,则获取锁
  • Condition newCondition() //返回绑定到此Lock实例的新Condition实例
  • boolean tryLock() //仅在调用时锁空闲状态时才获取锁,返回获取结果
  • boolean tryLock(long time, TimeUnit unit) //如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁,返回获取结果
  • void unlock() //释放锁

newCondition()方法一般是用在线程协作的情况下的,在这里暂时不说。

lock()

这个方法会比较常用,实现起来的效果跟synchronized关键字一样。使用lock()方法尝试去获取锁,如果获取不到锁,则会一直等待,等待过程中,不可响应中断。

1
2
3
4
5
6
7
8
9
Lock lock = ...;
lock.lock();
try{
//doSomething();
}catch(Exception e){
//e.printStackTrace();
}finally{
lock.unlock();
}

这里之所以使用try-catch-finally结构,是为了保证无论在什么情况下,这个锁都可以被释放。

tryLock()与tryLock(long time, TimeUnit unit)

这两个方法都是有返回值的,tryLock()方法会即时的返回获取锁的结果,如果在调用的时候成功获取到了锁,则返回true,否则返回false,相比lock()方法,它不会在那里一直等待,直到获取成功。tryLock(long time, TimeUnit unit)方法类似于tryLock()方法,但是它允许等待一段时间,在这段时间内,如果得到锁,返回true,否则在这段时间后,返回false。time参数指定时间,而unit参数指定时间单位。

值得注意的是,tryLock(long time, TimeUnit unit)支持响应中断,而tryLock()不支持,关于响应中断,见下面的lockInterruptibly()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Lock lock = ...;
if(lock.tryLock()){
try{
//doSomething();
}catch(Exception e){
//e.printStackTrace();
}finally{
lock.unlock();
}
}

//等待50s
boolean rs = false;
try{
rs = lock.tryLock(50L, TimeUnit.SECONDS);
}catch(InterruptedException ie){
//这里对中断产生的异常进行捕获处理,但是一般的做法更应该抛出给上一级处理,参见下面
}
if(rs){
try{
//doSomething();

}catch(Exception e){
//e.printStackTrace();
}finally{
lock.unlock();
}
}

lockInterruptibly()

同lock()方法一样,会一直等待获取锁,但是,它是可以响应中断的,也就是说,在它等待获取锁的过程中,可以由于调用Thread.interrupt()方法来中断这个阻塞的等待过程,从而抛出一个中断异常。

在一个线程正常执行时,是不会被interrupt()方法中断的,只有在阻塞等待过程中的线程才会被interrupt()方法中断。

1
2
3
4
5
6
7
8
9
10
11
function() throws InterruptedException{
Lock lock = ...;
lock.lockInterruptibly();
try{
//doSomething();
}catch(Exception e){
//e.printStackTrace();
}finally{
lock.unlock();
}
}

ReentrantLock

ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且在ReenTrantLock中提供了更多的方法,以实现更加广泛的锁操作。

构造方法

  • ReentrantLock() //创建一个ReentrantLock的实例

  • ReentrantLock(boolean fair) //创建一个具有给定公平策略的ReentrantLock

    这里说一下第二个构造方法,公平参数的作用。当设置为true时,在多个线程的争用下,这些锁将倾向于将访问全授予等待时间最长的线程。否则此锁将无法保证任何特定的访问顺序。与采用默认设置(不公平)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

常用方法

  • boolean isFair() //该锁是否是公平的
  • boolean isLocked() //该锁是否被获取了
  • boolean isHeldByCurrentThread() //当前线程是否获得该锁
  • int getQueuedLength() //返回正等待获取此锁的线程估计数
  • boolean hasQueuedThread(Thread thread) //指定线程是否在等该获取该锁
  • boolean hasQueuedThreads() //是否有线程在等待获取该锁

例子

  1. 关于公平参数的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.locks.*;
public class ReentrantLockTest1{
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws Exception{
ReentrantLockTest1 test = new ReentrantLockTest1();
Thread a = new Thread(){
public void run(){
test.waitTime("a", 3000);
};
};
Thread b = new Thread(){
public void run(){
test.waitTime("b", 2000);
};
};
Thread c = new Thread(){
public void run(){
test.waitTime("c", 3000);
};
};
a.start();
Thread.sleep(1000); //保证a先执行
b.start();
//Thread.sleep(10); //保证b比c要先等待
c.start();
}
public void waitTime(String n, long time){
System.out.println("线程" + n + "开始执行");
lock.lock();
try{
System.out.println(n + "得到了锁");
long start = System.currentTimeMillis();
while(System.currentTimeMillis()-start<time){
;
}
}catch(Exception e){

}finally{
System.out.println(n + "释放了锁");
lock.unlock();
}
}
}

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@dcLunatic:~# java ReentrantLockTest1 
线程a开始执行
a得到了锁
线程b开始执行
线程c开始执行
a释放了锁
b得到了锁
b释放了锁
c得到了锁
c释放了锁
root@dcLunatic:~# java ReentrantLockTest1
线程a开始执行
a得到了锁
线程b开始执行
线程c开始执行
a释放了锁
c得到了锁
c释放了锁
b得到了锁
b释放了锁

两次执行结果,两次b都会比c先被调度执行函数waitTime,但是在第一次的时候,是b先获得锁,在第二次的时候却是c先获得锁了,按照先后执行顺序而言,这里的b在等待获取锁的时间应该会比c要长一点点的。但他们在等待获取锁的机会是相同的,所以谁先谁后都是有可能的。

如果这里要使得b如果比c执行了waitTime函数,然后b必先在c之前得到锁的话,就可以考虑加上公平参数了。(当然,上面你也可以加Thread.sleep(10)来保证b在c之前执行)

原文作者:dcLunatic

原文链接:http://dclunatic.github.io/java-Lock%E5%8C%85%E5%AE%9E%E7%8E%B0%E9%94%81.html

发表日期:September 21st 2018, 1:39:18 pm

更新日期:July 11th 2021, 9:13:50 pm

版权声明:转载的时候,记得注明来处

CATALOG
  1. 1. Java并发中lock的使用
    1. 1.1. 简介
    2. 1.2. synchronized的缺点
    3. 1.3. Lock的优点
    4. 1.4. Lock的注意事项
    5. 1.5. Lock接口
      1. 1.5.1. 方法
      2. 1.5.2. lock()
      3. 1.5.3. tryLock()与tryLock(long time, TimeUnit unit)
      4. 1.5.4. lockInterruptibly()
    6. 1.6. ReentrantLock
      1. 1.6.1. 构造方法
      2. 1.6.2. 常用方法
      3. 1.6.3. 例子