Java可重入锁及Condition线程间通信
java.util.concurrent.locks包对锁提供了支持,锁是一些对象,他们为使用synchronized控制对共享资源的访问提供了替代技术。大体而言,锁的工作原理如下:在访问共享资源之前,申请用于保护资源的锁;当资源访问完成,释放锁。当某个线程正在使用锁时,如果另一个线程尝试申请锁,那么后者将被挂起,直到锁释放为止。通过这种方式,可以防止多线程对共享资源的冲突访问。
对比,使用Java内置的同步特性(使用同步块或者同步方法),使用锁的的优势在什么地方?我们知道,只要是有线程进入到对象监视器的内部(即同步块或者同步方法中),其他的线程就只能等待。但是,我们还想到,如果这几个线程中有只读线程也有写线程,那么,对于只读线程可以让他们一下子都进去监视器内部访问,而对于写线程则是互斥访问,这是我们想的,但是同步块和同步方法是不分读写的,只要是有线程进去就不让其他线程进来了。而锁可以做到这一点,因为锁除了有和同步特性对应的ReentrantLock,还有ReentrantReadWriteLock。
可重入锁:同步块同步方法、ReentrantLock/ReentrantReadWriteLock都是可重入锁。Java为每个锁关联了一个请求计数器和占有它的线程,当请求计数器为0时,表示这个锁没有被请求持有,如果不为零,表示有线程请求持有,每次有一个线程请求,计数器加一,线程退出监视器后,计数器减一。那可重入锁是什么呢?线程A进入到了X对象的监视器内部,那线程A已经拿到了X对象的锁,如果在X对象内部调用了其他对象Y监视器内部的操作,而且Y对象监视器没有被其他对象持有,那线程A就要进入到Y对象监视器内部,但是,如果锁不是可重入的,JVM会判断:线程A已经拿到了X对象监视器的锁,不能再拿其他对象监视器的锁了,所以就造成了『死锁』(不同于平常的死锁,这里的死锁是因为线程A不能正常终结而不能释放锁);而锁的可重入性就是用来解决这个问题的:Java中锁的分配是按照线程分配的(即使该线程进入到了不同对象的监视器内部,表面上拿到了许多对象的锁),每个线程分配一个锁,这个锁是可重入的(一个锁可以进入到不同对象的监视器内部);而不是基于调用分配锁的,基于调用分配锁,是每次进入到对象监视器的内部都分配一个锁给这个线程。简而言之:Java为每个线程分配一个锁,而不是为每次调用分配一个锁。可重入锁的参考。
对应于同步块和同步方法的ReentrantLock使用大致如下:多个需要同步的线程使用同一个锁,在访问资源前,先获取锁,访问资源之后,释放锁。实例代码如下:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(()->{
int i = 0;
while(i<3){
//获取锁,获取不到就等待
reentrantLock.lock();
//获取之后进行一系列操作
System.out.println("Thread-0 get the lock...");
System.out.println("I'm Thread-0,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("I'm Thread-0,I'm awake now,I'll release the lock..");
i++;
reentrantLock.unlock();
}
}).start();
new Thread(()->{
int i = 0;
while(i<3){
//获取锁,获取不到就等待
reentrantLock.lock();
//获取之后进行一系列操作
System.out.println("Thread-1 get the lock...");
System.out.println("I'm Thread-1,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("I'm Thread-1,I'm awake now,I'll release the lock..");
i++;
reentrantLock.unlock();
}
}).start();
}
// 运行结果:
// Thread-0 get the lock...
// I'm Thread-0,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-0,I'm awake now,I'll release the lock..
// Thread-0 get the lock...
// I'm Thread-0,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-0,I'm awake now,I'll release the lock..
// Thread-0 get the lock...
// I'm Thread-0,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-0,I'm awake now,I'll release the lock..
// Thread-1 get the lock...
// I'm Thread-1,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-1,I'm awake now,I'll release the lock..
// Thread-1 get the lock...
// I'm Thread-1,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-1,I'm awake now,I'll release the lock..
// Thread-1 get the lock...
// I'm Thread-1,I hold the lock,but I'll sleep for a while,give others the chance to try to get the lock..
// I'm Thread-1,I'm awake now,I'll release the lock..
/*
* 很显然这是由于锁的作用哦
*/
}
ReentrantReadWriteLock支持了读锁和写锁的分离,读锁之间可以同时进行,写锁可以下降为读锁,读锁不能上升为写锁,写锁和读写锁都互斥。使用的实例代码如下:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockTest {
public static void main(String[] args) {
/*
* 有两个线程对公共资源进行写,使用写锁,有两个线程对公共资源进行读,使用读锁
*/
ReentrantReadWriteLock rrrl = new ReentrantReadWriteLock();
new Thread(()->{
//第一个写线程
int i = 0;
while(i<2){
rrrl.writeLock().lock();
System.out.println("the first write thread...");
try {
//给其他线程竞争锁的机会
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("the first write thread done...");
i++;
rrrl.writeLock().unlock();
}
}).start();
new Thread(()->{
//第二个写线程
int i = 0;
while(i<2){
rrrl.writeLock().lock();
System.out.println("the second write thread...");
try {
//给其他线程竞争锁的机会
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("the second write thread done...");
i++;
rrrl.writeLock().unlock();
}
}).start();
new Thread(()->{
//第一个读线程
int i = 0;
while(i<2){
rrrl.readLock().lock();
System.out.println("the first read thread...");
try {
//给其他线程竞争锁的机会
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("the first read thread done...");
i++;
rrrl.readLock().unlock();
}
}).start();
new Thread(()->{
//第二个读线程
int i = 0;
while(i<2){
rrrl.readLock().lock();
System.out.println("the second read thread...");
try {
//给其他线程竞争锁的机会
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("the second read thread done...");
i++;
rrrl.readLock().unlock();
}
}).start();
}
}
//运行结果:
//the first write thread...
//the first write thread done...
//the first write thread...
//the first write thread done...
//the second write thread...
//the second write thread done...
//the second write thread...
//the second write thread done...
//the first read thread...
//the second read thread...
//the second read thread done...
//the second read thread...
//the first read thread done...
//the first read thread...
//the second read thread done...
//the first read thread done...
我们知道,线程间通信是基于线程同步的,不同的同步方式可能对应不同的线程间通信方式,的确,之前使用Java内置的同步特性进行线程间通信使用的是Object类库的wait和notify方法,那使用锁之后的线程间通信是怎么进行的呢?Lock接口有一个newCondition()方法,这个方法返回一个Condition对象,而Condition是用来进行线程间通信的类。对于Condition来说,其await()/signal()/signalAll()分别对应Object类库中的wait()/notify()/notifyAll()三个方法,示例代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class OneCondition {
public static void main(String[] args) {
ReentrantLock rl = new ReentrantLock();
Condition condition = rl.newCondition();
new Thread(()->{
rl.lock();
//进来之后就进行等待
try {
condition.await();
System.out.println("I'm thread-0, I'm awake now..");
} catch (Exception e) {
e.printStackTrace();
}
rl.unlock();
}).start();
new Thread(()->{
rl.lock();
//进来之后,先睡一会儿,给线程上一个线程运行的机会,即使给了他机会,因为进去之后就等待了,所以也不会向下执行
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("I'm thread-1, I will be in the firstline in the output of the console absolutely..");
condition.signalAll();
rl.unlock();
}).start();
// 运行结果:
// I'm thread-1, I will be in the firstline in the output of the console absolutely..
// I'm thread-0, I'm awake now..
}
}
但是,Condition类要比Object类库的三个通信方法更强大,原因就是他可以不同的线程创建多个Condition,这样和Object类库的三个方法相比,就相当于一下子有了多组这样的三个方法。实例代码如下:(
实例代码本来是想实现生产者-消费者案例,如果无限生产和无限消费,是没有问题的,但是如果指定了要生产和消费的数量,有一个bug,测试了一个下午,也没找到在哪儿:最后一次只有生产输出而没有消费输出,还求大神们看出是哪的问题?我测试的结果是最后一次生产之后,虽然在代码上唤醒了消费者,但实际上消费者并没有被唤醒。求大神不吝指教)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TwoConditions {
public static void main(String[] args) {
ReentrantLock rt = new ReentrantLock();
Queue4 queue4 = new Queue4(rt);
// 生产者
new Thread(() -> {
while (queue4.count < 5) {
queue4.set();
}
}).start();
// 消费者
new Thread(() -> {
while (queue4.count <= 5) {
queue4.get();
}
}).start();
}
// 运行结果:
// Set: 1
// Got: 1
// Set: 2
// Got: 2
// Set: 3
// Got: 3
// Set: 4
// Got: 4
// Set: 5
// #然后程序一直运行,Got:5输出不了。
}
class Queue4 {
int count = 0;
boolean isCon = true;
ReentrantLock rt = null;
Condition pro = null;
Condition con = null;
public Queue4(ReentrantLock rt) {
this.rt = rt;
pro = rt.newCondition();
con = rt.newCondition();
}
public void get() {
// 消费者
rt.lock();
if(isCon==false){
System.out.println("Got: "+count);
isCon = true;
pro.signalAll();
return;
}
try {
con.await();
/*
* 等待时候,count为4,等活了之后,count为5,然而count为4等待之后,就一直等待了,没有被唤醒,即使在代码上唤醒了
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
rt.unlock();
}
public void set() {
// 生产者
rt.lock();
if(isCon==true){
count ++;
System.out.println("Set: "+count);
isCon = false;
con.signalAll();
return;
}
try {
pro.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
rt.unlock();
}
}