Java多线程——synchronized的使用示例

2,211 阅读12分钟

  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秒,也足够另一个线程获取锁,也就是说,两个线程都在等待获取对方释放锁,显然他们不能获取对方的锁的话,就不会释放锁,因此造成死锁。