我们使用synchronized通常都有这样一个误区:synchronized锁住的代码块一定是所有线程都互斥的。
其实不然!
首先我们明确一点,synchronized锁住的是一个对象!如果锁住的这个对象,在多个线程中相同,那么这些线程访问synchronized修饰的代码块时,总是互斥的。
但是!如果锁住的这个对象,在多个线程中是不同的,那么这些线程访问synchronized修饰的代码块,是不会互斥的!
固定测试类
class T1{
public T1(String id) {
this.id = id;
}
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
public class SynchronzedStringTest {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
try {
m1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
m1();
Thread.sleep(10000);
}
/**
* 加锁
*/
public static void m1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
}
}
执行结果:
main进来了Mon Aug 15 10:19:47 CST 2022
main获取到锁Mon Aug 15 10:19:47 CST 2022
Thread-0进来了Mon Aug 15 10:19:47 CST 2022
Thread-0获取到锁Mon Aug 15 10:19:49 CST 2022
main结束了Mon Aug 15 10:19:49 CST 2022
Thread-0结束了Mon Aug 15 10:19:51 CST 2022
我们可以看到,Thread-0线程和main线程,执行synchronized代码块是互斥的。
但是,这种加锁方式太笨重,每一个线程执行到synchronized代码块都会互斥。
public class SynchronzedStringTest {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("t1");
T1 t2 = new T1("t1");
new Thread(() ->{
try {
m1(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
m1(t2);
Thread.sleep(10000);
}
/**
* 加锁
*/
public static void m1(T1 t1) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
synchronized (t1) {
System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
}
}
执行结果:
main进来了Mon Aug 15 10:45:40 CST 2022
main获取到锁Mon Aug 15 10:45:40 CST 2022
Thread-0进来了Mon Aug 15 10:45:40 CST 2022
Thread-0获取到锁Mon Aug 15 10:45:40 CST 2022
main结束了Mon Aug 15 10:45:42 CST 2022
Thread-0结束了Mon Aug 15 10:45:42 CST 2022
我们可以看到,Thread-0线程和main线程,执行synchronized代码块并不互斥。
因为两个线程传参的T1其实是两个不同的对象(虽然对象中的id值是一样的),所以synchronized虽然加锁了,但是由于两个对象是不一样的,所以两个线程并不会互斥。
public class SynchronzedStringTest {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("t1");
T1 t2 = new T1("t1");
new Thread(() ->{
try {
m1(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
m1(t2);
Thread.sleep(10000);
}
/**
* 加锁
*/
public static void m1(T1 t1) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
synchronized (t1.getId()) {
System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
}
}
执行结果:
Thread-0进来了Mon Aug 15 10:48:11 CST 2022
Thread-0获取到锁Mon Aug 15 10:48:11 CST 2022
main进来了Mon Aug 15 10:48:11 CST 2022
main获取到锁Mon Aug 15 10:48:13 CST 2022
Thread-0结束了Mon Aug 15 10:48:13 CST 2022
main结束了Mon Aug 15 10:48:15 CST 2022
我们可以发现,Thread-0线程和main线程,执行synchronized代码块是互斥的。
因为synchronized锁住的字符串,其实是已经定义在字符串常量池中的。
其中t1和t2中显式定义的id,已经存在了字符串常量池中,而t1和t2的id,在字符串常量池中的指向是一致的,所以synchronized锁住的字符串被认为是同一个对象。
关于字符串常量池的说法请移步String的Intern()方法,详解字符串常量池!
public class SynchronzedStringTest {
public static void main(String[] args) throws InterruptedException {
String s1 = "a";
String s2 = "b";
T1 t1 = new T1(s1 + s2);
T1 t2 = new T1(s1 + s2);
new Thread(() ->{
try {
m1(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
m1(t2);
Thread.sleep(10000);
}
/**
* 加锁
*/
public static void m1(T1 t1) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
synchronized (t1.getId()) {
System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
}
}
执行结果:
Thread-0进来了Mon Aug 15 10:52:41 CST 2022
Thread-0获取到锁Mon Aug 15 10:52:41 CST 2022
main进来了Mon Aug 15 10:52:41 CST 2022
main获取到锁Mon Aug 15 10:52:41 CST 2022
Thread-0结束了Mon Aug 15 10:52:43 CST 2022
main结束了Mon Aug 15 10:52:43 CST 2022
我们可以发现,Thread-0线程和main线程,执行synchronized代码块不是互斥的。
这是因为虽然t1和t2的id都是一样的,但是字符串[a+b] 最终的结果实际是new 的新的String,并不会存在字符串常量池中,所以synchronized认为其锁住的字符串其实并不是同一个对象。
public class SynchronzedStringTest {
public static void main(String[] args) throws InterruptedException {
String s1 = "a";
String s2 = "b";
T1 t1 = new T1(s1 + s2);
T1 t2 = new T1(s1 + s2);
new Thread(() ->{
try {
m1(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
m1(t2);
Thread.sleep(10000);
}
/**
* 加锁
*/
public static void m1(T1 t1) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
synchronized (t1.getId().intern()) {
System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
}
}
执行结果:
Thread-0进来了Mon Aug 15 10:58:48 CST 2022
Thread-0获取到锁Mon Aug 15 10:58:48 CST 2022
main进来了Mon Aug 15 10:58:48 CST 2022
Thread-0结束了Mon Aug 15 10:58:50 CST 2022
main获取到锁Mon Aug 15 10:58:50 CST 2022
main结束了Mon Aug 15 10:58:52 CST 2022
我们可以发现,Thread-0线程和main线程,执行synchronized代码块是互斥的。
因为我们调用了.intern()方法,将字符串统一放到字符串常量池中管理,相同的字符串代表的对象地址是一样的。
synchronized是一个对象锁,在单机环境下,我们最好不要使用固定的对象进行加锁,使用业务id对指定业务进行加锁可以提高很多并发量。
集群环境下还是不要使用synchronized了。