JAVA多线程(五)--Lock-乐观锁和悲观锁

1.悲观锁:
假设最坏的情况,每次去拿数据都会认为别的线程会去修改,所以每次拿数据之前先上锁,这样别的线程想拿这个数据就会阻塞直到拿到锁。synchronized的实现是一个悲观锁。
悲观锁存在的问题:
1)多线程竞争下,加锁、解锁都会导致线程上下文切换和cpu调度延迟,性能问题存在
2)一个线程持有锁会导致其他需要此锁的线程阻塞
3)如果一个优先级高的线程等待优先级低的线程释放锁会导致优先级导致,性能问题存在
2.乐观锁
每次去拿数据都会认为别的线程不会去修改,所以每次拿数据之前先不上锁,但是在更新的时候判断一下在此期间有没有别的线程去更改这个数据。乐观锁主要适用于多读的场景,这样可以提高的吞吐量。
3.乐观锁的实现方式:CAS机制、版本号机制
1)CAS
CAS包括三个操作数:
a.需要读写的内存位置(V)
b.进行比较的预期值(A)
c.拟写入的新值(B)
CAS操作逻辑:如果内存位置V的值和预期值A相匹配,那么处理器会自动将位置更新为新值B,反之处理器不会做任何操作。CAS指令是原子指令。
JAVA对CAS的支持,java.util.concurrent建立在CAS之上,CAS是非阻塞算法的一种常见实现。我们以java.util.concurrent中AtomicInteger为例,看一下在不使用锁的情况下如何保证线程安全。主要理解getAndIncrment方法,这个方法相当于++i操作。
我们看一个例子:

public class TestDemo2 {
    private static int value1 = 0; //线程不安全
    private static AtomicInteger value2 = new AtomicInteger(0);//CAS
    private static int value3 = 0; //synchronized
    private static CountDownLatch countDownLatch = new CountDownLatch(1000);

    public static synchronized void inceaseValue3(){
        value3++;
    }
    public static void main(String[] args) {
        for(int i=0; i<1000; i++){
            new Thread(){
                @Override
                public void run() {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    value1++;
                    value2.getAndIncrement();
                    inceaseValue3();

                    countDownLatch.countDown();
                }
            }.start();

        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程不安全:"+value1);
        System.out.println("乐观锁:"+value2);
        System.out.println("悲观锁:"+value3);

    }
}

在这里插入图片描述
通过结果可以发现乐观锁和悲观锁保证了原子性。
通过对AtomicInteger源码分析可以得到:
a.Unsafe 用来帮助java访问操作系统底层资源
b.value是被volatile修饰
c.valueOffset value在内存中的偏移量
CAS保证原子性,volatile保证可见性和顺序性,在AtomicInteger中,volatile和CAS 一起来保证线程安全。
2)版本号机制
版本号机制的基本思路就是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号+1。
我们举一个简单的例子:
比如现在有一个银行卡,账户余额为100元,还有一个版本号version是1;A来读入账户,扣除50元,此时账户里剩下50元,版本号version变为2,但是与此同时在A操作的过程中B也来读入账户,扣除20元,账户里就剩下80元,此时B就会在修改版本号之前拿自己的版本号与账户对应版本号比较是否一致,一致就可以更新,要是不一致就会被驳回。
4.乐观锁和悲观锁的区别与联系
从概念上来看
悲观锁和乐观锁是两种思想,用于解决并发场景下的数据竞争问题。

  1. 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此在操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据;
  2. 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只有在执行更新的时候判断一下在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
    从实现方式上来看
  3. 悲观锁的实现方法是加锁,加锁既可以是对代码块加锁(如synchronized关键字),也可以是对数据加锁(如mysql中的排他锁);
  4. 乐观锁的实现方式主要有两种:CAS机制和版本号机制
    a. CAS机制(Compare And Swap)
    CAS操作包括3个操作数:需要读写的内存位置(V)、进行比较的预期值(A)、拟写入的新值(B)
    CAS操作过程为:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。特别指出:许多CAS的操作都是自旋的,如果操作不成功,会一直重试,直到操作成功为止。
    Java中为了支持CAS操作,jdk1.5之后提供了很多原子类,如AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference等,利用CPU提供的CAS操作来保证原子性,底层利用volatile关键字保证可见性和一定的有序性,即也保证了线程安全;
    b. 版本号机制
    版本号机制的基本思路是在数据中增加一个字段version,表示该数据的额版本号,每当数据被修改,版本号加+1。当某个线程查询数据时,将该数据的版本号一起查出来,当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
    从使用场景上来看
  5. 悲观锁的使用场景更广泛,乐观锁的使用场景受到了更多的限制,因为CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS也是无能为力的,而悲观锁synchronized则可以通过对整个代码块加锁来处理。而版本号机制,如果查询和更新数据分别在不同的数据表,也很难通过简单的版本号去实现;
  6. 考虑竞争激烈程度,当竞争不激烈时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而加锁和解锁也需要消耗一定的资源;当竞争激烈时,悲观锁会更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费cpu资源。
    5.CAS的缺点
    1)循环时间开销很大
    在源码中会发现有do while,如果CAS失败,则会一直进行尝试,如果长时间不成功,那么就可能会给CPU带来很大的开销;
    2)只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
    3)引出了ABA问题
    6.其他锁
    不可重入锁:当前线程执行某个方法已经获取到了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞;
    可重入锁:与上面不可重入锁相反。
    读写锁
    读读不用互斥,读写互斥,写写互斥。
    注意:读往往远远大于写,一般情况下独占锁的效率低下,主要因为高并发下对临界区
    的激烈竞争会导致线程上下文切换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值