IT评测·应用市场-qidao123.com技术社区

标题: 多线程初阶(二)- 线程安全标题 [打印本页]

作者: 徐锦洪    时间: 2024-7-24 18:02
标题: 多线程初阶(二)- 线程安全标题
目录
  1.观察count++
   原因总结
   2.办理方案-synchronized关键字
  (1)synchronized的特性
  (2)怎样精确使用
  语法格式
  3.死锁
  (1)造成死锁的情况
  (2)死锁的四个必要条件
  4.Java标准库中的线程安全类
  5.volatile关键字
  (1)内存可见性标题
  原因 
  办理方案 
  (2)不办理原子性标题
  6.wait和notify 
  (1)wait()
  (2)notify()
  (3)线程饿死标题 
  7.wait和sleep的对比(口试题)
  
  
  1.观察count++

     我们观察以下代码:
  
  1. public class Demo20 {
  2.     private static int count = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1 = new Thread(() ->{
  5.             for (int i = 0; i < 50000; i++) {
  6.                 count++;
  7.             }
  8.         });
  9.         Thread t2 = new Thread(() ->{
  10.             for (int i = 0; i < 50000; i++) {
  11.                 count++;
  12.             }
  13.         });
  14.         t1.start();
  15.         t2.start();
  16.         t1.join();
  17.         t2.join();
  18.         System.out.println(count);
  19.     }
  20. }
复制代码
  他的逻辑是将count在差别的线程下进行五万次++操作,抱负的结果是100000,但由于是并发实验,结果并不能到达预期,每次的结果都不雷同,因为多个线程并发实验,引起的bug 
这样的bug称为“"线程安全标题"或者叫做"线程不安全"
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是 线程安全的。
   

       我们从cpu的视角来观察count++操作,它是由3个指令的:
      由于CPU是随即调度,抢占式先行所以在调度线程的时间不知道什么时间会切换线程
指令是cpu实验的最基本单位,要调度,至少把当前实验完,不会实验一半调度走,所以当针对一条指令的时间就不会出现安全性标题;但是由于count++是三个指令,大概会出现cpu 实验了此中的1个指令或者2个指令或者3个指令调度走的情况,这样就会出现线程安全标题产生bug。

   
无bug的情况:
   

   有bug的情况(出现了覆盖的状态):
   

        原因总结

      
原子性:原子是不可分割的最小单位,cpu视角不可分割的最小单位就是一条指令,cpu在进行调度切换线程的时间势必会确保实验完一条指令才能调度走再实验下一条下令,所以像count++, +=,-=之类的操作都不具备原子性  赋值操作a=b是具备原子性的
     2.办理方案-synchronized关键字

     针对原因一我们无法干预,操作系统内核,负责的工作,咱们作为应用层的程序员,无法干预

针对原因二取决于现实的需求.有的场景能这么改,有的场景不能这么改取决于现实的需求
在Java中这个方案不算很普适的方案.

针对原因三我们重点进行探讨,该操作不是原子的那怎么可以变成原子的呢
 
   进行加锁操作,想象一个上厕所的场景,你对门进行了加锁,这样别人就不能进来,只有当你上完厕所出来才算解锁
   留意:此处的加锁操作并非是将count++操作变成原子的,也没有干预到线程的调度,只是通过这种加锁的方式来保证一个线程在实验count++操作的过程中其他线程的count++不能插队进来
    (1)synchronized的特性

         (2)怎样精确使用

     
  
  1. synchronized() {
  2. }
复制代码
  
synchronized不是函数而是关键字,括号内也不是参数,而是用来指定一个锁对象(可以指定任何对象),通过锁对象来进行后续的判定
   {}里面的代码,就是要打包到一起的代码~~
{}还可以放任意的其他代码,包括调用别的方法等正当的java代码
进入代码块就会进行加锁,出代码块就会进行解锁
  
  1. public class Demo21 {
  2.     private static int count = 0;
  3.     private static Object locker = new Object();
  4.     public static void main(String[] args) throws InterruptedException {
  5.         Thread t1 = new Thread(() ->{
  6.             for (int i = 0; i < 50000; i++) {
  7.                 synchronized (locker){
  8.                     count++;
  9.                 }
  10.             }
  11.         });
  12.         Thread t2 = new Thread(() ->{
  13.             for (int i = 0; i < 50000; i++) {
  14.                 synchronized (locker){
  15.                     count++;
  16.                 }
  17.             }
  18.         });
  19.         t1.start();
  20.         t2.start();
  21.         t1.join();
  22.         t2.join();
  23.         System.out.println(count);
  24.     }
复制代码
  代码表明:t1,t2针对同一个对象locker进行加锁,t1先进行加锁,实验代码块中的代码,此时t2进行等候,t1实验完毕后,t2进行加锁再实验该线程下的代码
(这两者的++操作,不会穿插实验了,也就不会相互覆盖掉对方的结果了)
本质上是把随机的并发实验过程,强制变成了串行,从而办理了刚才的线程安全标题
上述操作能够精确实验的原因是,两个线程都加锁了,并且针对的是同一个对象加锁了

以下两种情况就不能精确实验
      以上情况为两个线程针对同一个对象加锁,当第一个线程解锁之后就会实验第二个线程进行加锁;如果是三个线程针对同一个对象加锁,当某个线程先加上锁,另外两个线程开始壅闭等候,此时这两个线程谁先拿到锁是无法预期的,但不存在线程安全标题

多个线程针对同一个对象加锁(大于2)
  
  1. public class Demo21 {
  2.     private static int count = 0;
  3.     private static Object locker1 = new Object();
  4.     private static Object locker2 = new Object();
  5.     public static void main(String[] args) throws InterruptedException {
  6.         Thread t1 = new Thread(() ->{
  7.             for (int i = 0; i < 50000; i++) {
  8.                 synchronized (locker1){
  9.                     count++;
  10.                 }
  11.             }
  12.         });
  13.         Thread t2 = new Thread(() ->{
  14.             for (int i = 0; i < 50000; i++) {
  15.                 synchronized (locker2){
  16.                     count++;
  17.                 }
  18.             }
  19.         });
  20.         Thread t3 = new Thread(() ->{
  21.             for (int i = 0; i < 50000; i++) {
  22.                 synchronized (locker2){
  23.                     count++;
  24.                 }
  25.             }
  26.         });
  27.         t1.start();
  28.         t2.start();
  29.         t3.start();
  30.         t1.join();
  31.         t2.join();
  32.         t3.join();
  33.         System.out.println(count);
  34.     }
复制代码
  
   锁对象的作用:用来区分多个线程是否针对“同一个对象”加锁,
是同一个就会发生“壅闭”(锁竞争/锁冲突)
不是同一个对象就不会发生壅闭,两个线程仍旧是随即调度的并发实验
       留意事项: 
synchronized关键字本质上比join的串行实验,效率照旧要高的
join的串行化是针对线程与线程之间,而synchronized关键字是针对线程中的一小部分逻辑进行加锁来实现串行化
    语法格式

     修饰类对象
在编写Java代码,自己是.java文件,通过javac编译成.class文件,jvm运行的时间把.class文件加载到内存中进而形成对应的类对象
   
   一个 java进程中一个类的类对象只有唯一一个
  
  1. private static int count = 0;
  2.     public static void main(String[] args) throws InterruptedException {
  3.         Thread t1 = new Thread(() ->{
  4.             for (int i = 0; i < 50000; i++) {
  5.                 synchronized (Demo21.class){
  6.                     count++;
  7.                 }
  8.             }
  9.         });
  10.         Thread t2 = new Thread(() ->{
  11.             for (int i = 0; i < 50000; i++) {
  12.                 synchronized (Demo21.class){
  13.                     count++;
  14.                 }
  15.             }
  16.         });
  17.         t1.start();
  18.         t2.start();
  19.         t1.join();
  20.         t2.join();
  21.         System.out.println(count);
  22.     }
复制代码
  
修饰普通方法
  
  1. class Counter {
  2.     public int count = 0;
  3.     public synchronized void add(){
  4.         count++;
  5.     }
  6. }
  7. class Counter {
  8.     public int count = 0;
  9.     public void add(){
  10.         synchronized (this) {
  11.             count++;
  12.         }
  13.     }
  14. }
  15. public class Demo23 {
  16.     public static void main(String[] args) throws InterruptedException {
  17.         Counter counter = new Counter1();
  18.       
  19.         Thread t1 = new Thread(() -> {
  20.             for (int i = 0; i < 50000; i++) {
  21.                 counter1.add();
  22.             }
  23.         });
  24.         Thread t2 = new Thread(() -> {
  25.             for (int i = 0; i < 50000; i++) {
  26.                 counter1.add();
  27.             }
  28.         });
  29.         t1.start();
  30.         t2.start();
  31.         t1.join();
  32.         t2.join();
  33.         System.out.println(Counter.count);
  34.     }
  35. }
复制代码
  
   修饰静态方法
  
  1. class Counter {
  2.     public static int count = 0;
  3.     public synchronized static void add(){
  4.         count++;
  5.     }
  6.    
  7. }
  8. public class Demo22 {
  9.     public static int count = 0;
  10.     public static void main(String[] args) throws InterruptedException {
  11.         Thread t1 = new Thread(() ->{
  12.             for (int i = 0; i < 50000; i++) {
  13.                 synchronized (Counter.class){
  14.                     count++;
  15.                 }
  16.             }
  17.         });
  18.         Thread t2 = new Thread(() ->{
  19.             for (int i = 0; i < 50000; i++) {
  20.                 synchronized (Counter.class){
  21.                     count++;
  22.                 }
  23.             }
  24.         });
  25.         t1.start();
  26.         t2.start();
  27.         t1.join();
  28.         t2.join();
  29.         System.out.println(count);
  30.     }
  31. }
复制代码
  
    3.死锁

  (1)造成死锁的情况

         (2)死锁的四个必要条件

        以上两点对于synchronized这样的锁,互斥和不可抢占都是基本特性,我们无法进行干预
 
      如果在获取多把锁的时间,不要构成循环等候,就可以了~一~
假设代码按照请求和保持的方式,获取到N个锁,怎样避免出现循环等候呢??一个简单有效的办法:给锁编号,1,2,3....N
约定所有的线程在加锁的时间,都必须按照肯定的顺序来加锁.(比如,必须先针对编号小的锁,加锁,后针对编号大的锁加锁)
  
  1. public class Demo24 {
  2.     private static Object locker1 = new Object();
  3.     private static Object locker2 = new Object();
  4.     public static void main(String[] args) throws InterruptedException {
  5.         Thread t1 = new Thread(() ->{
  6.             synchronized (locker1){
  7.                 System.out.println("t1加锁成功locker1");
  8.                 try {
  9.                     Thread.sleep(1000);
  10.                 } catch (InterruptedException e) {
  11.                     e.printStackTrace();
  12.                 }
  13.                 synchronized (locker2){
  14.                     System.out.println("t1加锁成功locker2");
  15.                 }
  16.             }
  17.         });
  18.         Thread t2 = new Thread(() ->{
  19.             synchronized (locker1){
  20.                 System.out.println("t2加锁成功locker1");
  21.                 try {
  22.                     Thread.sleep(1000);
  23.                 } catch (InterruptedException e) {
  24.                     e.printStackTrace();
  25.                 }
  26.                 synchronized (locker2){
  27.                     System.out.println("t2加锁成功locker2");
  28.                 }
  29.             }
  30.         });
  31.         t1.start();
  32.         t2.start();
  33.     }
  34. }
复制代码
  

    4.Java标准库中的线程安全类

     Java标准库中很多都是线程不安全的.这些类大概会涉及到多线程修改共享数据,又没有任何加锁措施.

ArrayList;LinkedList;HashMap;TreeMap;HashSet;TreeSet;StringBuilder

但是还有一些是线程安全的.使用了一些锁机制来控制.
Vector (不保举使用)
HashTable(不保举使用)
ConcurrentHashMap


StringBuffer

    5.volatile关键字

  (1)内存可见性标题

     观察以下代码
  
  1. public class Demo25 {
  2.     private static int n = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1= new Thread(() ->{
  5.            while (n == 0){
  6.                //
  7.            }
  8.         });
  9.         Thread t2= new Thread(() ->{
  10.             Scanner scanner = new Scanner(System.in);
  11.             System.out.println("请输入一个整数:");
  12.             n = scanner.nextInt();
  13.         });
  14.         t1.start();
  15.         Thread.sleep(2000);
  16.         t2.start();
  17.     }
  18. }
复制代码
  

   
 
    原因 

     
怎样进行优化导致出现内存可见性标题标?



此时JVM实验这个代码时发现每次循环过程中(1)操作的开销非常大,而且每次实验(1)操作它的结果都是一样的,并且JVM根本没意识到用户大概将来会修改n,于是JVM就做了一个大胆的操作直接将(1)操作给优化掉了,每次循环不会去读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存的结果)

当JVM做出上述决定后此时意味着,循环的开销大幅度的低落,但是当用户修改n的时间返现内存中的n已经改变了,但是t1线程每次循环不会真的读内存,并没有感知到n的改变,也就是说对于线程t1来说n的改变是“不可见的”,这样就引起了内存可见性的标题
   
   内存可见性标题本质上是编译器/JVM对代码进行优化出现的bug,如果代码是单线程,优化后的代码非常精确,但在多线程中大概会出现误判,这就导致了内存可见性的标题
 
    办理方案 

     办理方案一:
   在t1线程中添加sleep等候
和读内存相比,sleep相比之下就更慢了,足以等到你scanner输入之后t2线程修改后t1感知
 
  
  1. public class Demo25 {
  2.     private static int n = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1= new Thread(() ->{
  5.            while (n == 0){
  6.                try {
  7.                    Thread.sleep(1000);
  8.                } catch (InterruptedException e) {
  9.                    e.printStackTrace();
  10.                }
  11.            }
  12.             System.out.println("t1线程结束");
  13.         });
  14.         Thread t2= new Thread(() ->{
  15.             Scanner scanner = new Scanner(System.in);
  16.             System.out.println("请输入一个整数:");
  17.             n = scanner.nextInt();
  18.         });
  19.         t1.start();
  20.         t2.start();
  21.     }
  22. }
复制代码
  办理方案二:
添加volatile关键字,该关键字用来修饰一个变量,用来提示编译器这个变量是“易变”的,优化的条件是变量是频仍读取的,而且结果是固定的,此时编译器就会克制上述优化,以此来确保灭磁都实验从内存中从新读取数据

引入该变量后,编译器生成该代码时,就会给这个变量的读取操作附近生成一些特殊指令,称为“内存屏蔽”,后续JVM实验到此处时就不会进行优化
  
  1. public class Demo25 {
  2.     private static volatile int n = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1= new Thread(() ->{
  5.            while (n == 0){
  6.                //
  7.            }
  8.             System.out.println("t1线程结束");
  9.         });
  10.         Thread t2= new Thread(() ->{
  11.             Scanner scanner = new Scanner(System.in);
  12.             System.out.println("请输入一个整数:");
  13.             n = scanner.nextInt();
  14.         });
  15.         t1.start();
  16.         t2.start();
  17.     }
  18. }
复制代码
   (2)不办理原子性标题

   
  1. public class Demo26 {
  2.     private static volatile int count = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         Thread t1 = new Thread(() -> {
  5.             for (int i = 0; i < 50000; i++) {
  6.                 count++;
  7.             }
  8.         });
  9.         Thread t2 = new Thread(() -> {
  10.             for (int i = 0; i < 50000; i++) {
  11.                 count++;
  12.             }
  13.         });
  14.         t1.start();
  15.         t2.start();
  16.         t1.join();
  17.         t2.join();
  18.         System.out.println(count);
  19.     }
  20. }
复制代码
   

    6.wait和notify 

     线程在操作系统上的调度是随机的,多个线程需要控制线程直接某个逻辑的先后顺序,此时就可以让后实验的逻辑使用wait,先实验的线程,完成某些逻辑之后,通过notify唤醒对方的wait
    (1)wait()

     作用:
   使当前实验代码的线程进行等候(将线程放到等候队列中)
   开释当前锁
   满足肯定条件时被唤醒,重新尝试获取这个锁

竣事等候的条件: 
其他线程调用该对象的notify方法.
   wait等候时间超时(wait方法提供⼀个带有timeout参数的版本,来指定等候时间).
   其他线程调用该等候线程的interrupted方法,导致wait抛出InterruptedException 异常.
      
  1. public class Demo27 {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         Object object = new Object();
  4.         System.out.println("wait之前");
  5.         object.wait();
  6.         System.out.println("wait之后");
  7.     }
  8. }
复制代码
  

   非法的监视器状态异常,这里的意思是在调用wait方法时,当前锁的状态是不精确的;很显着此处我们都没加锁又何谈解锁呢?wait方法会针对对象先进行解锁所以要使用synchronized关键字来上锁

加上锁之后由于没有notify解锁,所以会一直等候

   wait在实验时会将进行解锁,壅闭等候(目标是为了收到关照)同时实验,这两个操作方法内部已经做好了
    (2)notify()

     notify方法是唤醒等候的线程.
   
   
  
  1. public class Demo27 {
  2.     private static Object locker1 = new Object();
  3.     public static void main(String[] args) {
  4.         Thread t1 = new Thread(() ->{
  5.             System.out.println("wait之前");
  6.             synchronized (locker1) {
  7.                 try {
  8.                     locker1.wait();
  9.                 } catch (InterruptedException e) {
  10.                     e.printStackTrace();
  11.                 }
  12.             }
  13.             System.out.println("wait之后");
  14.         });
  15.         Thread t2 = new Thread(() ->{
  16.             System.out.println("notify之前");
  17.             synchronized (locker1) {
  18.                 locker1.notify();
  19.             }
  20.             System.out.println("notify之后");
  21.         });
  22.         t1.start();
  23.         t2.start();
  24.     }
复制代码
  

    (3)线程饿死标题 

     
   定义︰线程饿死是指一个或多个线程由于某种原因无法获取所需的资源或实验机会,导致它们无法继续正常实验,从而被壅闭在某个状态,不能完成其使命。这种情况通常是由于资源竞争或优先级设置不当导致的。

举例说明,第一个人(t1线程)去取钱并上了锁,但机器里没钱,第一个人可以先出来,可以反反复复收支,这就导致其他人只能干等着,无法获取到锁,此时就会产生线程饿死的情况
 

   办理方案:
   让第一个人拿到锁的同时进行判定,判定当前能否实验取钱的操作,能则正常实验,不能则主动开释锁,并且进行“壅闭等候”(调用wait实现),此时线程就就不会在后续参与锁的竞争,一直壅闭到取钱的条件具备,此时再由别的线程关照唤醒(notify实现)唤醒这个线程
    7.wait和sleep的对比(口试题)

     一个是用于线程之间的通信的,一个是让线程壅闭一段时间,
   唯—的雷同点就是都可以让线程放弃实验一段时间.
   
1. wait需要搭配synchronized使用. sleep 不需要.
2. wait是Object的方法sleep是Thread的静态方法.

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! 更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4