java线程(三)
线程代码同步与线程锁
为什么要有同步代码块?
线程同步的出现是为了解决多个线程对统一资源操作而引发的数据混乱问题。这里引用一个经典demo-银行转账操作,场景如下,小明的账户目前有1000人民币,他在商场买衣服给商家转账500元,而就在同一时间小明的朋友小张给小明转账500让他帮忙也买一件衣服带给他,如下面代码。
1 package cn.wz.traditional.wf; 2 3 /** 4 * 模拟银行转帐的demo 5 * Created by WangZhe on 2017/5/5. 6 */ 7 public class BankTransferDemo { 8 public static void main(String[] args) { 9 10 final Account xiaoMing=new Account("小明",Double.valueOf(1000)); 11 final Account shangJia=new Account("商家",Double.valueOf(5000)); 12 final Account xiaoZhang=new Account("小张",Double.valueOf(3000)); 13 new Thread(new Runnable() { 14 public void run() { 15 //模拟小明向商家付款 16 boolean falg = xiaoMing.transfer(shangJia, Double.valueOf(500)); 17 if(falg){ 18 System.out.println("转账成功"); 19 }else { 20 System.out.println("系统异常转账失败"); 21 } 22 } 23 }).start(); 24 new Thread(new Runnable() { 36 public void run() { 37 //模拟小张向小明转账 38 boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500)); 39 if(falg){ 40 System.out.println("转账成功"); 41 }else { 42 System.out.println("系统异常转账失败"); 43 } 44 } 45 }).start(); 46 try { 47 Thread.currentThread().sleep(100);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); 52 53 } 54 55 /** 56 * 账户类 57 * Created by WangZhe on 2017/5/5. 58 */ 59 static class Account{ 60 61 public Account() { 62 } 63 64 /** 65 * 创建Account对象实例并进行初始化 66 * @param name 账户名称 67 * @param money 账户金额 68 */ 69 public Account(String name, Double money) { 70 this.name = name; 71 this.money = money; 72 } 73 74 /** 75 * 账户名称 76 */ 77 private String name; 78 /** 79 * 账户金额 80 */ 81 private Double money; 82 83 /** 84 * 转账操作 85 * @param a 被转账的账户实体 86 * @param money 转账金额 87 * @return 88 */ 89 public boolean transfer(Account a,Double money){ 90 Double thisMoney = getMoney();//获取当前账户余额 91 Double aMoney=a.getMoney(); 92 try{ 93 Double newMoney= this.getMoney()-money; 94 a.setMoney(a.getMoney()+money);//给被转入的账户金额上加上转账金额 95 this.setMoney(newMoney);//减去需要转出的金额 96 97 return true; 98 }catch (Exception e){ 99 //系统错误操作取消 100 this.setMoney(thisMoney);//恢复转账用户的账户金额 101 a.setMoney(aMoney);//恢复被转账用户的账户金额 102 return false; 103 } 104 } 105 public String getName() { 106 return name; 107 } 108 109 public void setName(String name) { 110 this.name = name; 111 } 112 113 public Double getMoney() { 114 return money; 115 } 116 117 public void setMoney(Double money) { 118 this.money = money; 119 } 120 } 121 122 }
我们预期的效果是转账后小明的账户金额还有1000元,但是运行上面代码后有两种可能的结果如图所示:
其实还有第三种结果就是转账后余额为500,但大多数情况下其余额为1000,后两种情况出现的几率非常小(基本上和中彩票差不多),但是这种逻辑错误虽然出现的概率很低,但依然是错误,还是要解决的。而这种错误引发的原因就是因为多个线程同时操作一个资源数据而引发的(因为线程占用cpu资源执行的时间、顺序等都是不固定的,有可能该程拿到的数据时cpu刚好调度另一个线程执行,对数据进行更改,再到该线程执行时,其已经拿到了数据但并不知到该数据已经被更改过所以就会引发逻辑错误)
这时就需要对访问竞争资源的代码块进行更改,使当一个线程访问该资源时对该资源进行标记其他线程无法访问该资源只能等该资源访问结束后在进行访问,这样就不会出现上面的情况了。 Java 提供了synchronized 关键字来解决上述问题。用synchronized关键字标示代码我们称之为同步代码块,标示的方法为同步方法。下面是使用synchronized关键字后的代码示例
synchronized 同步关键字
1 package cn.wz.traditional.wf; 2 3 /** 4 * 模拟银行转帐的demo 5 * Created by WangZhe on 2017/5/5. 6 */ 7 public class BankTransferDemo { 8 public static void main(String[] args) { 9 final Account xiaoMing=new Account("小明",Double.valueOf(1000)); 10 final Account shangJia=new Account("商家",Double.valueOf(5000)); 11 final Account xiaoZhang=new Account("小张",Double.valueOf(3000)); 12 new Thread(new Runnable() { 13 public void run() { 14 //模拟小张向商家付款 15 boolean falg = xiaoMing.transfer(shangJia, Double.valueOf(500)); 16 if(falg){ 17 System.out.println("转账成功"); 18 }else { 19 System.out.println("系统异常转账失败"); 20 } 21 } 22 }).start(); 23 new Thread(new Runnable() { 24 public void run() { 25 //模拟小张向小明转账 26 boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500)); 27 if(falg){ 28 System.out.println("转账成功"); 29 }else { 30 System.out.println("系统异常转账失败"); 31 } 32 } 33 }).start(); 34 try { 35 Thread.currentThread().sleep(100);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); 40 41 } 42 43 /** 44 * 账户类 45 * Created by WangZhe on 2017/5/5. 46 */ 47 static class Account{ 48 49 public Account() { 50 } 51 52 /** 53 * 创建Account对象实例并进行初始化 54 * @param name 账户名称 55 * @param money 账户金额 56 */ 57 public Account(String name, Double money) { 58 this.name = name; 59 this.money = money; 60 } 61 62 /** 63 * 账户名称 64 */ 65 private String name; 66 /** 67 * 账户金额 68 */ 69 private Double money; 70 71 /** 72 * 转账操作 73 * @param a 被转账的账户实体 74 * @param money 转账金额 75 * @return 76 */ 77 public synchronized boolean transfer(Account a,Double money){ 78 synchronized (a) { 79 Double thisMoney = getMoney();//获取当前账户余额 80 81 Double aMoney = a.getMoney(); 82 try { 83 Double newMoney = this.getMoney() - money; 84 a.setMoney(a.getMoney() + money);//给被转入的账户金额上加上转账金额 85 this.setMoney(newMoney);//减去需要转出的金额 86 87 return true; 88 } catch (Exception e) { 89 //系统错误操作取消 90 this.setMoney(thisMoney);//恢复转账用户的账户金额 91 a.setMoney(aMoney);//恢复被转账用户的账户金额 92 return false; 93 } 94 } 95 } 96 97 public String getName() { 98 return name; 99 } 100 101 public void setName(String name) { 102 this.name = name; 103 } 104 105 public Double getMoney() { 106 return money; 107 } 108 109 public void setMoney(Double money) { 110 this.money = money; 111 } 112 } 113 114 }
从上面代码中我们可以看到,synchronized关键字有两种写法,一种是直接在方法上另一种是在方法内部单独形成代码块,其实这两种方式是由区别的,其锁定的资源对象是不同的。下面介绍synchronized关键字作用在不同地方是锁定的资源对象。
作用在成员方法上:
synchronized作用在成员方法上锁定的是当前对象的实例
synchronized作用在类方法上锁定的是类的字节码
synchronized单独形成功代码块是锁定的是其后面笑小括号()中的资源对象。
死锁
sychronized同步代码块虽然可以解决因资源竞争所引发的数据混乱为题,但使用syschronized时一定要小心造成死锁,那什么是死锁呢?简单来说就是线程A拥有资源A的锁,线程B拥有线程B的锁
然后线程A在不释放资源A的锁的情况下尝试获取资源B的锁,因为资源B的锁被线程B占用所以线程A只能等待线程B释放资源B的锁之后才能继续执行,而同时线程B在不释放资源B的前提下尝试获取资源A的锁,但资源A被线程A占用,线程B只能等线程A释放资源A的锁再能继续执行,就这样两个线程一直互相等待对方释放锁无法继续执行,从而引发系统崩溃。如下面代码:
1 package cn.wz.traditional.wf; 2 3 /** 4 * 模拟银行转帐的demo 5 * Created by WangZhe on 2017/5/5. 6 */ 7 public class BankTransferDemo { 8 public static void main(String[] args) { 9 final Account xiaoMing=new Account("小明",Double.valueOf(1000)); 10 final Account shangJia=new Account("商家",Double.valueOf(5000)); 11 final Account xiaoZhang=new Account("小张",Double.valueOf(3000)); 12 new Thread(new Runnable() { 13 public void run() { 14 //模拟小张向商家付款 15 boolean falg = xiaoMing.transfer(xiaoZhang, Double.valueOf(500)); 16 if(falg){ 17 System.out.println("转账成功"); 18 }else { 19 System.out.println("系统异常转账失败"); 20 } 21 } 22 }).start(); 23 new Thread(new Runnable() { 24 public void run() { 25 //模拟小张向小明转账 26 boolean falg = xiaoZhang.transfer(xiaoMing, Double.valueOf(500)); 27 if(falg){ 28 System.out.println("转账成功"); 29 }else { 30 System.out.println("系统异常转账失败"); 31 } 32 } 33 }).start(); 34 try { 35 Thread.currentThread().sleep(10000);//主线程休眠1秒,等转账操作完成之后在输出小明的账户余额 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 System.out.println("转账后小明的账务余额为:"+xiaoMing.getMoney()); 40 41 } 42 43 /** 44 * 账户类 45 * Created by WangZhe on 2017/5/5. 46 */ 47 static class Account{ 48 49 public Account() { 50 } 51 52 /** 53 * 创建Account对象实例并进行初始化 54 * @param name 账户名称 55 * @param money 账户金额 56 */ 57 public Account(String name, Double money) { 58 this.name = name; 59 this.money = money; 60 } 61 62 /** 63 * 账户名称 64 */ 65 private String name; 66 /** 67 * 账户金额 68 */ 69 private Double money; 70 71 /** 72 * 转账操作 73 * @param a 被转账的账户实体 74 * @param money 转账金额 75 * @return 76 */ 77 public synchronized boolean transfer(Account a,Double money){ 78 try { 79 Thread.currentThread().sleep(100); 80 } catch (InterruptedException e) { 81 e.printStackTrace(); 82 } 83 synchronized (a) { 84 Double thisMoney = getMoney();//获取当前账户余额 85 86 Double aMoney = a.getMoney(); 87 try { 88 Double newMoney = this.getMoney() - money; 89 this.setMoney(newMoney);//减去需要转出的金额 90 a.setMoney(a.getMoney() + money);//给被转入的账户金额上加上转账金额 91 92 return true; 93 } catch (Exception e) { 94 //系统错误操作取消 95 this.setMoney(thisMoney);//恢复转账用户的账户金额 96 a.setMoney(aMoney);//恢复被转账用户的账户金额 97 return false; 98 } 99 } 100 } 101 102 public String getName() { 103 return name; 104 } 105 106 public void setName(String name) { 107 this.name = name; 108 } 109 110 public Double getMoney() { 111 return money; 112 } 113 114 public void setMoney(Double money) { 115 this.money = money; 116 } 117 } 118 119 }
死锁发生的概率是极低的,真要让你认真的写出一个死锁的代码可能还真不一定能写出来,但是一个完整的程序还是要尽可能的避免死锁的出现。
补充
Object对象改变线程状态的三个方法
在之前提到过Object对象有三个方法可以改变线程的状态,分别是wait、notify和notifyall。这里需要注意的是这三个方法只能在同步代码块儿里进行调用,不然将引发异常(具体什么异常自己试下就知道了)。然后notify和notifyall方法虽然会唤醒线程,但并不会释放锁。
1 package cn.wz.traditional.wf; 2 3 import java.util.Date; 4 5 /** 6 * Created by WangZhe on 2017/5/4. 7 */ 8 public class WaitAndNotify { 9 public static void main(String[] args) { 10 final WaitAndNotify.A a=new WaitAndNotify().new A(4); 11 new Thread(new Runnable() { 12 public void run() { 13 a.sayHi(); 14 } 15 }).start(); 16 17 new Thread(new Runnable() { 18 public void run() { 19 try { 20 synchronized (a){ 21 System.out.println("您好!"); 22 a.notify(); 23 System.out.println(System.currentTimeMillis());//记录唤醒线程的时间 24 Thread.currentThread().sleep(10000);//使当前想成持有对象a的锁睡眠10秒钟 25 } 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 } 30 }).start(); 31 } 32 class A{ 33 34 private Integer data; 35 public A(Integer data){ 36 this.data=data; 37 } 38 public void sayHi(){ 39 synchronized (this){ 40 System.out.println("输出data:"+data); 41 System.out.println("调用wait方法"); 42 try { 43 this.wait(); 44 } catch (InterruptedException e) { 45 e.printStackTrace(); 46 } 47 } 48 synchronized (this){ 49 this.data+=1; 50 System.out.println(System.currentTimeMillis());//记录该线程再次获取对象所得时间 51 System.out.println("更改data后输出:"+data); 52 } 53 } 54 55 } 56 }