在多线程运行中,Java对于一个对象中的共享资源提供了可重入锁机制,允许在使用共享资源或代码时加锁、使用完毕解锁为代码段赋予原子性。
下面通过产生死锁的例子,来分析这个机制:
public class MethodBlock {
private ReentrantLock lock1 = new ReentrantLock();
private ReentrantLock lock2 = new ReentrantLock();
public void method1(String name) throws InterruptedException {
lock1.lock();
System.out.println("这里是方法1 " + name);
Thread.sleep(500);
System.out.println("方法1结束调用方法2 " + name);
method2(name);
lock1.unlock();
}
public void method2(String name) throws InterruptedException {
lock2.lock();
System.out.println("这里是方法2 " + name);
Thread.sleep(500);
method1(name);
System.out.println("方法2结束调用方法1 " + name);
lock2.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
MethodBlock obj1 = new MethodBlock();
// MethodBlock obj2 = new MethodBlock();
//测试死锁
Runnable r1 = () -> {
try {
obj1.method1("t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable r2 = () -> {
try {
obj1.method2("t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
运行结果:
发生死锁
运行过程如下:
1.线程t1启动,调用start()方法进入可运行状态并运行
2.t1执行method1方法并得到可重入锁lock1,执行到sleep(500)时进入阻塞态持续500ms,此时cpu空闲。
3.线程t2启动,运行,还行method2方法并得到可重入锁lock2,同样执行到sleep(500)时进入阻塞态持续500ms。
4.t1阻塞结束重新执行,希望调用method2方法,但此时method2方法被线程t2加上了lock2锁,因此t1阻塞。
5.t2阻塞结束重新执行,希望调用method1方法,但此时method1方法被线程t1加上了lock1锁,t2也进入了阻塞态。
此时两个线程互相等待,且占有资源不可剥夺。出现死锁。
因此可以看出,锁是对于一个对象的共享资源来说的,多个线程访问这些资源时,会等待有锁的进程释放锁才可以访问。
什么是空重入锁?
持有该锁的线程可以重复地获得已经持有的锁。锁保持一个持有技术来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可继续访问另一个使用相同锁的方法。
比如,将MethodBlock类做如下修改:
private ReentrantLock lock1 = new ReentrantLock(); //所有方法全部使用同一个锁
// private ReentrantLock lock2 = new ReentrantLock();
public void method1(String name) throws InterruptedException {
lock1.lock();
System.out.println("这里是方法1 " + name);
Thread.sleep(2000);
System.out.println("方法1结束调用方法2 " + name);
method2(name);
lock1.unlock();
}
public void method2(String name) throws InterruptedException {
lock1.lock();
System.out.println("这里是方法2 " + name);
Thread.sleep(500);
method1(name);
System.out.println("方法2结束调用方法1 " + name);
lock1.unlock();
}
重新执行:
可以看到,一直都是线程t1在执行,这是因为只有一个锁lock1,t1执行方法method1(..)获得锁之后,t2想要访问代码只能阻塞等待t1释放锁lock1。然而,t1在获得锁之后可以继续调用使用相同锁的method2(..)方法。
PS:由于run中产生未捕获的异常也会导致线程结束,若该线程还持有锁,则这个锁将永远不会释放,因此最好将代码写成如下形式:
lockObj.lock()
try
{
do something..
}
finally
{
lockObj.unlock(); //锁最后一定会被释放
}