一文理清synchronized 作用范围:验证+总结

2023年 9月 3日 33.1k 0

首先,要区分类锁和对象锁:

  • 对象锁:synchronized修饰普通方法、锁对象的同步代码块
  • 类锁:synchronized修饰静态方法、锁类的同步代码块

然后要明确一个核心点:类锁在全局有且只有一个,对象锁在本对象中有且仅有一个,类锁对象锁互不干扰,并且类锁和对象锁都可以在本类和本对象以外的地方被其他代码段占有。看代码过程中带着这个概念来看,万变不离其宗。把握要点,会清晰很多。

不多废话,直接上代码

synchronized可以修饰静态方法、普通方法、代码块;锁的类型有两种:对象锁、类锁。

// 修饰静态方法    
    public static synchronized void lockedStaticMethod() {
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
// ----------- 修饰普通方法 -------------
    public synchronized void lockedMethod(String name) {
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

// ------- 修饰代码块 ----------
    // 锁某个类
    public void lockedCodeBlockClass(String name) {
        synchronized (TestSynchronized.class) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    // 锁某个对象
     public void lockedCodeBlockObject(String name) {
        synchronized (this) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

测试代码准备

创建一个类TestSynchronized.class,类中包含六种类型的测试对象:静态方法是否被synchronized修饰两种、普通方法是否被synchronized修饰两种、被synchronized修饰的代码块锁住类或对象两种:

public class TestSynchronized {


    // 静态方法有锁
    public static synchronized void lockedStaticMethod(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        System.out.println();
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }
    // 静态方法无锁
    public static void staticMethod(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }

    // 普通方法有锁
    public synchronized void lockedMethod(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }

    // 普通方法无锁
    public void method(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }

    // 代码块:类锁
    public void lockedCodeBlockClass(String name, long timeSleep, Class clazz) {
        synchronized (clazz) {
            System.out.println(name+"-进入-等待"+timeSleep+"ms");
            try {
                Thread.sleep(timeSleep);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(name+"-退出");
        }
    }
    // 代码块:对象锁
    public void lockedCodeBlockObject(String name, long timeSleep, Object obj) {
        synchronized (obj) {
            System.out.println(name+"-进入-等待"+timeSleep+"ms");
            try {
                Thread.sleep(timeSleep);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(name+"-退出");
        }
    }

}

创建一个测试类:MyTest

public class MyTest {
    // 整一个线程池
    private static final int processorCount = Runtime.getRuntime().availableProcessors();
    private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(processorCount, processorCount * 2, 20,
            TimeUnit.SECONDS, new LinkedBlockingQueue(20000));
    
    // 主要验证逻辑
    public static void main(String[] args) {
    
    }
}

验证

我们从锁类型的角度来进行测试

类锁

与类锁相关的有:静态方法、代码块锁对象为class

修饰静态方法:基础用法,同步代码

两个线程之间休眠了500ms,是为了保证第一个线程先执行,并且第一个线程休眠时间远比第二个线程长,如果第二个线程在第一个线程休眠时执行完成,说明未被阻塞,反之则说明被阻塞了。

    public static void main(String[] args) {
        threadPool.execute(()->TestSynchronized.lockedStaticMethod("1", 5000L));
        try {
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }
        threadPool.execute(()->TestSynchronized.lockedStaticMethod("2", 1000L));
    }

输出: 可以看到先后创建的两个线程调用同一个静态方法,第二个线程调用时未获取到锁资源,被阻塞。

1-进入-等待5000ms
1-退出
2-进入-等待1000ms
2-退出

现在提出一个问题:当一个线程进入被synchronized修饰的静态方法,占有了类锁。另外的线程调用类、类对象其他方法会被阻塞吗?

通过下面的代码来测试,问题中的“其他方法”包含这些:被synchronized修饰的其他静态方法、普通静态方法、被synchronized修饰的普通方法、普通方法、同步代码块(类锁和对象锁)

根据文章开头提出的:类锁在全局有且只有一个,对象锁在本对象中有且仅有一个,类锁对象锁互不干扰,并且类锁和对象锁都可以在本类和本对象以外的地方被其他代码段占有这个点,我们可以来预估一下测试结果

  • ①被synchronized修饰的其他静态方法:阻塞,被synchronized修饰的静态方法与被synchronized修饰的其他静态方法,都是获取的类锁,而类锁在全局中有且仅有一个,被其中一个获取后,其他被synchronized修饰的静态方法获取同一个类锁时就会被阻塞
  • ②普通静态方法:不阻塞,普通静态方法调用不需要获取类锁,因此不会被影响
  • ③被synchronized修饰的普通方法:不阻塞,被synchronized修饰的普通方法获取对象锁,与类锁无关
  • ④普通方法: 不阻塞,同普通静态方法
  • ⑤同步代码块(对象锁):不阻塞,同被synchronized修饰的普通方法
  • ⑥同步代码块(类锁):与synchronized修饰的静态方法获取相同的类锁时会被阻塞。获取不同的类锁时,若将获取那个类锁可被获取,就不会阻塞。

验证①:测试类TestSynchronized 中新增一个被synchronized修饰的静态方法lockedStaticMethod1():

    public static synchronized void lockedStaticMethod1(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }

测试代码:

public static void main(String[] args) {

    threadPool.execute(()->TestSynchronized.lockedStaticMethod("加锁静态方法", 10000L));
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    threadPool.execute(()->TestSynchronized.lockedStaticMethod1("其他加锁静态方法", 1000L));
}

输出:结果表明,类中加锁的静态方法执行时,也会阻塞其他加锁静态方法的执行

加锁静态方法-进入-等待10000ms
加锁静态方法-退出
其他加锁静态方法-进入-等待1000ms
其他加锁静态方法-退出

验证⑥:对同步代码块(类锁)的调用是否受影响,预期相同的锁资源互斥执行,不同的锁资源互不影响
测试代码:


public static void main(String[] args) {
    TestSynchronized testSynchronized = new TestSynchronized();
    threadPool.execute(()->TestSynchronized.lockedStaticMethod("加锁静态方法", 10000L));
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    threadPool.execute(()->testSynchronized.lockedCodeBlockClass("同步代码块,类锁,锁其他类", 1000L, Temp.class));
    threadPool.execute(()->testSynchronized.lockedCodeBlockClass("同步代码块,类锁", 1000L, TestSynchronized.class));

}

输出:第一个线程和最后一个线程的锁资源都是TestSynchronized.class类,而中间线程锁的是Temp.class。因此第一个线程和最后一个线程互斥执行,而中间线程执行顺序不受影响。验证⑥成功

加锁静态方法-进入-等待10000ms
同步代码块,类锁,锁其他类-进入-等待1000ms
同步代码块,类锁,锁其他类-退出
加锁静态方法-退出
同步代码块,类锁-进入-等待1000ms
同步代码块,类锁-退出

验证:②③④⑤,这几个方法或代码块都与类锁无关,预期:都不会被阻塞
测试代码:

    public static void main(String[] args) {

        TestSynchronized testSynchronized = new TestSynchronized();
        threadPool.execute(()->TestSynchronized.lockedStaticMethod("加锁静态方法", 10000L));
        try {
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }
        threadPool.execute(()->TestSynchronized.staticMethod("普通静态方法", 1000L));
        threadPool.execute(()->testSynchronized.lockedMethod("加锁普通方法", 1000L));
        threadPool.execute(()->testSynchronized.method("普通方法", 1000L));
        threadPool.execute(()->testSynchronized.lockedCodeBlockObject("同步代码块,对象锁", 1000L, testSynchronized));
    }

输出:可以看到,加锁静态方法依旧是最后一个执行完成的,说明加锁静态方法被执行时,对普通静态方法、被synchronized修饰的普通方法、普通方法、同步代码块(对象锁)的调用不影响。类锁与对象锁互不干扰,验证成功。

加锁静态方法-进入-等待10000ms
普通静态方法-进入-等待1000ms
加锁普通方法-进入-等待1000ms
普通方法-进入-等待1000ms
普通方法-退出
普通静态方法-退出
加锁普通方法-退出
同步代码块,对象锁-进入-等待1000ms
同步代码块,对象锁-退出
加锁静态方法-退出

再来一个问题:当类锁被类外的代码获取,会影响类自身的方法获取类锁吗?

按照类锁在全局有且只有一个,对象锁在本对象中有且仅有一个,类锁对象锁互不干扰,并且类锁和对象锁都可以在本类和本对象以外的地方被其他代码段占有 这个观点,类锁被类以外的代码占有了,并且类锁在全局有且仅有一个,那么类自身的方法要获取类锁也是获取不了的。就像钥匙只有一把,别人拿走了,你自己家里人就开不了门了。
直接上代码,新建一个类,类中有个静态方法调用时需要获取类锁:

public class Temp {
    // 静态方法有锁
    public static synchronized void lockedStaticMethod(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }
}

测试代码:首先将Temp.class作为参数传进了同步代码块(类锁),然后再调用Temp的加锁静态方法也去获取类锁

    public static void main(String[] args) {

        TestSynchronized testSynchronized = new TestSynchronized();
        // Temp的类锁被TestSynchronized 的lockedCodeBlockClass方法占有
        threadPool.execute(()->testSynchronized.lockedCodeBlockClass("同步代码块,类锁-锁资源:Temp.class", 10000L, Temp.class));
        try {
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }
        threadPool.execute(()->Temp.lockedStaticMethod("Temp执行加锁静态方法", 1000L));
    }

输出:可以看到,互斥执行。

同步代码块,类锁-锁资源:Temp.class-进入-等待10000ms
同步代码块,类锁-锁资源:Temp.class-退出
Temp执行加锁静态方法-进入-等待1000ms
Temp执行加锁静态方法-退出

小节:反正记住,类锁全局有且仅有一个就可以了。

对象锁

类锁的探究到此为止,来看下对象锁的作用范围,被synchronized修饰的普通方法、针对于对象的代码块锁。

下面不再测试与类锁相关的内容,上面类锁相关代码已经说明,对象锁与类锁互不干扰...

修饰普通方法:基础用法,同步代码

public static void main(String[] args) {

    TestSynchronized testSynchronized = new TestSynchronized();
    threadPool.execute(()->testSynchronized.lockedMethod("1", 10000L));
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    threadPool.execute(()->testSynchronized.lockedMethod("2", 1000L));

输出: 线程顺序执行

1-进入-等待10000ms
1-退出
2-进入-等待1000ms
2-退出

排除了类锁相关的方法后,来看下当一个对象中加锁的普通方法获取锁时,可能会影响其他哪些方法的执行?

对象的其他加锁普通方法、普通方法、代码块(对象锁)、其他对象的对象锁代码

再次引入我们的要点来进行判断:类锁在全局有且只有一个,对象锁在本对象中有且仅有一个,类锁对象锁互不干扰,并且类锁和对象锁都可以在本类和本对象以外的地方被其他代码段占有,预估一下测试结果

  • ①加锁的其他普通方法:阻塞,由于对象锁被加锁的普通方法获取,而对象锁在本对象中有且仅有一个,因此会被阻塞
  • ②普通方法:不阻塞,因为不需要获取锁
  • ③代码块(对象锁):分情况讨论,如果代码块要获取的对象锁是本对象的,就会被阻塞;获取的其他对象的就可能不会被阻塞(为何是可能,因为考虑其他对象的对象锁是否被占有)
  • ④其他对象的对象锁代码:此处分类讨论,如果是其他对象的加锁普通方法,加锁普通方法获取的是自身对象的对象锁,因此其他对象执行他的加锁普通方法时并不会被阻塞(如果能获取锁的话)。如果是代码块,则根据代码块锁的对象来确定

验证①,在TestSynchronized类中新增方法

    // 普通方法有锁
    public synchronized void lockedMethod1(String name, long timeSleep) {
        System.out.println(name+"-进入-等待"+timeSleep+"ms");
        try {
            Thread.sleep(timeSleep);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(name+"-退出");
    }

测试代码:

    public static void main(String[] args) {

        TestSynchronized testSynchronized = new TestSynchronized();
        threadPool.execute(()->testSynchronized.lockedMethod("加锁普通方法", 10000L));
        try {
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }
        threadPool.execute(()->testSynchronized.lockedMethod1("其他加锁普通方法", 1000L));
    }

输出:互斥执行,说明当对象中某个方法获取对象锁时,对象中其他方法想要获取对象锁会被阻塞,因为对象锁一个对象有且仅有一个对象锁

加锁普通方法-进入-等待10000ms
加锁普通方法-退出
其他加锁普通方法-进入-等待1000ms
其他加锁普通方法-退出

验证②③


public static void main(String[] args) {


    TestSynchronized testSynchronized = new TestSynchronized();
    // 线程1
    threadPool.execute(()->testSynchronized.lockedMethod("线程1-加锁普通方法", 10000L));
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    // 线程2
    threadPool.execute(()->testSynchronized.lockedCodeBlockObject("线程2-同步代码块(对象锁)", 1000L, testSynchronized));
    Temp temp = new Temp();
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    // 线程3
    threadPool.execute(()->testSynchronized.method("线程3-普通方法", 1000L));
    // 线程4
    threadPool.execute(()->testSynchronized.lockedCodeBlockObject("线程4-同步代码块锁temp对象", 1000L, temp));
}

输出:可以看到,线程1和线程2是获取的同一个对象锁,因此互斥执行,而普通方法不需要获取锁,因此不受锁的影响。线程4获取的是另一个对象的锁,与线程1和2的对象锁不同,因此也不受干扰

线程1-加锁普通方法-进入-等待10000ms
线程3-普通方法-进入-等待1000ms
线程4-同步代码块锁temp对象-进入-等待1000ms
线程3-普通方法-退出
线程4-同步代码块锁temp对象-退出
线程1-加锁普通方法-退出
线程2-同步代码块(对象锁)-进入-等待1000ms
线程2-同步代码块(对象锁)-退出

验证④:对象与对象之间获取锁的代码是否互相干扰


public static void main(String[] args) {


    TestSynchronized testSynchronized = new TestSynchronized();
    TestSynchronized testSynchronized1 = new TestSynchronized();
    // 线程1
    threadPool.execute(()->testSynchronized.lockedMethod("线程1-加锁普通方法", 10000L));
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    // 线程2
    threadPool.execute(()->testSynchronized1.lockedMethod("线程2-加锁普通方法-其他对象", 2000L));
    // 线程3
    threadPool.execute(()->testSynchronized.lockedCodeBlockObject("线程3-同步代码块(对象锁)", 1000L, testSynchronized));
    // 线程4
    threadPool.execute(()->testSynchronized.lockedCodeBlockObject("线程4-同步代码块(对象锁)-锁其他对象", 1000L, testSynchronized1));
}

输出:线程1和线程3是获取的同一个对象锁,线程2和线程4是获取的同一个锁,可以看到1,3互斥执行,2,4互斥执行。说明无论锁在何处被获取了但是一个对象中有且只有一个,不同对象之间互不干扰

线程1-加锁普通方法-进入-等待10000ms
线程2-加锁普通方法-其他对象-进入-等待2000ms
线程2-加锁普通方法-其他对象-退出
线程4-同步代码块(对象锁)-锁其他对象-进入-等待1000ms
线程4-同步代码块(对象锁)-锁其他对象-退出
线程1-加锁普通方法-退出
线程3-同步代码块(对象锁)-进入-等待1000ms
线程3-同步代码块(对象锁)-退出

原理

对象锁:对象头中的MarkWord存储了对象自身的运行时数据,也记录了对象和锁有关的信息。当锁升级成重量级锁时,对象会生成并关联一个Monitor对象,在这个Monitor对象中有一个字段指向某个线程,来标识锁被这个线程所持有。因此对象锁是以对象为单位,对象有且只有一个对象锁,对象之间的对象锁互不干扰。

类锁:因为Class数据存在于永久代,在全局有且仅有一个Monitor对象

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论