当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果摸个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:
public class Father
{
public synchronized void doSomething(){
......
}
}
public class Child extends Father
{
public synchronized void doSomething(){
......
super.doSomething();
}
}
子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。由于Father和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
说实话,读这本书时,我本着一个学生对作者无比崇敬的心情战战兢兢的欣赏着,生怕自己看不懂。当我看完了作者的文字内容之后,除了认同和佩服之外,没有产生任何的疑问。可是当我看到作者的示例代码是,我完全搞不懂了,心情一落千丈。我一度对作者的水平产生了怀疑,但是觉得不太可能是作者的问题,因为我深知自己的水平有多低,应该是我的问题。
怀疑如下:假设一个线程t1调用了Childl类某个实例c1的doSomething方法,那么在成功调用之前t1应该先获得c1的锁。接下来调用Father(super:大家都知道,在创建c1需要先创建其父类的实例f1,super作为f1的引用)类某个实例f1的doSomething方法,那么在成功调用之前t1应该先获得f1的锁。也就是说t1线程先后或得了两个不同对象的锁,这怎么能叫重入呢?
1. 第一步探索
class _Father{
public synchronized void dosomething(){
System.out.println("the dosomething method of father");
}
public synchronized void mydosomething() throws InterruptedException{
System.out.println("the mysomething method of father");
Thread.sleep(3000);
}
}
public class _JavaConcurrency_01 extends _Father{
public synchronized void dosomething() {
System.out.println("the dosomething method of son");
super.dosomething();
}
public void mydosomething() throws InterruptedException {
super.mydosomething();
}
public static void main(String[] args) throws InterruptedException {
final _JavaConcurrency_01 s1 = new _JavaConcurrency_01();
// 启动t1执行son.mydosomething方法(不需要锁)
// 继续调用super.mydosomething方法,获取到了f1(f1在疑问中阐述)的锁,并让t1续修3秒钟
new Thread(new Runnable() {
public void run() {
try {
s1.mydosomething();
} catch (InterruptedException e) {
}
}
}, "t1").start();
// 确保t1线程先执行
Thread.sleep(100);
// 主线程中son.dosomething方法中需要调用f1的dosomething方法,f1的锁被t1抢占,必须等t1释放锁之后主线程才能进入
s1.dosomething();
}
}
预测结果:
the mysomething method of father
the dosomething method of son
三秒之后打印下面内容
the dosomething method of father
预测结果分析:
t1调用son.mydosomething方法时不需要获取s1对象的锁,但是son.mydosomething方法中调用了super.mydosomething()方法,获取到f1实例的锁,打印“the mysomething method of father”,然后停顿三秒钟。
主线程停顿100毫秒后执行s1.dosomething方法,该方法需要获取s1实例的锁(获取成功,因为t1没有获取s1实例的锁),打印“the dosomething method of son”,接下来调用super.dosomething,需要获取f1实例的锁(获取失败,f1已经被t1获取,三秒后才会释放f1的锁),主线程阻塞,三秒之后打印“the dosomething method of father”
真实结果:
the mysomething method of father
三秒之后打印下面内容
the dosomething method of son
the dosomething method of father
结果分析:
看到真实结果后,觉得自己太傻太无知了。在真实结果面前,我好像感觉到了一丝丝真相的味道:好像t1在执行_Father类中的mydosomething方法时获得是实例s1的锁并不是f1的锁,也就是说作者说的没错。
2. 第二步探索
这一次我干脆一不做二不休,直接在_JavaConcurrency_01 中创建了一个Father类的实例f1来代替所有super。
class _Father{
public synchronized void dosomething(){
System.out.println("the dosomething method of father");
}
public synchronized void mydosomething() throws InterruptedException{
System.out.println("the mysomething method of father");
Thread.sleep(3000);
}
}
public class _JavaConcurrency_01 extends _Father{
_Father f1 = new _Father();
public synchronized void dosomething() {
System.out.println("the dosomething method of son");
f1.dosomething();
}
public void mydosomething() throws InterruptedException {
f1.mydosomething();
}
public static void main(String[] args) throws InterruptedException {
final _JavaConcurrency_01 s1 = new _JavaConcurrency_01();
new Thread(new Runnable() {
public void run() {
try {
s1.mydosomething();
} catch (InterruptedException e) {
}
}
}, "t1").start();
Thread.sleep(100);
s1.dosomething();
}
}
预测结果:
the mysomething method of father
the dosomething method of son
三秒之后打印下面内容
the dosomething method of father
预测结果分析:
与第一步探索雷同
真实结果:
与预测结果完全一致,也就说我的说法好像不太对,真想打自己的脸。
3. 第三步探索
s1.mydosomething() ->super.mydosomething()
这次我要探索的是super.mydosomething()方法调用时,默认会传递一个“this”参数,而且this指向调用此方法的实例。我就是想看看这个默认的this指向了s1还是我说的f1。
class Father {
public void doSomething() {
System.out.print(this);
}
public String toString() {
return "Father";
}
}
public class Child extends Father {
public void doSomething() {
super.doSomething();
}
public String toString() {
return "Son";
}
public static void main(String[] args) {
Child child = new Child();
child.doSomething();
}
}
这里我就不再预测了,人得学会有自知之明呀,直接上结果,结果很可怕,至少我这么觉得,因为太无知,受不了一点惊吓。
真实结果
Son
结果分析,原来super.mydosomething()中默认的”this”参数指向了s1,我的天呢,作者说的一点都没有错呀,真实重入呀。
4. 第四步探索
赶紧利用 javap -verbose Child 命令看了看字节码命令是怎么执行的。
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: new #1; //class _1/Child
3: dup
4: invokespecial #23; //Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #24; //Method doSomething:()V
12: return
字节码命令描述:
0-7行创建了Child类型的一个实例,也就是s1。
astore_1,将操作数栈顶引用类型数值存入本地变量表的第二个(从零开始计数)本地变量位置,也就是把s1存入本地变量表的第一个位置。
aload_1,将s1在推入栈顶。
invokevirtual,执行实例方法doSomething,此时传入的“this”为栈顶元素s1。
public void doSomething();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #15; //Method _1/Father.doSomething:()V
4: return
现在我们来看关键的doSomething方法
0: aload_0,把this推入栈顶,这个“this”是s1的引用。
1: invokespecial #15; 调用父类方法,但是默认传入的“this”参数仍旧指向s1
最后,我找到了一个说服自己的理由,但是其中还是有好多问题,知其然不知其所以然,希望在未来的日子里可以慢慢解决这些问题,让自己变得有学问起来,哈哈。