1、买票练习
测试下面代码是否存在线程安全问题,并尝试改正
模拟多个线程调用同一个购票窗口对象进行买票操作是否有线程安全问题(
证明线程线程安全:
购买数量+余票数量=1000)
publicclassExerciseSell{publicstaticvoidmain(String[] args){// 售票窗口(余票数:2000)TicketWindow ticketWindow =newTicketWindow(1000);// 所有线程的集合List<Thread> list =newArrayList<>();// 卖出的票数统计(可对其进行累加求和)List<Integer> sellCount =newVector<>();// 模拟2000同时在购票窗口抢票for(int i =0; i <2000; i++){Thread t =newThread(()->{// 买票int count = ticketWindow.sell(randomAmount());// 加入随机睡眠时间,使其可以产生指令交错(指令交错现象可较为明显些)try{Thread.sleep(random(10));}catch(InterruptedException e){
e.printStackTrace();}// add( )是线程安全的,底层代码使用synchronized对其做了线程安全保护
sellCount.add(count);});
list.add(t);
t.start();}// 等待2000个线程对象运行结束后再做统计(主线程等所有线程执行完毕)
list.forEach((t)->{try{//
t.join();}catch(InterruptedException e){
e.printStackTrace();}});// 买出去的票求和【 使用流的API方便其进行累加计算(将其包装类变为int整形)】
log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());}// Random 为线程安全staticRandom random =newRandom();// 随机 1~5(模拟每个人买票数量)publicstaticintrandomAmount(){return random.nextInt(5)+1;}}// 售票数量classTicketWindow{// 初始余票数privateint count;publicTicketWindow(int count){this.count = count;}// 获取余票数publicintgetCount(){return count;}//售票(购买票的数量)publicintsell(int amount){// 余票数>=购买数量===>售票成功if(this.count >= amount){this.count -= amount;return amount;}else{// 购票失败(返回0)return0;}}}
需将程序多次运行(发生指令交错后才可能会出现线程不安全的情况)
运行结果:
测试问题
:多线程环境下进行测试某个类的线程安全,安全问题不容易复现,上述测试中使用sleep()增加其时间间隔的方式期望线程上下文切换的机率增大,使现象更容易出现。为节省时间我们还可以使用编写测试脚本的方式(一旦出现线程安全问题,会在某次循环中将其展示出来)。
解题
若要分析线程安全问题,需考虑哪部分代码属于临界区(找出临界区便可对其加锁)
临界区
:对共享变量有读写操作的代码片段
以上案例中sell(int amount)方法内部对共享变量即有读又有写操作,可以在方法上加上synchronized关键字进行保护
// 售票 (一个售票窗口只有一个余额数)publicsynchronizedintsell(int amount){if(this.count >= amount){this.count -= amount;return amount;}else{return0;}}
2、转账练习
测试下面代码是否存在线程安全问题,并尝试改正
importlombok.extern.slf4j.Slf4j;importjava.util.Random;@Slf4j(topic ="c.ExerciseTransfer")publicclassExerciseTransfer{publicstaticvoidmain(String[] args)throwsInterruptedException{// 账户a,b初始余额均为1000Account a =newAccount(1000);Account b =newAccount(1000);// 线程t1模拟账户a向账户b转账1000次Thread t1 =newThread(()->{for(int i =0; i <1000; i++){// a向b每次转账一个随机金额
a.transfer(b,randomAmount());}},"t1");// 线程t2模拟账户b向账户a转账1000次Thread t2 =newThread(()->{for(int i =0; i <1000; i++){// b向a每次转账一个随机金额
b.transfer(a,randomAmount());}},"t2");
t1.start();
t2.start();
t1.join();
t2.join();// 查看转账2000次后的总金额
log.debug("total:{}",(a.getMoney()+ b.getMoney()));}// Random 为线程安全staticRandom random =newRandom();// 随机 1~100publicstaticintrandomAmount(){return random.nextInt(100)+1;}}// 账户类classAccount{// 余额privateint money;//publicAccount(int money){this.money = money;}publicintgetMoney(){return money;}publicvoidsetMoney(int money){this.money = money;}// 转账(转账对象,转账金额)向账户target转账publicvoidtransfer(Account target,int amount){// 判断余额是否足够if(this.money >= amount){// 修改自身余额this.setMoney(this.getMoney()- amount);// 修改对方余额
target.setMoney(target.getMoney()+ amount);}}}
如何验证线程安全性:模拟多次转账,若多次转账后账户总金额不变(2000),则说明以上代码线程安全
运行结果:(显然是线程不安全的)
解题
分析共享变量,需考虑哪部分代码属于临界区(找出临界区便可对其加锁)
transfer 涉及对共享变量的读写(a,b均为共享变量)。其有两个需要保护共享变量;在transfer 方法上加synchronize不可行。synchronize加在成员方法上等价于加在了this对象上,而this对象保护的为 this.money共享变量,其只能保护自身,并不能影响到另一个对象上的getMoney() 、setMoney()。锁加在两个对象上相当于两个线程进入了两个房间,起不到保护线程作用。因此锁要锁住的对象应该为*this.getMoney()、target.getMoney()*。对着两个对象来说Account类是共享的【
Account类对它的所有对象都是共享的
】;因此可以将锁加在Account类上
// 转账(转账对象,转账金额)向账户target转账publicvoidtransfer(Account target,int amount){synchronized(Account.class){// 判断余额是否足够if(this.money >= amount){// 修改自身余额this.setMoney(this.getMoney()- amount);// 修改对方余额
target.setMoney(target.getMoney()+ amount);}}}
版权归原作者 new一个对象_ 所有, 如有侵权,请联系我们删除。