黑马程序员-java基础-多线程2

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() 方法。

时间: 2024-12-22 08:19:39

黑马程序员-java基础-多线程2的相关文章

黑马程序员——java基础——多线程

 黑马程序员--java基础--多线程 ------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 进程:是一个正在执行中的程序.每一个进程执行都有一个执行顺序.该顺序是一个执行路径,或者叫一个控制单元. 线程:就是进程中的一个独立的控制单元.线程在控制着进程的执行.一个进程中至少有一个线程. 一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区.自己的变量.

黑马程序员--Java基础--多线程|线程同步

--Java培训.Android培训.iOS培训..Net培训 期待与您共同交流!-- 多线程基础.线程同步 1. 多线程基础 1.1. 进程和线程 1.1.1. 什么是进程 所谓进程(process)就是一块包含了某些资源的内存区域.操作系统利用进程把它的工作划分为一些功能单元.进程中所包含的一个或多个执行单元称为线程(thread).进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问.线程只能归属于一个进程并且它只能访问该进程所拥有的资源.当操作系统创建一个进程后,该进程会自动

黑马程序员——java基础---多线程

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 学习多线程之前,需对以下几个概念有所认知: 进程:进程是动态的.是一个正在执行中的程序.每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元. 线程:线程依附于进程,可以理解为进程下的一个子执行路径,但没有进程线程无法单独执行. 两者间的区别:进程是重量级的计算机任务,需要给它分配独立的地址空间和系统资源等.不同进程的内部数据和状态都是完全独立,所以不同进程之间的通信或转换

黑马程序员——java基础---多线程(二)

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! -------  线程间的通信:简单来说,就是多个线程在操作同一资源,但操作的动作不同. 试想一下,对于同一个资源做不同的操作,这势必会在操作的过程中产生矛盾.为了避免这种情况的发生,就需要用的synchronized来保证,每次对共享资源的操作,只能是一条线程在进行.在用到同步的时候,就会因需求问题用到wait().notify().notifyAll()这三个方法. wait()方法:作用是使调用线程自动

黑马程序员-java基础-多线程1

---恢复内容开始--- 单线程的程序只有一个顺序流:而多线程的程序则可以包括多个顺序执行流,并且多个顺序流之间互不干扰.就像单线程程序如同只雇佣了一个服务员的餐厅,他只有做完一件事情后才可以做下面一件事情:而多线程程序则是雇佣了多名服务员的餐厅,他们可以同时进行着多件事情. JAVA多线程编程的相关知识:创建.启动线程.控制线程.以及多线程的同步操作. 1.概述: 进程是指正在运行中的程序.每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或叫一个执行单元. 线程是指进程中能够独立执行的控

黑马程序员-Java基础-多线程

第一讲  多线程概述 1. 定义 进程:是一个正在执行中的程序.每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元.在程序运行时,会被分配一个内存空间,进程就用于标识这个空间,封装单元,线程才是线程中真正执行的哦部分. 线程:就是进程中的一个独立的控制单元,线程在控制着进程的执行. 一个进程中至少有一个线程. 例子:java JVM 启动时会有一个进程java.exe.该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程就称为主线程

黑马程序员——Java基础知识之多线程协同

多线程协同 线程间的通讯:对资源的操作动作不同,比如说两个卡车一个拉煤一个装煤,但是他们共享了一个资源. 怎么样把这个资源拿出来?怎样把车装满?这个资源当然是一个类,他里面的组成元素就是对象!!现在我们就要有操作对象的思想了,用对象把这车装满,现在一车装一个对象. 等待唤醒机制: 用的不是sleep是wait.flag标记,这是两人沟通的方式.其实每个标记就要做一次等待或者notify,判断wait,改值notify.线程池.notify唤醒里面的线程,按顺序唤醒.wait和notify必须用在

黑马程序员——Java基础---IO(下)

黑马程序员——Java基础---IO(下) ------<a href="http://www.itheima.com" target="blank">Java培训.Android培训.iOS培训..Net培训</a>.期待与您交流! ------ 一.概述 Java除了基本的字节流.字符流之外,还提供了File类.properties类.打印流.序列流等和输入输出相关的类,它们能够帮助我们更好的处理信息.下面将对它们进行简单的介绍. 一.正

黑马程序员——Java基础---集合框架工具类

黑马程序员——Java基础<a href="http://www.itheima.com" target="blank">Java培训.Android培训.iOS培训..Net培训</a>.期待与您交流! ------ 一.概述 Java为操作Set.List和Map提供了一系列工具类,主要有Collections和Arrays.这两个工具类的特点:类中的方法都是静态的,不需要创建对象,直接使用类名调用即可.Collections:是集合对象