0%

Java中Lock接口解析

本文非原创,为转载文章,原文链接:https://www.jianshu.com/p/2344a3e68ca9



一、Lock

synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
void unlock();
Condition newCondition();
}

1.Lock一般的使用如下:

1
2
3
4
5
6
7
8
9
Lock lock= ...;//获取锁
lock.lock();
try{
//处理任务
}catch(Exception e){

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

lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的,其放在finally块里执行,可以保证锁一定被释放,newCondition方法下面会做介绍(通过该方法可以生成一个Condition对象,而Condition是一个多线程间协调通信的工具类)。


2.Lock接口的主要方法介绍:

  • lock():获取不到锁就不罢休,否则线程一直处于block状态。
  • tryLock():尝试性地获取锁,不管有没有获取到都马上返回,拿到锁就返回true,不然就返回false
  • tryLock(long time, TimeUnit unit):如果获取不到锁,就等待一段时间,超时返回false。
  • lockInterruptibly():该方法稍微难理解一些,在说该方法之前,先说说线程的中断机制,每个线程都有一个中断标志,不过这里要分两种情况说明:
    1. 线程在sleepwait或者join, 这个时候如果有别的线程调用该线程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException
    2. 如果线程处在运行状态, 则在调用该线程的interrupt()方法时,不会响应该中断。
      lockInterruptibly()和上面的第一种情况是一样的, 线程在获取锁被阻塞时,如果调用lockInterruptibly()方法,该线程会被唤醒并被要求处理InterruptedException。下面给出一个响应中断的简单例子:
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
public class Test{
public static void main(String[] args){
MyRunnable myRunnable = new Test().new MyRunnable();
Thread thread1 = new Thread(myRunnable,"thread1");
Thread thread2 = new Thread(myRunnable,"thread2");
thread1.start();
thread2.start();
thread2.interrupt();
}

public class MyRunnable implements Runnable{
private Lock lock=new ReentrantLock();
@Override
public synchronized void run() {
try{
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() +"获取了锁");
Thread.sleep(5000);
}catch(InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() +"响应中断");
}finally{
lock.unlock();
System.out.println(Thread.currentThread().getName() +"释放了锁");
}
}
}
}

执行结果如下:

1
2
3
thread1获取了锁
thread1释放了锁
thread2响应中断

thread2在响应中断后,在finally块里执行unlock方法时,会抛出java.lang.IllegalMonitorStateException异常(因为thread2并没有获取到锁,只是在等待获取锁的时候响应了中断,这时再释放锁就会抛出异常)。


3.newCondition()方法

上面简单介绍了ReentrantLock的使用,下面具体介绍使用ReentrantLock的中的newCondition方法实现一个生产者消费者的例子。
生产者、消费者
例子:两个线程A、B,A生产牙刷并将其放到一个缓冲队列中,B从缓冲队列中购买(消费)牙刷(说明:缓冲队列的大小是有限制的),这样就会出现如下两种情况。

  1. 当缓冲队列已满时,A并不能再生产牙刷,只能等B从缓冲队列购买牙刷;
  2. 当缓冲队列为空时,B不能再从缓冲队列中购买牙刷,只能等A生产牙刷放到缓冲队列后才能购买。
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class ToothBrushDemo {
public static void main(String[] args) {
final ToothBrushBusiness toothBrushBusiness =
new ToothBrushDemo().new ToothBrushBusiness();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, true);
}
}, "牙刷生产者1").start();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, false);
}
}, "牙刷消费者1").start();
}

//循环执行50次
public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
boolean isProducer) {
for (int i = 0; i < 50; i++) {
if (isProducer) {
toothBrushBusiness.produceToothBrush();
} else {
toothBrushBusiness.consumeToothBrush();
}
}
}

public class ToothBrushBusiness {
//定义一个大小为10的牙刷缓冲队列
private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
private int number = 1;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public ToothBrushBusiness() {
}

//生产牙刷
public void produceToothBrush() {
lock.lock();
try {
//牙刷缓冲队列已满,则生产牙刷线程等待
while (toothBrushQueue.isFull()) {
notFull.await();
}
ToothBrush toothBrush = new ToothBrush(number);
toothBrushQueue.enQueue(toothBrush);
System.out.println("生产: " + toothBrush.toString());
number++;
//牙刷缓冲队列加入牙刷后,唤醒消费牙刷线程
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

//消费牙刷
public void consumeToothBrush() {
lock.lock();
try {
//牙刷缓冲队列为空,则消费牙刷线程等待
while (toothBrushQueue.isEmpty()) {
notEmpty.await();
}
ToothBrush toothBrush = toothBrushQueue.deQueue();
System.out.println("消费: " + toothBrush.toString());
//从牙刷缓冲队列取出牙刷后,唤醒生产牙刷线程
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

public class ToothBrush {
private int number;
public ToothBrush(int number) {
this.number = number;
}
@Override
public String toString() {
return "牙刷编号{" +
"number=" + number +
'}';
}
}
}

这里缓冲队列的大小设成了10,定义了一个可重入锁lock,两个状态标记对象notEmpty,notFull,分别用来标记缓冲队列是否为空,是否已满。

  1. 当缓冲队列已满时,调用notFull.await方法用来阻塞生产牙刷线程。
  2. 当缓冲队列为空时,调用notEmpty.await方法用来阻塞购买牙刷线程。
  3. notEmpty.signal用来唤醒消费牙刷线程,notFull.signal用来唤醒生产牙刷线程。

4.Object和Conditon对应关系如下:

Object Condition
休眠 wait await
唤醒特定线程 notify signal
唤醒所有线程 notifyAll signalAll

对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。



二、ReadWriteLock

ReentrantLock(可重入锁)是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

synchronized和ReentrantLock都是可重入锁,可重入性举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。


ReentrantReadWriteLock简介

上面的响应中断的例子已经地使用到了ReentrantLock,下面来介绍另外一种锁,可重入读写锁ReentrantReadWriteLock,该类实现了ReadWriteLock接口,该接口的源码如下:

1
2
3
4
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。

  • 线程进入读锁的前提条件:
    1. 没有其他线程的写锁
    2. 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程
  • 进入写锁的前提条件:
    1. 没有其他线程的读锁
    2. 没有其他线程的写锁
  • 需要提前了解的概念:
    • 锁降级:从写锁变成读锁;
    • 锁升级:从读锁变成写锁。

读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。

ReadWriteLock接口只有获取读锁和写锁的方法,而ReentrantReadWriteLock是实现了ReadWriteLock接口,接着对其应用场景做简单介绍。


应用场景:

假设一个共享的文件,其属性是可读,如果某个时间有100个线程在同时读取该文件,如果通过synchronized或者Lock来实现线程的同步访问,那么有个问题来了,当这100个线程的某个线程获取到了锁后,其它的线程都要等该线程释放了锁才能进行读操作,这样就会造成系统资源和时间极大的浪费,而ReentrantReadWriteLock正好解决了这个问题。下面给一个简单的例子,并根据代码以及输出结果做简要说明:

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
public class Test {
public static void main(String[] args) {
MyRunnable myRunnable = newTest().new MyRunnable();
Thread thread1 = new Thread(myRunnable, "thread1");
Thread thread2 = new Thread(myRunnable, "thread2");
Thread thread3 = new Thread(myRunnable, "thread3");
thread1.start();
thread2.start();
thread3.start();
}

public class MyRunnable implements Runnable {
private ReadLock lock = new ReentrantReadWriteLock().readLock();

@Override
public synchronized void run() {
try {
lock.lock();
int i = 0;
while (i < 5) {
System.out.println(Thread.currentThread().getName() + "正在进行读操作");
i++;
}
System.out.println(Thread.currentThread().getName() + "读操作完毕");
} finally {
lock.unlock();
}
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1读操作完毕
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3读操作完毕
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2读操作完毕

从输出结果可以看出,三个线程并没有交替输出,这是因为这里只是读取了5次,但将读取次数i的值改成一个较大的数值如100000时,输出结果就会交替的出现。


看了好多人的博文,在我看来,这个Lock的用处就是可以细化加锁和解锁的操作,使锁操作更加直观,可控