悲观锁和乐观锁

一、背景

1.1 问题代码

        有 1 个变量,有 100 个线程,每个线程对其加 100 次,代码如下:

public class MyRunnable implements Runnable{
    // 记录浏览人次
    private int count;

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println("count====="+(++count));
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        for(int i=0;i<100;i++){
            new Thread(myRunnable).start();
        }
    }
}

        测试结果如下,可以发现并没有预期的得出 10000 的结果,因为出现了线程安全问题。为什么会少加了 2 次呢?我们分析下原因。

        现在有 100 个线程针对一个 count 变量,假设此时 count 的值为 10,此时 100 个线程要同时对 count 进行 +1 操作,每个线程都需要先把 count 的值拿出来,然后进行 +1,最后再把 count 的值放进去,这样就造成了其他线程浪费了执行次数。

        这就是多个线程访问同一共享资源出现的线程安全问题,接下来我们分别用悲观锁和乐观锁来解决这个问题。 

二、悲观锁

2.1 背景

        javajdk1.5 之前都是靠 synchronized 关键字保证同步的,这样可以确保无论哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量,其实就是一种悲观锁,故 synchronized 是悲观锁。

2.2 存在的问题

        在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

        一个线程持有锁会导致其他所有需要此锁的线程挂起。若一个高优先级的线程等待一个低优先级的线程会导致优先倒置,引起性能问题。

2.3 概念

        一上来就加锁,每次只能有一个线程访问,访问完毕后再解锁。即使有 100 个线程竞争资源,也要有 99 个线程排队。

2.4 特点

        线程安全,性能较差。

2.5 常用的实现方式

        传统的关系型数据库里面用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。再比如 java 里面的同步原语 synchronized Lock 等关键字的实现也是悲观锁的具体实现。

2.6 代码展示

        使用 synchronized 关键字来解决上面出现的线程安全的问题,代码如下所示:

public class MyRunnable implements Runnable{
    // 记录浏览人次
    private int count;

    String a = "abd";
    @Override
    public void run() {
        synchronized (a){
            for(int i=0;i<100;i++){
                System.out.println("count====="+(++count));
            }
        }

    }
}


三、乐观锁

3.1 概念

        一开始不加锁,认为是没有问题的,线程一起运行,等要出现线程安全问题的时候才开始控制。

        每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

3.2 特点

        线程安全,性能较好。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

3.3 实现方式

        乐观锁是一种思想,CAS 是这种思想的一种实现方式。

        乐观锁的具体实现主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 CASCompare And Swap 比较并交换)。

        JAVACAS 的支持:在 jdk1.5 中新增的 java.util.concurrent 就是建立在 CAS 之上的,相对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现,在性能上有很大的提高。

        乐观锁适用于多读少写的场景 ( SVN 就是很好的例子),一般案例就是版本号控制,即在数据库表中增加一个 version 字段,这个数据标识当前数据的版本,每次更新操作都会 version+=1

3.4 原理

        CAS:乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是被告知这次竞争失败,并可以再次尝试。

        CAS 操作中包含三个操作数 ---- 需要读写的内存位置(V),进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置的值更新为新值 B。否则处理器不做任何操作。

        无论哪种情况,他都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将返回 CAS 是否成功,而不提取当前值) CAS 有效的说明了 “我认为位置 V 应该包含 A 值;如果包含 A 值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现有的值即可”。这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。

3.5 代码展示

        使用乐观锁解决线程安全的问题。

public class MyRunnable implements Runnable{
    // 记录浏览人次
    // 整数修改的乐观锁,是原子类实现的
    private AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"--count====="+(count.incrementAndGet()));
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        for(int i=0;i<100;i++){
            new Thread(myRunnable).start();
        }
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值