当前位置:   article > 正文

java---线程安全详解_java线程安全

java线程安全

目录

前言

一、线程不安全产生的原因

1.多个线程同时修改一个变量

2.非原子性操作

3.内存可见性问题

4.指令重排序问题

 二、线程安全的解决

1.加锁排队执行

1. 同步锁synchronized

2.可重入锁ReentrantLock

2.原子类AtomicInteger

总结


前言

线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。

一、线程不安全产生的原因

1.多个线程同时修改一个变量

如果是多线程同时修改不同的变量(每个线程只修改自己的变量),也是不会出现非线程安全的问题了,比如以下代码,线程 1 修改 number1 变量,而线程 2 修改 number2 变量,最终两个线程执行完之后的结果如下:

  1. public class test2{
  2. // 全局变量
  3. private static int number = 0;
  4. // 循环次数(100W)
  5. private static final int COUNT = 1_000_000;
  6. // 线程 1 操作的变量 number1
  7. private static int number1 = 0;
  8. // 线程 2 操作的变量 number2
  9. private static int number2 = 0;
  10. public static void main(String[] args) throws InterruptedException {
  11. // 线程1:执行 100W 次 number+1 操作
  12. Thread t1 = new Thread(() -> {
  13. for (int i = 0; i < COUNT; i++) {
  14. number1++;
  15. }
  16. });
  17. t1.start();
  18. // 线程2:执行 100W 次 number-1 操作
  19. Thread t2 = new Thread(() -> {
  20. for (int i = 0; i < COUNT; i++) {
  21. number2--;
  22. }
  23. });
  24. t2.start();
  25. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  26. t1.join();
  27. t2.join();
  28. number = number1 + number2;
  29. System.out.println("number=number1+number2 最终结果:" + number);
  30. }
  31. }

 程序的运行结果:

 从上述结果可以看出,多线程只要不是同时修改同一个变量,也不会出现线程安全问题

2.非原子性操作

原子性操作是指操作不能再被分隔就叫原子性操作。首先,要明确java代码中的一条语句可能代表多个指令,例如 r++ 、r--都是对应多条指令,所以在这里他们的原子性没有得到保证。

以上就是一个经典的错误,r 原本等于 1,线程 1 进行 -1 操作,而线程 2 进行加 1,最终的结果 n,r 应该还等于 1 才对,但通过上面的执行,number 最终被修改成了 0,这就是非原子性导致的问题。

3.内存可见性问题

在 Java 编程中内存分为两种类型:工作内存和主内存,而工作内存使用的是 CPU 寄存器实现的,而主内存是指电脑中的内存,我们知道 CPU 寄存器的操作速度是远大于内存的操作速度的,它们的性能差异如下图所示:

 在 Java 语言中,为了提高程序的执行速度,所以在操作变量时,会将变量从主内存中复制一份到工作内存,而主内存是所有线程共用的,工作内存是每个线程私有的,这就会导致一个线程已经把主内存中的公共变量修改了,而另一个线程不知道,依旧使用自己工作内存中的变量,这样就导致了问题的产生,也就导致了线程安全问题。

4.指令重排序问题

例如在实例化对象的时候:

SomeObeject  so = new SomeObject();

上述代码可以分为三步:

1.根据类计算对象的大小:在堆内存中分配内存空间给该对象。

2.对对象进行初始化(构造代码块、构造方法)。

3.把引用交给 so。

但是由于代码重排序的原因,将本来的顺序变为 1 -> 3 ->2 ;假设在执行到 3的时候,发生了线程调度,假如此时新来的线程需要 so对象,所以就使用了,但是,因为代码重排序的问题,上一个线程还没有进行对象的初始化,所以导致发生错误。

 二、线程安全的解决

1.加锁排队执行

Java 中有两种锁:synchronized 同步锁和 ReentrantLock 可重入锁。

1. 同步锁synchronized

synchronized 是 JVM 层面实现的自动加锁和自动释放锁的同步锁,它的实现代码如下:

  1. public class Test2 {
  2. // 全局变量
  3. private static int number = 0;
  4. // 循环次数(100W)
  5. private static final int COUNT = 1_000_000;
  6. public static void main(String[] args) throws InterruptedException {
  7. // 线程1:执行 100W 次 ++ 操作
  8. Thread t1 = new Thread(() -> {
  9. for (int i = 0; i < COUNT; i++) {
  10. // 加锁排队执行
  11. synchronized (Test2.class) {
  12. number++;
  13. }
  14. }
  15. });
  16. t1.start();
  17. // 线程2:执行 100W 次 -- 操作
  18. Thread t2 = new Thread(() -> {
  19. for (int i = 0; i < COUNT; i++) {
  20. // 加锁排队执行
  21. synchronized (Test2.class) {
  22. number--;
  23. }
  24. }
  25. });
  26. t2.start();
  27. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  28. t1.join();
  29. t2.join();
  30. System.out.println("number 最终结果:" + number);
  31. }
  32. }

 运行结果如下:

2.可重入锁ReentrantLock

ReentrantLock 可重入锁需要程序员自己加锁和释放锁,它的实现代码如下: 

  1. public class Test2{
  2. // 全局变量
  3. private static int number = 0;
  4. // 循环次数(100W)
  5. private static final int COUNT = 1_000_000;
  6. // 创建 ReentrantLock
  7. private static ReentrantLock lock = new ReentrantLock();
  8. public static void main(String[] args) throws InterruptedException {
  9. // 线程1:执行 100W 次 ++ 操作
  10. Thread t1 = new Thread(() -> {
  11. for (int i = 0; i < COUNT; i++) {
  12. lock.lock(); // 手动加锁
  13. number++; // ++ 操作
  14. lock.unlock(); // 手动释放锁
  15. }
  16. });
  17. t1.start();
  18. // 线程2:执行 100W 次 -- 操作
  19. Thread t2 = new Thread(() -> {
  20. for (int i = 0; i < COUNT; i++) {
  21. lock.lock(); // 手动加锁
  22. number--; // -- 操作
  23. lock.unlock(); // 手动释放锁
  24. }
  25. });
  26. t2.start();
  27. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  28. t1.join();
  29. t2.join();
  30. System.out.println("number 最终结果:" + number);
  31. }
  32. }

 程序运行结果如下:

 

2.原子类AtomicInteger

AtomicInteger 是线程安全的类,使用它可以将 ++ 操作和 -- 操作,变成一个原子性操作,这样就能解决非线程安全的问题了,如下代码所示:

  1. public class Test2 {
  2. // 创建 AtomicInteger
  3. private static AtomicInteger number = new AtomicInteger(0);
  4. // 循环次数
  5. private static final int COUNT = 1_000_000;
  6. public static void main(String[] args) throws InterruptedException {
  7. // 线程1:执行 100W 次 ++ 操作
  8. Thread t1 = new Thread(() -> {
  9. for (int i = 0; i < COUNT; i++) {
  10. // ++ 操作
  11. number.incrementAndGet();
  12. }
  13. });
  14. t1.start();
  15. // 线程2:执行 100W 次 -- 操作
  16. Thread t2 = new Thread(() -> {
  17. for (int i = 0; i < COUNT; i++) {
  18. // -- 操作
  19. number.decrementAndGet();
  20. }
  21. });
  22. t2.start();
  23. // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
  24. t1.join();
  25. t2.join();
  26. System.out.println("number 最终结果:" + number.get());
  27. }
  28. }

程序的运行结果:


 

总结

加油偶~~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/650113
推荐阅读
相关标签
  

闽ICP备14008679号