Java学习笔记 24 - 线程之多线程安全、死锁、同步机制

本文主要内容
1、多线程安全问题
2、等待唤醒机制

01线程安全

  • A:线程安全问题引发
    多线程并发访问同一个数据资源时,线程休眠,会导致线程安全问题

    /*
     * 3个线程,对一个票资源,出售
     */
    public class ThreadDemo {
     public static void main(String[] args) {
     //创建Runnable接口实现类对象
     Tickets t = new Tickets();
     //创建3个Thread类对象,传递Runnable接口实现类
     Thread t0 = new Thread(t);
     Thread t1 = new Thread(t);
     Thread t2 = new Thread(t);
     
     t0.start();
     t1.start();
     t2.start();
     
     }
    }
    
    /*
     *  通过线程休眠,出现安全问题
     */
    public class Tickets implements Runnable{
    
     //定义出售的票源
     private int ticket = 10;
     private Object obj = new Object();
    
     public void run(){
     while(true){
    
       //对票数判断,大于0,可以出售,变量--操作
         if( ticket > 0){
           try{
              Thread.sleep(10); //加了休眠让其他线程有执行机会
           }catch(Exception ex){}
           System.out.println(Thread.currentThread().getName()+"正在售票 "+ticket--);
         }
     }
     }
    }
    

输出结果:
窗口3正在售票10
窗口2正在售票9
窗口1正在售票8
窗口1正在售票7
窗口3正在售票7
窗口2正在售票6
窗口3正在售票5
窗口1正在售票5
窗口2正在售票4
窗口3正在售票3
窗口2正在售票2
窗口1正在售票3
窗口3正在售票1
窗口1正在售票0
窗口2正在售票-1
上面程序存在以下问题:
票出现了重复的票
错误的票 0、-1
这里由多个线程同时对变量执行写操作而引起的线程安全问题,解决该问题需要考虑线程同步。

  • B:同步代码块解决线程安全问题
    通过线程休眠,导致的安全问题,解决方案:同步代码块
    公式:
    synchronized(任意对象){
    线程要操作的共享数据
    }

        /*
         * 3个线程,对一个票资源,出售
         */
        public class ThreadDemo {
         public static void main(String[] args) {
           //创建Runnable接口实现类对象
           Tickets t = new Tickets();
           //创建3个Thread类对象,传递Runnable接口实现类
           Thread t0 = new Thread(t);
           Thread t1 = new Thread(t);
           Thread t2 = new Thread(t);
           
           t0.start();
           t1.start();
           t2.start();
           
         }
        }
      
        public class Tickets implements Runnable{
         
         //定义出售的票源
         private int ticket = 100;
         private Object obj = new Object();
         
         public void run(){
           while(true){
             //线程共享数据,保证安全,加入同步代码块
             synchronized(obj){
             //对票数判断,大于0,可以出售,变量--操作
               if( ticket > 0){
                 try{
                    Thread.sleep(10);
                 }catch(Exception ex){}
                 System.out.println(Thread.currentThread().getName()+" 正在售票 "+ticket--);
               }
             }
           }
         }
        }
    

输出结果:
窗口1正在售票10
窗口1正在售票9
窗口1正在售票8
窗口1正在售票7
窗口1正在售票6
窗口1正在售票5
窗口1正在售票4
窗口1正在售票3
窗口1正在售票2
窗口1正在售票1

  • C:同步代码块的执行原理
    同步代码块: 在代码块声明上 加上synchronized
    synchronized (锁对象) {
    可能会产生线程安全问题的代码
    }
    同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

  • D:同步方法的使用
    同步方法是将线程共享数据、同步,抽取到一个方法中。在方法的声明上,加入同步关键字
    同步普通方法和同步代码块一样,有锁且对象锁是本类对象的引用,即this
    同步静态方法的对象锁是本类类名.class属性

     public class Tickets implements Runnable{
    
    //定义出售的票源
    private  int ticket = 100;
    
    public void run(){
      while(true){
        payTicket();
      }
    }
    
    public  synchronized void payTicket(){  
        if( ticket > 0){
          try{
             Thread.sleep(10);
          }catch(Exception ex){}
          System.out.println(Thread.currentThread().getName()+" 正在售票 "+ticket--);
        }
      
    }
     }
    

输出结果:
窗口2正在售票10
窗口2正在售票9
窗口2正在售票8
窗口2正在售票7
窗口2正在售票6
窗口2正在售票5
窗口2正在售票4
窗口2正在售票3
窗口2正在售票2
窗口2正在售票1

02 Lock接口

  • A:JDK1.5新特性Lock接口
    Lock接口提供了比使用 synchronized 方法和语句更加面向对象的锁,在该锁中提供了更多的操作锁的功能。
    Lock接口中的常用方法
    void lock()
    void unlock()
    使用Lock接口,以及其中的lock()方法和unlock()方法可替代同步

  • B:Lock接口的使用

    /*
     * 3个线程,对一个票资源,出售
     */
    public class ThreadDemo {
      public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Tickets t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        
        t0.start();
        t1.start();
        t2.start();
        
      }
    }
    
    public class Tickets implements Runnable{
      
      //定义出售的票源
      private int ticket = 100;
      //在类的成员位置,创建Lock接口的实现类对象
      private Lock lock = new ReentrantLock();
      
      public void run(){
        while(true){
          //调用Lock接口方法lock获取锁
            lock.lock();
          //对票数判断,大于0,可以出售,变量--操作
            if( ticket > 0){
              try{
                 Thread.sleep(10);
                 System.out.println(Thread.currentThread().getName()+" 正在售票 "+ticket--);
              }catch(Exception ex){
                
              }finally{
                //释放锁,调用Lock接口方法unlock
                lock.unlock();
              }
            }
        }
      }
    }
    

03线程的死锁

  • A:线程的死锁原理
    当线程任务中出现了多个同步(多个锁) 时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。

        synchronzied(A锁){
          synchronized(B锁){
                    
          }
      }
    
  • B:线程的死锁代码实现

     public class DeadLock implements Runnable{
      private int i = 0;
      public void run(){
        while(true){
          if(i%2==0){
            //先进入A同步,再进入B同步
            synchronized(LockA.locka){
              System.out.println("if...locka");
              synchronized(LockB.lockb){
                System.out.println("if...lockb");
              }
            }
          }else{
            //先进入B同步,再进入A同步
            synchronized(LockB.lockb){
              System.out.println("else...lockb");
              synchronized(LockA.locka){
                System.out.println("else...locka");
              }
            }
          }
          i++;
        }
      }
     }
    
    public class DeadLockDemo {
      public static void main(String[] args) {
        DeadLock dead = new DeadLock();
        Thread t0 = new Thread(dead);
        Thread t1 = new Thread(dead);
        t0.start();
        t1.start();
      }
    }
    
    
    public class LockA {
      private LockA(){}
      
      public  static final LockA locka = new LockA();
    }
    
    
    public class LockB {
      private LockB(){}
      
      public static final LockB lockb = new LockB();
    }
    

04线程等待与唤醒

  • A:线程等待与唤醒
    等待唤醒机制所涉及到的方法:
    wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
    notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。唤醒:即让线程池中的线程具备执行资格
    notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
    上述方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

  • B:案例

    /*
     *  定义资源类,有2个成员变量
     *  name,sex
     *  同时有2个线程,对资源中的变量操作
     *  1个对name,age赋值
     *  2个对name,age做变量的输出打印
     */
    public class Resource {
    public String name;
    public String sex;
    }
    
    
     /*
     *  输入的线程,对资源对象Resource中成员变量赋值
     *  一次赋值 张三,男
     *  下一次赋值 lisi,nv
     */
    public class Input implements Runnable {
      private Resource r=new Resource();
     
      public void run() {
        int i=0;
        while(true){
          if(i%2==0){
             r.name="张三";
             r.sex="男";
           }else{
              r.name="lisi";
              r.sex="女";
            }
          i++;
        }
      }
    }
    
    /*
     *  输出线程,对资源对象Resource中成员变量,输出值
     */
    public class Output implements Runnable {
      private Resource r=new Resource() ;
       
      public void run() {
        while(true){
           System.out.println(r.name+"..."+r.sex); 
          }
        }
    }
    
    
    /*
     *  开启输入线程和输出线程,实现赋值和打印值
     */
    public class ThreadDemo{
      public static void main(String[] args) {
        
        Resource r = new Resource();
        
        Input in = new Input();
        Output out = new Output();
        
        Thread tin = new Thread(in);
        Thread tout = new Thread(out);
        
        tin.start();
        tout.start();
      }
    }
    

以上程序运行后,输出结果中Resource对象会有很多Null值
null.....null
null.....null
null.....null
null.....null
null.....null
null.....null
null.....null
null.....null
null.....null
.....

解决上述Null值,线程等待与唤醒案例null值解决

    /*
    *  输入的线程,对资源对象Resource中成员变量赋值
    *  一次赋值 张三,男
    *  下一次赋值 lisi,nv
  */
   public class Input implements Runnable {
     private Resource r;
     public Input(Resource r){
       this.r=r;
     }
    
     public void run() {
       int i=0;
       while(true){
         if(i%2==0){
            r.name="大王";
            r.sex="男";
          }else{
             r.name="王后"
             r.sex="女"
           }
         i++;
       }
     }
   }

   /*
    *  输出线程,对资源对象Resource中成员变量,输出值
    */ 
   public class Output implements Runnable {
     private Resource r;
     public Output(Resource r){
        this.r=r;
     } 
     public void run() {
       while(true){
          System.out.println(r.name+"..."+r.sex); 
         }
       }
     }

   }
   /*
    *  开启输入线程和输出线程,实现赋值和打印值
    */
   public class ThreadDemo{
     public static void main(String[] args) {
       
       Resource r = new Resource();
       
       Input in = new Input(r);
       Output out = new Output(r);
       
       Thread tin = new Thread(in);
       Thread tout = new Thread(out);
       
       tin.start();
       tout.start();
     }
   }

以上程序运行,打印输出的结果发现,会有交叉错乱
大王.....男
王后.....女
王后.....男
大王.....女
王后.....女
王后.....女
大王.....女
王后.....男
王后.....男
王后.....男
大王.....女
王后.....男
大王.....男
.....
解决方案:使用同步代码块

        /*
          *  输入的线程,对资源对象Resource中成员变量赋值
          *  一次赋值 张三,男
          *  下一次赋值 lisi,nv
        */
         public class Input implements Runnable {
           private Resource r;
           public Input(Resource r){
             this.r=r;
           }
          
           public void run() {
             int i=0;
             while(true){
              synchronized(r){
               if(i%2==0){
                   r.name="大王";
                   r.sex="男";
              }else{
                   r.name="王后"
                   r.sex="女"
                 }
               i++;
             }

           }
         }

         /*
          *  输出线程,对资源对象Resource中成员变量,输出值
          */ 
         public class Output implements Runnable {
           private Resource r;
           public Output(Resource r){
              this.r=r;
           } 
           public void run() {
             while(true){
                synchronized(r){
                 System.out.println(r.name+"..."+r.sex); 
                }
               }
             }
           }

         }
         /*
          *  开启输入线程和输出线程,实现赋值和打印值
          */
         public class ThreadDemo{
           public static void main(String[] args) {
             
             Resource r = new Resource();
             
             Input in = new Input(r);
             Output out = new Output(r);
             
             Thread tin = new Thread(in);
             Thread tout = new Thread(out);
             
             tin.start();
             tout.start();
           }
         }

以上程序运行,打印输出的结果发现,仍会有错乱

...男
大王.....男
大王.....男
大王.....男
大王.....男
大王.....男
大王.....男
大王.....男
......
解决方案:使用线程等待与唤醒
输入:赋值后,执行方法wait()永远等待
输出:变量值打印输出,在输出等待之前,唤醒
输入的notify(),自己在wait()永远等待
输入:被唤醒后,重新对变量赋值,赋值后,必须唤醒输出的线程notify(),自己的wait()

 /*
  *  定义资源类,有2个成员变量
  *  name,sex
  *  同时有2个线程,对资源中的变量操作
  *  1个对name,age赋值
  *  2个对name,age做变量的输出打印
  */
 public class Resource {
  public String name;
  public String sex;
  public boolean flag = false;
 }

 /*
  *  输入的线程,对资源对象Resource中成员变量赋值
  *  一次赋值 张三,男
  *  下一次赋值 lisi,nv
  */
 public class Input implements Runnable {
  private Resource r ;
  
  public Input(Resource r){
    this.r = r;
  }
  
  public void run() {
    int i = 0 ;
    while(true){
      synchronized(r){
        //标记是true,等待
          if(r.flag){
            try{r.wait();}catch(Exception ex){}
          }
        
        if(i%2==0){
            r.name="大王";
            r.sex="男";
          }else{
             r.name="王后"
             r.sex="女"
        }
        //将对方线程唤醒,标记改为true
        r.flag = true;
        r.notify();
      }
      i++;
    }
  }

 }
 
 /*
  *  输出线程,对资源对象Resource中成员变量,输出值
  */
 public class Output implements Runnable {
  private Resource r ;
  
  public Output(Resource r){
    this.r = r;
  }
  public void run() {
    while(true){
      synchronized(r){  
        //判断标记,是false,等待
      if(!r.flag){
        try{r.wait();}catch(Exception ex){}
        }
      System.out.println(r.name+".."+r.sex);
      //标记改成false,唤醒对方线程
      r.flag = false;
      r.notify();
      }
    }
  }

 }

 /*
  *  开启输入线程和输出线程,实现赋值和打印值
  */
 public class ThreadDemo{
  public static void main(String[] args) {
    
    Resource r = new Resource();
    
    Input in = new Input(r);
    Output out = new Output(r);
    
    Thread tin = new Thread(in);
    Thread tout = new Thread(out);
    
    tin.start();
    tout.start();
  }
 }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容