介绍

多个线程对共享资源的进行读写操作的时候,由于cpu指令执行的顺序不同,导致每次的结果可能不一样。为了解决这一问题,可以用加锁的方式解决。

临界区和竞态条件

临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

    synchronized加锁

    synchronized让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,保证了临界区代码的原子性。
    可以做如下类比:

  • sychronized(对象)中的对象好比是房间,每个房间只有一把钥匙,多个线程相当于人,人需要有钥匙(持有锁)才能进入房间。

  • 当线程t1执行临界区时,相当于第一个人拿到了钥匙进入房间做事情。
  • 当线程t2也运行到了临界区时,相当于第二个人来到房间门口,由于没有钥匙,只能阻塞住。
  • 如果在这中间线程t1的cpu时间片不幸用完,它会被踢出门外(不要以为持有了锁,它就会一直执行下去),t1仍然拿着钥匙,下次t1再次被cpu分配到时间片时,它继续开门进入工作。
  • 当t1执行完sychronized的代码,它会开门,交出钥匙,同时唤醒其他阻塞的线程,让他们竞争钥匙,进门工作。

    synchronized实践

    synchronized加在方法上,相当于对当前对象加锁,synchronized加在静态方法上,相当于对类对象加锁。进行synchronized的加锁分析时,主要看不同的线程是否持有同一把锁,或者拿上面的比方来说,是不是进入同一个房间。如果是类对象,那么一个类就是一个房间,对象的话,一个对象是同一个房间。

    synchronized加在方法上

    ```shell public class SychronizedTest { public static void main(String[] args) {
    1. Number n1 = new Number();
    2. new Thread(()->{ n1.a(); }).start();
    3. new Thread(()->{ n1.b(); }).start();
    } }

@Slf4j class Number{ @SneakyThrows public synchronized void a() { log.debug(“a start……”); Thread.sleep(1000); log.debug(“a end……”); } @SneakyThrows public synchronized void b() { log.debug(“b start……”); Thread.sleep(1000); log.debug(“b end……”); } }

  1. **结果:**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/384158/1643027706344-c464d932-982e-4e32-8fbf-36da50acdee6.png#clientId=uc4a4bccc-f9e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=151&id=uc11b4afc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=302&originWidth=1314&originalType=binary&ratio=1&rotation=0&showTitle=false&size=169330&status=done&style=none&taskId=u3a0ffc38-d81c-4a91-b4e6-d2051cad989&title=&width=657)<br />synchronized加在方法上相当于对当前这个对象加锁,他们想要进的是同一个房间(同一把锁)。
  2. <a name="siGTz"></a>
  3. ## synchronized加在其中一个方法上
  4. ```shell
  5. public class SychronizedTest2 {
  6. public static void main(String[] args) {
  7. Number2 n1 = new Number2();
  8. new Thread(() -> {
  9. n1.a();
  10. }).start();
  11. new Thread(() -> {
  12. n1.b();
  13. }).start();
  14. }
  15. }
  16. @Slf4j
  17. class Number2 {
  18. @SneakyThrows
  19. public synchronized void a() {
  20. log.debug("a start......");
  21. Thread.sleep(1000);
  22. log.debug("a end......");
  23. }
  24. @SneakyThrows
  25. public void b() {
  26. log.debug("b start......");
  27. Thread.sleep(1000);
  28. log.debug("b end......");
  29. }
  30. }

结果:
image.png
方法a加了锁,方法b未加锁,所以两个线程没有持有同一个锁,所以不会阻塞。

synchronized加在不同锁上

  1. public class SychronizedTest3 {
  2. public static void main(String[] args) {
  3. Number3 n1 = new Number3();
  4. Number3 n2 = new Number3();
  5. new Thread(() -> {
  6. n1.a();
  7. }).start();
  8. new Thread(() -> {
  9. n2.b();
  10. }).start();
  11. }
  12. }
  13. @Slf4j
  14. class Number3 {
  15. @SneakyThrows
  16. public synchronized void a() {
  17. log.debug("a start......");
  18. Thread.sleep(1000);
  19. log.debug("a end......");
  20. }
  21. @SneakyThrows
  22. public synchronized void b() {
  23. log.debug("b start......");
  24. Thread.sleep(1000);
  25. log.debug("b end......");
  26. }
  27. }

结果:
image.png
本质上,两个线程是在不同的两个锁,也就是说进入的两个不同的房间,所以不会阻塞。

synchronized加在静态方法上

  1. public class SychronizedTest4 {
  2. public static void main(String[] args) {
  3. Number4 n1 = new Number4();
  4. new Thread(() -> {
  5. n1.a();
  6. }).start();
  7. new Thread(() -> {
  8. n1.b();
  9. }).start();
  10. }
  11. }
  12. @Slf4j
  13. class Number4 {
  14. @SneakyThrows
  15. public synchronized static void a() {
  16. log.debug("a start......");
  17. Thread.sleep(1000);
  18. log.debug("a end......");
  19. }
  20. @SneakyThrows
  21. public synchronized void b() {
  22. log.debug("b start......");
  23. Thread.sleep(1000);
  24. log.debug("b end......");
  25. }
  26. }

结果:
image.png
synchronized加在静态方法上,相当于对这个类对象加锁,和加在方法上不是同一个锁,他们进入的不是同一个房间,所以不会阻塞。

synchronized全加在静态方法上

  1. public class SychronizedTest5 {
  2. public static void main(String[] args) {
  3. Number5 n1 = new Number5();
  4. new Thread(() -> {
  5. n1.a();
  6. }).start();
  7. new Thread(() -> {
  8. n1.b();
  9. }).start();
  10. }
  11. }
  12. @Slf4j
  13. class Number5 {
  14. @SneakyThrows
  15. public synchronized static void a() {
  16. log.debug("a start......");
  17. Thread.sleep(1000);
  18. log.debug("a end......");
  19. }
  20. @SneakyThrows
  21. public synchronized static void b() {
  22. log.debug("b start......");
  23. Thread.sleep(1000);
  24. log.debug("b end......");
  25. }
  26. }

结果:
image.png
两个线程加的是同一个锁,这个Numer4的这个类对象的锁,所以会阻塞。

变量的线程安全分析

通过加锁synchronized让线程安全,那什么情况下需要加锁,换句话说,什么情况下变量会线程不安全呢?

成员变量和静态变量是否线程安全?

  • 如果他们没有共享,则线程安全
  • 如果他们被共享了

    • 如果只有读操作,线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

      局部变量是否线程安全?

  • 局部变量是线程安全的

  • 局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全。

1.