java.util.concurrent.locks包提供了锁和等待条件的接口和类, 可用于替代JDK1.5之前的同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).
一、互斥锁--Lock接口及其实现类ReentrantLock
所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别:
-
synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 而Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁:
-
public class LockTest {
-
private static Lock lock = new ReentrantLock();
-
public static void main(String[] args) {
-
lock.lock();
-
invokeMethod();
-
}
-
-
private static void invokeMethod() {
-
lock.unlock();
-
}
-
}
为了确保锁被释放, 通常会采用如下的代码形式:
-
Lock lock = new ReentrantLock();
-
// 获取锁
-
lock.lock();
-
try {
-
// access the resource protected by this lock
-
} finally {
-
// 释放锁
-
lock.unlock();
-
}
|--void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
|--void unlock(): 执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
-
Lock提供了一个非块结构的获取锁尝试--tryLock(), 一个获取可中断锁的尝试--lockInterruptibly()和一个获取超时失效锁的尝试--tryLock(long time, TimeUnit unit).
|--boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法:
-
Lock lock = new ReentrantLock();
-
if (lock.tryLock()) {
-
try {
-
// manipulate protected state
-
} finally {
-
lock.unlock();
-
}
-
} else {
-
// perform alternative actions
-
}
此用法可确保如果获取了锁, 则会释放锁; 如果未获取锁, 则不会试图将其释放.
-
Lock的newCondition()方法可以获得与该锁绑定的Condition对象, Condition的详细介绍如下.
二、条件--Condition
调用Condition对象的相关方法, 可以方便的挂起和唤醒线程. Object对象的wait(), notify(), notifyAll()方法当然也可以做到这一点, 但是Object对象的这些方法存在很不方便的地方--如果多个线程调用了obj的wait()方法而挂起, 那么我们无法做到调用obj的notify()和notifyAll()方法唤醒其中特定的一个线程. 而Condition对象就可以做到这一点.
-
void await(): 调用Condition对象的await()方法将导致当前线程被挂起, 并释放该Condition对象所绑定的锁. Condition对象只能通过Lock类的newCondition()方法获取, 因此一个Condition对象必然有一个与其绑定的Lock锁. 调用Condition对象的await()方法的前提是: 当前线程必须持有与该Condition对象绑定的锁, 否则程序可能抛出异常.
-
void signal(): 唤醒一个在该Condition对象上挂起的线程. 如果存在多个线程等待这个Condition对象的唤醒, 则随机选择一个. 线程被唤醒之前, 必须重新获取到锁(与该Condition对象绑定的Lock对象).
-
void signalAll(): 唤醒所有在该Condition对象上挂起的线程. 所有被唤醒的线程将竞争与该Condition对象绑定的锁, 只有获取到锁的线程才能恢复到运行状态.
问题的描述
启动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
解法一: 采用原始的synchronized, wait(), notify(), notifyAll()等方式控制线程.
-
public class NumberPrintDemo {
-
// n为即将打印的数字
-
private static int n = 1;
-
// state=1表示将由线程1打印数字, state=2表示将由线程2打印数字, state=3表示将由线程3打印数字
-
private static int state = 1;
-
-
public static void main(String[] args) {
-
final NumberPrintDemo pn = new NumberPrintDemo();
-
new Thread(new Runnable() {
-
public void run() {
-
// 3个线程打印75个数字, 单个线程每次打印5个连续数字, 因此每个线程只需执行5次打印任务. 3*5*5=75
-
for (int i = 0; i < 5; i++) {
-
// 3个线程都使用pn对象做锁, 以保证每个交替期间只有一个线程在打印
-
synchronized (pn) {
-
// 如果state!=1, 说明此时尚未轮到线程1打印, 线程1将调用pn的wait()方法, 直到下次被唤醒
-
while (state != 1)
-
try {
-
pn.wait();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
// 当state=1时, 轮到线程1打印5次数字
-
for (int j = 0; j < 5; j++) {
-
// 打印一次后n自增
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
// 线程1打印完成后, 将state赋值为2, 表示接下来将轮到线程2打印
-
state = 2;
-
// notifyAll()方法唤醒在pn上wait的线程2和线程3, 同时线程1将退出同步代码块, 释放pn锁.
-
// 因此3个线程将再次竞争pn锁
-
// 假如线程1或线程3竞争到资源, 由于state不为1或3, 线程1或线程3将很快再次wait, 释放出刚到手的pn锁.
-
// 只有线程2可以通过state判定, 所以线程2一定是执行下次打印任务的线程.
-
// 对于线程2来说, 获得锁的道路也许是曲折的, 但前途一定是光明的.
-
pn.notifyAll();
-
}
-
}
-
}
-
}, "线程1").start();
-
-
new Thread(new Runnable() {
-
public void run() {
-
for (int i = 0; i < 5; i++) {
-
synchronized (pn) {
-
while (state != 2)
-
try {
-
pn.wait();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
for (int j = 0; j < 5; j++) {
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
state = 3;
-
pn.notifyAll();
-
}
-
}
-
}
-
}, "线程2").start();
-
-
new Thread(new Runnable() {
-
public void run() {
-
for (int i = 0; i < 5; i++) {
-
synchronized (pn) {
-
while (state != 3)
-
try {
-
pn.wait();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
for (int j = 0; j < 5; j++) {
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
state = 1;
-
pn.notifyAll();
-
}
-
}
-
}
-
}, "线程3").start();
-
}
-
}
解法二: 采用JDK1.5并发包提供的Lock, Condition等类的相关方法控制线程.
总结: 对比解法一和解法二, 显然解法二是更好的解决方案. 解法一的问题在于无法进行精确唤醒, 比如线程1执行完打印任务并调用pn.notifyAll()方法后, 3个线程将再次竞争锁, 而不是精确唤醒线程2. 虽然线程2最终将赢得锁, 下一次的打印任务也肯定会由线程2执行, 但是竞争的持续时间是不可预知的, 只能看线程2的人品.
最糟糕的情形可以是: 线程3竞争到了锁, 紧接着wait. 接下来线程1也竞争到了锁, 然后线程1也wait. 此时就再也没有其他线程跟线程2竞争了, 线程2终于艰难的赢得了锁...
-
public class NumberPrint implements Runnable {
-
private int state = 1;
-
private int n = 1;
-
// 使用lock做锁
-
private ReentrantLock lock = new ReentrantLock();
-
// 获得lock锁的3个分支条件
-
private Condition c1 = lock.newCondition();
-
private Condition c2 = lock.newCondition();
-
private Condition c3 = lock.newCondition();
-
-
@Override
-
public void run() {
-
new Thread(new Runnable() {
-
public void run() {
-
for (int i = 0; i < 5; i++) {
-
try {
-
// 线程1获得lock锁后, 其他线程将无法进入需要lock锁的代码块.
-
// 在lock.lock()和lock.unlock()之间的代码相当于使用了synchronized(lock){}
-
lock.lock();
-
while (state != 1)
-
try {
-
// 线程1竞争到了lock, 但是发现state不为1, 说明此时还未轮到线程1打印.
-
// 因此线程1将在c1上wait
-
// 与解法一不同的是, 三个线程并非在同一个对象上wait, 也不由同一个对象唤醒
-
c1.await();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
// 如果线程1竞争到了lock, 也通过了state判定, 将执行打印任务
-
for (int j = 0; j < 5; j++) {
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
// 打印完成后将state赋值为2, 表示下一次的打印任务将由线程2执行
-
state = 2;
-
// 唤醒在c2分支上wait的线程2
-
c2.signal();
-
} finally {
-
// 打印任务执行完成后需要确保锁被释放, 因此将释放锁的代码放在finally中
-
lock.unlock();
-
}
-
}
-
}
-
}, "线程1").start();
-
-
new Thread(new Runnable() {
-
public void run() {
-
for (int i = 0; i < 5; i++) {
-
try {
-
lock.lock();
-
while (state != 2)
-
try {
-
c2.await();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
for (int j = 0; j < 5; j++) {
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
state = 3;
-
c3.signal();
-
} finally {
-
lock.unlock();
-
}
-
}
-
}
-
}, "线程2").start();
-
-
new Thread(new Runnable() {
-
public void run() {
-
for (int i = 0; i < 5; i++) {
-
try {
-
-
lock.lock();
-
while (state != 3)
-
try {
-
c3.await();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
for (int j = 0; j < 5; j++) {
-
System.out.println(Thread.currentThread().getName()
-
+ ": " + n++);
-
}
-
System.out.println();
-
state = 1;
-
c1.signal();
-
} finally {
-
lock.unlock();
-
}
-
}
-
}
-
}, "线程3").start();
-
}
-
-
public static void main(String[] args) {
-
new NumberPrint().run();
-
}
-
}
总结: 对比解法一和解法二, 显然解法二是更好的解决方案. 解法一的问题在于无法进行精确唤醒, 比如线程1执行完打印任务并调用pn.notifyAll()方法后, 3个线程将再次竞争锁, 而不是精确唤醒线程2. 虽然线程2最终将赢得锁, 下一次的打印任务也肯定会由线程2执行, 但是竞争的持续时间是不可预知的, 只能看线程2的人品.
最糟糕的情形可以是: 线程3竞争到了锁, 紧接着wait. 接下来线程1也竞争到了锁, 然后线程1也wait. 此时就再也没有其他线程跟线程2竞争了, 线程2终于艰难的赢得了锁...
三、读写锁--ReadWriteLock接口及其实现类ReentrantReadWriteLock
ReentrantReadWriteLock中定义了2个内部类, ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock, 分别用来代表读取锁和写入锁. ReentrantReadWriteLock对象提供了readLock()和writeLock()方法, 用于获取读取锁和写入锁.
读取锁允许多个reader线程同时持有, 而写入锁最多只能有一个writter线程持有.
读写锁的使用场合: 读取共享数据的频率远大于修改共享数据的频率. 在上述场合下, 使用读写锁控制共享资源的访问, 可以提高并发性能.
如果一个线程已经持有了写入锁, 则可以再持有读写锁. 相反, 如果一个线程已经持有了读取锁, 则在释放该读取锁之前, 不能再持有写入锁.
可以调用写入锁的newCondition()方法获取与该写入锁绑定的Condition对象, 此时与普通的互斥锁并没有什么区别. 但是调用读取锁的newCondition()方法将抛出异常.
使用读写锁的一个例子:
-
public class ReadWriteLockTest {
-
private static ReadWriteLock lock = new ReentrantReadWriteLock();
-
private static Person person = new Person("David Beckham", true);
-
-
public static void main(String[] args) {
-
new Thread() {
-
public void run() {
-
while(true) {
-
try {
-
lock.readLock().lock();
-
System.out.print("name = " + person.getName());
-
System.out.println(", isMan = " + person.isMan());
-
} finally {
-
lock.readLock().unlock();
-
}
-
}
-
};
-
}.start();
-
new Thread() {
-
public void run() {
-
boolean state = true;
-
while(true) {
-
try {
-
lock.writeLock().lock();
-
if (state) {
-
person.setName("Lady GaGa");
-
person.setMan(false);
-
state = false;
-
} else {
-
person.setName("David Beckham");
-
person.setMan(true);
-
state = true;
-
}
-
-
} finally {
-
lock.writeLock().unlock();
-
}
-
}
-
};
-
}.start();
-
}
-
}
-
-
class Person {
-
private String name;
-
private boolean isMan;
-
-
public Person(String name, boolean isMan) {
-
this.name = name;
-
this.isMan = isMan;
-
}
-
-
public String getName() {
-
return name;
-
}
-
-
public void setName(String name) {
-
this.name = name;
-
}
-
-
public boolean isMan() {
-
return isMan;
-
}
-
-
public void setMan(boolean isMan) {
-
this.isMan = isMan;
-
}
-
}
不管程序运行多久, 也不会有人妖的出现。
这里获取写锁的线程在写的时候,其它的线程如果没有使用读锁的话,读到的数据可能会有中间态数据。所以读锁和写锁要同时使用。
参考:
http://coolxing.iteye.com/blog/1236909
和
http://coolxing.iteye.com/blog/1236696
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/11627468/viewspace-1793388/
,如需转载,请注明出处,否则将追究法律责任。