synchronized是我们常用的锁,synchronized锁是互斥的,同一时间只能有一个线程获得锁,因此能够保证线程安全;synchronized又是可重入的锁。 synchronized常用范围:
- 修饰普通同步方法,锁定当前对象;
- 也可以修饰静态方法,锁定的是当前类的class对象;
- 还可以修改代码块,锁定括号中的内容。
1. synchronized同步普通方法
1.1 应用示例
线程安全问题最常见就是对象的成员变量的计算问题,先看在没有synchronized的情况下,我们的计算会出现什么问题。
@Slf4j
@Getter
public class HasSelfNum implements Runnable {
private int num = 0;
@Override
public void run() {
add();
}
public void add() {
for (int i = 0; i < 10; i++) {
num++;
log.info("当前线程:{},num = {}", Thread.currentThread().getName(), num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试方法:
@Slf4j
public class Main {
public static void main(String[] args) throws Exception {
HasSelfNum hasSelfNum = new HasSelfNum();
Thread threadA = new Thread(hasSelfNum, "线程A");
Thread threadB = new Thread(hasSelfNum, "线程B");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
log.info("最终num = {}", hasSelfNum.getNum());
}
}
输出结果:
15:20:49.075 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 2
15:20:49.075 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 1
15:20:50.080 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 3
15:20:50.080 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 3
15:20:51.081 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 5
15:20:51.081 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 5
15:20:52.082 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 6
15:20:52.082 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 6
15:20:53.082 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 7
15:20:53.082 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 7
15:20:54.083 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 8
15:20:54.083 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 9
15:20:55.084 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 10
15:20:55.084 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 10
15:20:56.085 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 12
15:20:56.085 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 12
15:20:57.086 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 14
15:20:57.086 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 13
15:20:58.086 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 15
15:20:58.087 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 16
15:20:59.088 [main] INFO com.sachin.threadlearn.sync.sync1.Main - 最终num = 16
根据上边的实验,在不使用synchronized的情况下,多个线程可能会同时得到相同的数据,在后边的计算中造成错误,多运行几次,每次计算的结果都会有所不同,但是大部分都是不正确的。为了保证当前线程能够获取到正确的数据,我们引入synchronized锁,每次只能有一个线程获取数据计算:
public synchronized void add() {
for (int i = 0; i < 10; i++) {
num++;
log.info("当前线程:{},num = {}", Thread.currentThread().getName(), num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试程序计算结果:
15:25:35.115 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 1
15:25:36.120 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 2
15:25:37.121 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 3
15:25:38.122 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 4
15:25:39.122 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 5
15:25:40.123 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 6
15:25:41.124 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 7
15:25:42.125 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 8
15:25:43.125 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 9
15:25:44.126 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 10
15:25:45.127 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 11
15:25:46.128 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 12
15:25:47.129 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 13
15:25:48.130 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 14
15:25:49.131 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 15
15:25:50.131 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 16
15:25:51.132 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 17
15:25:52.133 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 18
15:25:53.133 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 19
15:25:54.134 [线程B] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程B,num = 20
15:25:55.136 [main] INFO com.sachin.threadlearn.sync.sync1.Main - 最终num = 20
根据测试结果,可以看到,synchronized锁保证了线程顺序执行,最后结果也能保证正确。关于上边测试代码中的join()方法,其主要作用是,保证主线程等待子线程销毁之后再继续运行,避免子线程还没结束,主线程就输出了结果,造成输出结果错误。
1.2 验证锁定当前对象
最开始,我们说过synchronized修饰普通方法时,锁定的是当前对象,为了验证这个观点,我们创建一个类中有两个同步方法,然后创建两个线程,分别调用不同的方法:
@Slf4j
public class MyService {
public synchronized void methodA() {
try {
log.info("当前线程:{}, 开始调用方法A", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
log.info("当前线程:{}, 离开方法A", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodB() {
try {
log.info("当前线程:{}, 开始调用方法B", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
log.info("当前线程:{}, 离开方法B", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@AllArgsConstructor
public class ThreadA extends Thread {
private MyService service;
@Override
public void run() {
service.methodA();
}
}
@AllArgsConstructor
public class ThreadB extends Thread {
private MyService service;
@Override
public void run() {
service.methodB();
}
}
测试程序:
public static void main(String[] args) {
MyService service = new MyService();
ThreadA threadA = new ThreadA(service);
ThreadB threadB = new ThreadB(service);
threadA.start();
threadB.start();
}
测试结果:
15:40:19.011 [Thread-0] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-0, 开始调用方法A
15:40:24.015 [Thread-0] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-0, 离开方法A
15:40:24.016 [Thread-1] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-1, 开始调用方法B
15:40:29.017 [Thread-1] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-1, 离开方法B
可以看到,另一个线程必须等第一个线程释放锁之后,才能获得锁,执行程序,说明他们获取的同一个锁,都是service对象。
如果两个线程,构造方法中是不同的对象,那么他们就会获取不同的锁,也就不会等待了,修改测试方法验证:
public static void main(String[] args) {
MyService service = new MyService();
ThreadA threadA = new ThreadA(service);
ThreadB threadB = new ThreadB(new MyService());
threadA.start();
threadB.start();
}
测试结果:
15:44:07.772 [Thread-1] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-1, 开始调用方法B
15:44:07.772 [Thread-0] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-0, 开始调用方法A
15:44:12.776 [Thread-0] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-0, 离开方法A
15:44:12.776 [Thread-1] INFO com.sachin.threadlearn.sync.lockInstance.MyService - 当前线程:Thread-1, 离开方法B
根据以上一个例子,可以看到synchronized作用在普通实例方法上时,锁定的是当前对象。
1.3 可重入验证
之前我们说synchronized锁是可重入锁,可重入锁就是说,一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的;也就是说在一个synchronized方法内部调用另一个synchronized方法,是永远能够得到锁的。下边我们用程序验证:
@Slf4j
@Getter
public class HasSelfNum implements Runnable {
private int num = 0;
@Override
public void run() {
add();
}
public synchronized void add() {
for (int i = 0; i < 10; i++) {
num++;
printLog();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void printLog() {
log.info("当前线程:{},num = {}", Thread.currentThread().getName(), num);
}
}
将之前计算的方法拆成两个,根据上边锁定对象的验证,如果是两个线程分别调用者两个方法是需要等待了,但是如果是同一个线程已经获得锁还未释放,去调用另一个方法,将不用再次去请求锁,能够直接调用。
测试方法:
HasSelfNum hasSelfNum = new HasSelfNum();
Thread threadA = new Thread(hasSelfNum, "线程A");
threadA.start();
测试结果:
15:47:00.249 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 1
15:47:01.253 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 2
15:47:02.253 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 3
15:47:03.253 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 4
15:47:04.254 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 5
15:47:05.254 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 6
15:47:06.255 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 7
15:47:07.256 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 8
15:47:08.257 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 9
15:47:09.258 [线程A] INFO com.sachin.threadlearn.sync.sync1.HasSelfNum - 当前线程:线程A,num = 10
可以看到,同一个线程获得锁之后,在一个同步方法中,调用另一个同步方法,是永远能够成功的。
2. synchronized同步静态方法
synchronized修饰静态方法时,锁定的是class对象,下边我们通过程序,验证该观点,创建一个类,分别有一个synchronized修饰的静态方法和synchronized修饰的普通方法,然后创建两个线程分别调用不同的方法:
@Slf4j
public class MyService {
public synchronized static void methodA() {
try {
log.info("开始执行静态方法A");
TimeUnit.SECONDS.sleep(5);
log.info("离开静态方法A");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodB() {
try {
log.info("开始执行实例方法B");
TimeUnit.SECONDS.sleep(5);
log.info("离开实例方法B");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@AllArgsConstructor
public class ThreadA extends Thread {
private MyService service;
@Override
public void run() {
service.methodA();
}
}
@AllArgsConstructor
public class ThreadB extends Thread {
private MyService service;
@Override
public void run() {
service.methodB();
}
}
测试程序:
public static void main(String[] args) {
MyService service = new MyService();
ThreadA threadA = new ThreadA(service);
ThreadB threadB = new ThreadB(service);
threadA.start();
threadB.start();
}
测试结果:
15:20:55.192 [Thread-1] INFO com.sachin.threadlearn.sync.sync2.MyService - 开始执行实例方法B
15:20:55.192 [Thread-0] INFO com.sachin.threadlearn.sync.sync2.MyService - 开始执行静态方法A
15:21:00.197 [Thread-1] INFO com.sachin.threadlearn.sync.sync2.MyService - 离开实例方法B
15:21:00.197 [Thread-0] INFO com.sachin.threadlearn.sync.sync2.MyService - 离开静态方法A
两个线程几乎同时开始执行相应的方法,说明两个线程获得锁不是同一个锁。
3. synchronized修饰代码块
在上边的例子中synchronized都是修饰的整个方法,在这种情况下,每个线程调用该方法时,都得等待获得锁的线程释放锁之后才可以执行,但是,某些情况下,方法中只有部分需要使用锁,其他的即使多线程调用也不会存在问题,这种情况下,我们只需要将需要锁定的代码块用synchronized修饰就可以了,下边看一下具体的使用:
@Slf4j
public class MyService implements Runnable {
private int num;
@Override
public void run() {
add();
}
private void add() {
log.info("线程:{},进入方法中", Thread.currentThread().getName());
synchronized (this) {
log.info("线程:{},进入同步代码块", Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
num++;
}
log.info("线程:{},离开同步代码块", Thread.currentThread().getName());
}
log.info("线程:{},离开方法", Thread.currentThread().getName());
}
}
测试代码:
public static void main(String[] args) {
MyService service = new MyService();
Thread thread1 = new Thread(service);
Thread thread2 = new Thread(service);
thread1.start();
thread2.start();
}
执行结果:
16:42:22.333 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-0,进入方法中
16:42:22.333 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-1,进入方法中
16:42:22.336 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-0,进入同步代码块
16:42:22.336 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-0,离开同步代码块
16:42:22.336 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-0,离开方法
16:42:22.336 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-1,进入同步代码块
16:42:22.336 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-1,离开同步代码块
16:42:22.336 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode.MyService - 线程:Thread-1,离开方法
根据执行结果可以看到,两个线程几乎同时进入方法中,但是,在synchronized代码块,两个线程依次执行。说明了,该部分代码被锁定,只有获取锁才能获得。
上边synchronized修饰普通方法和静态方法时,分别锁定了当前对象和class对象,那么,修饰同步代码块时,锁定的时哪个部分呢?可以看到synchronized后边有一个括号,里边写了this,这种情况下,锁定当前对象,也可以写Myservice.class甚至其他的对象;
下边验证锁定当前的情况,创建一个类,其中有三个方法,分别使用synchronized锁定代码块,普通方法,静态方法,然后,创建三个线程,分别调用三个方法:
@Slf4j
public class MyService {
public void methodA() {
synchronized (this) {
try {
log.info("线程:{},进入同步代码块", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},离开同步代码块", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void methodB() {
try {
log.info("线程:{},进入同步方法", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},离开同步方法", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void methodC() {
try {
log.info("线程:{},进入同步静态方法", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},离开同步静态方法", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
调用线程:
@AllArgsConstructor
public class ThreadA implements Runnable {
private MyService service;
@Override
public void run() {
service.methodA();
}
}
...... 省略其他两个线程
测试方法:
public class Main {
public static void main(String[] args) {
MyService service = new MyService();
new Thread(new ThreadA(service)).start();
new Thread(new ThreadB(service)).start();
new Thread(new ThreadC(service)).start();
}
}
验证结果:
22:03:24.832 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-0,进入同步代码块
22:03:24.832 [Thread-2] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-2,进入同步静态方法
22:03:27.836 [Thread-2] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-2,离开同步静态方法
22:03:27.836 [Thread-0] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-0,离开同步代码块
22:03:27.837 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-1,进入同步方法
22:03:30.838 [Thread-1] INFO com.sachin.threadlearn.sync.syncCode2.MyService - 线程:Thread-1,离开同步方法
根据上边的代码可以看到,synchronized (this)锁定的是当前对象,和修饰普通方法时相同;如果修改为synchronized (MyService.class),则锁定的class对象;另外还可以锁定任意的一个对象synchronized (object),这种情况下几个线程能够分别获得锁,因为每个线程的锁都不同。
4. 其他场景
在多线程的其他场景中,也伴随着synchronized的出现,例如:线程之间的通信(等待/通知机制),死锁等等。这里,我们先看看死锁是怎么回事。
死锁,是指不同的线程都在等待不可能释放的锁,从而导致所有的任务都无法继续完成。下边使用一个互相等待锁的例子,来查看死锁的情况:
@Slf4j
@AllArgsConstructor
public class DeadThread implements Runnable {
private String username;
private Object lock1;
private Object lock2;
@Override
public void run() {
if (username.equals("a")) {
methodA();
} else {
methodB();
}
}
private void methodA() {
synchronized (lock1) {
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},已经获得lock1等待获取lock2", Thread.currentThread().getName());
synchronized (lock2) {
log.info("线程:{},已经获得lock1,并且获取lock2", Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void methodB() {
synchronized (lock2) {
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},已经获得lock2等待获取lock1", Thread.currentThread().getName());
synchronized (lock1) {
log.info("线程:{},已经获得lock2,并且获取lock1", Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试方法:
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(new DeadThread("a", lock1, lock2), "线程a").start();
new Thread(new DeadThread("b", lock1, lock2), "线程b").start();
}
创建两个线程,一个先获取lock1锁,然后,试图获取lock2锁;另外,一个线程先获取lock2锁,然后,获取lock1锁;由于两个线程基本同时启动,在获取另一个或之前sleep了3秒,也足够另一个线程获取锁,也就是说,两个线程都在等待获取对方释放锁,显然他们不能获取对方的锁的话,就不会释放锁,因此造成死锁。