多线程编程时有趣的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在
运行过程中偶尔出现问题,那也是由于我们编程不当引起的。当使用多个线程来访问同一个数据时,非常容易出现线程安全
可以看到取消DrawThread类注释掉的那段代码和未取消注释的运行结果完全不同,并且跟现实情况不相符。
上面程序中增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰修改方法,把该方法编程同步方法。同步
方法的监视器是this,因此对于同一个账户Account而言,任何时刻只能有一条线程获得对Account对象的锁定,然后进入draw
方法执行取钱操作--这样可以保证多条线程并发取钱的线程安全。
注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
因为Account类中已经提供了draw方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该类只要调用
Account对象的draw方法来执行取钱操作。程序如下:
注意:此时的程序把draw方法定义在Account里,而不是直接在run方法中实现取钱逻辑,这种做法更符合面向对象规则。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
== 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源 (竞争资源也就是共享资源)的方法进行同步
。例如上面的Account类中accountNo属性无须同步,所以程序只对draw方法进行同步控制。
== 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程
安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境下使用线程安全版本。
运行过程中偶尔出现问题,那也是由于我们编程不当引起的。当使用多个线程来访问同一个数据时,非常容易出现线程安全
问题。
线程安全问题
关于线程安全的问题,有一个经典的问题:银行取钱的问题。银行取钱的基本流程基本可以分为如下几个步骤:
1、用户输入账户、密码,系统判断用户的账户、密码是否匹配。
2、用户输入取款金额。
3、系统判断账户余额是否大于取款金额。
4、根据以上判断,要么取款成功,要么失败。
咋一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程并没有任何问题。但一旦将这个流程放在多线程
并发的场景下,就有可能出现问题。我们按照上面的流畅去编写取款程序,而且我们使用两条线程来模拟取钱操作,模拟
两个人使用同一个账户并发取钱的问题。当然我们不管检查账户和密码的操作,仅仅模拟了后面的三部操作。下面先定义了
一个账户类,该账户类封装了账户编号和余额两个属性。
1、用户输入账户、密码,系统判断用户的账户、密码是否匹配。
2、用户输入取款金额。
3、系统判断账户余额是否大于取款金额。
4、根据以上判断,要么取款成功,要么失败。
咋一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程并没有任何问题。但一旦将这个流程放在多线程
并发的场景下,就有可能出现问题。我们按照上面的流畅去编写取款程序,而且我们使用两条线程来模拟取钱操作,模拟
两个人使用同一个账户并发取钱的问题。当然我们不管检查账户和密码的操作,仅仅模拟了后面的三部操作。下面先定义了
一个账户类,该账户类封装了账户编号和余额两个属性。
- public class Account {
- /** 封装账户编号、账户余额两个属性 */
- private String accountNo;
- private double balance;
- public Account() {
- }
- public Account(String accountNo, double balance) {
- this.accountNo = accountNo;
- this.balance = balance;
- }
- public String getAccountNo() {
- return accountNo;
- }
- public void setAccountNo(String accountNo) {
- this.accountNo = accountNo;
- }
- public double getBalance() {
- return balance;
- }
- public void setBalance(double balance) {
- this.balance = balance;
- }
- //下面两个方法根据accountNo来计算Account的hashCode和判断equals
- public int hashCode(){
- return accountNo.hashCode();
- }
- public boolean equals(Object obj){
- if(obj != null && obj.getClass() == Account.class){
- Account target = (Account)obj;
- return target.getAccountNo().equals(accountNo);
- }
- return false;
- }
- }
- public class DrawThread extends Thread{
- /** 模拟用户账户 */
- private Account account;
- /**当前取钱线程 所希望取的钱数 */
- public double drawAmount;
- public DrawThread(String name,Account account,double drawAmount){
- super(name);
- this.account = account;
- this.drawAmount = drawAmount;
- }
- /**
- * 当多条线程修改同一个共享数据时,将涉及数据安全问题
- */
- @Override
- public void run() {
- super.run();
- if(account.getBalance() >= drawAmount){
- //吐出钞票
- System.out.println(getName() + "取钱成功!吐出钞票" + drawAmount);
- //这一段代码先注释掉
- /* try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }*/
- //修改余额
- account.setBalance(account.getBalance() - drawAmount);
- System.out.println("\t余额为:" + account.getBalance());
- }else{
- System.out.println(getName() + "取钱失败!余额不足");
- }
- }
- }
- public static void main(String[] args) {
- //创建一个账户
- Account account = new Account("123", 1000);
- //模拟两个线程对同一个账户取钱
- new DrawThread("甲", account, 800).start();
- new DrawThread("乙", account, 800).start();
- }
可以看到取消DrawThread类注释掉的那段代码和未取消注释的运行结果完全不同,并且跟现实情况不相符。
同步代码块
之所以会出现如上图片的情况,其语音是因为run方法的方法体不具有同步安全性:程序中有两条并发线程在修改Account
对象,而且系统恰好在粗体字代码处执行线程切换,切换给另一条线程修改Account对象,所以就出现了问题。
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块
。同步代码块的用法格式如下:
synchronized(obj){
...
//此处的代码就是同步代码块
}
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先
获得对同步监视器的锁定。
注意:任何时候只有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步
监视器的锁定。
虽然Java程序允许使用任何对象来作为同步监视器,但想以下同步监视器的目的:阻止两条以上的线程对同一个共享资源
进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑
使用账户(Account)作为同步监视器。我们只需要把程序修改如下
对象,而且系统恰好在粗体字代码处执行线程切换,切换给另一条线程修改Account对象,所以就出现了问题。
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块
。同步代码块的用法格式如下:
synchronized(obj){
...
//此处的代码就是同步代码块
}
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先
获得对同步监视器的锁定。
注意:任何时候只有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步
监视器的锁定。
虽然Java程序允许使用任何对象来作为同步监视器,但想以下同步监视器的目的:阻止两条以上的线程对同一个共享资源
进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑
使用账户(Account)作为同步监视器。我们只需要把程序修改如下
- public class DrawThread extends Thread {
- /** 模拟用户账户 */
- private Account account;
- /** 当前取钱线程 所希望取的钱数 */
- public double drawAmount;
- public DrawThread(String name, Account account, double drawAmount) {
- super(name);
- this.account = account;
- this.drawAmount = drawAmount;
- }
- /**
- * 当多条线程修改同一个共享数据时,将涉及数据安全问题
- */
- @Override
- public void run() {
- super.run();
- /**
- * 使用Account作为同步监视器,热河线程进入下面同步代码块之前,必须先获得对account账户的锁定,其他线程
- * 无法获得锁,也就无法修改它 这种做法逻辑符合:加锁--修改完成--释放锁 逻辑
- */
- synchronized (account) {
- if (account.getBalance() >= drawAmount) {
- // 吐出钞票
- System.out.println(getName() + "取钱成功!吐出钞票" + drawAmount);
- // 这一段代码先注释掉
- /*
- * try { Thread.sleep(1); } catch (InterruptedException e) {
- * e.printStackTrace(); }
- */
- // 修改余额
- account.setBalance(account.getBalance() - drawAmount);
- System.out.println("\t余额为:" + account.getBalance());
- } else {
- System.out.println(getName() + "取钱失败!余额不足");
- }
- }// 同步代码块结束,该线程释放同步锁
- }
- }
同步方法
与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个
方法,则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是
该对象本身。
通过使用同步方法可以非常方便的将某类变成线程安全的类,线程安全的类具有如下特征:
== 该类的对象可以被多个线程安全的访问
== 每个线程调用该对象的任意方法之后都将得到正确结果。
== 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
我们知道有可变类和不可变类,其中不可变类总是线程安全的,因为它的对象的状态不可改变。但可变对象需要额外的
方法来保证其线程安全。例如上面的Account就是一个可变类,它的account和balance两个属性都可变,当两个线程同时
修改Account对象的balance属性时,程序就出现了异常。下面我们将Account类对balance的访问设置成线程安全的,那么
程序只要把修改的balance的方法修改成同步方法即可。程序如下:
方法,则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是
该对象本身。
通过使用同步方法可以非常方便的将某类变成线程安全的类,线程安全的类具有如下特征:
== 该类的对象可以被多个线程安全的访问
== 每个线程调用该对象的任意方法之后都将得到正确结果。
== 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
我们知道有可变类和不可变类,其中不可变类总是线程安全的,因为它的对象的状态不可改变。但可变对象需要额外的
方法来保证其线程安全。例如上面的Account就是一个可变类,它的account和balance两个属性都可变,当两个线程同时
修改Account对象的balance属性时,程序就出现了异常。下面我们将Account类对balance的访问设置成线程安全的,那么
程序只要把修改的balance的方法修改成同步方法即可。程序如下:
- public class Account {
- /** 封装账户编号、账户余额两个属性 */
- private String accountNo;
- private double balance;
- public Account() {
- }
- public Account(String accountNo, double balance) {
- this.accountNo = accountNo;
- this.balance = balance;
- }
- public String getAccountNo() {
- return accountNo;
- }
- public void setAccountNo(String accountNo) {
- this.accountNo = accountNo;
- }
- // 因为账户余额不允许随便修改,所以取消balance属性的setter方法
- public double getBalance() {
- return balance;
- }
- // 下面两个方法根据accountNo来计算Account的hashCode和判断equals
- public int hashCode() {
- return accountNo.hashCode();
- }
- public boolean equals(Object obj) {
- if (obj != null && obj.getClass() == Account1.class) {
- Account1 target = (Account1) obj;
- return target.getAccountNo().equals(accountNo);
- }
- return false;
- }
- // 提供一个线程安全的draw方法来完成取钱操作
- public synchronized void draw(double drawAmount) {
- // 账户余额大于取钱数目
- if (balance >= drawAmount) {
- // 吐出钞票
- System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票"
- + drawAmount);
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 修改余额
- balance -= drawAmount;
- System.out.println("\t余额为:" + balance);
- } else {
- System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足");
- }
- }
- }
方法的监视器是this,因此对于同一个账户Account而言,任何时刻只能有一条线程获得对Account对象的锁定,然后进入draw
方法执行取钱操作--这样可以保证多条线程并发取钱的线程安全。
注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
因为Account类中已经提供了draw方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该类只要调用
Account对象的draw方法来执行取钱操作。程序如下:
- public class DrawThread extends Thread{
- /** 模拟用户账户 */
- private Account account;
- /** 当前取钱线程 所希望取的钱数 */
- public double drawAmount;
- public DrawThread(String name, Account account, double drawAmount) {
- super(name);
- this.account = account;
- this.drawAmount = drawAmount;
- }
- @Override
- public void run() {
- super.run();
- account.draw(drawAmount);
- }
- }
注意:此时的程序把draw方法定义在Account里,而不是直接在run方法中实现取钱逻辑,这种做法更符合面向对象规则。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
== 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源 (竞争资源也就是共享资源)的方法进行同步
。例如上面的Account类中accountNo属性无须同步,所以程序只对draw方法进行同步控制。
== 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程
安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境下使用线程安全版本。