java多线程编程核心技术
第四章、Lock的使用
4.1 使用ReentrantLock类
ReentrantLock可以达到synchronized同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能。
4.1.1 使用ReentrantLock实现同步:测试
调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。
调用lock()方法的线程会持有“对象监视器”,其他线程只有等待锁被释放时再次争抢。
代码示例:
/**
* 测试多线程使用ReentrantLock同步
*/
public class Test42ReentrantLock {
public static void main(String[] args) {
MyService42 myService42 = new MyService42();
Thread t1 = new Thread() {
@Override
public void run() {
myService42.methodA();
}
};
t1.setName("A");
Thread t2 = new Thread() {
@Override
public void run() {
myService42.methodB();
}
};
t2.setName("B");
t1.start();
t2.start();
/*
调用lock()代码的线程就获得了"对象监视器",其他线程只能等待锁被释放后再执行
运行结果
methodA begin threadName=A time=1572234673305
methodA end threadName=A time=1572234676306
methodB begin threadName=B time=1572234676306
methodB end threadName=B time=1572234679307
*/
}
}
class MyService42 {
private Lock lock = new ReentrantLock();
public void methodA() {
try {
lock.lock();
System.out.println("methodA begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("methodA end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB() {
try {
lock.lock();
System.out.println("methodB begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("methodB end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.1.3 使用Condition实现等待/通知:错误用法与解决
关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模型,类ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。使用ReentrantLock有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Conditin(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
Reentrant结合Condition类是可以实现“选择性通知”的,而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上,不够灵活。
condition的await()方法调用之前,必须先调用lock()方法获取锁(否则会抛出IllegalMonitorStateException异常),这一点和synchronized相同。
4.1.4 正确使用Condition实现等待/通知
Object类中的wait()方法相当于Condition类中的await()方法。
Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。
Object类中的notify()方法相当于Condition类中的signal()方法。
Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
代码示例:
/**
* Condition的正确用法
*/
public class Test43Condition {
public static void main(String[] args) throws InterruptedException {
MyService43 myService43 = new MyService43();
Thread t = new Thread() {
@Override
public void run() {
myService43.await();
}
};
t.start();
Thread.sleep(3000);
myService43.signal();
/*
调用condition的await和signal方法前必须要获得所属lock的锁,否则会抛异常IllegalMonitorStateException
运行结果:
await 时间为1572317198190
signal 时间为1572317201194
*/
}
}
class MyService43 {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println("await 时间为" + System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 注意,异常情况下也需要自己释放锁
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println("signal 时间为" + System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
}
4.1.5 使用多个Condition实现通知部分线程
如果想单独唤醒部分线程该怎么办呢?这时就有必要使用多个Condition对象类,也就是Condition对象可以唤醒部分指定线程,有助于提升程序运行的效率。
4.1.8 实现生产者/消费者模式:多对多交替打印
代码示例:
/**
* 基于lock、condition的交替打印示例
*/
public class Test44AlternatePrint {
public static void main(String[] args) {
MyService44 myService44 = new MyService44();
Thread[] a = new Thread[10];
Thread[] b = new Thread[10];
for (int i = 0; i < 10; i++) {
a[i] = new Thread() {
@Override
public void run() {
while (true) {
myService44.set();
}
}
};
b[i] = new Thread() {
@Override
public void run() {
while (true) {
myService44.get();
}
}
};
a[i].start();
b[i].start();
}
/*
可以看出,【打印-】和【打印=】是交替出现的
运行结果:
打印=
有可能==连续
打印-
有可能--连续
打印=
有可能==连续
打印-
*/
}
}
class MyService44 {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean hasValue = false;
public void set() {
try {
lock.lock();
while (hasValue == true) {
System.out.println("有可能--连续");
condition.await();
}
System.out.println("打印-");
hasValue = true;
// 这里如果使用signal,则会出现由于仅通知同类线程而造成程序卡住的情况
condition.signalAll();
// 如果不注释掉下面两行,则从这里的输出可以看出,condition.signalAll()也是需要执行完lock.unlock才会真正释放锁
// Thread.sleep(5000);
// System.out.println("unlock...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get() {
try {
lock.lock();
while (hasValue == false) {
System.out.println("有可能==连续");
condition.await();
}
System.out.println("打印=");
hasValue = false;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
4.1.9 公平锁与非公平锁
锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
代码示例:
/**
* 公平锁与非公平锁
*/
public interface Test45FairUnFairLock {
public static void main(String[] args) {
// 测试公平锁
testFair(true);
// 测试非公平锁
testFair(false);
/*
从结果来看,公平锁更容易呈现有序状态,非公平锁更容易出现乱序的结果
输出结果:
test fair:true
ThreadName=Thread-0获得锁定
ThreadName=Thread-1获得锁定
ThreadName=Thread-2获得锁定
ThreadName=Thread-3获得锁定
ThreadName=Thread-4获得锁定
test fair:false
ThreadName=Thread-0获得锁定
ThreadName=Thread-2获得锁定
ThreadName=Thread-1获得锁定
ThreadName=Thread-3获得锁定
ThreadName=Thread-4获得锁定
*/
}
public static void testFair(boolean isFair) {
System.out.println("test fair:" + isFair);
MyService45 myService45 = new MyService45(isFair);
Thread[] tArr = new Thread[5];
for (int i = 0; i < 5; i++) {
tArr[i] = new Thread() {
@Override
public void run() {
myService45.serviceMethod();
}
};
tArr[i].setName("Thread-" + i);
}
for (int i = 0; i < 5; i++) {
tArr[i].start();
}
for (int i = 0; i < 5; i++) {
try {
tArr[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyService45 {
private ReentrantLock lock;
public MyService45(boolean isFair) {
lock = new ReentrantLock(isFair);
}
public void serviceMethod() {
try {
lock.lock();
System.out.println("ThreadName=" + Thread.currentThread().getName() + "获得锁定");
} finally {
lock.unlock();
}
}
}
4.1.10 方法getHoldCount()、getQueueLength()和getWaitQueueLength()的测试
- ReentrantLock的方法int getHoldCount()的作用是查询当前线程保持此锁定的个数,也就是调用lock()方法的次数,即当前锁的锁重入的次数。
- ReentrantLock的方法int getQueueLength()的作用是返回正等待获取此锁定的线程估计数。比如有5个线程都试图获取同一个锁lock,1个线程首先执行lock()方法获取了锁,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待lock的释放。
- ReentrantLock的方法int getWaitQueueLength(Condition condition)的作用是返回等待与此锁相关的Condition的线程估计数。比如有5个线程都调用了同一个condition对象的await()方法,则调用此方法的返回值就是5。
4.1.11 方法hasQueueThread()、hasQueueThreads()、hasWaiters的测试
- ReentrantLock的方法boolean hasQueueThread(Thread thread)的作用是查询指定的线程是否正在等待获取此锁。
- ReentrantLock的方法boolean hasQueueThreads()的作用是查询是否有线程正在等待获取此锁定。
- ReentrantLock的方法boolean hasWaiters(Condition condition)的作用是查询是否有线程正在等待与此锁定有关的condition。
4.1.12 方法isFair()、isHeldByCurrentThread()和isLocked()的测试
- ReentrantLock的方法boolean isFair()的作用是判断是不是公平锁。
- ReentrantLock的方法boolean isHeldByCurrentThread()的作用是查询当前线程是否持有这个锁。
- ReentrantLock的方法boolean isLocked()的作用是查询此锁是否由任意线程持有。
4.1.13 方法lockInterruptibly()、tryLock()和tryLock(long timeout,TimeUnit unit)的测试
- ReentrantLock的方法void lockInterruptibly()的作用是:首先获取锁,在持有锁的过程中,如果线程被interrupt,则线程中断并抛出异常。而我们常用的lock方法遇到interrupt不会受任何影响。
- ReentrantLock的方法boolean tryLock()的作用是,仅尝试一次获取锁的操作,如果成功获取返回true,未能获取锁则返回false。
- ReentrantLock的方法boolean tryLock(long timeout, TimeUnit unit)的作用是,在给定时间内一直尝试获取锁,成功获取锁则返回true,如果在给定时间内都没能获取锁,则返回false。
4.1.14 方法awaitUninterruptibly()的使用
- Condition的方法void awaitUninterruptibly()的作用是,当调用此方法后,无法用interrupt方法打断。而如果是我们常用的await方法遇到interrupt方法后,会抛出异常InterruptedException。
4.1.15 方法awaitUntil()的使用
- Condition的方法boolean awaitUntil(Date deadline)的作用是,指定一个截止时间,在这个时间之前,其他线程可以使用signal/signalAll方法唤醒【调用awaitUntil方法的线程】,而如果到了截止时间,则【调用awaitUntil方法的线程】自动唤醒。
4.1.16 使用Conditiong实现顺序打印
使用Condition对象可以对线程执行的业务进行排序规划。
代码示例:
/**
* 使用Condition实现三种输出按顺序打印
*/
public class Test46ConditionSequentialPrint {
volatile private static int nextPrintWho = 1;
private static ReentrantLock lock = new ReentrantLock();
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Thread a = new Thread() {
@Override
public void run() {
try {
lock.lock();
while (nextPrintWho != 1) {
conditionA.await();
}
System.out.println("ThreadA...");
nextPrintWho = 2;
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread b = new Thread() {
@Override
public void run() {
try {
lock.lock();
while (nextPrintWho != 2) {
conditionB.await();
}
System.out.println("ThreadB...");
nextPrintWho = 3;
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread c = new Thread() {
@Override
public void run() {
try {
lock.lock();
while (nextPrintWho != 3) {
conditionC.await();
}
System.out.println("ThreadC...");
nextPrintWho = 1;
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread[] aArr = new Thread[5];
Thread[] bArr = new Thread[5];
Thread[] cArr = new Thread[5];
for (int i = 0; i < 5; i++) {
aArr[i] = new Thread(a);
bArr[i] = new Thread(b);
cArr[i] = new Thread(c);
aArr[i].start();
bArr[i].start();
cArr[i].start();
}
/*
通过使用不同的Condition,可以使输出按照一定顺序执行
输出结果:
ThreadA...
ThreadB...
ThreadC...
ThreadA...
ThreadB...
ThreadC...
ThreadA...
ThreadB...
ThreadC...
ThreadA...
ThreadB...
ThreadC...
ThreadA...
ThreadB...
ThreadC...
*/
}
}
4.2 使用ReentrantReadWriteLock类
类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务。这样做保证类安全性,但效率较低。
JDK提供了一种读写锁ReentrantReadWriteLock类,使它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock来提升该方法的代码运行速度。
读写锁表示有两个锁,一个是读操作相关的锁,也成为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
代码示例:
/**
* ReentrantReadWriteLock简单测试
*/
public class Test47ReentrantReadWriteLock {
public static void main(String[] args) {
MyService47 myService47 = new MyService47();
Thread t1 = new Thread() {
@Override
public void run() {
myService47.read();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
myService47.read();
// myService47.write();
}
};
t1.start();
t2.start();
/*
由于读读不互斥,所以两个线程同时获得读锁
如果将t2线程读read方法改为write方法,则两个线程变为竞争关系,只能一个执行完另一个才能执行
运行结果:
获得读锁 Thread-1 1572332543264
获得读锁 Thread-0 1572332543264
修改t2线程为write方法后,写锁需要在读锁释放后才能获取,运行结果:
获得读锁 Thread-0 1572332603008
获得写锁 Thread-1 1572332606010
*/
}
}
class MyService47 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
lock.readLock().lock();
System.out.println("获得读锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("获得写锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
}
4.3 ReentrantLock参考资料
读写锁是由AbstractQueuedSynchronizer实现,它内部维护了一个同步等待队列。并且用一个int类型的state来表示锁是否被占用(0或1)、锁是否被重入(1或>1)。底层实现是由compareAndSetState方法提供的,也就是使用来CAS的方式。
相关源码分析文章可参考:https://www.cnblogs.com/takumicx/p/9402021.html
最后
1、所有代码示例都在github中
https://github.com/llbqhh/LlbStudy/tree/master/StudyJava/src/main/java/org/llbqhh/study/java/book/java_multi_thread_programming