常见的锁策略

一、乐观锁 & 悲观锁

1.1乐观锁

1.1.1乐观锁的定义:

乐观锁认为⼀般情况下不会出现冲突,所以只会在更新数据的时候才对冲突进行检测,如果发生没有发生冲突直接进行修改,如果发生冲突了不做任何修改,然后把结果返回给用户,让用户自行决定处理。

1.1.2乐观锁的实现——CAS

CAS 比较并替换机制
流程:
CAS 中包含了三个操作单位:
V(内存值,)、A(预期的旧址)、B(新值),比较 V 值和 A 是否相等,如果相等的话说明没有发生锁冲突,则将 V值更换成 B值;否则说明发生了锁冲突,乐观锁执行失败,就提示用户修改失败,从而实现了 CAS 的机制。

1.1.3CAS应用

CAS的底层实现是靠Unsafe类实现的,Unsafe是CAS的核心类,在getAndIncrement方法中还调用了unsafe的方法,因此我们可以利用Atomic+包装类实现线程安全的问题。

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo1 {
    private static int number = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private final static int MAX_COUNT = 100000;

    public static void main(String[] args) throws InterruptedException {

//        System.out.println("执行结果:"+atomicInteger.get());
//        //++
//        atomicInteger.getAndIncrement();
//        System.out.println("++:"+atomicInteger.get());
//
//        //--
//        atomicInteger.getAndDecrement();
//        System.out.println("--:"+atomicInteger.get());


        //++
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < MAX_COUNT; i++) {
                atomicInteger.getAndIncrement();
                //number ++;
            }
        });
        t1.start();


        //--
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < MAX_COUNT; i++) {
                atomicInteger.getAndDecrement();
                //number --;
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终结果:"+atomicInteger.get());
    }

}

在这里插入图片描述

1.1.4ABA问题

CSA的缺点是存在ABA问题
ABA问题描述:
A有100元,A向B转钱,第一次转了50元,但是点完转账按钮没有反应,于是又点击了一次。第一次转账成功后A还剩50元,而这时C给A转了50元,A的余额变为100元,第二次的CAS判断(100,100,50),A的余额与预期的值一样,于是将A的余额修改为50元。

//ABA问题演示
import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo1 {
    private static AtomicInteger money = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        //第一次点击转账按钮 -50
        Thread t1 = new Thread(() -> {
            int old_money = money.get();
            //花费了2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money - 50);
        });
        t1.start();


        //第二次点击转账按钮 -50(不小心点击的,因为第一次点击之后没有反应)
        Thread t2 = new Thread(() -> {
            int old_money = money.get();
            money.compareAndSet(100,50);
            money.compareAndSet(old_money,old_money - 50);
        });
        t2.start();

        //给账户+50元
        Thread t3 = new Thread(() -> {
            //花费了1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money = money.get();
            money.compareAndSet(old_money,old_money + 50);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账户余额:"+money.get());
    }
}

在这里插入图片描述

1.1.5ABA问题解决

ABA解决⽅案:引入版本号AtomicStampedReference
每次操作之后让版本号+1,执行的时候判断版本号和值,就可以解决问题。

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * ABA问题解决
 */
public class ABADemo2 {
    private static AtomicStampedReference<Integer> money =
            new AtomicStampedReference(100,0);

    public static void main(String[] args) throws InterruptedException {
        //第一次点击转账按钮 -50
        Thread t1 = new Thread(() -> {
            int old_money = money.getReference();//得到余额
            int version = money.getStamp();//得到版本号
            //花费了2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money - 50,version,version + 1);
        });
        t1.start();


        //第二次点击转账按钮 -50(不小心点击的,因为第一次点击之后没有反应)
        Thread t2 = new Thread(() -> {
            int old_money = money.getReference();//得到余额
            int version = money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money - 50,version,version + 1);
        });
        t2.start();

        //给账户+50元
        Thread t3 = new Thread(() -> {
            //花费了1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money = money.getReference();
            int version = money.getStamp();
            money.compareAndSet(old_money,old_money + 50,version,version + 1);
        });
        t3.start();

        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账户余额:"+money.getReference());
    }
}

在这里插入图片描述

1.2 悲观锁

悲观锁就是比较悲观,总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿数据就会阻塞直到它拿到锁。

比如我们之前提到的synchronized和Lock都是悲观锁。

二、公平锁 & 非公平锁

公平锁:按照线程来的先后顺序获取锁,当一个线程释放锁之后,那么就唤醒阻塞队列中第一个线程获取锁。
非公平锁:不是按照线程来的先后顺序唤醒锁,而是当有一个线程释放锁之后,唤醒阻塞队列中的所有线程,随机获取锁。
大多数锁默认为非公平锁,因为非公平锁的效率更高。

非公平锁:ReentrantLock lock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是⾮公平锁。
非公平锁:ReentrantLock lock = new ReentrantLock(false)。如果构造函数不传递参数,则默认非公平锁。

三、读写锁

3.1读写锁

把一把锁分为两部分:读锁和写锁
读锁允许多个线程同时获得,写锁是互斥锁,不允许多个线程同时获得。
读写锁的特点是:读读不互斥、读写互斥、写写互斥。
使用场景:多读少写的业务操作,使用读写锁可以发挥到最大性能。

Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
1.ReentrantReadWriteLock.ReadLock 类表示⼀个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
2.ReentrantReadWriteLock.WriteLock 类表示⼀个写锁. 这个对象也提供了 lock / unlock方法进行加锁解锁.

import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 演示读写锁的使用
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        //创建读写锁
        final ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
        //创建读锁
        final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
        //创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
        //线程池
        ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));
        //启动线程执行任务【读操作1】
        executor.submit(()->{
            //加锁操作
            readLock.lock();
            try{
                //执行业务逻辑
                System.out.println("执行读锁1:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        });

        //启动线程执行任务【读操作2】
        executor.submit(()->{
            //加锁操作
            readLock.lock();
            try{
                //执行业务逻辑
                System.out.println("执行读锁2:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                readLock.unlock();
            }
        });

        //启动线程执行【写操作1】
        executor.submit(()->{
            //加锁
            writeLock.lock();
            try {
                System.out.println("执行写锁1:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                writeLock.unlock();
            }
        });

        //启动线程执行【写操作2】
        executor.submit(()->{
            //加锁
            writeLock.lock();
            try {
                System.out.println("执行写锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                writeLock.unlock();
            }
        });
    }
}



在这里插入图片描述

3.2独占锁

独占锁就是指任何时候只能有一个线程能执行资源操作,是互斥的。
比如写锁,就是一个独占锁,任何时候只能有一个线程执行写操作,synchronized、Lock都是独占锁。

3.3共享锁

共享锁是指可以同时被多个线程获取,但是只能被一个线程修改。读写锁(读读不互斥) 就是一个典型的共享锁,它允许多个线程进行读操作 ,但是只允许一个线程进行写操作。

三、可重入锁和自旋锁

3.1可重入锁

可重⼊锁指的是该线程获取了该锁之后,可以无限次的进⼊该锁锁住的代码。
synchronized、ReentrantLock都是可重入锁。

3.2自旋锁

⾃旋锁是指尝试获取锁的线程不会⽴即阻塞,⽽是采用循环的方式去尝试获取锁,这样的好处是减少线程上下⽂切换的消耗,缺点是循环会消耗 CPU。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值