Java 多线程 - 初识 Synchronized
Synchronized 简介
本文出自汪文君老师的《Java 并发编程》课程,如需转载,请注明源出处!
先来看一个例子,这个例子是模拟银行叫号的,使用三个线程模拟三个柜台一起叫号,总共50个号。在不加 synchronized 的关键字的情况下,很容易就会出现并发问题。
public class BankRunnable {
public static void main(String[] args) {
// 一个runnable实例被多个线程共享
TicketWindowRunnable ticketWindow = new TicketWindowRunnable();
Thread windowThread1 = new Thread(ticketWindow, "一号窗口");
Thread windowThread2 = new Thread(ticketWindow, "二号窗口");
Thread windowThread3 = new Thread(ticketWindow, "三号窗口");
windowThread1.start();
windowThread2.start();
windowThread3.start();
}
}
public class TicketWindowRunnable implements Runnable {
private int index = 1;
private static final int MAX = 50;
@Override
public void run() {
while (true) {
if (index > MAX) {//1
break;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 的号码是:"+(index++));//2
}
}
}
多运行几遍程序,就会出现下面这个问题:
在一号窗口拿完最后一个号码之后,二号窗口和三号窗口又后续拿到了 52 和 51 号。为什么会出现这种现象呢?
首先当 index=499
的时候,三个线程均不满足 index > MAX
,都会向下执行。三个线程都可以向下执行,将 index 加 1。
为了解决这个问题,这里引入了 synchronized 。
什么是 synchronized
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。
上面这段话是oracle官网对synchronized关键字的解释,具体表现如下:
- synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存(在本书的第三部分会重点介绍)。
- synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。
synchronized关键字的用法
Java通过 synchronized 对共享数据的线程访问提供了一种避免竞争条件的机制。synchronized 可以修饰方法或者代码块,被修饰的方法或者代码块同一时间只会允许一个线程执行,这条执行的线程持有同步部分的锁。synchronized 方法不能用于对class及其变量进行修饰。
synchronized 关键字可以修饰方法或者代码块,那么这两者有什么区别呢?
// 同步代码块
public class TicketWindowRunnable implements Runnable {
private int index = 1;
private static final int MAX = 500;
private final Object MONITOR = new Object();
@Override
public void run() {
while (true) {
synchronized (MONITOR) {
if (index > MAX) {
break;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
}
}
}
}
synchronized 方法修饰代码块的时候,使用的是 monitor 锁。再来用 synchronized 修饰一下同步方法:
@Override
public synchronized void run() {
while (true) {
if (index > MAX) {
break;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
}
}
运行之后发现都是同一个线程在跑,另外两个线程无法执行。这是因为 synchronized 在修饰方法的时候使用的是 this 锁,当其中一个线程拿到锁进到 while 循环之后,就一直去做事情,直到满足条件退出为止。将 while 里面的代码抽出来放到一个方法里,用 synchronized 来修饰该方法就可以解决这个问题。
@Override
public void run() {
while (true) {
if (ticket()) {
break;
}
}
}
private synchronized boolean ticket() {
if (index > MAX) {
return true;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
return false;
}
synchronized 修饰方法时默认是使用的 this 锁,修饰代码块时使用的是对象锁。synchronized 关键字还可以用来修饰静态方法和静态代码块。
public class SynchronizedStatic {
public synchronized static void m1() {
System.out.println("m1 " + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void m2() {
System.out.println("m2 " + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SynchronizedStaticTest {
public static void main(String[] args) {
new Thread("T1") {
@Override
public void run() {
SynchronizedStatic.m1();
}
}.start();
new Thread("T2") {
@Override
public void run() {
SynchronizedStatic.m2();
}
}.start();
}
}
// output
m1 T1
m2 T2
静态方法 m1 和 m2 同时被 synchronized 修饰,这个时候线程 T2 会等到线程 T1 执行完再执行,说明这两个方法使用的是同一把锁,这就是 Class 锁。我们把 sleep 的时间变长一点来观察一下是不是 Class 锁。
可以看到,线程 T1 执行的时候,持有的是 Class 锁,此时线程 T2 在等待 T1 执行完释放锁,当 T1 执行完之后,T2 拿到 Class 锁执行代码。
了解了 synchronized 修饰静态方法使用的是 Class 锁之后,我们再来验证一下当它修饰静态方法的时候是不是也是使用 Class 锁?
public class SynchronizedStatic {
public synchronized static void m1() {
System.out.println("m1 " + Thread.currentThread().getName());
try {
Thread.sleep(100_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void m3() {
System.out.println("m3 " + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SynchronizedStaticTest {
public static void main(String[] args) {
new Thread("T1") {
@Override
public void run() {
SynchronizedStatic.m1();
}
}.start();
new Thread("T3") {
@Override
public void run() {
SynchronizedStatic.m3();
}
}.start();
}
}
这里加了一个没有 synchronized 修饰的静态方法 m3,运行之后很容易知道,这两个线程是同时运行的。我们在 SynchronizedStatic 开始的地方加一个静态代码块,静态代码块内部使用 synchronized 锁。
public class SynchronizedStatic {
static {
synchronized (SynchronizedStatic.class) {
System.out.println("static " + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized static void m1() {
System.out.println("m1 " + Thread.currentThread().getName());
try {
Thread.sleep(100_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void m3() {
System.out.println("m3 " + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//output
static T1
m1 T1
m3 T3
可以发现,T1 线程要先执行静态代码块才能往下走,说明静态代码块使用的锁和静态方法是一样的,另外这个时候没有用 synchronized 修饰的 m3 也要等静态代码块执行实例化才行。
总结一下,synchronized 关键字能够避免多线程竞争导致的数据不一致,被 synchronized 修饰的方法或者代码块同一时间只会允许一个线程执行,这条执行的线程持有同步部分的锁。synchronized 关键字修饰普通方法时,使用的是 this 锁,修饰静态方法和静态代码块时,使用 Class 锁,修饰代码块时,使用 LOCK 锁。