常⻅锁策略(1. 乐观锁 & 悲观锁2. 公平锁 & 非公平锁3. 读写锁4. 可重入锁 & 自旋锁)

41 篇文章 2 订阅

目录

1. 乐观锁 & 悲观锁

 1.1乐观锁定义 

 1.2 乐观锁实现 -- CAS

 1.3 悲观锁定义和应⽤

2. 公平锁 & 非公平锁

3. 读写锁

3.1 读写锁

3.2 独占锁

3.3 共享锁 

4. 可重入锁 & 自旋锁

4.1 可重入锁

4.2 自旋锁


1. 乐观锁 & 悲观锁

 1.1乐观锁定义 

乐观锁认为⼀般情况下不会出现冲突,所以只会在更新数据的时候才对冲突进⾏检测,如果检查出来冲突,不做任何修改,如果没有冲突才进行修改。

 1.2 乐观锁实现 -- CAS

CAS(Compare And Swap)⽐较并替换,CAS ⽐较并替换的流程是这样的,CAS 中包含了三个操作单位:V(内存值,)、A(预期的旧址)、B(新值),⽐较  ,如果相等的话则将 V的值更换成 B,否则就提示⽤户修改失败,从⽽实现了 CAS 的机制。

举例:

        线程1:  V = 10, A = 10, B = 12

        线程2: V = 10, A = 10, B = 11

线程1,假设还没有开始对比,时间片用完了 ------------》 线程2,V = A ,所以将 B = 11 给V ,线程2执

行完 ---------------》线程1,A:10 == V:11,不相等,修改失败 ----------------》线程1,A: 11  B: 12

--------------------》线程1, A: 11 == V : 11 ,所以将 B = 12 的值给V --------------》线程12

CAS使用--非线程安全问题

public class CASDemo1 {
    private static int count = 0;;
    private final static int MAX_SIZE = 100000;

    public static void main(String[] args) throws InterruptedException {
        // 执⾏ i++ 操作
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAX_SIZE; i++) {
                    count++;
                }
            }
        });
        t1.start();

        // 执⾏ i-- 操作
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAX_SIZE; i++) {
                    count--;
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println("最终结果:" + count);
    }
}
输出:
最终结果:646(随机)

CAS应⽤-AtomicInteger

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < MAX_COUNT; i++) {
                atomicInteger.getAndIncrement();//相当于i++
               // 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());
    }
}
输出:
最终结果:0

 CAS存在ABA问题:     

ABA 转账问题,X 给 Y 转账,系统卡顿点击了两次转账按钮,X 原来是 300 ,正常是转完账(100元)还剩下200 ,第⼀次转账成功之后变成了 200 ,此时 Z 给 X ⼜转了 100 元,余额⼜变成了 300 ,第⼆次CAS 判断(300, 300 200 )成功,于是⼜扣了 100 元。
ABA问题:
public class ABADemo1 {
    private static AtomicInteger money = new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {

        // 第 1 次点击转账按钮(-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();

        // 第 2 次点击转账按钮(-50)【不小心点击的,因为第一次点击之后没反应,所以不小心又点了一次】
        Thread t2 = new Thread(() -> {
            int old_money = money.get(); // 先得到余额
            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());
    }
}
输出:
最终账号余额:50

ABA问题解决方案: 

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

ABA问题解决
public class ABADemo2 {
    private static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(100,0);
    public static void main(String[] args) throws InterruptedException {

        // 第 1 次点击转账按钮(-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();

        // 第 2 次点击转账按钮(-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());
    }
}
输出:
最终账号余额:100

 1.3 悲观锁定义和应⽤

定义:总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会阻塞直到它拿到锁。
应⽤:synchronized、Lock 都是悲观锁。

2. 公平锁 & 非公平锁

⾮公平锁 抢占式执⾏ ,有⼀些先来的任务还在排队,刚好释放锁的时候新来了⼀个任务,此时是随机获得锁的。
公平锁:所有任务来了之后先排队,线程空闲之后去任务队列按顺序执⾏最早任务。

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

3. 读写锁

3.1 读写锁

读写锁(Readers-Writer Lock)顾名思义是⼀把锁分为两部分: 读锁和写锁 ,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,⽽写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的,总结来说,读写锁的特点是: 读读不互斥、读写互斥、写写互斥。

 Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁:

ReentrantReadWriteLock.ReadLock 类表示⼀个读锁. 这个对象提供了 lock / unlock ⽅法进⾏加锁解锁。

ReentrantReadWriteLock.WriteLock 类表示⼀个写锁. 这个对象也提供了 lock / unlock ⽅法进⾏加解锁。

读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是⾮常⼴泛存在的).  

读写锁实例:
public class ReadWriteLockDemo1 {
    public static void main(String[] args) {
        //创建读写锁
        final ReentrantReadWriteLock readwriteLock = new ReentrantReadWriteLock();
        //创建读锁
        final ReentrantReadWriteLock.ReadLock readLock = readwriteLock.readLock();
        // 创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock = readwriteLock.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(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                readLock.unlock();
            }
        });
        //启动线程任务(读操作2)
        executor.submit(() -> {
            //加锁操作
            readLock.lock();
            try {
                //执行业务逻辑
                System.out.println("执行读锁2:" + LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                readLock.unlock();
            }
        });

        // 创建新线程执行写任务
        executor.submit(() -> {
            // 加锁
            writeLock.lock();
            try {
                System.out.println("执行写锁1:" + LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            } finally {
                // 释放锁
                writeLock.unlock();
            }
        });

        // 创建新线程执行写任务
        executor.submit(() -> {
            // 加锁
            writeLock.lock();
            try {
                System.out.println("执行写锁2:" + LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            } finally {
                // 释放锁
                writeLock.unlock();
            }
        });
    }
}
输出:
执行读锁2:2022-04-18T09:46:16.293
执行读锁1:2022-04-18T09:46:16.293
执行写锁1:2022-04-18T09:46:19.296
执行写锁2:2022-04-18T09:46:20.296

3.2 独占锁

独占锁是指任何时候都只有⼀个线程能执⾏资源操作。

synchronized、Lock。  

3.3 共享锁 

共享锁指定是可以同时被多个线程读取,但只能被⼀个线程修改。⽐如 Java 中的
ReentrantReadWriteLock 就是共享锁的实现⽅式,它允许⼀个线程进⾏写操作,允许多个线程读操作。

4. 可重入锁 & 自旋锁

4.1 可重入锁

可重⼊锁指的是该线程获取了该锁之后,可以⽆限次的进⼊该锁锁住的代码。
可重入锁:
public class ThreadDemo22 {
    public static void main(String[] args) {
        synchronized (ThreadDemo22.class) {
            System.out.println("线程执行进入了方法");
            synchronized (ThreadDemo22.class) {
                System.out.println("线程执行又进入了方法");
                synchronized (ThreadDemo22.class) {
                    System.out.println("线程执行又又进入了方法");
                }
            }
        }

    }
}
输出:
线程执行进入了方法
线程执行又进入了方法
线程执行又又进入了方法

4.2 自旋锁

⾃旋锁是指尝试获取锁的线程不会⽴即阻塞,⽽是采⽤循环的⽅式去尝试获取锁,这样的好处是减少线程上下⽂切换的消耗,缺点是循环会消耗 CPU。
  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值