多线程(三):线程安全

一.一个典型的Java线程安全例子

复制代码
 1 public class ThreadTest {
 2 
 3     public static void main(String[] args) {
 4         Account account = new Account("123456", 1000);
 5         DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
 6         Thread myThread1 = new Thread(drawMoneyRunnable);
 7         Thread myThread2 = new Thread(drawMoneyRunnable);
 8         myThread1.start();
 9         myThread2.start();
10     }
11 
12 }
13 
14 class DrawMoneyRunnable implements Runnable {
15 
16     private Account account;
17     private double drawAmount;
18 
19     public DrawMoneyRunnable(Account account, double drawAmount) {
20         super();
21         this.account = account;
22         this.drawAmount = drawAmount;
23     }
24 
25     public void run() {
26         if (account.getBalance() >= drawAmount) {  //1
27             System.out.println("取钱成功, 取出钱数为:" + drawAmount);
28             double balance = account.getBalance() - drawAmount;
29             account.setBalance(balance);
30             System.out.println("余额为:" + balance);
31         }
32     }
33 }
34 
35 class Account {
36 
37     private String accountNo;
38     private double balance;
39 
40     public Account() {
41 
42     }
43 
44     public Account(String accountNo, double balance) {
45         this.accountNo = accountNo;
46         this.balance = balance;
47     }
48 
49     public String getAccountNo() {
50         return accountNo;
51     }
52 
53     public void setAccountNo(String accountNo) {
54         this.accountNo = accountNo;
55     }
56 
57     public double getBalance() {
58         return balance;
59     }
60 
61     public void setBalance(double balance) {
62         this.balance = balance;
63     }
64 
65 }
复制代码

上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:

1 取钱成功, 取出钱数为:700.0
2 余额为:300.0
3 取钱成功, 取出钱数为:700.0
4 余额为:-400.0

也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。

经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。

因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。

二.同步方法

对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...

在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synshronized关键字即可。

1 public synchronized void run() {
2        
3     // ....
4  
5 }

三.同步代码块

正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

1 synchronized (obj) {
2             
3     //...
4 
5 }

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)

 四:同步方法和同步代码块的区别

  同步方法:同步方法直接在方法上加synchronized实现加锁,同步方法锁的范围比较大,一般同步的范围越大,性能就越差

  同步代码块:同步代码块则在方法内部加锁,范围较小,性能较好

五.Lock对象同步锁

上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。

使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

复制代码
 1 class X {
 2     
 3     // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
 4     private final Lock lock = new ReentrantLock();
 5     
 6     public void m(){
 7         // 加锁
 8         lock.lock();
 9         
10         //...  需要进行线程安全同步的代码
11         
12         // 释放Lock锁
13         lock.unlock();
14     }
15     
16 }
复制代码

 六:wait()/notify()/notifyAll()线程通信

  wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

  notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

  notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

 七:挂起和恢复

  线程挂起:暂停当前运行的线程,使之进入阻塞状态,并且不会自动恢复运行。suspend()

  线程恢复:让一个挂起的线程恢复运行。resume()

原文地址:http://www.cnblogs.com/lwbqqyumidi/p/3821389.html

原文地址:https://www.cnblogs.com/-scl/p/7727553.html