一、疑问来源

最近学习《java并发编程实战》对下面这句话有些疑惑

“由于Widget和LoggingWidget中doSomething方法都是synchronized方法,因此每个doSomthing方法在执行前都会获得Widget上的锁”--《java并发编程实战》2.3.2 重入

读到这里比较疑惑。书上例子是如下代码:

    public class Widget{
        public synchronized void doSomething(){
            // 代码逻辑
        }
    }
    
    public class LoggingWidget extends Widget{
        public synchronized void doSomething(){
            System.out.println(toString() + ":calling doSomething");
            super.doSomething();
        }
    }

调用Widget中doSomething方法是可以获取Widget上的锁,这一步比较好理解,就是线程获取所调用的同步方法所在对象的锁,此时的对象就是Widget的实例super。

可是为什么调用LoggingWidget中doSomething的方法时也会获得Widget上的锁,此时线程所调用的同步方法所在的对象是LoggingWidget的实例,而不是Widget的实例super。

因此这里对这个问题做出猜想和验证。

二、提出问题

  理论支持:新建一个子类实例时,JVM会自动新建一个父类实例,且子类实例中包含该实例的引用,用super表示。

  问题:当线程需要获取或释放子类实例,或子类中的父类实例的对象锁时,子类实例对象锁与父类实例对象锁存在什么关系?

三、猜想

  猜想一:一个线程获取子类实例对象锁的同时会获得父类实例对象锁,释放子类实例对象锁的同时会释放父类实例对象锁
  猜想二:一个线程获取父类实例对象锁的同时会获得子类实例对象锁,释放父类实例对象锁的同时会释放子类实例对象锁

四、验证

  先编写基础代码,创建两个线程。后续为了测试,会在原代码基础上进行修改后运行测试

/**
 * 父类
 */
package com.test.thread;
public class Father {
    private boolean ifbreak = false;
    public void setIfbreak( boolean ifbreak ){
        this.ifbreak = ifbreak;
    }
    public synchronized void doSomething(){
        System.out.println("执行父类方法--doSomething--start");
        System.out.println("执行父类方法--doSomething--end");
    }
    public synchronized void doOtherthing(){
        System.out.println("执行父类方法--doOtherthing--start");
        System.out.println("执行父类方法--doOtherthing--end");
    }
}

/**
 * 子类
 */
package com.test.thread;
public class Son extends Father {
    @Override
    public synchronized void doSomething(){
        System.out.println("执行子类方法--doSomething--start");
        super.doSomething();
        System.out.println("执行子类方法--doSomething--end");
    }
    @Override    
    public void doOtherthing(){
        System.out.println("执行子类方法--doOtherthing--start");
        super.doOtherthing();
        System.out.println("执行子类方法--doOtherthing--end");
    }
}

/**
 * 线程类一
 */
package com.test.thread;
public class MyThread01 extends Thread{
    private Son son ;
    public MyThread01(Son son){
        this.son = son;
    }
    @Override
    public void run() {
        son.doSomething();
    }
}

/**
 * 线程类二
 */
package com.test.thread;
public class MyThread02 extends Thread{
    private Son son ;
    public MyThread02(Son son){
        this.son = son;
    }
    @Override
    public void run() {
        son.doOtherthing();
    }
}

/**
 * 测试类
 */
package com.test.thread;
public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        Thread myThread01 = new MyThread01(son);
        Thread myThread02 = new MyThread02(son);
        myThread01.start();
        myThread02.start();
    }
}

  实验一:一个线程获取子类实例对象锁的同时会不会获得父类实例对象锁,释放子类实例对象锁的同时会不会释放父类实例对象锁?

  此时创造两种情况用以验证猜想一:

  1.新建两个线程,线程一先请求获取子类实例对象锁,并获取成功,不释放。线程二后请求获取父类实例对象锁。这种情况下如果,线程二始终无法获取父类实例对象锁。则说明一个线程获取子类实例对象锁的同时会获得父类实例对象锁。

  2.新建两个线程,线程一先请求获取子类实例对象锁,并获取成功,sleep等待10秒后释放。线程二后请求获取父类实例对象锁。这种情况下如果,线程二将在10秒后获取父类实例对象锁。则进一步说明一个线程获取子类实例对象锁的同时会获得父类实例对象锁,释放子类实例对象锁的同时会释放父类实例对象锁。

  为了创造情况1将上面代码中的子类加以修改,这种情况下若线程调用子类doSomething方法,将永远不会释放子类实例的对象锁。

/**
 * 子类
 */
package com.test.thread;
public class Son extends Father {
    private boolean ifbreak = false;
    public void setIfbreak( boolean ifbreak ){
        this.ifbreak = ifbreak;
    }
    @Override
    public synchronized void doSomething(){
        System.out.println("执行子类方法--doSomething--start");
        super.doSomething();
        while( true ){
            if(ifbreak){
                break;
            }
        }
        System.out.println("执行子类方法--doSomething--end");
    }
    
    @Override    
    public void doOtherthing(){
        System.out.println("执行子类方法--doOtherthing--start");
        super.doOtherthing();
        System.out.println("执行子类方法--doOtherthing--end");
    }
    
}

  运行测试类,观察运行结果(注意:这里对线程一做了处理,只需要考虑线程一先获取对象锁的情况,线程二先获取锁的情况没有参考意义。)

执行子类方法--doOtherthing--start
执行子类方法--doSomething--start
执行父类方法--doSomething--start
执行父类方法--doSomething--end

  对运行结果进行分析

执行子类方法--doSomething--start
执行父类方法--doSomething--start
执行父类方法--doSomething--end    // 线程一执行super.doSomething后进入死循环,无法跳出子类实例doSomething方法,此时仍然持有子类实例对象锁和父类实例对象锁
执行子类方法--doOtherthing--start  // 线程二执行到这里获取不到父类实例对象锁。无法继续往后执行

  为了创造情况2将上面代码中的子类加以修改,这种情况下若线程调用子类doSomething方法,将在sleep等待十秒后释放子类实例的对象锁。

/**
 * 子类
 */
package com.test.thread;
public class Son extends Father {
    private boolean ifbreak = false;
    public void setIfbreak( boolean ifbreak ){
        this.ifbreak = ifbreak;
    }
    @Override
    public synchronized void doSomething(){
        System.out.println("执行子类方法--doSomething--start");
        super.doSomething();
        while( true ){
            System.out.println("ifbreak=" + ifbreak);
            if(ifbreak){
                break;
            }
            try {
                Thread.sleep(10000);
                System.out.println("十秒后");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setIfbreak(true);
        }
        System.out.println("执行子类方法--doSomething--end");
    }
    @Override    
    public void doOtherthing(){
        System.out.println("执行子类方法--doOtherthing--start");
        super.doOtherthing();
        System.out.println("执行子类方法--doOtherthing--end");
    }
}

  运行测试类,观察运行结果(注意:这里对线程一做了处理,只需要考虑线程一先获取对象锁的情况,线程二先获取锁的情况没有参考意义。)

执行子类方法--doSomething--start
执行父类方法--doSomething--start
执行父类方法--doSomething--end
ifbreak=false
执行子类方法--doOtherthing--start
十秒后
ifbreak=true
执行子类方法--doSomething--end
执行父类方法--doOtherthing--start
执行父类方法--doOtherthing--end
执行子类方法--doOtherthing--end

  对运行结果进行分析

执行子类方法--doSomething--start
执行父类方法--doSomething--start
执行父类方法--doSomething--end    // 线程一执行super.doSomething后进入等待,未跳出子类实例doSomething方法,此时仍然持有子类实例对象锁和父类实例对象锁
ifbreak=false
执行子类方法--doOtherthing--start  // 线程二执行到这里获取不到父类实例对象锁。无法继续往后执行
十秒后
ifbreak=true
执行子类方法--doSomething--end    // 线程一执行子类实例doSomething方法结束。释放子类实例对象锁,同时释放父类实例对象锁
执行父类方法--doOtherthing--start  // 线程二执行到这里,由于线程一已经释放父类实例对象锁,父类实例对象锁处于空闲,线程二成功获取到父类对象锁。继续执行
执行父类方法--doOtherthing--end
执行子类方法--doOtherthing--end

  实验一结论:一个线程获取子类实例对象锁的同时会获得父类实例对象锁,释放子类实例对象锁的同时会释放父类实例对象锁

 

  实验二:一个线程获取父类实例对象锁的同时会不会获得子类实例对象锁,释放父类实例对象锁的同时会不会释放子类实例对象锁?

  此时创造两种情况用以验证猜想二:

  1.新建两个线程,线程二先请求获取父类实例对象锁,并获取成功,不释放。线程一后请求获取子类实例对象锁。这种情况下如果,线程一始终无法获取子类实例对象锁。则说明一个线程获取父类实例对象锁的同时会获得子类实例对象锁。

  2.新建两个线程,线程二先请求获取父类实例对象锁,并获取成功,sleep等待10秒后释放。线程一后请求获取子类实例对象锁。这种情况下如果,线程一将在10秒后获取子类实例对象锁。则进一步说明一个线程获取父类实例对象锁的同时会获得子类实例对象锁,释放父类实例对象锁的同时会释放子类实例对象锁。

  为方便创造两种情况,将测试类中线程启动顺序修改一下,具体如下:

/**
 * 测试类
 */
package com.test.thread;
public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        Thread myThread01 = new MyThread01(son);
        Thread myThread02 = new MyThread02(son);
        myThread02.start();
        myThread01.start();
    }
}

  为了创造情况1,先将上面修改过的子类还原到原始状态,将上面代码中的父类加以修改,这种情况下若线程调用子类doOtherthing方法,会同时调用super.doOtherthing,将永远不会释放父类实例的对象锁

/**
 * 父类
 */
package com.test.thread;
public class Father {
    private boolean ifbreak = false;
    public void setIfbreak( boolean ifbreak ){
        this.ifbreak = ifbreak;
    }
    public synchronized void doSomething(){
        System.out.println("执行父类方法--doSomething--start");
        System.out.println("执行父类方法--doSomething--end");
    }
    public synchronized void doOtherthing(){
        System.out.println("执行父类方法--doOtherthing--start");
        while( true ){
            if(ifbreak){
                break;
            }
        }
        System.out.println("执行父类方法--doOtherthing--end");
    }
}

  运行测试类,观察运行结果(注意:这里对线程二做了处理,只需要考虑线程二先获取对象锁的情况,线程一先获取锁的情况没有参考意义。)

执行子类方法--doOtherthing--start
执行父类方法--doOtherthing--start

  对运行结果进行分析

执行子类方法--doOtherthing--start
执行父类方法--doOtherthing--start  // 线程二执行super.doOtherthing时进入死循环,无法跳出父类实例doSomething方法,此时仍然持有子类实例对象锁和父类实例对象锁
                         // 线程一获取不到子类实例对象锁。无法调用子类实例方法doSomething,无法继续往后执行

  为了创造情况2先将上面修改过的子类还原到原始状态,将上面代码中的父类加以修改,这种情况下若线程调用子类doOtherthing方法,会同时调用super.doOtherthing,将sleep等待10秒后释放父类实例的对象锁

/**
 * 父类
 */
package com.test.thread;
public class Father {
    private boolean ifbreak = false;
    public void setIfbreak( boolean ifbreak ){
        this.ifbreak = ifbreak;
    }
    public synchronized void doSomething(){
        System.out.println("执行父类方法--doSomething--start");
        System.out.println("执行父类方法--doSomething--end");
    }
    public synchronized void doOtherthing(){
        System.out.println("执行父类方法--doOtherthing--start");
        while( true ){
            System.out.println("ifbreak=" + ifbreak);
            if(ifbreak){
                break;
            }
            try {
                Thread.sleep(10000);
                System.out.println("十秒后");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setIfbreak(true);
        }
        System.out.println("执行父类方法--doOtherthing--end");
    }
}

  运行测试类,观察运行结果(注意:这里对线程二做了处理,只需要考虑线程二先获取对象锁的情况,线程一先获取锁的情况没有参考意义)

执行子类方法--doOtherthing--start
执行父类方法--doOtherthing--start
ifbreak=false
十秒后
ifbreak=true
执行父类方法--doOtherthing--end
执行子类方法--doSomething--start
执行父类方法--doSomething--start
执行父类方法--doSomething--end
执行子类方法--doSomething--end
执行子类方法--doOtherthing--end

  对运行结果进行分析

执行子类方法--doOtherthing--start
执行父类方法--doOtherthing--start    // 线程二执行super.doOtherthing时进入等待,未跳出父类实例doSomething方法,此时仍然持有子类实例对象锁和父类实例对象锁
                                    // 线程一获取不到子类实例对象锁。无法调用子类实例方法doSomething,无法继续往后执行
ifbreak=false
十秒后
ifbreak=true
执行父类方法--doOtherthing--end        // 线程二执行super.doOtherthing方法结束。释放父类实例对象锁,同时释放子类实例对象锁
执行子类方法--doOtherthing--end
执行子类方法--doSomething--start    // 线程一执行到这里,由于线程二已经释放子类实例对象锁,子类实例对象锁处于空闲,线程一成功获取到子类对象锁。继续执行
执行父类方法--doSomething--start
执行父类方法--doSomething--end
执行子类方法--doSomething--end

  实验二结论:一个线程获取父类实例对象锁的同时会获得子类实例对象锁,释放父类实例对象锁的同时会释放子类实例对象锁

五、结论
  由上述两个结论可知:一个线程获取子类实例的对象锁、获取子类实例对应父类实例的对象锁是原子执行的;释放子类实例的对象锁、释放子类实例对应父类实例的对象锁是原子执行的。

  即对子类实例,父类实例对象锁是同时获取、或同时释放的。当然这里还有一种可能,就是父类实例子类实例的对象锁是同一对象锁。