文章目录
多线程团队合作:同步控制和锁
同步控制是并发程序必不可少的重要手段。synchronized关键字就是一种最简单的控制方法。同时,wait()和notify()方法起到了线程等待和通知的作用。这些工具对于实现复杂的多线程协作起到了重要的作用。接下来将介绍synchronized,wait,notify方法的代替品(或者说是增强版)——重入锁,这个专题需要大家对多线程基本的内部锁synchronized,wait, notify方法先有基本的认识。
1 synchronized的功能扩展: 重入锁
重入锁完全可以代替synchronized关键字。在早期JDK版本,重入锁的性能远远优于synchronized关键字,在JDK后期版本,对synchronized关键字做了大量的优化,使得两者的性能差不多。
下面展示一段简单的重入锁ReentrantLock使用案例:
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i=0;
public void run() {
for(int j=0;j<10000000;j++){
lock.lock();
// lock.lock();
try {
i++;
}finally {
lock.unlock();
// lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock r1 = new ReenterLock();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上述代码创建了一个全局的ReentrantLock对象,这个对象就是重入锁对象,该对象的lock()和unlock()方法之间7~12行的代码区域就是重入锁的保护临界区,确保了多线程对i变量的操作安全性。
从这段代码可以看到,与synchronized相比,重入锁有着显示操作的过程。开发人员必须手动指定何时加锁 ,何时释放锁。也正是因为这样,重入锁逻辑控制远远要好于synchronized。但值得注意的是,在退出临界区时,必须记得要释放锁,否者永远没有机会再访问临界区了,会造成其线程的饥饿甚至是死锁。
重入锁之所以被称作重入锁是因为重入锁是可以反复进入的。当然,这里的反复进入仅仅局限于一个线程。上诉代码还可以这样写:
public void run() {
for(int j=0;j<10000000;j++){
lock.lock();
lock.lock();
try {
i++;
}finally {
lock.unlock();
lock.unlock();
}
}
}
在这种情况下,一个线程连续两次获得同一把锁。这是允许的!但要注意的是,如果一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。如果释放的次数多了,那么会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放所得次数少了,MAME相当于县城还持有这个锁,因此,其他线程也无法进入临界区
处使用上的灵活性以外,重入所还提供了一些高级功能。比如重入锁提供的中断处理的能力
1.1 中断响应
重入锁除了提供上述的基本功能外,还提供了一些高级功能。比如,重入锁可以提供中断处理的能力。这是一个非常重要的功能,synchronized是没有中断功能的。在等待锁的过程中,程序可以根据需要取消对锁的请求。这是synchronized办不到的。也就是说,重入锁具有解除死锁的功能。
比如你和朋友越好一起去打球,如果你等了半个小时朋友没有到,你突然接到一个电话,说由于突发情况,朋友不能如期钱来了,那么你一定扫兴的达到回府了。中断正是提供了一套类似的机制。如果一个县城正在等待锁,那么他依然可以收到一个通知,被告知无需等待,可以停止工作了,这种情况对于处理死锁是有一定帮助的。
下面的代码产生了一个死锁,得益于锁的中断,我们可以轻易的解决这个死锁:
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntLock(int lock) {
this.lock = lock;
}
public void run() {
try {
if (this.lock == 1) {
lock1.lockInterruptibly();
Thread.sleep(500);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread())
lock1.unlock();
if (lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(this.lock + "线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
线程t1和线程t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1。这样很容易形成t1和t2之间的互相等待,造成死锁。在这里,对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中可以响应中断。
在t1和t2线程start后,主线程39行main进入休眠,此时t1和t2线程处于死锁状态,然后主线程第40行main中断t2线程,故t2会放弃对lock1的请求,同时释放lock2。这个操作使得t1可以获得lock2从而继续执行下去。
执行上诉代码,将输出:
2线程退出
1线程退出
java.lang.InterruptedException
at com.lxs.demo.IntLock.run(IntLock.java:24)
at java.lang.Thread.run(Thread.java:745)