1、买票练习
测试下面代码是否存在线程安全问题,并尝试改正
模拟多个线程调用同一个购票窗口对象进行买票操作是否有线程安全问题(证明线程线程安全:
购买数量+余票数量=1000)
public class ExerciseSell {
public static void main(String[] args) {
// 售票窗口(余票数:2000)
TicketWindow ticketWindow = new TicketWindow(1000);
// 所有线程的集合
List<Thread> list = new ArrayList<>();
// 卖出的票数统计(可对其进行累加求和)
List<Integer> sellCount = new Vector<>();
// 模拟2000同时在购票窗口抢票
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 买票
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 为线程安全
static Random random = new Random();
// 随机 1~5(模拟每个人买票数量)
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
// 售票数量
class TicketWindow {
// 初始余票数
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 获取余票数
public int getCount() {
return count;
}
//售票(购买票的数量)
public int sell(int amount) {
// 余票数>=购买数量===>售票成功
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
// 购票失败(返回0)
return 0;
}
}
}
需将程序多次运行(发生指令交错后才可能会出现线程不安全的情况)
运行结果:
测试问题
:多线程环境下进行测试某个类的线程安全,安全问题不容易复现,上述测试中使用sleep()增加其时间间隔的方式期望线程上下文切换的机率增大,使现象更容易出现。为节省时间我们还可以使用编写测试脚本的方式(一旦出现线程安全问题,会在某次循环中将其展示出来)。
解题
若要分析线程安全问题,需考虑哪部分代码属于临界区(找出临界区便可对其加锁)
临界区
:对共享变量有读写操作的代码片段
以上案例中sell(int amount)方法内部对共享变量即有读又有写操作,可以在方法上加上synchronized关键字进行保护
// 售票 (一个售票窗口只有一个余额数)
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
2、转账练习
测试下面代码是否存在线程安全问题,并尝试改正
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
// 账户a,b初始余额均为1000
Account a = new Account(1000);
Account b = new Account(1000);
// 线程t1模拟账户a向账户b转账1000次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
// a向b每次转账一个随机金额
a.transfer(b, randomAmount());
}
}, "t1");
// 线程t2模拟账户b向账户a转账1000次
Thread t2 = new Thread(() -> {
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 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户类
class Account {
// 余额
private int money;
//
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账(转账对象,转账金额)向账户target转账
public void transfer(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转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
// 判断余额是否足够
if (this.money >= amount) {
// 修改自身余额
this.setMoney(this.getMoney() - amount);
// 修改对方余额
target.setMoney(target.getMoney() + amount);
}
}
}
更多文章请关注《万象专栏》
转载请注明出处:https://www.wanxiangsucai.com/read/cv170878