多线程(五):解决线程不安全方案

线程不安全原因 解决方案
①CPU抢占 执行(万恶之源) 无法解决
②代码非原子性 在关键代码处,让使用的CPU排队执行(加锁)
③(内存)不可见 可使用 volatile 关键字
④编译器/代码优化(指令重排序) 可使用 volatile 关键字
⑤多个线程同时修改了同一个变量 不通用,修改难度大

volatile关键字

volatile 关键字 轻量级解决线程不安全的方案
代码示例如下:

public class ThreadDemo29 {     private static volatile boolean flag = false;      public static void main(String[] args) {         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 while (!flag) {                  }                 System.out.println("终止执行");             }         });         t1.start();          Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 try {                     Thread.sleep(1000);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 System.out.println("设置flag = true");                 flag = true;             }         });         t2.start();     } } 

该代码执行结果为:
多线程(五):解决线程不安全方案

我们发现,此代码与上篇博客中的ThreadDemo27的代码基本相同,就是在就是在定义全局变量 flag 时,添加了 volatile 关键字,通过解决内存不可见的方法,解决了线程不安全的问题。

volatile 作用:
①禁止指令重排序
②解决线程可见性的问题,实现原理:当操作完变量之后,强制删除掉线程工作内存中的此变量。

注意:
volatile 关键字,无法解决多线程非原子性问题。

public class ThreadDemo30 {      static class Counter {         //定义私有变量         private volatile int num = 0;         //定义任务执行次数         private final int maxSize = 100000;          //num++         public void incrment() {             for (int i = 0; i < maxSize; i++) {                 num++;             }         }          //num--         public void decrment() {             for (int i = 0; i < maxSize; i++) {                 num--;             }         }          public int getNum() {             return num;         }     }      public static void main(String[] args) throws InterruptedException {         Counter counter = new Counter();          Thread t1 = new Thread(() -> {             counter.incrment();         });         t1.start();          Thread t2 = new Thread(() -> {             counter.decrment();         });         t2.start();                  t1.join();         t2.join();          System.out.println("最终执行结果:" + counter.getNum());     } } 

代码执行结果:
多线程(五):解决线程不安全方案
可见,volatile 关键字,无法解决多线程非原子性问题,进而无法解决线程非安全。

锁操作

Java中解决线程安全操作(锁的操作)

1.使用 public class ThreadDemo31 { //全局变量 private static int number = 0; //定义循环次数 private static final int maxSize = 100000; public static void main(String[] args) throws InterruptedException { //创建锁 Object lock = new Object(); //线程1:自增10W次 Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < maxSize; i++) { //实现加锁操作 synchronized (lock) { number++; } } } }); t1.start(); //线程2:自减10W次 Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < maxSize; i++) { synchronized (lock) { number--; } } } }); t2.start(); //等待线程1和线程2执行完毕 t1.join(); t2.join(); System.out.println("最终执行结果:" + number); } }

注意事项:在进行加锁操作的时候,同一组业务必须为共同的锁对象。
该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现,此程序就是线程安全的。
多线程(五):解决线程不安全方案
synchronized 实现原理:
1.操作:互斥锁 mutex
多线程(五):解决线程不安全方案

2.JVM:帮我们实现的监视器锁的加锁和释放锁的操作
多线程(五):解决线程不安全方案

3.Java:
a) 锁对象 mutex
b) 锁存放的地方:变量的对象头
多线程(五):解决线程不安全方案

synchronized 在 JDK 6 之前,使用重量级锁实现的,性能非常低,所以用到的并不多。
JDK 6 对 synchronized 做了优化(锁升级 )
多线程(五):解决线程不安全方案

synchronized 的使用场景:
1.使用 synchronized 来修饰代码块 (加锁对象可以自定义)
上述 ThreadDemo31 就是 synchronized 来修饰代码块的使用场景

2.使用 synchronized 来修饰静态方法,示例如下:

public class ThreadDemo35 {     //全局变量     private static int number = 0;     //定义循环次数     private static final int maxSize = 100000;      public static synchronized void incrment() {         for (int i = 0; i < maxSize; i++) {             number++;         }     }      public static synchronized void decrment() {         for (int i = 0; i < maxSize; i++) {             number--;         }     }      public static void main(String[] args) throws InterruptedException {          //线程1:自增10W次         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 incrment();             }         });         t1.start();          //线程2:自减10W次         Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 decrment();             }         });         t2.start();          //等待线程1和线程2执行完毕         t1.join();         t2.join();          System.out.println("最终执行结果:" + number);      } } 

该代码的执行结果:
多线程(五):解决线程不安全方案
我们发现,使用synchronized 来修饰静态方法,也能使该程序线程安全。

3.使用 synchronized 可以用来修饰普通方法(加锁对象是当前类的实例),示例如下:

public class ThreadDemo35 {     //全局变量     private static int number = 0;     //定义循环次数     private static final int maxSize = 100000;      //    public static synchronized void incrment() {     public synchronized void incrment() {         for (int i = 0; i < maxSize; i++) {             number++;         }     }      //    public static synchronized void decrment() {     public synchronized void decrment() {         for (int i = 0; i < maxSize; i++) {             number--;         }     }      public static void main(String[] args) throws InterruptedException {          ThreadDemo35 threadDemo35 = new ThreadDemo35();          //线程1:自增10W次         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 threadDemo35.incrment();             }         });         t1.start();          //线程2:自减10W次         Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 threadDemo35.decrment();             }         });         t2.start();          //等待线程1和线程2执行完毕         t1.join();         t2.join();          System.out.println("最终执行结果:" + number);      } } 

该代码的执行结果:
多线程(五):解决线程不安全方案
我们发现,使用synchronized 来修饰普通方法,也能使该程序线程安全。

Lock 的使用

Lock 的使用,示例代码如下:

public class ThreadDemo32 {     //全局变量     private static int number = 0;     //定义循环次数     private static final int maxSize = 100000;      public static void main(String[] args) throws InterruptedException {          //1.创建手动锁         Lock lock = new ReentrantLock();          //线程1:自增10W次         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 for (int i = 0; i < maxSize; i++) {                     //2.加锁                     lock.lock();                     try {                         number++;                     } finally {                         //3.释放锁                         lock.unlock();                     }                 }             }         });         t1.start();          //线程2:自减10W次         Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 for (int i = 0; i < maxSize; i++) {                     lock.lock();                     try {                         number--;                     } finally {                         lock.unlock();                     }                 }             }         });         t2.start();          //等待线程1和线程2执行完毕         t1.join();         t2.join();          System.out.println("最终执行结果:" + number);      } } 

注意事项:一定要把 lock( ) 放在 try 外面,原因如下:
1.如果将1ock()方法放在 try 里面,那么当 try 里面的代码出现异常之后,那么就会执行 finally 里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。
2.如果将 lock( ) 方法放在try里面,那么当执行 finally 里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。

演示:将 lock( ) 方法放入 try 里面,示例代码如下:

public class ThreadDemo33 {     public static void main(String[] args) {          Lock lock = new ReentrantLock();          try {             int num = 1 / 0;//异常业务             lock.lock();         } finally {             lock.unlock();         }     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现,该程序执行时会报错,且异常类型为锁操作异常,并非业务异常信息。
如果将lock()方法放在try的外面,示例代码如下:

public class ThreadDemo33 {     public static void main(String[] args) {          Lock lock = new ReentrantLock();          lock.lock();         try {             int num = 1 / 0;//异常业务         } finally {             lock.unlock();         }     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现:异常类型为我们预期的业务异常类型。

Lock 声明公平锁示例代码如下:

public class ThreadDemo34 {     public static void main(String[] args) throws InterruptedException {          //声明一个公平锁         Lock lock = new ReentrantLock(true);          //业务逻辑处理:打印AABBCCDD         //公共任务         Runnable runnable = new Runnable() {             @Override             public void run() {                 for (char item : "ABCD".toCharArray()) {                     lock.lock();                     try {                         System.out.print(item);                     } finally {                         lock.unlock();                     }                  }             }         };          Thread t1 = new Thread(runnable, "t1");         Thread t2 = new Thread(runnable, "t1");          Thread.sleep(10);         t1.start();         t2.start();      } } 

该代码执行结果为:
多线程(五):解决线程不安全方案
Lock 的使用场景:只能用来修饰代码块。

小结

volatile 和 synchronized 有什么区别?

  1. volatile 可以解决内存可见性问题和禁止指令重排序,但 volatile 不能解决原子性问题;
  2. synchronized 可以解决任何关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作;原子性问题…)

synchronized 和 Lock 有什么区别?

  1. synchronized 既可以修饰代码块,又可以修饰静态方法和普通方法;而 Lock 只能修饰代码块。
  2. synchronized 只有非公平锁的锁策略,而 Lock(ReentrantLock) 默认也是非公平锁策略,也可以通过构造函数声明成公平锁。
  3. 使用 Lock(ReentrantLock) 更加灵活(比如 tryLock )。
  4. synchronized 是自动加锁释放锁的,而 Lock(ReentrantLock) 需要程序员来加锁和手动释放锁的。

死锁问题

基本概念

线程和锁的关系(一对多):一个线程可以拥有多把锁;但是一个锁只能被一个线程拥有。

定义:在多线程编程中(两个或两个以上的线程),因为资源抢占,造成线程无限等待的问题。
多线程(五):解决线程不安全方案
死锁问题,示例代码:

public class ThreadDemo36 {     public static void main(String[] args) {         //创建锁A(资源A)         Object lockA = new Object();         //创建锁B(资源B)         Object lockB = new Object();          Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 //线程1得到锁A                 synchronized (lockA) {                     System.out.println("线程t1得到了锁A");                     try {                         //休眠1S,让线程2先得到锁B                         Thread.sleep(1000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     System.out.println("线程1等待获取锁B");                     //线程1尝试获取锁B                     synchronized (lockB) {                         System.out.println("线程1:得到了锁B");                     }                 }             }         }, "t1");         t1.start();          Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 //线程2得到锁B                 synchronized (lockB) {                     System.out.println("线程t2得到了锁B");                     try {                         //休眠1S,让线程1先得到锁A                         Thread.sleep(1000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     System.out.println("线程2等待获取锁A");                     //线程2尝试获取锁A                     synchronized (lockA) {                         System.out.println("线程2:得到了锁A");                     }                 }             }         }, "t2");         t2.start();     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现,线程进入的无限等待状态从而无法使程序执行完毕。

排查死锁工具

1.使用 Java监控和管理控制台(jconsole)可以检测出,该线程出现了死锁问题。多线程(五):解决线程不安全方案

2.使用 jvisualvm 工具可以检测出,该线程出现了死锁问题。
多线程(五):解决线程不安全方案
多线程(五):解决线程不安全方案

3.使用 jmc 工具可以检测出,该线程出现了死锁问题。
多线程(五):解决线程不安全方案

死锁程序的三个关键点

1.获得锁A操作
2.线程休眠
3.获得锁B操作
多线程(五):解决线程不安全方案

死锁操作的四个条件(同时满足)

  1. 互斥条件:一个资源只能被一个线程持有,当被一个线程持有之后就不能被其他线程持有。
  2. 请求拥有条件:一个线程持有了一个资源之后,又试图请求另一个资源。
  3. 不可剥夺条件:一个资源被一个线程拥有之后,如果这个线程不释放此资源,其他线程不能尝试获得此资源。
  4. 环路等待条件:多个线程在获取资源时,形成了环形链。

解决死锁问题

上面造成死锁的四个条件中,互斥条件与不可剥夺条件无法修改,只能从请求拥有条件和环路等待条件入手。
从以下条件入手,修改任意一个条件即可:
1.请求拥有条件
2.环路等待条件
其中,最容易实现的方法就是修改环路等待条件。

我们可以修改 控制请求锁的有序性,如图:
多线程(五):解决线程不安全方案

通过修改 控制请求锁的有序性 即让线程1和线程2都先请求锁A,再让线程1和线程2再去请求锁B。
我们修改 ThreadDemo36 的部分代码,即可解决死锁问题,代码如下:

public class ThreadDemo37 {     public static void main(String[] args) {         //创建锁A(资源A)         Object lockA = new Object();         //创建锁B(资源B)         Object lockB = new Object();          Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 //线程1得到锁A                 synchronized (lockA) {                     System.out.println("线程t1得到了锁A");                     try {                         //休眠1S,让线程2先得到锁B                         Thread.sleep(1000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     System.out.println("线程1等待获取锁B");                     //线程1尝试获取锁B                     synchronized (lockB) {                         System.out.println("线程1:得到了锁B");                     }                 }             }         }, "t1");         t1.start();          Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 //线程2得到锁B                 synchronized (lockA) {                     System.out.println("线程t2得到了锁A");                     try {                         //休眠1S,让线程1先得到锁A                         Thread.sleep(1000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     System.out.println("线程2等待获取锁B");                     //线程2尝试获取锁A                     synchronized (lockB) {                         System.out.println("线程2:得到了锁B");                     }                 }             }         }, "t2");         t2.start();     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现,死锁问题得到解决。

线程等待

基本概念

之前在学习线程休眠 Thread.sleep() 的时候,这个方法有一个弊端:必须有明确的结束时间,在休眠期间无法唤醒。为了解决这个问题,Java提供了 wait(休眠)/ notify(唤醒)/ notifyall(唤醒全部) 机制
线程通讯机制:一个线程的动作可以让另一个线程感知到就叫做线程通讯。

示例代码如下:

public class ThreadDemo38 {     public static void main(String[] args) throws InterruptedException {          Object lock = new Object();          Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程方法。");                 synchronized (lock) {                     //wait 的使用                     try {                         //线程等待                         lock.wait();                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                 }                 System.out.println("线程1:执行完成");             }         }, "t1");         t1.start();          Thread.sleep(1000);         System.out.println("唤醒线程1");         synchronized (lock) {             //唤醒线程             lock.notify();         }     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案

wait 为什么要加锁:
wait 在使用的时候,必须要释放锁,在释放锁之前,必须要有一把锁,所以要加锁。
wait 为什么要释放锁:
wait 默认是不传任何值的,当不传递任何值的时候,表示永久等待,这样就会造成一把锁被一个线程一直持有,为了这个问题的发生,所以在使用 wait 时,一定要释放锁。

wait / notify /notifyAll 使用注意事项

  1. 使用以上方法时,必须要加锁。
  2. 加锁对象和 wait / notify /notifyAll 的对象必须保持一致。
  3. 一组wait 和 notify /notify 必须是同一个对象。
  4. notifyAll 只能唤醒当前对象的所有等待线程。

多线程(五):解决线程不安全方案
wait 传参示例代码如下:

public class ThreadDemo39 {     public static void main(String[] args) throws InterruptedException {          Object lock = new Object();          Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程" + new Date());                 synchronized (lock) {                     //wait 的使用                     try {                         //线程等待                         lock.wait(3000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                 }                 System.out.println("线程1:执行完成" + new Date());             }         }, "t1");         t1.start();          Thread.sleep(2000); //        System.out.println("唤醒线程1"); //        synchronized (lock) {         //唤醒线程 //            lock.notify(); //        }     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现该线程等待3秒会自动唤醒。

notifyAll的使用,示例代码如下:

public class ThreadDemo39 {     public static void main(String[] args) throws InterruptedException {          Object lock = new Object();          Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程");                 synchronized (lock) {                     //wait 的使用                     try {                         //线程等待                         lock.wait(0);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                 }                 System.out.println("线程1:执行完成");             }         }, "t1");         t1.start();          Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程2:进入线程");                 synchronized (lock) {                     try {                         lock.wait();                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                 }                 System.out.println("线程2:执行完成");             }         }, "t2");         t2.start();          Thread.sleep(2000);         System.out.println("唤醒线程1和线程2");         synchronized (lock) {             //唤醒线程             lock.notifyAll();         }     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案

Thread.sleep(0)Object.wait(0) 的区别:
1.sleep 是 Thread 的静态方法;而 lock 是Object 的方法。
2.sleep(0) 立即触发一次CPU资源的抢占;而 lock(0) 会让线程永久等待下去。

wait 和 sleep 的异同

相同点:

  1. 两者都可以使当前的线程休眠。
  2. 两者都要处理一个 Interrupt 的异常。

不同点:

  1. wait 来自于Object 中的一个方法;而 sleep 来自于 Thread 中的一个静态方法。
  2. 传递参数不同:wait 可以没有参数;而 sleep 必须有一个大于等于0的参数。
  3. wait 使用时,必须加锁;而 sleep 使用时,不用加锁。
  4. wait 使用时,会释放锁;而 sleep 使用时,不会释放锁。
  5. 不传参的情况下 wait 会进入WAITING 状态;而 sleep 会进入TIMED_WAITING 状态。

为什么 wait 释放锁;而 sleep 不释放锁?
答:wait 默认等待无限期。

为什么 wait 要放在 Object 中而不是 Thread 中?
答:wait 操作必须要加锁和释放锁,而锁属于对象级别,而非线程级别(线程和锁是一对多的关系,也就是一个线程可以有多把锁),为了灵活起见(一个线程会有多把锁),就把 wait 放在了 Object 中。

LockSupport 的使用

LockSupport.park();同样会使线程进入 WAITING 状态,示例代码如下:

public class ThreadDemo42 {     public static void main(String[] args) throws InterruptedException {         Object lock = new Object();         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程");                 //线程休眠                 LockSupport.park();                 System.out.println("线程1:执行完成");             }         }, "t1");         t1.start();     } } 

代码执行时,使用 jconsole 工具可以观察线程状态,如下:
多线程(五):解决线程不安全方案

使用LockSupport.unpark(线程名);唤醒线程,示例代码如下:

public class ThreadDemo42 {     public static void main(String[] args) throws InterruptedException {         Object lock = new Object();         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程");                 //线程休眠                 LockSupport.park();                 System.out.println("线程1:执行完成");             }         }, "t1");         t1.start();         Thread.sleep(1000);         System.out.println("唤醒线程");         LockSupport.unpark(t1);     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案

使用LockSupport.unpark(线程名);唤醒线程,可以指定唤醒线程顺序,示例代码如下:

public class ThreadDemo42 {     public static void main(String[] args) throws InterruptedException {         Object lock = new Object();         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程1:进入线程");                 //线程休眠                 LockSupport.park();                 System.out.println("线程1:执行完成");             }         }, "t1");           Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程2:进入线程");                 //线程休眠                 LockSupport.park();                 System.out.println("线程2:执行完成");             }         }, "t2");           Thread t3 = new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程3:进入线程");                 //线程休眠                 LockSupport.park();                 System.out.println("线程3:执行完成");             }         }, "t3");          t1.start();         t2.start();         t3.start();          Thread.sleep(1000);         System.out.println("唤醒线程");         LockSupport.unpark(t1);         LockSupport.unpark(t2);         LockSupport.unpark(t3);     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
使用LockSupport.park(参数);传参,示例代码如下:

import java.util.Date; import java.util.concurrent.locks.LockSupport;  public class ThreadDemo44 {     public static void main(String[] args) {          new Thread(new Runnable() {             @Override             public void run() {                 System.out.println("线程进入休眠:" + new Date());                 LockSupport.parkUntil(System.currentTimeMillis() + 1000);                 System.out.println("线程终止休眠:" + new Date());             }         }).start();     } } 

该代码的执行结果如下:
多线程(五):解决线程不安全方案
我们发现,使用LockSupport.park(参数);传参,线程是可以自动唤醒的。

wait 和 LockSupport 异同

相同点:
1.两者都可以使线程休眠。
2.两者都可以无参或者传递参数,并且两者的线程状态也是一致的。

不同点:
1.wait 必须要配合 synchronized 一起使用(必须加锁),而 wait LockSupport 不许加锁。
2.wait 只能唤醒全部或随机的一个线程,而 LockSupport 可以按顺序唤醒指定线程。

版权声明:玥玥 发表于 2021-05-18 12:32:13。
转载请注明:多线程(五):解决线程不安全方案 | 女黑客导航