zoukankan      html  css  js  c++  java
  • 黑马程序员JAVA基础多线程(下)

    5.多线程的安全问题:多线程同步

      当使用多个线程同时访问一个数据时,经常会出现线程安全问题。如下面程序: 

     1 package Thread;
     2 
     3 /* 
     4  * 多个线程同时访问一个数据时,出现的安全问题。
     5  * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票
     6  */
     7 class Ticks implements Runnable 
     8 {
     9     private int ticks = 100 ; 
    10     public void run()
    11     {
    12         while (ticks > 0)
    13         {
    14 //            加入sleep 方法 是为了更明显的看到该程序中出现的安全问题。
    15             try{Thread.sleep(10);}catch (Exception e) {} 
    16             System.out.println(Thread.currentThread().getName()
    17                     +"...卖出了第"+ticks+"张票");
    18             ticks -- ;
    19         }
    20     }
    21 }
    22 public class Text1 {
    23     public static void main(String args[])
    24     {
    25 //        创建 Runnable 实现类 Ticks 的对象。
    26         Ticks t = new Ticks() ;  
    27 //        开启4个线程处理同一个 t 对象。
    28         new Thread(t , "一号窗口").start() ; 
    29         new Thread(t , "二号窗口").start() ; 
    30         new Thread(t , "三号窗口").start() ; 
    31         new Thread(t , "四号窗口").start() ; 
    32     }
    33 }

      运行的结果会出现"X号窗口...卖出了第-1张票"、“X号窗口...卖出了第-2张票”这样的安全问题。

      导致的原因:当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致了共享数据的错误。

      解决这类问题的方法:

      1、同步代码块。

      2、同步函数。

     5.1 同步代码块

      同步代码块的格式如下: 

        synchronized(obj)
        {
            // 此处的代码就是同步代码块
        }

      synchronized 后括号里面的 obj 就是同步监视器。代码含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。即只有获得对同步监视器的锁定的线程可以在同步中执行,没有锁定的线程即使获得执行权,也不能在同步代码块中执行。

      注意:虽然JAVA 程序允许使用任何对象来作为同步监视器。但是还是推荐使用可能被并发访问的共享资源来充当同步监视器。

      通过修改代码如下:

     1 package Thread;
     2 
     3 /* 
     4  * 多个线程同时访问一个数据时,出现的安全问题。
     5  * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票
     6  */
     7 class Ticks implements Runnable 
     8 {
     9     private int ticks = 100 ; 
    10     public void run()
    11     {
    12             while (ticks > 0)
    13             {
    14                 synchronized (Ticks.class)
    15                 {  
    16                     if ( ticks > 0)
    17                     {
    18         //            加入sleep 方法 是为了更明显的看到该程序中出现的安全问题。
    19                     try{Thread.sleep(10);}catch (Exception e) {} 
    20                     System.out.println(Thread.currentThread().getName()
    21                             +"...卖出了第"+ticks+"张票");
    22                     ticks -- ;
    23                     }
    24                 }
    25             } 
    26     }
    27 }
    28 public class Text1 {
    29     public static void main(String args[])
    30     {
    31 //        创建 Runnable 实现类 Ticks 的对象。
    32         Ticks t = new Ticks() ;  
    33 //        开启4个线程处理同一个 t 对象。
    34         new Thread(t , "一号窗口").start() ; 
    35         new Thread(t , "二号窗口").start() ; 
    36         new Thread(t , "三号窗口").start() ; 
    37         new Thread(t , "四号窗口").start() ; 
    38     }
    39 }

      加入同步监视器之后的程序就不会出现数据上的错误了。

      虽然同步监视器的好处是解决了多线程的安全问题。但也也因为多个线程需要判断锁,较为消耗资源。

      注意:同步的前提:

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

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

      如果加入了synchronized 同步监视器,还出现了安全问题,则可以按照如下步骤找寻错误:

      1、明确那些代码是多线程代码。

      2、明确共享数据。

      3、明确多线程运行代码中那些代码是操作共享数据的。

     5.2 同步函数

      把 synchronized 作为修饰符修饰函数。则该函数称为同步函数。

      注意:同步函数无需显示的指定同步监视器,同步函数的同步监视器是this,也就是该对象本身。

      注意:synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。

        上面通过模拟火车卖票系统的小程序,通过加入 synchronized 同步监视器,来解决多线程中的安全问题。下面模拟银行取钱问题,通过同步函数来解决多线程的安全问题。 

    * 银行取钱的基本流程如下:
    * 1、用户输入账户、密码,系统判断用户的账户、密码是否匹配。
    * 2、用户输入取钱金额。
    * 3、系统判断账户余额是否大于取钱金额。
    * 4、如果余额大于取款金额,取款成功;否则取款失败

      在这里只模拟后面三步: 

     1 package Thread;
     2  
     3 class Account  {
     4 //    账户 余额 
     5     private double balance ; 
     6     public Account (  double balance)
     7     { 
     8         this.balance = balance ; 
     9     }
    10 //    get和set方法  
    11     public double getBalance() {
    12         return balance;
    13     }
    14     public void setBalance(double balance) {
    15         this.balance = balance;
    16     }
    17 //    同步函数
    18 //    提供一个线程安全的draw的方法来完成取钱操作。
    19     public synchronized void Draw(double drawAmount)
    20     {
    21         if (balance > drawAmount)
    22         { 
    23             System.out.println(Thread.currentThread().getName()+"取钱成功!吐出金额"
    24                     + drawAmount );
    25             
    26             try { Thread.sleep(10) ;  } catch (Exception e) {  } 
    27             balance  -= drawAmount ;
    28             System.out.println("卡上余额:"+balance);
    29         }
    30         else 
    31         {
    32             System.out.println(Thread.currentThread().getName()+"取钱失败!卡上余额:"
    33                     + balance); 
    34         }
    35     }
    36 }
    37 class DrawThread implements Runnable
    38 {
    39 //    模拟账户
    40     private Account  account ; 
    41 //    希望所取钱的金额
    42     private double drawAmount ; 
    43     
    44     private  boolean flag = true ; 
    45     
    46     public DrawThread(Account account , double drawAmount)
    47     {
    48         this.account = account ; 
    49         this.drawAmount = drawAmount ; 
    50     }
    51 //    当前取钱
    52     public void run()
    53     {
    54         try
    55         {
    56             while(flag)
    57             {
    58 //                调用取钱函数
    59                 account.Draw(drawAmount) ; 
    60             }
    61         }
    62         catch(Exception e)
    63         {
    64             flag = false  ;
    65         }
    66     } 
    67 }
    68 public class AccountText
    69 {
    70     public static void main(String args[])
    71     {
    72         Account account = new Account(10000) ;
    73         DrawThread draw = new DrawThread( account , 300 ) ;
    74         Thread t = new Thread(draw , "A.....") ; 
    75         Thread t1 = new Thread(draw , "B.") ; 
    76         t.start() ; 
    77         t1.start() ;   
    78     }
    79 }

      同步方法的监视器是 this ,因此对于同一个 Account 而言,任意时刻只能有一条线程获得 Account 对象的锁定。

      提示:可变类的线程安全是以降低运行程序的运行效率作为代价,为了减少线程安全所带来的负面影响,程序可以采用如下策略:

      > 只对会改变竞争资源的方法进行同步。

      > 在两个或两个以上的线程操作同一个锁的环境中使用同步。

      当如下情况发生时会释放同步监视器的锁定:

      > 当前线程的同步方法、同步代码块执行结束。

      > 当前线程的同步方法、同步代码块中遇到break 、 return终止了该代码块、该方法的继续执行。

      > 当前线程的同步方法、同步代码块出现了未处理的Error或Exception,导致该代码块、该方法异常结束时会释放同步锁。

      > 当线程执行同步方法、同步代码块,程序执行了同步监视器对象的wait() 方法时。

     5.5 死锁

      当两线程相互等待对方释放锁时,就会发生死锁。由于JVM没有监测,也没有采用措施来处理死锁,所以多线程编成时应该采取措施来避免死锁。

    6. 线程通信

      模拟生产消费者:系统在有两条线程,分别代表生成者和消费者。

      程序的基本流程:

      1、生成者生产出一件商品。

      2、消费者消费生成出的商品。

      通过上诉流程要了解:生成者和消费者不能连续生成或消费商品,同时消费者只能消费以生产出的商品。 

     1 package Thread;
     2 
     3 class Phone 
     4 {
     5 //    定义商品的编号 
     6     private int No   ;
     7 //    定义商品的名字
     8     private String name ; 
     9     private boolean flag = true ; 
    10 //    初始化商品的名字和编号,同时编号是自增的 
    11     public Phone (String name , int  No)
    12     {
    13         this.name = name ;
    14         this.No =  No ; 
    15     }
    16 //    定义商品中的消费方法和生产方法。用synchronized 修饰符修饰
    17     public synchronized void Production() 
    18     {    
    19         
    20 //        导致当前线程等待,知道其他线程调用notify()或notifyAll()方法来唤醒
    21 //        if (!flag)
    22         while(!flag)
    23             try {this.wait() ;}catch(Exception e){}  
    24         System.out.println(Thread.currentThread().getName()+
    25                 ":生产"+name+";编号为:"+ ++ No );
    26 //        唤醒在此同步监视器上等待的单个线程。
    27 //        this.notify() ;
    28 //        唤醒在此同步监视器上等待的所有线程。
    29         this.notifyAll() ; 
    30         flag = false ;
    31     }
    32     public synchronized void Consumption()
    33     {
    34 //        if(flag)
    35         while(flag)
    36             try {this.wait() ;}catch(Exception e){}  
    37         System.out.println(Thread.currentThread().getName()+
    38         ";消费商品:"+name+"商品的编号为"+ No );
    39 //        this.notify() ;
    40         this.notifyAll() ; 
    41         flag = true ; 
    42     }
    43 }
    44 
    45 class ProducerThread implements Runnable
    46 {
    47     Phone phone  ;
    48     private boolean flag = true ; 
    49 //    同步监视器的对象
    50     public ProducerThread (Phone phone)
    51     {
    52         this.phone = phone ; 
    53     }  
    54     public void run()
    55     {
    56         try 
    57         {
    58             while (flag) 
    59                 phone.Production() ; 
    60         }
    61         catch(Exception e) { flag = false ;}
    62     }
    63 }
    64 class ConsumptionThread implements Runnable
    65 {
    66     Phone phone  ;
    67     private boolean flag = true ; 
    68 //    同步监视器的对象
    69     public ConsumptionThread (Phone phone)
    70     {
    71         this.phone = phone ; 
    72     }  
    73     public void run()
    74     {
    75         try 
    76         {
    77             while (flag) 
    78                 phone.Consumption() ; 
    79         }
    80         catch(Exception e) { flag = false ;}
    81     }
    82 }
    83 public class ProducerConsumerText { 
    84     public static void main(String args[])
    85     {
    86         Phone phone = new Phone("iPhone 5",0) ;  
    87         new Thread(new ProducerThread(phone),"生成者000").start() ; 
    88         new Thread(new ProducerThread(phone),"生成者111").start() ; 
    89         new Thread(new ConsumptionThread(phone),"消费者000").start() ; 
    90         new Thread(new ConsumptionThread(phone),"消费者111").start() ; 
    91     }
    92 }

      上面的程序中:第22 和第34 行中的flag 标志位 是判断 是由生产者生成还是由消费者进行消费。其实,在现实生活中,不可能只有一个生成者和消费者,而是多个生成者和消费者。所以需 在 第22 和 第34 行用 while 循环来进行 flag 的判断  , 而不是用 if 。如果用if 容易出现线程安全问题;而且在用while 循环进行flag的判断时,则必须用 notifyAll() 方法来唤醒同步监视器中所有等待中的线程,而不是 用notify() 方法。用notify()  则会导致所有线程进入等待状态。

      上面的小程序借助Object 类提供的 wait()、notify()、notifyAll 三个方法实现 。 

      > wait() :导致当前线程等待,知道其他线程调用该同步监视器的notify()或notifyAll() 方法来唤醒线程。

      > notify() : 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择一个其中一个唤醒。

      > notifyAll() :唤醒此同步监视器上等待的所有单个线程。

      注意:这三个方法必须用同步监视器对象来调用:

      > 同步函数:因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用。

      > 同步代码块:必须使用 synchronized 括号中的对象来调用。 

      

    7.同步锁LOCK

      JDK 1.5之后,JAVA提供了另外一种线程同步机制:显示定义同步锁来实现同步,同步锁应该使用Lock对象充当。 

     1 class X
     2 {
     3 //    定义锁对象
     4     private final ReentrantLock lock = new ReentrantLock() ; 
     5 //    ...
     6 //    定义需要保证线程安全的方法
     7     public void m()
     8     {
     9 //        加锁
    10         lock.lock() ; 
    11         try
    12         {
    13 //            需要保证线程安全的代码
    14         }
    15         finally
    16         {
    17             lock.unlock() ; 
    18         }
    19     }
    20 }

      当使用Lock 对象来保证同步时,JAVA提供了 Condition 类保持协调,即Condition 代替了同步监视器的功能。

      Condition 实例实质上被绑定在一个Lock 对象上。如: 

    1 //    定义锁对象
    2     private final ReentrantLock lock = new ReentrantLock() ; 
    3 //    指定Lock 对象对应的条件变量
    4     private final Condition condition = lock.newCondition() ; 

      > await() : 类似 wait() 方法。

      > signal() : 类似 notify() 方法。

      > signalAll() : 类似 notifyAll() 方法。

  • 相关阅读:
    vue全家桶
    uniapp——如何配置scss和uview ui框架
    uniapp——自定义input清除事件
    响应式页面中的echart
    elementui 切换下拉框值,改变验证规则prop的表单项是否为必填项
    小程序view标签内容 文本过长,自动换行的问题
    vue 中使用图片查看器插件Viewer.js
    跳转不同导航,滚动条滚回初始
    vue项目中回显当前时间的农历时间
    移动端点击导航滑动展示全部选项,以为跳转页面定位到相应位置
  • 原文地址:https://www.cnblogs.com/jbelial/p/2964877.html
Copyright © 2011-2022 走看看