1. 线程共享带来的问题

1.1 线程安全问题

线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了。

代码示例

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0么?

  1. @Slf4j(topic = "Test")
  2. public class Test {
  3. public static int cnt = 0;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread t1 = new Thread(() -> {
  6. for (int i = 0; i < 5000; i++) {
  7. cnt++;
  8. }
  9. }, "t1");
  10. Thread t2 = new Thread(() -> {
  11. for (int i = 0; i < 5000; i++) {
  12. cnt--;
  13. }
  14. }, "t2");
  15. t1.start();
  16. t2.start();
  17. t1.join();
  18. t2.join();
  19. log.debug("{}", cnt);
  20. }
  21. }
  1. 17:22:35.234 [main] DEBUG Test - 1049

结果不是0

  • 字节码: ```java count++; // 操作字节码如下: getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i

count—; // 操作字节码如下: getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i

  1. CPU 时间片分给 t1 线程时,t1 线程去读取变量值为 0 并且执行 ++ 的操作,如上在字节码自增操作中,当 t1 执行完自增,还没来得急将修改后的值存入静态变量时,假如线程的时间片用完了,并且 CPU 将时间片分配给 t2 线程,t2 线程拿到时间片执行自减操作,并且将修改后的值存入静态变量,此时 count 的值为 -1,但是当 CPU 将时间片分给经历了上下文切换的 t1 线程时,t1 将修改后的值存入静态变量,此时 counter 的值为 1,覆盖了 t2 线程执行的结果,出现了丢失更新,这就是多线对共享资源读取的问题。
  2. - 正常情况下:
  3. ![截屏2021-04-29 下午5.26.34.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688397162-03a9407f-d5ed-427a-9e88-926d6d8cdeab.png#clientId=ufb484f9b-faa0-4&from=drop&id=ude4904c6&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.26.34.png&originHeight=639&originWidth=584&originalType=binary&size=78200&status=done&style=none&taskId=uc141cf09-28d5-436f-a604-734c205c489)
  4. - 交错运行,出现负数:
  5. ![截屏2021-04-29 下午5.27.10.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688432379-0d9a1789-1e27-489e-ab8d-85634380303e.png#clientId=ufb484f9b-faa0-4&from=drop&height=565&id=u5c3496df&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.27.10.png&originHeight=718&originWidth=800&originalType=binary&size=104680&status=done&style=none&taskId=u98e2a423-0f0b-4502-99d6-7297690fc12&width=630)
  6. - 交错运行,出现正数
  7. ![截屏2021-04-29 下午5.32.17.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688741127-f78bc57e-f665-44b7-9a0b-ed34ca44e277.png#clientId=ufb484f9b-faa0-4&from=drop&height=593&id=u701cbd51&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.32.17.png&originHeight=723&originWidth=694&originalType=binary&size=99606&status=done&style=none&taskId=u7795f3f2-3ece-4f5e-a07d-6ec4a3ca9b3&width=569)
  8. <a name="KDujD"></a>
  9. ## 1.2 临界区**Critical Section**
  10. - 一个程序运行多个线程本身是没有问题的
  11. - 问题出在多个线程访问共享资源
  12. - 多个线程读共享资源其实也没有问题
  13. - 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  14. - 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
  15. 例如,下面代码中的临界区
  16. ```java
  17. static int counter = 0;
  18. static void increment()
  19. // 临界区
  20. {
  21. counter++;
  22. }
  23. static void decrement()
  24. // 临界区
  25. {
  26. counter--;
  27. }

1.3 竞态条件Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2.synchronized 解决方案

2.1 解决手段

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

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

现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

2.2 synchronized 语法

  1. synchronized(对象) {
  2. //临界区
  3. }

2.3 synchronized使用

  1. @Slf4j(topic = "Test")
  2. public class Test {
  3. public static int cnt = 0;
  4. public static final Object object = new Object();
  5. public static void main(String[] args) throws InterruptedException {
  6. Thread t1 = new Thread(() -> {
  7. for (int i = 0; i < 5000; i++) {
  8. synchronized (object){
  9. cnt++;
  10. }
  11. }
  12. }, "t1");
  13. Thread t2 = new Thread(() -> {
  14. for (int i = 0; i < 5000; i++) {
  15. synchronized (object) {
  16. cnt--;
  17. }
  18. }
  19. }, "t2");
  20. t1.start();
  21. t2.start();
  22. t1.join();
  23. t2.join();
  24. log.debug("{}", cnt);
  25. }
  26. }
  1. 20:21:58.972 [main] DEBUG Test - 0

可以看到,加上锁之后,得到的数字是正确的。

截屏2021-04-29 下午8.27.29.png

  • synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断。
  • 问题:
    • 如果把synchronized(obj)放在for循坏的外面,如何理解?

之前是对一个cnt++进行锁,其实是保证了字节码中的4行代码;对for循环加锁,就是保证了20000行字节码的原子性。

  • 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎样运作?

    1. 要保护共享资源,必须让多个线程锁同一个对象。
  • 如果t1 synchronized(obj) 而 t2 没有加锁会怎样?

在t1加锁之后。t2尝试获取锁时不会被阻塞,也保护不了共享资源

  • 使用面向对象的思维改进 ```java @Slf4j(topic = “Test”) public class Test {
  1. public static void main(String[] args) throws InterruptedException {
  2. Room room = new Room();
  3. Thread t1 = new Thread(() -> {
  4. for (int i = 0; i < 5000; i++) {
  5. room.increment();
  6. }
  7. }, "t1");
  8. Thread t2 = new Thread(() -> {
  9. for (int i = 0; i < 5000; i++) {
  10. room.decrement();
  11. }
  12. }, "t2");
  13. t1.start();
  14. t2.start();
  15. t1.join();
  16. t2.join();
  17. log.debug("{}", room.getCnt());
  18. }

}

class Room { public static int cnt = 0;

  1. public void increment() {
  2. synchronized (this) {
  3. cnt++;
  4. }
  5. }
  6. public void decrement() {
  7. synchronized (this) {
  8. cnt--;
  9. }
  10. }
  11. public int getCnt() {
  12. synchronized (this){
  13. return cnt;
  14. }
  15. }

}

  1. ```java
  2. 20:45:46.608 [main] DEBUG Test - 0

2.4 方法上的synchronized

2.4.1 成员方法加锁

  • 加在成员方法上,锁住的是对象 ```java public class Test { // 在方法上加上synchronized关键字 public synchronized void test() {

    } // 等价于 public void test() {

    1. synchronized(this) { // 锁住的是this对象
    2. }

    } }

  1. <a name="xIsaw"></a>
  2. ### 2.4.2 静态方法加锁
  3. - 加载静态方法上,锁住的是类
  4. ```java
  5. public class Test {
  6. // 在静态方法上加上 synchronized 关键字
  7. public synchronized static void test() {
  8. }
  9. //等价于
  10. public static void test() {
  11. synchronized(Test.class) { // 锁住的是类对象
  12. }
  13. }
  14. }

线程八锁

考察synchronized锁住的是哪个对象?

  • 情况1: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Thread t1 = new Thread(()-> {
    3. log.debug("begin");
    4. n1.a();
    5. }, "t1");
    6. Thread t2 = new Thread(()-> {
    7. log.debug("begin");
    8. n1.b();
    9. }, "t2");
    10. t1.start();
    11. t2.start();

    } }

@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ log.debug(“1”); }

  1. public synchronized void b(){
  2. log.debug("2");
  3. }

}

  1. 结果:t1(n1)和t2(n1)锁住同一个对象(方法上,this), 互斥:
  2. - 12
  3. - 21
  4. ```java
  5. 23:10:12.127 [t1] DEBUG c.Test2 - begin
  6. 23:10:12.127 [t2] DEBUG c.Test2 - begin
  7. 23:10:12.133 [t2] DEBUG c.Number - 2
  8. 23:10:12.133 [t1] DEBUG c.Number - 1
  9. 23:13:28.607 [t1] DEBUG c.Test2 - begin
  10. 23:13:28.607 [t2] DEBUG c.Test2 - begin
  11. 23:13:28.610 [t1] DEBUG c.Number - 1
  12. 23:13:28.611 [t2] DEBUG c.Number - 2
  • 情况2: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Thread t1 = new Thread(()-> {
    3. log.debug("begin");
    4. n1.a();
    5. }, "t1");
    6. Thread t2 = new Thread(()-> {
    7. log.debug("begin");
    8. n1.b();
    9. }, "t2");
    10. t1.start();
    11. t2.start();

    } }

@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public synchronized void b(){
  3. log.debug("2");
  4. }

}

  1. 结果:t1(n1)与t2(n1)锁住同一个对象(方法上,this), 互斥:
  2. - 休眠1s,先12
  3. - 2,休眠1秒,后1
  4. ```java
  5. 23:20:15.998 [t1] DEBUG c.Test2 - begin
  6. 23:20:15.998 [t2] DEBUG c.Test2 - begin
  7. 23:20:17.008 [t1] DEBUG c.Number - 1
  8. 23:20:17.009 [t2] DEBUG c.Number - 2
  9. 23:19:55.184 [t1] DEBUG c.Test2 - begin
  10. 23:19:55.184 [t2] DEBUG c.Test2 - begin
  11. 23:19:55.188 [t2] DEBUG c.Number - 2
  12. 23:19:56.192 [t1] DEBUG c.Number - 1
  • 情况3: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Thread t1 = new Thread(()-> {
    3. log.debug("begin");
    4. n1.a();
    5. }, "t1");
    6. Thread t2 = new Thread(()-> {
    7. log.debug("begin");
    8. n1.b();
    9. }, "t2");
    10. Thread t3 = new Thread(()-> {
    11. log.debug("begin");
    12. n1.c();
    13. }, "t3");
    14. t1.start();
    15. t2.start();

    } }

@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public synchronized void b(){
  3. log.debug("2");
  4. }
  5. public void c(){
  6. log.debug("3");
  7. }

}

  1. 结果:t1(n1)和t2(n2)锁住同一个对象(方法上,this),互斥执行;3没有加synchronized,并行执行
  2. - 3,休眠1s12
  3. - 32,休眠1s1
  4. - 23,休眠1s1
  5. ```java
  6. 23:27:32.560 [t1] DEBUG c.Test2 - begin
  7. 23:27:32.560 [t2] DEBUG c.Test2 - begin
  8. 23:27:32.560 [t3] DEBUG c.Test2 - begin
  9. 23:27:32.565 [t3] DEBUG c.Number - 3
  10. 23:27:33.568 [t1] DEBUG c.Number - 1
  11. 23:27:33.569 [t2] DEBUG c.Number - 2
  12. 23:26:48.360 [t1] DEBUG c.Test2 - begin
  13. 23:26:48.360 [t3] DEBUG c.Test2 - begin
  14. 23:26:48.360 [t2] DEBUG c.Test2 - begin
  15. 23:26:48.363 [t3] DEBUG c.Number - 3
  16. 23:26:48.363 [t2] DEBUG c.Number - 2
  17. 23:26:49.368 [t1] DEBUG c.Number - 1
  18. 23:27:11.653 [t1] DEBUG c.Test2 - begin
  19. 23:27:11.653 [t3] DEBUG c.Test2 - begin
  20. 23:27:11.653 [t2] DEBUG c.Test2 - begin
  21. 23:27:11.656 [t2] DEBUG c.Number - 2
  22. 23:27:11.656 [t3] DEBUG c.Number - 3
  23. 23:27:12.659 [t1] DEBUG c.Number - 1
  • 情况4: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Number n2 = new Number();
    3. Thread t1 = new Thread(()-> {
    4. log.debug("begin");
    5. n1.a();
    6. }, "t1");
    7. Thread t2 = new Thread(()-> {
    8. log.debug("begin");
    9. n1.b();
    10. }, "t2");
  1. t1.start();
  2. t2.start();
  3. }

}

@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public synchronized void b(){
  3. log.debug("2");
  4. }

}

  1. 结果:t1(n1)和t2(n2)锁的是不同的对象(加在方法上,this),并行执行
  2. - 2,休眠1s1
  3. ```java
  4. 23:31:53.335 [t2] DEBUG c.Test2 - begin
  5. 23:31:53.335 [t1] DEBUG c.Test2 - begin
  6. 23:31:53.339 [t2] DEBUG c.Number - 2
  7. 23:31:54.344 [t1] DEBUG c.Number - 1
  • 情况5: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Thread t1 = new Thread(()-> {
    3. log.debug("begin");
    4. n1.a();
    5. }, "t1");
    6. Thread t2 = new Thread(()-> {
    7. log.debug("begin");
    8. n1.b();
    9. }, "t2");
  1. t1.start();
  2. t2.start();
  3. }

}

@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public synchronized void b(){
  3. log.debug("2");
  4. }

}

  1. 结果:t1加了static,锁的是Number类对象;t2锁的还是this对象(n1),不同的对象,并行
  2. - 2,休眠1s1
  3. ```java
  4. 23:37:52.532 [t1] DEBUG c.Test2 - begin
  5. 23:37:52.532 [t2] DEBUG c.Test2 - begin
  6. 23:37:52.536 [t2] DEBUG c.Number - 2
  7. 23:37:53.538 [t1] DEBUG c.Number - 1
  • 情况6: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) {

    1. Number n1 = new Number();
    2. Thread t1 = new Thread(()-> {
    3. log.debug("begin");
    4. n1.a();
    5. }, "t1");
    6. Thread t2 = new Thread(()-> {
    7. log.debug("begin");
    8. n1.b();
    9. }, "t2");
  1. t1.start();
  2. t2.start();
  3. }

}

@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public static synchronized void b(){
  3. log.debug("2");
  4. }

}

  1. 结果:t1t2都加了static,锁的是同一个Number对象,互斥,结果同情况2
  2. - 休眠1s,先12
  3. - 2,休眠1秒,后1
  4. - 情况7
  5. ```java
  6. @Slf4j(topic = "c.Test2")
  7. public class Test2 {
  8. public static void main(String[] args) {
  9. Number n1 = new Number();
  10. Number n2 = new Number();
  11. Thread t1 = new Thread(()-> {
  12. log.debug("begin");
  13. n1.a();
  14. }, "t1");
  15. Thread t2 = new Thread(()-> {
  16. log.debug("begin");
  17. n2.b();
  18. }, "t2");
  19. t1.start();
  20. t2.start();
  21. }
  22. }
  23. @Slf4j(topic = "c.Number")
  24. class Number {
  25. public static synchronized void a(){
  26. try {
  27. Thread.sleep(1000);
  28. log.debug("1");
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. public synchronized void b(){
  34. log.debug("2");
  35. }
  36. }

结果:t1加了static锁Number;t2是this,锁n2,对象不同,并行。与情况4一样

  • 先2,休眠1s后1

    • 情况8: ```java @Slf4j(topic = “c.Test2”) public class Test2 {

    public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); Thread t1 = new Thread(()-> {

    1. log.debug("begin");
    2. n1.a();

    }, “t1”);

    Thread t2 = new Thread(()-> {

    1. log.debug("begin");
    2. n2.b();

    }, “t2”);

  1. t1.start();
  2. t2.start();
  3. }

}

@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }

  1. }
  2. public static synchronized void b(){
  3. log.debug("2");
  4. }

}

  1. 结果:t1t2都是static,锁的是同一个对象(Number),互斥,与结果2相同。
  2. - 休眠1s,先12
  3. - 2,休眠1秒,后1
  4. <a name="R2CAm"></a>
  5. ## 2.5 变量的线程安全分析
  6. <a name="Rrdgq"></a>
  7. ### 2.5.1 成员变量和静态变量
  8. - 如果变量没有在线程间共享,那么线程对该变量操作是安全的
  9. - 如果变量在线程间共享
  10. - 如果只有读操作,则线程安全
  11. - 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题
  12. <a name="ue3ap"></a>
  13. ### 2.5.2 局部变量线程安全分析
  14. - 局部变量【局部变量被初始化为基本数据类型】是安全的
  15. - 局部变量是引用类型或者是对象引用则未必是安全的
  16. - 如果局部变量引用的对象没有引用线程共享的对象,那么是线程安全的
  17. - 如果局部变量引用的对象引用了一个线程共享的对象,那么要考虑线程安全问题
  18. <a name="HgX8D"></a>
  19. ### 2.5.3 局部变量线程安全分析
  20. - 局部变量本身
  21. ```java
  22. public static void test() {
  23. int i = 10;
  24. i++;
  25. }

每个线程调用test()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
字节码:只有第3行一条指令
截屏2021-04-30 上午9.32.02.png
截屏2021-04-30 上午9.33.21.png

  • 局部变量引用 ```java @Slf4j(topic = “c.Code_18_Test”) public class Code_18_Test {

    public static void main(String[] args) {

    1. UnsafeTest unsafeTest = new UnsafeTest();
    2. for(int i = 0; i < 10; i++) {
    3. new Thread(() -> {
    4. unsafeTest.method1();
    5. }, "t" + i).start();
    6. }

    }

}

class UnsafeTest {

  1. List<Integer> list = new ArrayList<>();
  2. public void method1() {
  3. // 临界区
  4. for (int i = 0; i < 200; i++) {
  5. method2();
  6. method3();
  7. }
  8. }
  9. private void method2() {
  10. list.add(1);
  11. }
  12. private void method3() {
  13. list.remove(0);
  14. }

}

  1. 实例变量list是被共享的,其中一种情况是,线程2还没有add,线程3romove了,没有数据是没办法remove的。<br />![截屏2021-04-30 上午9.35.51.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619746555730-b2377242-6b59-4052-ae0d-575da0e41fcc.png#clientId=u331f982e-2d25-4&from=drop&id=u5407e0dd&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-30%20%E4%B8%8A%E5%8D%889.35.51.png&originHeight=686&originWidth=900&originalType=binary&size=311052&status=done&style=none&taskId=u75ff7a1a-9974-4eb8-8a7e-b3f0269582a)
  2. - list修改成局部变量,没有发生逃逸
  3. ```java
  4. class SafeTest {
  5. public void method1() {
  6. List<Integer> list = new ArrayList<>();
  7. for (int i = 0; i < 200; i++) {
  8. method2(list);
  9. method3(list);
  10. }
  11. }
  12. private void method2(List<Integer> list) {
  13. list.add(1);
  14. }
  15. private void method3(List<Integer> list) {
  16. list.remove(0);
  17. }
  18. }

可以将 list 修改成局部变量,然后将 list 作为引用传入方法中,因为局部变量是每个线程私有的,会创建不同的实例,不会出现共享问题。而method2或method3的参数是从method1中传递过来的,与method1中引用同一个对象。
截屏2021-04-30 上午9.43.22.png

  • 局部变量,发生逃逸

将mehtod2和method3方法改成public

  1. class ThreadSafe {
  2. public final void method1(int loopNumber) {
  3. ArrayList<String> list = new ArrayList<>();
  4. for (int i = 0; i < loopNumber; i++) {
  5. method2(list);
  6. method3(list);
  7. }
  8. }
  9. public void method2(ArrayList<String> list) {
  10. list.add("1");
  11. }
  12. public void method3(ArrayList<String> list) {
  13. list.remove(0);
  14. }
  15. }
  16. class ThreadSafeSubClass extends ThreadSafe{
  17. @Override
  18. public void method3(ArrayList<String> list) {
  19. new Thread(() -> {
  20. list.remove(0);
  21. }).start();
  22. }
  23. }

情况1:有其它线程调用method2和method3,不会引发线程安全问题,因为直接调用method2传入的对象不是method1中的对象。
情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2和method3方法

  1. class ThreadSafe {
  2. public final void method1(int loopNumber) {
  3. ArrayList<String> list = new ArrayList<>();
  4. for (int i = 0; i < loopNumber; i++) {
  5. method2(list);
  6. method3(list);
  7. }
  8. }
  9. private void method2(ArrayList<String> list) {
  10. list.add("1");
  11. }
  12. private void method3(ArrayList<String> list) {
  13. list.remove(0);
  14. }
  15. }
  16. class ThreadSafeSubClass extends ThreadSafe{
  17. @Override
  18. public void method3(ArrayList<String> list) {
  19. new Thread(() -> {
  20. list.remove(0);
  21. }).start();
  22. }
  23. }

存在线程安全问题,list被method1中的线程和子类重写的method3的线程共享了。

2.6 常见线程安全的类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector (List的线程安全实现类)
  • Hashtable (Hash的线程安全实现类)
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
可以理解为:

  • 他们的每个方法是原子的
  • 但是他们多个方法的组合不是原子的

    2.6.1 线程安全类的方法组合

    例如:

  1. Hashtable table = new Hashtable();
  2. // 线程1,线程2
  3. if( table.get("key") == null) {
  4. table.put("key", value);
  5. }

截屏2021-04-30 上午10.21.11.png

2.6.2 不可变类的线程安全

String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了!
源码:

  1. public String substring(int beginIndex, int endIndex) {
  2. if (beginIndex < 0) {
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. }
  5. if (endIndex > value.length) {
  6. throw new StringIndexOutOfBoundsException(endIndex);
  7. }
  8. int subLen = endIndex - beginIndex;
  9. if (subLen < 0) {
  10. throw new StringIndexOutOfBoundsException(subLen);
  11. }
  12. return ((beginIndex == 0) && (endIndex == value.length)) ? this
  13. : new String(value, beginIndex, subLen); // 新建一个对象,然后返回,没有修改等操作,是线程安全的。
  14. }

2.7 实例分析

分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。

例1:

  1. public class MyServlet extends HttpServlet{
  2. Map<String, Object> map = new HashMap<>(); // 线程不安全
  3. String s1 = "..."; // 线程安全,字符串不可变
  4. final String s2 = "..."; // 线程安全
  5. Date d1 = new Date(); // 线程不安全
  6. final Date d2 = new Date(); // 线程不安全,日期内的属性时可变的
  7. public void doGet(HttpServletRequest request, HttpServletResponse response){
  8. // 使用上述变量
  9. }
  10. }

例2:

  1. public class MyServlet extends HttpServlet {
  2. // 是否安全
  3. private UserService userService = new UserServiceImpl(); // 线程不安全,count被共享
  4. public void doGet(HttpServletRequest request, HttpServletResponse response) {
  5. userService.update(...);
  6. }
  7. }
  8. public class UserServiceImpl implements UserService {
  9. // 记录调用次数
  10. private int count = 0;
  11. public void update() {
  12. //...
  13. count++;
  14. }
  15. }

例3:

  1. @Aspect
  2. @Component
  3. public class MyAspect {
  4. // 是否安全?
  5. private long start = 0L; // 线程不安全,单例的会被共享,start会被多个线程同时读写操作
  6. @Before("execution(* *(..))")
  7. public void before() {
  8. start = System.nanoTime();
  9. }
  10. @After("execution(* *(..))")
  11. public void after() {
  12. long end = System.nanoTime();
  13. System.out.println("cost time:" + (end-start));
  14. }
  15. }

例4:

  1. public class MyServlet extends HttpServlet {
  2. // 是否安全
  3. private UserService userService = new UserServiceImpl(); //线程安全,同理
  4. public void doGet(HttpServletRequest request, HttpServletResponse response) {
  5. userService.update(...);
  6. }
  7. }
  8. public class UserServiceImpl implements UserService {
  9. // 是否安全
  10. private UserDao userDao = new UserDaoImpl(); // 线程安全,虽然会被共享,但是UserDaoImpl是线程安全的(没有可更改的东西,不可变)
  11. public void update() {
  12. userDao.update();
  13. }
  14. }
  15. public class UserDaoImpl implements UserDao {// 线程安全,没有成员变量
  16. public void update() {
  17. String sql = "update user set password = ? where username = ?";
  18. // 是否安全
  19. try (Connection conn = DriverManager.getConnection("","","")){
  20. // ... //线程安全,connection是局部变量,每个线程独一份
  21. } catch (Exception e) {
  22. // ...
  23. }
  24. }
  25. }

例5:

  1. public class MyServlet extends HttpServlet {
  2. // 是否安全
  3. private UserService userService = new UserServiceImpl(); //线程不安全,同理
  4. public void doGet(HttpServletRequest request, HttpServletResponse response) {
  5. userService.update(...);
  6. }
  7. }
  8. public class UserServiceImpl implements UserService {
  9. // 是否安全
  10. private UserDao userDao = new UserDaoImpl(); // 线程不安全,UserDaoImpl不安全了
  11. public void update() {
  12. userDao.update();
  13. }
  14. }
  15. public class UserDaoImpl implements UserDao {
  16. // 线程不安全,成员变量被共享了,出现多个读写操作
  17. private Connection conn = null;
  18. public void update() throws SQLException {
  19. String sql = "update user set password = ? where username = ?";
  20. conn = DriverManager.getConnection("","","");
  21. // ...
  22. conn.close();
  23. }
  24. }

例6

  1. public class MyServlet extends HttpServlet {
  2. // 是否安全
  3. private UserService userService = new UserServiceImpl();
  4. public void doGet(HttpServletRequest request, HttpServletResponse response) {
  5. userService.update(...);
  6. }
  7. }
  8. public class UserServiceImpl implements UserService {
  9. public void update() {
  10. UserDao userDao = new UserDaoImpl();
  11. userDao.update();
  12. }
  13. }
  14. public class UserDaoImpl implements UserDao {
  15. // 是否安全 UserDao是作为局部变量存在的,所以每个对象会创建新的UserDao,独有的,所以是线程安全的
  16. private Connection = null;
  17. public void update() throws SQLException {
  18. String sql = "update user set password = ? where username = ?";
  19. conn = DriverManager.getConnection("","","");
  20. // ...
  21. conn.close();
  22. }
  23. }

3. Monitor概念

3.1 Java对象头

以 32 位虚拟机为例,普通对象的对象头结构如下,其中的 Klass Word 为指针,指向对应的 Class 对象;

  • 普通对象(64位的话markword是12字节,KClass是4字节)

image.png

  • 数组对象

image.png

  • Mark Word结构为

image.png
image.png

3.2 Monitor工作原理

Monitor 被翻译为监视器或者说管程
每个 java 对象都可以关联一个 Monitor ,如果使用 synchronized 给对象上锁(重量级),该对象头的 Mark Word 中就被设置为指向 Monitor 对象的指针。
image.png

  • 刚开始时 Monitor 中的 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj){} 代码时就会将 Monitor 的所有者Owner 设置为 Thread-2,上锁成功,Monitor 中同一时刻只能有一个 Owner
  • 当 Thread-2 占据锁时,如果线程 Thread-3 ,Thread-4 也来执行synchronized(obj){} 代码,就会进入 EntryList(阻塞队列) 中变成BLOCKED(阻塞) 状态
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

    字节码角度

  1. public class Test3 {
  2. private static Object lock = new Object();
  3. private static int counter = 0;
  4. public static void main(String[] args) {
  5. synchronized (lock){
  6. counter++;
  7. }
  8. }
  9. }
  1. 0 getstatic #2 <com/ll/ch3/Test3.lock> // lock引用
  2. 3 dup
  3. 4 astore_1
  4. 5 monitorenter // 将lock对象markword置为monitor指针
  5. 6 getstatic #3 <com/ll/ch3/Test3.counter>
  6. 9 iconst_1
  7. 10 iadd
  8. 11 putstatic #3 <com/ll/ch3/Test3.counter>
  9. 14 aload_1
  10. 15 monitorexit // 将lock对象markword重置,唤醒了EntryLIst
  11. 16 goto 24 (+8)
  12. 19 astore_2 // 异常部分
  13. 20 aload_1
  14. 21 monitorexit
  15. 22 aload_2
  16. 23 athrow
  17. 24 return

4. synchronized 原理进阶

4.1 轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。

  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized 。

假设有两个方法同步块,利用同一个对象加锁

  1. static final Object obj = new Object();
  2. public static void method1() {
  3. synchronized( obj ) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public static void method2() {
  9. synchronized( obj ) {
  10. // 同步块 B
  11. }
  12. }
  1. 每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存将来要存储对象的 Mark Word 和对象引用 reference

image.png

  1. 让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。

image.png

  1. 如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 (表示轻量级锁),如下所示。(替换成功的条件是object对象的锁状态之前是01无锁)

image.png

  1. 如果cas失败,有两种情况
    1. 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
    2. 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。

image.png

  1. 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image.png

  1. 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象

    1. 成功则解锁成功
    2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

      4.2 锁膨胀

      如果在尝试加轻量级锁的过程中,cas 操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。
  2. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image.png

  1. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,
    • 即为对象申请Monitor锁,让Object指向重量级锁地址,此时Object对象头中的锁状态标志为10(重量级锁)
    • 然后自己进入Monitor 的EntryList 变成BLOCKED状态

image.png

  1. 当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头,对象的对象头指向 Monitor,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 Thread-1 线程

[

](https://blog.csdn.net/weixin_50280576/article/details/113033975)

4.3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

  1. 自旋重试成功的情况

image.png

  1. 自旋重试失败的情况

image.png

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

    4.4 偏向锁

    在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行 CAS 操作。那java6 开始引入了偏向锁,只有第一次使用 CAS 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。以后只要不发生竞争,这个对象就归该线程所有。 ```java static final Object obj = new Object(); public static void m1() { synchronized(obj) {
    1. // 同步块 A
    2. m2();
    } } public static void m2() { synchronized(obj) {
    1. // 同步块 B
    2. m3();
    } } public static void m3() { synchronized(obj) {
    1. // 同步块 C
    } }
  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619763179405-d8b2ef6b-396a-4003-b510-963a2110d405.png#clientId=u331f982e-2d25-4&from=paste&height=789&id=u01b46b6b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1045&originWidth=990&originalType=binary&size=312535&status=done&style=none&taskId=uaa9e4fc1-d899-4963-b20c-901efbbb002&width=747)
  2. <a name="v3Ajc"></a>
  3. ### 4.4.1 偏向状态
  4. 一个对象的创建过程
  5. - 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的 Threadepochage 都是 0 ,在加锁的时候进行设置这些的值.
  6. - 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
  7. -XX:BiasedLockingStartupDelay=0 来禁用延迟
  8. - 禁用偏向锁: -XX:UseBiasedLocking。如果没有开启偏向锁,那么对象创建后,markword的最后三位置为001,这时它的hashcodeage都为0,第一次用到hashcode时才赋值
  9. - 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
  10. <a name="LNNkx"></a>
  11. ### 4.4.2 撤销偏向
  12. 以下几种情况会使对象的偏向锁失效
  13. - 调用对象的 hashCode 方法
  14. - 多个线程使用该对象
  15. - 调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁)
  16. <a name="rzkgr"></a>
  17. ### 4.4.3 批量重偏向
  18. - 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 t1 的对象仍有机会重新偏向 t2
  19. - 重偏向会重置Thread ID
  20. - 当撤销超过20次后(超过阈值,第20次开始),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至该加锁线程。
  21. <a name="S1eCW"></a>
  22. ### 4.4.4 批量撤销
  23. 当撤销偏向锁的阈值超过 40(第40次开始) 以后,就会将**整个类的剩下的对象都改为不可偏向的,新建的对象也是不可偏向的**<br />总结:
  24. - 当线程140个对象加偏向锁(101)后,存储该线程1id,线程2重新给这40个对象加锁,之后线程3再个这40个对象加锁
  25. - 线程2
  26. - 19个对象为撤销偏向,线程2加锁后由原先线程1的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);
  27. - 从第20个对象开始,批量重偏向,线程2加锁后由原先的线程1的偏向锁变为该线程的偏向锁(101),存储该线程2id,同步代码块执行完成后状态不变;
  28. - 线程3
  29. - 19个对象已经是正常状态,执行加轻量锁(00)和释放锁(001)的。(该对象偏向锁已经失效)
  30. - 从第20个对象开始,撤销偏向,线程3加锁后由原先线程2的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);
  31. - 从第40个对象开始,剩下的对象会撤销偏向,由原先线程2的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);新建的对象会进入正常状态(001)
  32. <a name="gNKiV"></a>
  33. ## 4.5 锁消除
  34. 如果该加锁的对象是一个局部变量的话(没发生逃逸不会引起线程安全问题),该锁会自动消除。加不加都一样,加了会影响性能。
  35. ```java
  36. public void b() {
  37. Object o = new Object();
  38. synchronized(o){
  39. x++;
  40. }
  41. }

该锁会自动消除,对象o是一个局部变量且没有逃逸,不会引起线程安全,锁被优化掉了。

  • 关闭锁消除:-XX:-EliminateLocks