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

原因: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 请原谅我的无知:

最后,我找到了一个说服自己的理由,但是其中还是有好多问题,知其然不知其所以然,希望在未来的日子里可以慢慢解决这些问题,让自己变得有学问起来,哈哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值