java synchronized 探究

java synchronized关键字是并发编程最常用的工具,也是最重要的工具之一。今天来探究下其含义

写两个类,两段不同的临界区代码进行测试。

有以下4种测试方案,每种方案对应4种不同的锁类型:对象锁: synchronized(this),本类锁: synchronized(Me.class),父类锁: synchronized(Parent.class),自定义锁对象

  1. 同一个对象,在不同线程中运行

  2. 同一个类的不同对象,在不同线程中运行

  3. 不同类(有共同的父类)的不同对象,在不同线程中运行

  4. 不同类(没有共同的父类)的不同对象,在不同线程中运行

测试结果:

  用对象锁,只有1可以互斥访问临界区;

  用本类锁,则1和2可以互斥访问临界区;

  用共同父类的锁,全部都可以互斥访问临界区。(注意,使用第4种测试方案时,两个对象已经没有共同的父类了,因此这个所谓共同父类的锁,其实是一个第3者,与这两个类没有直接关系,类似于自定义的锁对象,只不过这个自定的锁对象不是普通的对象,而是一个Class对象)

  用自定义的锁对象,全部都可以互斥访问临界区。

/**
 * synchronized关键字测试
 *
 * @author zhangxz
 * @date 2019-11-18 11:07
 */

public class SynchronizedTest {

    private static final Sync sync = new Sync();

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA1 = new ThreadA();
        ThreadA threadA2 = new ThreadA();
        ThreadB threadB = new ThreadB();

//        testOneRunnable(threadA1);
//        testTwoRunnable(threadA1, threadA2);
        testTwoRunnable(threadA1, threadB);
    }

    static class Sync{}

    //使用一个runnable对象,在两个不同线程调用
    static void testOneRunnable(Runnable runnable) {
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
    }

    //使用两个runnable对象,在两个不同线程调用
    static void testTwoRunnable(Runnable runnable1, Runnable runnable2) {
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);

        thread1.start();
        thread2.start();
    }

    static class ABParent {
    }

    static class ThreadA/* extends ABParent */implements Runnable {
        @Override
        public void run() {
//            synchronized (this) {
//            synchronized (ThreadA.class) {
//            synchronized (ABParent.class) {
            synchronized (sync) {
                try {
                    Thread.sleep(10);
                    System.out.println("process in thread a " + Thread.currentThread().getName());
                    Thread.sleep(10);
                    System.out.println("process in thread a " + Thread.currentThread().getName());
                    Thread.sleep(10);
                    System.out.println("process in thread a " + Thread.currentThread().getName());
                    Thread.sleep(10);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }

            }
        }
    }

    static class ThreadB /*extends ABParent*/ implements Runnable {
        @Override
        public void run() {
//            synchronized (this) {
//            synchronized (ThreadB.class) {
//            synchronized (ABParent.class) {
            synchronized (sync) {
                try {
                    Thread.sleep(10);
                    System.out.println("process in thread b " + Thread.currentThread().getName());
                    Thread.sleep(10);
                    System.out.println("process in thread b " + Thread.currentThread().getName());
                    Thread.sleep(10);
                    System.out.println("process in thread b " + Thread.currentThread().getName());
                    Thread.sleep(10);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }


}

另外的测试  

  1. 把其中一个类的synchronized去除,即一个是临界区,另外一个不是。然后对其测试,发现无论临界区用的哪种锁,两者都无法实现互斥访问。

  2. 把两个类的锁对象改为不同的锁,则两个synchronized代码块因为持有不同的锁,无法构成临界区,因此也无法实现互斥访问。

  3. 测试同一个对象的不同synchronized方法,如下代码。测试结果:同一个对象的不同synchronized方法构成同一个临界区,可以互斥访问。

  

    static void testDifferentMethodsInOneObject(TestA testA) {
        Runnable runnable1 = () -> {
            try {
                testA.MethodA();
                testA.MethodB();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Runnable runnable2 = () -> {
            try {
                testA.MethodB();
                testA.MethodA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);

        thread1.start();
        thread2.start();

    }

    static class TestA {
        public synchronized void MethodA() throws InterruptedException {
            Thread.sleep(10);
            System.out.println("process in method a " + Thread.currentThread().getName());
            Thread.sleep(10);
            System.out.println("process in method a " + Thread.currentThread().getName());
            Thread.sleep(10);
            System.out.println("process in method a " + Thread.currentThread().getName());
            Thread.sleep(10);
        }

        public synchronized void MethodB() throws InterruptedException {
            Thread.sleep(10);
            System.out.println("process in method b " + Thread.currentThread().getName());
            Thread.sleep(10);
            System.out.println("process in method b " + Thread.currentThread().getName());
            Thread.sleep(10);
            System.out.println("process in method b " + Thread.currentThread().getName());
            Thread.sleep(10);
        }
    }

synchronized 使用的最终结论:

  1. 临界区和非临界区是无法实现互斥访问的。

  2. 同一个对象的临界区(即使是不同的synchronized代码块,也只能构成同一个临界区,因为synchronized锁的最小粒度是对象锁),只要加锁就可以互斥访问。

  3. 同一个类的不同对象/不同类的不同对象,两者的synchronized代码块虽然不再同一个地方,但是只要锁的粒度足够大,大到覆盖了两个代码块,则两个代码块就构成了一个临界区,就能实现互斥访问。

  4. 使用自定义的公共变量作为锁对象,只要规定进入代码块必须获得同一把锁,则可以对于任何地方的临界区实现互斥访问。

附加测试:

  当同一个对象,有两个方法,一个为static,一个普通的,然后两个方法都使用了synchronized,那么这两个方法可以实现互斥访问吗?

  答案是否定的,因为两个方法持有的锁是不同的两把锁,一个为类锁,一个为对象锁,因此两者不能构成临界区。(我一开始的思维是,两把锁,一个锁比较大,另外一个锁比较小,那么大的锁是覆盖了小粒度的锁的,但实验证明这个想当然是错误的。)代码如下:

/**
 * synchronized测试类2
 *
 * @author zhangxz
 * @date 2019-11-18 21:43
 */

public class SynchronizedTest2 {

    public static void main(String[] args) {
        TestA testA = new TestA();
        new Thread(() -> {
            testA.a();
        }).start();

        new Thread(() -> {
            testA.b();
        }).start();
    }

    static class TestA {
        static synchronized void a() {

            System.out.println("i am method a.");
            Thread.yield();
            System.out.println("i am method a.");
            Thread.yield();
            System.out.println("i am method a.");
        }

        synchronized void b() {

            System.out.println("i am method b.");
            Thread.yield();
            System.out.println("i am method b.");
            Thread.yield();
            System.out.println("i am method b.");
        }
    }

}

上面测试用到了 Thread类的 yield方法,这个方法的效果是当前线程做出cpu时间片的让步,在当前线程执行后续步骤之前,让其他线程有机会得到cpu的执行时间。在测试多线程问题中比较常用。

参考链接: 

  https://www.cnblogs.com/java-spring/p/8309931.html

原文地址:https://www.cnblogs.com/zhangxuezhi/p/11881417.html