• 多线程的安全问题


    一,多线程安全问题分析

     1、线程安全问题出现的原因:

          (1)多个线程操作共享的数据;(进行写操作时,读操作不影响)

          (2)线程任务操作共享数据的代码有多条(多个运算)。

           多线程,当CPU在执行的过程中,可能随时切换到其他的线程上执行。比如当线程1正在执行时,由于CPU的执行权被线程2抢走,于是线程1停止运行进入就绪队列,当线程2运行完,释放CPU的使用权,此时当线程1再次获得CPU的执行权时,由于线程2将某些共享数据的值已改变,所以此时线程1继续运行就会出现错误隐患。

    2举例分析:

           假设有三个线程在抢票。当线程1抢到CPU执行权,先对系统票数进行判断,发现票数是大于0的,接着准备购票,但由于其他原因,该线程1被阻塞,CPU执行权被线程2抢到,CPU开始执行线程2,线程2同样先对票数进行判断,如果大于0,就进行购票,但由于其他原因,该线程2也被阻塞,CPU执行权被线程3抢到,CPU开始执行线程3,线程3同样先对票数进行判断,如果大于0,就进行购票,由于需求较大,系统的票全部被线程3购完,此时线程3执行完毕释放了cpu执行权。这时CPU执行权又被线程1抢到,CPU开始执行线程1代码,因为之前线程1已经对系统票数进行判断过,所以此时不会再继续判断,而是直接购票,但由于系统的票已全部被线程3购完,这时线程1再继续购买就会出现票数错误(如用户买的票号为0号票或-1号票,而现实中不存在0号票和-1号票)。所以这时候线程就出现了不安全隐患。而线程2也同理。

            注意:由于CPU的执行顺序是随机的(谁优先级大就执行谁),所以代码中加 Thread.sleep(10); 以模拟上述情况。

     1 class Demo implements Runnable{  //1.实现Runnable接口
     2     public int ticket=5;//系统的票数
     3     public void run() { //2.重写run方法
     4        while (true){
     5             if(ticket>0){
     6                try{ Thread.sleep(10);} catch(Exception e){ };  
     7                 //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try!
     8                 System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
     9             }
    10         }
    11 
    12     }
    13 }
    14 public class TreadDemo {
    15     public static void main(String[] args) {//main函数也是一个线程(主线程)
    16         Demo d=new Demo();
    17         Thread t1=new Thread(d);//创建一个线程
    18         Thread t2=new Thread(d);
    Thread t3=new Thread(d);
    19 t1.start();//3.调用start方法d.run() 20 t2.start();
    t3.start();
    21 } 22 }

       运行结果:

              一般火车票都是从1号开始售卖,而代码运行结果是从-1号开始售卖的,所以存在安全隐患。

    二、多线程安全问题解决(内置锁,显示锁)

            只要让一个线程在执行线程任务时,将多条操作共享数据的代码一次执行完,在执行过程中,不要让其他线程参与运算。那么如何在代码中体现呢?

                (1)通过同步代码块完成,使用关键字synchronized。 

                           同步代码块使用的锁是任意对象(由使用者自己来手动的指定)。锁住的是指定的这个对象。

                (2)使用同步函数(方法)。

                          同步函数使用的锁是this,锁住的是当前调用的对象实例。

                          静态同步函数使用的锁是字节码文件对象,类名.class,锁住的是当前类的class实例。

             同步的前提:

               (1)必须要有两个或者两个以上的线程。

               (2)必须是多个线程使用同一个锁。

               (3)必须保证同步中只能有一个线程在运行。

    1,通过同步代码块完成

     格式: synchronized(对象)

                 {

                    需要被同步的代码

                 }

           通过分析可知,run()方法中的代码是线程运行的代码,但只有操作共享数据的代码才是需要被同步的代码。所以一般不建议把同步加在run方法,如果把同步加在了run方法上,导致任何一个线程在调用start方法开启之后,JVM去调用run方法的时候,首先都要先获取同步的锁对象,只有获取到了同步的锁对象之后,才能去执行run方法。而我们在run中书写的被多线程操作的代码,永远只会有一个线程在里面执行。只有这个线程把这个run执行完,出去之后,把锁释放了,其他某个线程才能进入到这个run执行,这时候代码的运行跟单线程类似。所以只有操作共享数据的代码才是需要被同步的代码

     1 class Demo implements Runnable{
     2     public int ticket=5;   //此处ticket(票)是共享数据
     3     Object obj=new Object();
     4     public void run() {   
     5         while (true){
     6            synchronized (obj){
     7                if(ticket>0){
     8                    try{ Thread.sleep(10);} catch(Exception e){ };  
     9                    //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try! 
    10                    System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
    11                }
    12            }
    13 
    14         }
    15 
    16     }
    17 }

    运行结果:

          通过结果可知,安全问题已解决。

          要注意运行结果没有第三个线程,并不是说第三个线程没有启动,它启动了,只是因为票数太少,在它抢到CPU执行权时,票已经被买光。。。

    分析过程:

     

    2,使用同步函数(方法)。

    就是将关键字synchronized放到修饰符位置上。

       public  synchronized  返回值类型  方法名()

       {  

              需要同步的代码

       }

               通过分析可知,若该方法中的所有代码都是操作共享数据的,则可以直接将关键字synchronized放到该方法修饰符位置上。若该方法中仅有部分代码是操作共享数据的,则将这些操作共享数据的代码重新封装在一个函数中,然后将关键字synchronized放到新函数(方法)修饰符位置上。

     1 class Demo implements Runnable{
     2     public int ticket=5;
     3     public   void run() { //因为run()方法中仅有部分代码是操作共享数据
     4        while (true){
     5           show();   //this.show();
     6         }
     7 
     8     }
     9     public synchronized  void show() {//所以将这些操作共享数据的代码重新封装在一个函数中。   同步函数使用的锁是this。
    10         if(ticket>0){
    11             try{ Thread.sleep(10);} catch(Exception e){ };  
    12             //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try!
    13             System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
    14         }
    15     }
    16 }

    2,使静态同步函数(方法)。

              如果同步函数被关键字static修饰后,则使用的锁不再是this。因为静态方法中不可以定义this,当静态进入内存时,内存中还没有本类对象,但是有该类对应的字节码文件对象(类名.class),该对象的类型是Class。所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。(类名.class)

       public  static  synchronized  返回值类型  方法名()

       {  

              需要同步的代码

       }

    三,解决线程问题要注意的问题

     1、同步的好处和弊端

      好处:可以保证多线程操作共享数据时的安全问题

      弊端:较消耗资源(要加锁),降低了程序的执行效率(每次要判断锁)。

        2、同步的前提

      要同步,必须有多个线程,多线程在操作共享的数据,同时操作共享数据的语句不止一条。

               (1)必须要有两个或者两个以上的线程。

               (2)必须是多个线程使用同一个锁。

               (3)必须保证同步中只能有一个线程在运行。

        3、加入了同步安全依然存在

      首先查看同步代码块的位置是否加在了需要被同步的代码上。如果同步代码的位置没有错误,这时就再看同步代码块上使用的锁对象是否是同一个。多个线程是否在共享同一把锁

     【代码演示】:模拟两个人去取银行取钱,假设银行总总资产为100。

     1 class Person implements Runnable{
     2     int bankmoney=100;
     3     Object obj=new Object();
     4     @Override
     5     public void run() {
     6         while (true){
     7             synchronized (obj){
     8                 if(bankmoney>=0){
     9                     System.out.println(Thread.currentThread().getName()+"取了一次钱,银行剩余钱数"+bankmoney--);
    10                 }else {
    11                     System.out.println("钱已取完");
    12                     break;
    13                 }
    14             }
    15         }
    16     }
    17 }
    18 public class Bank {
    19     public static void main(String[] args) {
    20         Person p1=new Person();
    21         Thread t1=new Thread(p1);
    22         t1.setName("A");
    23         t1.start();
    24         Person p2=new Person();
    25         Thread t2=new Thread(p2);
    26         t2.setName("B");
    27         t2.start();
    28     }
    29 }

    【代码演示】

     1 class  Res{
     2     String name=null;
     3     String sex=null;
     4 }
     5 class Write extends Thread {
     6     Res res;
     7     public Write(Res res){
     8         this.res=res;
     9     }
    10     @Override
    11     public void run() {
    12         int count=0;
    13         while (true){
    14             synchronized (res){
    15                 if(count==0){
    16                     res.name="小红";
    17                     res.sex="";
    18                     count++;
    19                 }else {
    20                     res.name="小军";
    21                     res.sex="";
    22                     count++;
    23                 }
    24                 count=count%2;
    25             }
    26             //如果不加锁,就可能会出现名字赋值完性别还没赋值就被其他读线程抢走的情况。如果这时读取就会出现信息误差,
    27             //因为姓名是现赋的,但名字还是赋值之前的。也就是说会出现,打印出来 小红:男 和小军:女的错误信息
    28         }
    29     }
    30 }
    31 class Read extends Thread {
    32     Res res;
    33     public Read(Res res){
    34         this.res=res;
    35     }
    36     @Override
    37     public void run() {
    38         while (true){
    39             synchronized (res){
    40                 System.out.println(res.name+":"+res.sex);
    41             }
    42            //因为在读的过程中也可能出现,把名字读取后,资源被另一个线程抢走,当在此抢到资源时,性别已被重新赋值,
    43             // 而导致读取失败,所以在这块也要加锁
    44         }
    45     }
    46 }
    47 public class ReadWriteDemo {
    48     public static void main(String[] args) {
    49         Res res=new Res();
    50         new Write(res).start();
    51         new Read(res).start();
    52     }
    53 }

      1.如何解决多线程之间线程安全问题?

      答:使用内置锁(同步代码synchronized)或显示锁(lock)。

      2.为什么使用同步代码块或使用显示锁可以解决线程安全问题?

      答:使用同步代码块可以让一个线程在执行线程任务时,将多条操作共享数据的代码一次执行完,代码执行完后在释放锁,然后其他线程才能进行执行。这样就解决了线程不安全的情况。

      3.什么是线程之间同步?

      答:多个线程共享一个资源,当一个线程在操作共享资源时,其他线程是不会影响的。

  • 相关阅读:
    在Electron中最快速预加载脚本
    Babel是什么?
    node、npm、chrome、v8、sandbox是什么?
    我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
    NN[神经网络]中embedding的dense和sparse是什么意思?
    记一次失败的docker排障经历
    paddlepaddle关于使用dropout小案例
    paddlepaddle如何预加载embedding向量
    最近在部署推荐系统环境时,遇到一个很奇葩都问题
    fluid.io.load_inference_model 载入多个模型的时候会报错 -- [paddlepaddle]
  • 原文地址:https://www.cnblogs.com/ljl150/p/12238400.html
Copyright © 2020-2023  润新知