您的位置:首页 > 编程语言 > Java开发

Java Concurrency in Practice中对象锁重入问题的理解

2015-08-07 20:07 766 查看

原因:Java Concurrency in Practice 中文版21页讲解了关于对象锁的重入的问题,一直没有读懂作者给的例子,今天琢磨了好久,找到了一个可以说服自己的理由……

1 原书内容如下:

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果摸个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为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对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。

2 我的问题如下:

说实话,读这本书时,我本着一个学生对作者无比崇敬的心情战战兢兢的欣赏着,生怕自己看不懂。当我看完了作者的文字内容之后,除了认同和佩服之外,没有产生任何的疑问。可是当我看到作者的示例代码是,我完全搞不懂了,心情一落千丈。我一度对作者的水平产生了怀疑,但是觉得不太可能是作者的问题,因为我深知自己的水平有多低,应该是我的问题。

怀疑如下:假设一个线程t1调用了Childl类某个实例c1的doSomething方法,那么在成功调用之前t1应该先获得c1的锁。接下来调用Father(super:大家都知道,在创建c1需要先创建其父类的实例f1,super作为f1的引用)类某个实例f1的doSomething方法,那么在成功调用之前t1应该先获得f1的锁。也就是说t1线程先后或得了两个不同对象的锁,这怎么能叫重入呢?

3 我的探索如下:

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 "<init>":()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

4 请原谅我的无知:

最后,我找到了一个说服自己的理由,但是其中还是有好多问题,知其然不知其所以然,希望在未来的日子里可以慢慢解决这些问题,让自己变得有学问起来,哈哈。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: