1、线程安全问题

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

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

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

账户类:

 public class Account {

     public int balance = 10;//账户余额

     //取钱的方法

     public void draw(int money){

        if (balance >= money) {

            //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误

            if ("Thread-1".equals(Thread.currentThread().getName())) {

               try {

                   Thread.sleep(1000);

               } catch (InterruptedException e) {

                   e.printStackTrace();

               }

            }

            balance = balance - money;

            System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);

        }else{

            System.out.println("取钱失败,余额不足。余额:"+balance);

        }

     }

 }

取钱线程:

 public class DrawThread implements Runnable{

     public Account account;

     public DrawThread(Account account){

        this.account = account;

     }

     @Override

     public void run() {

        //写个死循环,模拟不停取钱

        while(true){

            try {

               //此处睡眠500毫秒是为了让程序运行的慢一点,方便观察

               Thread.sleep(500);

            } catch (InterruptedException e) {

               e.printStackTrace();

            }

            //调用取钱方法,一次取4元

            account.draw(4);

        }

     }

 }

测试类:

 public class TestDraw {

     public static void main(String[] args) {

        //创建一个账户

        Account account = new Account();

        //创建两个线程,从同一个账户取钱

        DrawThread dtOne = new DrawThread(account);

        DrawThread dtTwo = new DrawThread(account);

        //启动线程

        new Thread(dtOne).start();

        new Thread(dtTwo).start();

     }

 }

测试结果:

 Thread-0取钱成功,余额:6

 Thread-0取钱成功,余额:2

 取钱失败,余额不足。余额:2

 Thread-1取钱成功,余额:-2

 取钱失败,余额不足。余额:-2

 取钱失败,余额不足。余额:-2

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

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

2、线程同步

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

同步代码块:

同步代码块的语法为:

synchronized (obj) {

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

}

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

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

  //取钱的方法

     public void draw(int money){

        //同步代码块开始

        synchronized (this) {

            if (balance >= money) {

               //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误

               if ("Thread-1".equals(Thread.currentThread().getName())) {

                   try {

                      Thread.sleep(1000);

                   } catch (InterruptedException e) {

                      e.printStackTrace();

                   }

               }

               balance = balance - money;

               System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);

            }else{

               System.out.println("取钱失败,余额不足。余额:"+balance);

            }

        }

        //同步代码块结束

     }

同步方法:

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

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

 //取钱的方法

     public synchronized void draw(int money){

        if (balance >= money) {

            //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误

            if ("Thread-1".equals(Thread.currentThread().getName())) {

               try {

                   Thread.sleep(1000);

               } catch (InterruptedException e) {

                   e.printStackTrace();

               }

            }

            balance = balance - money;

            System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);

        }else{

            System.out.println("取钱失败,余额不足。余额:"+balance);

        }

     }

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

释放同步监视器的锁定

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

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

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

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

同步锁

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

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

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

  //定义锁对象

     private final Lock lock = new ReentrantLock();

     //取钱的方法

     public void draw(int money){

        //加锁

        lock.lock();

        try {

            if (balance >= money) {

               //此处让线程Thread-1睡眠1秒,是为了模拟线程不安全造成的错误

                if ("Thread-1".equals(Thread.currentThread().getName())) {

                   try {

                      Thread.sleep(1000);

                   } catch (InterruptedException e) {

                      e.printStackTrace();

                   }

               }

               balance = balance - money;

               System.out.println(Thread.currentThread().getName()+"取钱成功,余额:"+balance);

            }else{

               System.out.println("取钱失败,余额不足。余额:"+balance);

            }

        } finally {

            //为了确保解锁,放在finally里

            lock.unlock();

        }

     }

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

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

3、死锁

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

java多线程回顾3:线程安全的更多相关文章

  1. Java多线程系列--“JUC线程池”06之 Callable和Future

    概要 本章介绍线程池中的Callable和Future.Callable 和 Future 简介示例和源码分析(基于JDK1.7.0_40) 转载请注明出处:http://www.cnblogs.co ...

  2. Java多线程系列--“JUC线程池”02之 线程池原理(一)

    概要 在上一章"Java多线程系列--“JUC线程池”01之 线程池架构"中,我们了解了线程池的架构.线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析Th ...

  3. Java多线程系列--“JUC线程池”03之 线程池原理(二)

    概要 在前面一章"Java多线程系列--“JUC线程池”02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代 ...

  4. Java多线程系列--“JUC线程池”04之 线程池原理(三)

    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509960.html 本章介绍线程池的生命周期.在"Java多线程系列--“基础篇”01之 基 ...

  5. Java多线程系列--“JUC线程池”05之 线程池原理(四)

    概要 本章介绍线程池的拒绝策略.内容包括:拒绝策略介绍拒绝策略对比和示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3512947.html 拒绝策略 ...

  6. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  7. 转:java多线程CountDownLatch及线程池ThreadPoolExecutor/ExecutorService使用示例

    java多线程CountDownLatch及线程池ThreadPoolExecutor/ExecutorService使用示例 1.CountDownLatch:一个同步工具类,它允许一个或多个线程一 ...

  8. Java多线程——进程和线程

    Java多线程——进程和线程 摘要:本文主要解释在Java这门编程语言中,什么是进程,什么是线程,以及二者之间的关系. 部分内容来自以下博客: https://www.cnblogs.com/dolp ...

  9. Java多线程之守护线程

    Java多线程之守护线程 一.前言 Java线程有两类: 用户线程:运行在前台,执行具体的任务,程序的主线程,连接网络的子线程等都是用户线程 守护线程:运行在后台,为其他前台线程服务 特点:一旦所有用 ...

  10. Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗

    在上一章,为大家介绍了线程的一些基础知识,线程的创建与终止.本期将为各位带来线程的生命周期与常用方法.关注我的公众号「Java面典」了解更多 Java 相关知识点. 线程生命周期 一个线程不是被创建了 ...

随机推荐

  1. Vue中插槽指令

    08.29自我总结 Vue中插槽指令 意义 就是在组件里留着差值方便后续组件内容新增 而且由于插件是写在父级中数据可以直接父级中传输而不需要传子再传父有些情况会减少写代码量 示例 <div id ...

  2. 关于sqlmapapi一点记录

    关于sqlmapapi自己练习的还是很少 今天看见freebuf上师傅的分享的内容 自己练习了一下 来自: https://www.freebuf.com/articles/web/204875.ht ...

  3. openssl之DH(Diffie–Hellman)加密

    //加密机制初始化 g_HDMgr.init(); //对方的public key BIGNUM* peerPubKey = NULL; peerPubKey = BN_bin2bn((unsigne ...

  4. HDU 3873 Invade the Mars(带限制条件的Dijkstra)

    题目网址:http://acm.hdu.edu.cn/showproblem.php?pid=3873 思路: 军队可以先等待在城市外面,等保护该城市的城市都被攻破后,直接进城(即进城不用耗费时间). ...

  5. 浅谈线段树 Segment Tree

    众所周知,线段树是algo中很重要的一项! 一.简介 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在 ...

  6. ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库

    1. 前言     前面的博文中,无论是作为client端还是server端,它们之间的通信都是通过具体的IP地址来寻址.通过IP地址来寻址,本身就是一个弊端,用户怎么会去记住这些魔法数字呢?那么有没 ...

  7. 百万年薪python之路 -- re模块

    re模块 re模块是python用来描述正则表达式的一个模块. 正则表达式本身也和python没有什么关系,就是匹配字符串内容的一种规则. 官方定义:正则表达式是对字符串操作的一种逻辑公式,就是用事先 ...

  8. Centos 新建用户

    Centos 新建用户 为什么要新建用户? 因为root的权限太多,不方便多人多角色使用,所以添加一个用户 添加用户 新建用户 adduser '用户名' 添加用户密码 passwd '用户名' 输入 ...

  9. redis之PubSub

    前面我们讲了 Redis 消息队列的使用方法,但是没有提到 Redis 消息队列的不足之处,那就是它不支持消息的多播机制. 消息多播 消息多播允许生产者生产一次消息,中间件负责将消息复制到多个消息队列 ...

  10. Redis(九)哨兵:Redis Sentinel

    Redis的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景这种故障处理的方式是无法接受的. Redis从2.8开始正式 ...