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