• 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、死锁

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

  • 相关阅读:
    Redis 的set集合使用及set集合和list列表的区别和应用场景
    c++ JsonCpp Parse对Json字符串解析转换判断的补充 Json格式验证
    使用程序将文件夹名字输出到文本文件中
    MVC 使用PageList进行分页
    sql从某不连续的数字中将其分段并找出缺失的数字并分段
    手机跟本机调试
    javascript中常用的一些功能及正则表达式的用法
    配置tomcat免安装版服务器
    xheditor上传图片的java实现
    eclipse 使用技巧
  • 原文地址:https://www.cnblogs.com/bailiyi/p/5310448.html
Copyright © 2020-2023  润新知