Java多线程基础-互斥锁Lock和Condition

互斥锁

  • JDK1.5版本提供了java.util.concurrent.locks包,该包中提供了锁和等待条件的接口和类,可以用于代替JDK1.5之前的synchronized同步和监视器机制
  • 互斥锁指的是一次最多只能有一个线程持有的锁
  • 互斥锁在Java中的体现是Lock接口和其实现类ReentrantLock
  • Lock接口的出现主要替代了synchronized关键字的用处,其提供了一个比sychronized机制更广泛的锁定操作

Lock和sychronized机制的主要区别

  • synchronized机制提供了对于每个对象相关的隐式监视器锁的访问,并强制所有锁获取和释放都要出现在一个块结构中
  • 获取了多个锁时(多个synchronized代码块嵌套时),它们必须以相反的顺序释放
  • synchronized机制对锁的释放是隐式的,只要线程运行的代码超出了synchronized语句块范围,持有的锁对象就会自动释放
  • 锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁
  • Lock机制必须是显式调用Lock对象的unlock()方法才能释放锁。
  • Lock机制可以不在同一个块结构中获取和释放锁,更加自由的释放锁

Lock接口

  • Lock 实现提供了使用 synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 tryLock()、一个获取可中断锁的尝试 lockInterruptibly() 和一个获取超时失效锁的尝试 tryLock(long, TimeUnit)
  • Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。
  • 注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock()方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。

lock

void lock() 获取锁

  • 如果锁处于空闲状态,当前线程将直接获取该lock对象锁。
  • 相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁.

unlock

void unlock() 释放锁

  • 当前线程将释放持有的锁。
  • 锁只能由持有者释放。
  • 如果线程并不持有锁,却执行了该方法,可能会导致异常的发生。

tryLock

boolean tryLock()

  • 仅在调用时锁为空闲状态才获取该锁。

  • 如果锁可用,则获取锁,并返回true

  • 如果锁不可用,立即返回false

  • 此方法可确保如果获取了锁,则会释放锁,如果未获取锁,则不会试图将其释放。即通常配合unlock()使用来释放锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Lock lock = new ReentrantLock();
    if(lock.tryLock()){
    try{
    //获取到锁的一些操作
    }finally{
    //确保了获取锁,才能释放
    lock.unlock();
    }
    }else{
    //未获取到锁的一些操作
    }
  • tryLock()lock()方法的区别:

    • tryLock()方法只是试图获取锁,如果锁不可用,当前线程仍然可以继续往下执行.
    • lock()方法是一定要获取到锁,如果锁不可用,就会一直等待下去,锁定当前线程,在未获取指定锁对象之前,当前线程不会继续向下执行

Condition接口 - 条件

  • ConditionObject监视器方法wait、notify、notifyAll方法分解成不同的对象,为了方便通过将这些对象与任意Lock对象实现组合使用,为每个对象提供了多个等待set(wait-set)。其中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用

  • 使用Condition对象的相关方法,可以方便的挂起和唤醒线程,而且可以特定的唤醒其其中某个线程。这也是和Object对象的wait()、notify()、notifyAll()方法的区别。

  • Object对象的wait()、notify()、notifyAll()方法存在一个问题:如果多个线程调用了obj的wait()方法而挂起,那么我们无法做到调用obj的notify()和notifyAll()方法唤醒其中特定的一个线程,而Conditon对象就可以做到。
  • Condition对象只能通过Lock类的newCondition()方法获取,因此一个Condition对象必然会有一个与其绑定的Lock锁

await

void await() 造成当前线程再接到信号或被中断之前一直处于等待状态

  • 将当前线程处于等待状态,并释放该Condition对象所绑定的锁.
  • 使用await()方法前当前线程必须持有与该Condition对象绑定的锁,否则程序可能会抛出异常。

signal

void signal() 唤醒一个在该Condition对象上挂起的线程

  • 如果存在多个线程同时等待该Condition对象的唤醒,则随机选择其中一个唤醒。
  • 线程被唤醒之前,必须重新获取到锁,即与该Condition对象绑定的Lock对象。

signalAll

void signalAll() 唤醒所有在该Condition对象上挂起的线程

  • 所有被唤醒的线程将竞争与该Condition对象绑定的锁,只有获取到锁的线程才能恢复到运行状态。

一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题的描述:

启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5,
然后是线程2打印6,7,8,9,10,然后是线程3打印11,12,13,14,15.
接着再由线程1打印16,17,18,19,20....以此类推,
直到打印到75. 程序的输出结果应该为:

线程1: 1
线程1: 2
线程1: 3
线程1: 4
线程1: 5
线程2: 6
线程2: 7
线程2: 8
线程2: 9
线程2: 10
...
线程3: 71
线程3: 72
线程3: 73
线程3: 74
线程3: 75

利用ReentrantLockCondition接口组合,可以轻松指定和分配各个线程该完成的操作。代码如下:

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

public class ReentrantLockTest {

public static void main(String[] args) {
NumPrinter n = new NumPrinter();
//此处确定每个线程应该执行几次,总共75个数分3个线程,每个线程分25个数字,
// 5*5=25,每个线程执行5次,每次打印5个数字
new Thread(()->{
//每个线程执行5次
for (int i = 0; i < 5 ; i++) {
try {
n.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 0; i <5 ; i++) {

try {
n.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
n.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

class NumPrinter{
private int num = 1;
private int flag = 1;
//使用ReentrantLock类和Condition接口来配合使用,指定唤醒哪个线程
private ReentrantLock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();

public void print1() throws InterruptedException {
//三个线程同时占用临界区的资源,应该使用同步
lock.lock(); //获取锁 和 下面的 unlock 代替了 synchronized
if(flag != 1) //当flag不为1时,c1进行等待,因为此时其他的线程正在执行他们的操作
c1.await();
if(num <= 75)
for (int i = 0; i < 5; i++) {
System.out.println("1---"+num++);
}
flag = 2; //标记指定为2,在print2中限制除了flag不为2时,c2进行等待
c2.signal(); //唤醒指定的c2
lock.unlock(); //释放锁
}

public void print2() throws InterruptedException {
lock.lock();
if(flag != 2)
c2.await();
if(num <= 75)
for (int i = 0; i < 5; i++) {
System.out.println("2---"+num++);
}
flag = 3;
c3.signal();
lock.unlock();
}

public void print3() throws InterruptedException {
lock.lock();
if(flag != 3)
c3.await();
if(num <= 75)
for (int i = 0; i < 5; i++) {
System.out.println("3---"+num++);
}
flag = 1;
c1.signal();
lock.unlock();
}

}