Synchronized 用法总结

技术 · 2021-07-24

Synchronized 是 Java 多线程编程中常用关键字。Synchronized 是防止线程干扰和内存一致性的简单策略。

Synchronized 关键字用法

类对象:类名.class 创建的对象。对类对象上锁,可以叫做类锁
实例对象:当前类,通过构造方法创建的实例对象。对实例对象上锁,可以叫做实例锁(对象锁)

Synchronized 是一种同步锁,也是可重入锁。在不同地方使用 Synchronized 都会获取一把锁,但这把锁是谁的呢?

修饰普通方法

锁的是当前实例对象——实例锁

public synchronized void run() {
    // some code...
}

此写法等价于

public void run() {
    synchronized(this) {
    	// some code...   
    }
}

修饰静态方法

锁的是当前类对象——类锁

public synchronized static void run() {
    // some code...
}

此写法等价于

public void run() {
    synchronized(className.class) {
    	// some code...  
    }
}

修饰代码块

obj 可以为类锁(ClassName.class),也可以为实例锁(new ClassName)

public void run() {
    synchronized(obj) {
    	// some code...  
    }
}

不可使用的对象锁

同步块不应该加在String或基本包装类型上,如Byte、Short、Integer、Long、Float、Double、Boolean、Character。
String不能用作同步块的参数是因为String为不可变对象,任何String对象的改变都将产生一个新的String对象,这也将导致前面加的锁不会被释放。
Integer、Boolean、Double、Long不能作为同步块参数的原因是他们是基本包装类型,包装类型有特殊的逻辑,用一句话说就是Java的自动封箱和解箱操作会导致这些对象在经过运算后不再是原来的对象。
用复杂的话说就是:当把基本变量赋值给包装类型的变量(其实编译过后的操作就是调用包装类型的静态方法valueOf)或者调用静态valueOf方法时:

  • Boolean返回的是缓存的对象。
  • 整型(Byte,Short,Integer,Long)会检查该数字是否在1个字节可表示的有符号整数范围内(-128~127),是则返回缓存对象,否则返回新对象。
  • Character会缓存整型值为0~127的字符,同样会检查字符是否落在缓存范围中,是则返回,否则返回新对象。
  • Double和Float的valueOf方法始终返回新对象。

同步代码块异常

当持有锁的线程,发生异常,此时锁会释放。所以需要注意在业务中,如果代码发生异常,是否会出现错误数据的情况。

public class SynchronizedTest {

    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread1.start();
        thread2.start();
    }

}


class SyncThread implements Runnable {

    int i = 0;

    @SneakyThrows
    public synchronized void run() {
        while (true) {
            System.out.println("Thread name : " + Thread.currentThread().getName() + " -> i = " + i);
            Thread.sleep(1000);
            if (i++ == 3) {
                int result = 1 / 0;
            }
        }
    }

}

结果为:

Thread name : SyncThread1 -> i = 0
Thread name : SyncThread1 -> i = 1
Thread name : SyncThread1 -> i = 2
Thread name : SyncThread1 -> i = 3
Exception in thread "SyncThread1" java.lang.ArithmeticException: / by zero
	at cn.liaocp.amireux.base.util.SyncThread.run(SynchronizedTest.java:31)
	at java.base/java.lang.Thread.run(Thread.java:829)
Thread name : SyncThread2 -> i = 4
Thread name : SyncThread2 -> i = 5
Thread name : SyncThread2 -> i = 6
Thread name : SyncThread2 -> i = 7
Thread name : SyncThread2 -> i = 8
...

分析结果

按代码逻辑,如果没有 int result = 1 / 0;,线程2 将永远没有机会执行。但在线程1 发生异常后,线程2 执行了,说明 线程1 发生异常,释放了锁,线程2 获得了锁,线程2 执行。

用法总结

  1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  3. 需注意线程发生异常处理。

参考链接
Java中Synchronized的用法
为什么Synchronized不能加在String和Integer等基本包装类型上

Java
Theme Jasmine by Kent Liao