java多线程回顾3:线程安全

1、线程安全问题

关于线程安全问题,有一个经典案例:银行取钱问题。

假设有一个账户,有两个线程从账户里取钱,如果余额大于取钱金额,则取钱成功,反之则失败。

下面来看下线程不安全的程序会出什么问题。

账户类:

 1 public class Account {
 2 
 3     public int balance = 10;//账户余额
 4 
 5    
 6 
 7     //取钱的方法
 8 
 9     public void draw(int money){
10 
11        if (balance >= money) {
12 
13            //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误
14 
15            if ("Thread-1".equals(Thread.currentThread().getName())) {
16 
17               try {
18 
19                   Thread.sleep(1000);
20 
21               } catch (InterruptedException e) {
22 
23                   e.printStackTrace();
24 
25               }
26 
27            }
28 
29           
30 
31            balance = balance - money;
32 
33            System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);
34 
35        }else{
36 
37            System.out.println("取钱失败,余额不足。余额:"+balance);
38 
39        }
40 
41     }
42 
43 }
View Code

取钱线程:

 1 public class DrawThread implements Runnable{
 2 
 3     public Account account;
 4 
 5     public DrawThread(Account account){
 6 
 7        this.account = account;
 8 
 9     }
10 
11     @Override
12 
13     public void run() {
14 
15        //写个死循环,模拟不停取钱
16 
17        while(true){
18 
19            try {
20 
21               //此处睡眠500毫秒是为了让程序运行的慢一点,方便观察
22 
23               Thread.sleep(500);
24 
25            } catch (InterruptedException e) {
26 
27               e.printStackTrace();
28 
29            }
30 
31            //调用取钱方法,一次取4元
32 
33            account.draw(4);
34 
35        }
36 
37     }
38 
39 }
View Code

测试类:

 1 public class TestDraw {
 2 
 3     public static void main(String[] args) {
 4 
 5        //创建一个账户
 6 
 7        Account account = new Account();
 8 
 9        //创建两个线程,从同一个账户取钱
10 
11        DrawThread dtOne = new DrawThread(account);
12 
13        DrawThread dtTwo = new DrawThread(account);
14 
15        //启动线程
16 
17        new Thread(dtOne).start();
18 
19        new Thread(dtTwo).start();
20 
21     }
22 
23 }
View Code

测试结果:

 1 Thread-0取钱成功,余额:6
 2 
 3 Thread-0取钱成功,余额:2
 4 
 5 取钱失败,余额不足。余额:2
 6 
 7 Thread-1取钱成功,余额:-2
 8 
 9 取钱失败,余额不足。余额:-2
10 
11 取钱失败,余额不足。余额:-2
View Code

这个结果显然是不对的,当余额小于取钱金额时,程序应该取钱失败,而不是把余额变成负数。之所以会出现这种情况,是因为当线程Thread-1通过balance >= money之后被阻塞了,这时候线程Thread-0也通过了balance >= money判断,并且把钱取走了。这之后,Thread-1重新开始运行,继续取钱,于是余额就变成负数了。

在实际的开发中,由于线程调度不可控,也可能出现类似的情况,所以对多线程操作一定要注意线程安全。

2、线程同步

为了解决线程安全问题,有三种方法:同步代码块、同步方法、同步锁

同步代码块:

同步代码块的语法为:

synchronized (obj) {

    …

    //此处代码就是同步代码块

}

以上代码的obj叫做同步监视器,以上代码的含义是,线程开始执行同步代码块之前,必须获得对同步监视器的锁定。一般来说,我们把并发时共享的资源作为同步监视器,例子中账户就是共享的资源,所以写this,表示对象本身。

使用同步代码块改造的账户类如下:

   

 1  //取钱的方法
 2 
 3     public void draw(int money){
 4 
 5        //同步代码块开始
 6 
 7        synchronized (this) {
 8 
 9            if (balance >= money) {
10 
11               //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误
12 
13               if ("Thread-1".equals(Thread.currentThread().getName())) {
14 
15                   try {
16 
17                      Thread.sleep(1000);
18 
19                   } catch (InterruptedException e) {
20 
21                      e.printStackTrace();
22 
23                   }
24 
25               }
26 
27              
28 
29               balance = balance - money;
30 
31               System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);
32 
33            }else{
34 
35               System.out.println("取钱失败,余额不足。余额:"+balance);
36 
37            }
38 
39        }
40 
41        //同步代码块结束
42 
43     }
View Code

同步方法:

同步方法即使用synchronized修饰方法,不用显示指定同步监视器,其同步监视器就是this,即对象本身。

使用同步方法改造的账户类如下:

 1 //取钱的方法
 2 
 3     public synchronized void draw(int money){
 4 
 5        if (balance >= money) {
 6 
 7            //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误
 8 
 9            if ("Thread-1".equals(Thread.currentThread().getName())) {
10 
11               try {
12 
13                   Thread.sleep(1000);
14 
15               } catch (InterruptedException e) {
16 
17                   e.printStackTrace();
18 
19               }
20 
21            }
22 
23           
24 
25            balance = balance - money;
26 
27            System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);
28 
29        }else{
30 
31            System.out.println("取钱失败,余额不足。余额:"+balance);
32 
33        }
34 
35     }
View Code

需要注意的是,synchronized不可以修饰属性和构造方法。

释放同步监视器的锁定

以下情况将释放对同步监视器的锁定:

  • 同步方法(代码块)执行完毕。
  • 执行中遇到return、break终止了同步方法(代码块)的执行。
  • 同步方法(代码块)抛出了未处理的异常或错误。
  • 调用了同步方法(代码块)的wait()方法,此时当前线程暂停,并释放对同步监视器的锁定。

以下情况不会释放对同步监视器的锁定:

  • 调用sleep、yield方法,当前线程会暂停,但不会释放锁定。
  • 其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放对同步监视器的锁定。注意,尽量不要使用suspend和resume方法,容易死锁。

同步锁

从JDK1.5开始,可以通过显示定义同步锁来实现线程安全。

使用方法和synchronized大同小异,基本上也是加锁—执行代码—解锁这么一个过程。

使用Lock改造的取钱方法如下:

   

 1  //定义锁对象
 2 
 3     private final Lock lock = new ReentrantLock();
 4 
 5     //取钱的方法
 6 
 7     public void draw(int money){
 8 
 9        //加锁
10 
11        lock.lock();
12 
13        try {
14 
15            if (balance >= money) {
16 
17               //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误
18 
19                if ("Thread-1".equals(Thread.currentThread().getName())) {
20 
21                   try {
22 
23                      Thread.sleep(1000);
24 
25                   } catch (InterruptedException e) {
26 
27                      e.printStackTrace();
28 
29                   }
30 
31               }
32 
33              
34 
35               balance = balance - money;
36 
37               System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);
38 
39            }else{
40 
41               System.out.println("取钱失败,余额不足。余额:"+balance);
42 
43            }
44 
45        } finally {
46 
47            //为了确保解锁,放在finally里
48 
49            lock.unlock();
50 
51        }
52 
53     }
View Code

以上代码中,为了确保最后能释放锁,所以把解锁代码放在finally中。

和synchronized相比,Lock在使用上更灵活。上例中使用的是可重入锁,即线程可以对已加锁的代码再加锁。此外还有读写锁等。

3、死锁

两个线程相互等待对方释放对同步监视器的锁定,这种情况叫死锁。

原文地址:https://www.cnblogs.com/bailiyi/p/5310448.html