案例场景
两个人抢着买票,票只有10张,不可以超卖,不可以同时买到同一张票。
案例实现
定义一个全局变量(也就是总票数10),抢购方法对这个全局变量加锁,保证同一时间只有一个线程拿到这把锁,进行-1操作。
代码实现
public static void main(String[] args) {
Thread why = new Thread(new TicketConsumer(), "小明");
Thread mx = new Thread(new TicketConsumer(), "小王");
why.start();
mx.start();
}
static class TicketConsumer implements Runnable {
private volatile static Integer ticket = 10;
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
synchronized (ticket) {
System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
if (ticket > 0) {
try {
//模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
复制代码
运行结果
出现问题,日志打印发生两个人同时抢到第9张票的情况,同时抢到了第9张票,并且对象的hashcode也一致。
为什么没锁住,进行梳理
- 首先多线程产生竞争,必须要有一个争夺的是同一把锁
- 上面出现了拿到同一把锁的情况,只有一种可能,就是他们实际上并不是同一把锁
- 我们定义了 ticket 这个全局变量,抢成功后有自减操作,但是这个变量是可变的,可变意味着每次变化的时候,多个线程去获取的不是同一个对象的锁
验证是否存在多把锁, 使用dump日志
我们先看抢10这张票的时候,小明和小王争抢的锁地址,然后看下第9张的时候,二者抢的地址:
第10张的时候,小明拿到了地址尾号是0d00的锁,小王没有拿到而是在等待0d00这把锁,第9张的时候小王拿到了 0d00 这把锁,而小明拿的已经是 0cf0 这把锁了,
这就意味着已经出现了多把锁,而且出现多把锁的原因就是因为 ticket 值发生了变化,开始双方锁的都是10,小明抢到10这个锁后,小王在等待,
小明抢完10后,并将 ticket - 1, 接着去抢9 ,释放了 10 这个锁,小王此时得到释放锁的通知,进入抢票阶段,
此时二者都进入抢票阶段,由于 ticket 此时正好是9,所以二者都输出了9和9的hashcode,此时输出的都是同一个对象9,所以我们看到的是一样的。
过程如下图:
第一轮
第二轮
破案
所以最终可以发现,是因为我们两个线程竞争的锁对象已经不是同一个导致了的,因此请在实际开发中避免这种情况发生,不要改变锁对象。
案例场景2
通过一个id查询,缓存中有就返回缓存中的,没有就返回db的,但是注意,防止在并发情况下同时返回db。
代码实现
static class TicketConsumer implements Runnable {
static final Map<Integer, Integer> res = new ConcurrentHashMap<>();
private Integer id;
public TicketConsumer(Integer id) {
this.id = id;
}
@Override
public void run() {
if (res.get(id) != null) {
System.out.println("查缓存");
return;
}
synchronized (id) {
if (res.get(id) != null) {
System.out.println("查缓存");
} else {
System.out.println("查数据库");
res.put(id, id);
}
}
}
}
复制代码
在同时查询id为10的情况下:
在同时查询id为200的情况下:
可以看到,查询200的时候,出现了并发情况多次查数据库的问题。
原因
不绕关子,直接说原因,大家还记得Integer的缓存范围吧,-128 到 127,所以如果是这个范围内的使用锁是生效的,超出这个范围,每次都会产生一个新的Integer对象,也就是10个线程用的都是单独的200对象,根本没锁住,不是一把锁,记住此坑,这个问题可以根据其他的方式解决,参考下面的博客:
本博客是通过这篇文章提炼总结出来的:baijiahao.baidu.com/s?id=172471…