不要用Integer作为锁对象(请将锁对象设置为全局static且不可变final的)

案例场景

两个人抢着买票,票只有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也一致。

为什么没锁住,进行梳理

  1. 首先多线程产生竞争,必须要有一个争夺的是同一把锁
  2. 上面出现了拿到同一把锁的情况,只有一种可能,就是他们实际上并不是同一把锁
  3. 我们定义了 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对象,根本没锁住,不是一把锁,记住此坑,这个问题可以根据其他的方式解决,参考下面的博客:

blog.csdn.net/qq_20517531…

本博客是通过这篇文章提炼总结出来的:baijiahao.baidu.com/s?id=172471…

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值