前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我去了,一篇文章,看懂锁???

我去了,一篇文章,看懂锁???

原创
作者头像
Joseph_青椒
发布2023-08-28 20:36:20
1850
发布2023-08-28 20:36:20
举报
文章被收录于专栏:java_josephjava_joseph

Lock接口

1、简洁

锁是一种工具、控制对共享资源的访问

Lock和synchronized,是最常见的锁,都可以达到线程安全的目的,功能常见不同

Lock不是来代替synchronized的,而是当synchronzed不够用的时候,提供更高级的功能

最常用的类,就是ReentrantLock

通常是只允许一个访问,但是有并发访问的,ReadWriteLock中的ReadLock

2、为什么synchronized不够用

1、效率低,

锁的释放情况少,只能执行完了释放,或者是异常,jvm来释放,比如想中途释放锁,或者超时,拿不到就不加锁了

2、不够灵活

加锁和释放时机太单一、不能分读写锁

3、不知道是否成功获取到了锁

对于我们开发来说,不知道到底获取到了锁没有

3、方法介绍

Lock、tryLock、tryLock(long time,TimeUnit unit)、lockInterruptibly()

Lock

不会有jvm帮忙在异常的时候释放锁,

因此,try finally里中,要释放

问题:不能被中断,一旦陷入死锁、lock就会陷入永久等待,所有就有了tryLock

代码语言:javascript
复制
/**
 * @Author:Joseph
 * Lock不会有jvm帮忙释放锁,需要finally里释放
 */
public class MustUnlock {
?
    private static Lock lock = new ReentrantLock();
?
    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
?
}

tryLock

可以判断是否能加上锁,来指定新的后续程序行为

方法会立即返回,拿不到锁,不会去等 ,而tryLock带参数的,会等待,超时就放弃

lockInterruptibly

这个方法,就是可以通过中断来丢掉锁,当在获取锁过程中,收到中断信号、或者在加上锁过程中收到中断信号,

都会进入catch逻辑,然后unlock锁

4、可见性保证(**)

看过我JMM的伙伴肯定知道,lock和synchronized都是可以保证可加性的,都有近朱者赤的特性,

回顾下,因为加锁,线程只能先后执行,后拿到锁的线程,一定是可以看到前面的线程做的事情

锁的分类

乐观锁悲观锁

为什么会诞生非互斥同步锁、互斥同步锁(悲观锁)的劣势

悲观锁,是独占的,其他线程想要获取,必须等待,阻塞唤醒带来性能劣势,用户态、内核态切换等待

乐观锁不需要把线程挂起!

永久阻塞:持有锁的线程,陷入死锁等永久阻塞,那么其他等待的会永久阻塞

优先级反转:优先级低的线程,拿到锁执行很慢的话,优先级高的线程就会拿不到!!!出现反转的情况

什么是乐观锁、悲观锁

悲观锁:任务这个资源,肯定会有人来抢,要锁住这个资源,比如synchronized lock

线程1拿到,线程2等待,

乐观锁:认为这个资源,不会有其他资源来干扰,对这个资源不会锁住,但是为了保证安全,要去检查有没有修改过,不一样的话,就会知道这个数据修改过了,一般通过CAS算法实现

乐观锁例子

原子类、并发容器

代码语言:javascript
复制
/**
 * @Author:Joseph
 * Lock不会有jvm帮忙释放锁,需要finally里释放
 */
public class MustUnlock {
?
    private static Lock lock = new ReentrantLock();
?
    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
?
?

这是悲观锁,需要提前加锁处理

再看下,Atomic类,与synchronized a++原子性的保证

代码语言:javascript
复制
public class PessimismOptimismLock {
    int a;
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
    }
?
    public synchronized void testMethod(){
        a++;
    }
?
}
?

还有、git,就是通过版本号、乐观锁,提交的,悲观锁,一个人修改代码,其他都阻塞,直接!倒闭了哈哈哈

当然,乐观锁不是万金油,比如任务执行很长,那么等待的开销固定,就是阻塞,而乐观锁,会一直去尝试修改,自旋等操作,消耗cpu资源

场景:

并发写入多、执行时间长的操作,适合使用悲观锁,避免无用的自旋cpu开销,比如io操作,

并发写入少,大部分是读取场景,不加锁,使性能更高

前期肯定是悲观锁开销大,但是随着后面自旋,乐观锁会更大,所以!要注意使用场景

可重入锁 不可重入锁

以ReentrantLock为例

用法演示:

代码语言:javascript
复制
**
 * @Author:Joseph
 * 演示多线程预定电影院座位
 */
public class CinemaBookSeat {
?
    private static ReentrantLock lock = new ReentrantLock();
    private static void bookSeat(){
        lock.lock();
        try {
?
            try {
                System.out.println(Thread.currentThread().getName()+"开始预定座位");
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"完成预定座位");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
?
    public static void main(String[] args) {
        new Thread(()->bookSeat()).start();
        new Thread(()->bookSeat()).start();
        new Thread(()->bookSeat()).start();
        new Thread(()->bookSeat()).start();
    }
}
?

可重入理解

就是同个线程,拿到这个锁,还没释放,想要再次执行逻辑,再拿到锁,就是可重入锁

ReentrantLock就是可重入锁

代码语言:javascript
复制
/**
 * @Author:Joseph
 * 验证可重入
 */
public class GetHoldCount {
    private static ReentrantLock lock = new ReentrantLock();
?
    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
?
?
    }
}
?

比如一个业务,需要处理好多次,才算处理完成

代码语言:javascript
复制
public class RecursionDemo {
?
    private static ReentrantLock lock = new ReentrantLock();
?
    private static void accessResource(){
?
        System.out.println("已经对资源进行了处理");
        lock.lock();
        try {
            //要让他处理5次,业务需求
            if(lock.getHoldCount()<5){
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        }finally {
            lock.unlock();
        }
    }
?
    public static void main(String[] args) {
        accessResource();
    }
?
}

ReentrantLock如何实现可重入锁的

image-20230827175442158
image-20230827175442158

依靠的AQS,这是可重入锁的逻辑,可能看的有些处理,我将在AQS的文章中,细致的讲,就能看懂了,这里先认识一下

再点下两个方法,调试可能会用到

image-20230827175748387
image-20230827175748387

公平锁 非公平锁

什么是公平、非公平

为什么要非公平!!!

公平就是按照线程请求的顺序分配,非公平指的是,在一些时机,可以插队

什么适合可插队?

线程从阻塞到唤醒,是有时间的,通过非公平锁,可以让”清醒“的线程,也就是运行的线程,利用这个时间,

比如A B C A持有锁,B等待,准备去拿锁,A释放了,B正在唤醒,这个时间,清醒的C线程,运行完了,又不影响B的执行,!双赢局面

当ReentrantLock传入参数的时候true的时候,就是公平锁,false是非公平锁

注意》》》》LLL:::::<<<<<<>>>>>

tryLock是大恶霸,一旦有线程释放锁,那么正在执行tryLock的线程就能获取到,有优先权,即使在等待队列里

代码上的实现:

tryAcqure方法,尝试获取锁。

公平锁会判断队列里有没有元素,而非公平锁不会去看队列有没有元素,直接去获取锁

image-20230827184310862
image-20230827184310862

共享锁 排他锁

这个玩意儿,在数据库中,innoDB中,我在事物中有讲,mysql专栏里,可以看哈,

如果没读写锁,那么读的操作和写的操作,都会加锁,而读操作,只读的话,往往是安全的,这就诞生了读写锁

也就是共享锁 、 排他锁 S锁 X锁

排他锁也叫独占锁

关系,总结就是读读共享,其他都是互斥,你想想,读操作,并发是没问题的,但是其他,比如读写,写写,都要保证安全

java代码中,就是通过ReentrantReadWriteLock实现读写锁

代码语言:javascript
复制
public class CinemaReadWrite {
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private  static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    private static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到了读锁,正在读取");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"释放了读锁");
            readLock.unlock();
        }
    }
    private static void write(){
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到了写锁,正在写入");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"释放了写锁");
            writeLock.unlock();
        }
    }
    public static void main(String[] args) {
        new Thread(()->read(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->write(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
    }
}
?

现在我们来升华一下思考

读写和写锁的怎么交互的?

插队:指的是从等待队列中选哪个来进来,写线程肯定要排队的,而读操作,是可以插队的,但是对其他线程不公平的

升降级问题:就是锁持有的时候,读锁能升级为写锁 吗 ,写锁能降级为读锁吗?

插队

首先,RenntrantLock是可传参数的,true的话,不会有插队的问题,因为本身公平,按照策略先后顺序。

但是传入false的时候,读线程正在获取锁,比如1,2线程写线程,等待,3线程读线程,那么是可以插队的

但是:插队会有问题,

两种策略:让3插队,效率很高,但是,这个策略,哎,后面还想插队,容易出现饥饿,2这个写锁,等死

策略2:避免饥饿,renntrantlock非公平锁,不可以插队

如何避免: 就是不允许插队,哈哈哈,对不起没有更好的方法,但是通过上面的分析,如果队列头节点也是个读锁,不插写锁的队,但是可以去插读锁呀!这个是允许的。

也就是说,reentrantLock禁止读锁插队!

但是,策略2,写锁是可以随时插队

总结下吧:公平锁,不允许插队!

非公平锁:写锁可以随时插队

读锁仅仅在头节点是获取读锁的时候插队,也就是说,插队,但是不能插写锁的队,避免锁饥饿

可以插队列头部是读锁的队

源码看看::::

image-20230828201450750
image-20230828201450750

RenntrantLock下有这两个内部类,分别对应公平和非公平

image-20230828201546857
image-20230828201546857

公平锁只会看有没有人排队,有了,那就排毒,这两个方法,对应读阻塞、写阻塞,也就是,只有队列中,你前面有元素,那就等着

image-20230828201747858
image-20230828201747858

非公平锁,写入,直接返回fasle,不用管,直接插队

读锁,回去看队列是否是有写,有的话,返回true,不让插队,阻塞等着。’

这就是源码中,读者应该阻塞、写着应该阻塞的实现,通过这个完成插入逻辑的实现。

锁的升降级

升降级,指的是,读锁升级为写锁,写锁降级为读锁。先说结论,读写锁允许锁的降级,不允许升级

为什么不允许读锁升级为写锁???

死锁,假设两个读锁都在读,都等对方释放锁,然后自己升级,就会出现写锁问题,所以ReentrantLockReadWriteLock不允许

场景

适合读多写少的场景,提高效率!

自旋锁 阻塞锁

阻塞唤醒需要操作系统切换cpu状态来完成,自旋操作的话,开销比阻塞唤醒小多的情况下,就适用于自旋锁了

适用于同步资源锁定时间很短,没必要去切换线程状态的情况

因为如果锁的占用时间,自旋的线程只会浪费cpu资源,随着时间增长,而增加

image-20230828185514138
image-20230828185514138

原子类中的各种操作,基本都是通过自旋锁+CAS操作实现的原子性,对资源保护,达到目的

手写自旋锁~

自旋锁有个要求,利用原子操作,把想要的值用cas换上去,通过cas去做自旋的状态判断

下面是一个例子

代码语言:javascript
复制
public class SpinLock {
?
    private AtomicReference<Thread> sign = new AtomicReference<>();
?
    public void lock(){
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null,current)){
            System.out.println("自旋获取失败,再次尝试");
        }
    }
?
    public void unlock(){
        Thread current = Thread.currentThread();
        sign.compareAndSet(current,null);
    }
?
    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = ()->{
            System.out.println(Thread.currentThread().getName()+"尝试获取自旋锁");
            spinLock.lock();
            System.out.println(Thread.currentThread().getName()+"获取到了自旋锁");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                spinLock.unlock();
                System.out.println(Thread.currentThread().getName()+"释放自旋锁");
            }
        };
        Thread thread1 = new Thread(runnable, "线程1");
        Thread thread2 = new Thread(runnable, "线程2");
        thread1.start();
        thread2.start();
?
?
    }
?
}

自旋锁会有不断的循环,开销大,一般适用于多核服务器,并发布不是很高的情况下,比阻塞锁的效率高

分析总结:

记住,阻塞锁,并不是差,只是在特定情况下的适合,用自旋锁会更快而已,比如并发不是很高的情况下,以及临界区短,适用于自旋锁

临界区长:线程拿锁的时间长,很久再释放

可中断锁 不可中 断锁

不可中断锁:synchronized

可中断锁:Lock,比如tryLock,lockInterruptibly

就是可不可中断,在前面已经演示过了,执行期间可被中断

锁优化

JVM虚拟机对锁的优化

自旋锁和自适应

自旋锁就是临界区短的操作用的,刚才说了,通过自旋锁代替阻塞唤醒过程,减小开销,但是出个问题,时间长了,那开销可就大了,于是jvm搞了一个自适应,也就是说出现问题,会从乐观锁变为阻塞锁

锁消除

一些场景下不必要加锁,不会有并发问题,jvm分析无需加锁

比如,代码在方法内部,不会有外人访问这个东西

锁粗化

锁粒度问题,但是粒度太小,密集的加了很多锁,不如加个范围大的锁,一个锁搞定,效率更高

开发写代码的优化

1、缩小同步代码块

2、使用同步代码块,而不是锁住整个方法

3、减少请求锁的次数

比如,日志框架,10个线程都想打印日志,可以把多个线程汇聚到一起,执行操作,加一个锁

4、避免人为制造热点

比如,hashMap,size方法,会遍历操作,我们可以自己维护一个,自己计数,不认为用size

5、锁中不要再包含锁,容易出现死锁

6、选择适合类型的锁

比如读写锁,使用乐观锁(原子类是java中的乐观锁 )

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、简洁
  • 2、为什么synchronized不够用
  • 3、方法介绍
    • Lock
      • tryLock
        • lockInterruptibly
        • 4、可见性保证(**)
        • 锁的分类
          • 乐观锁悲观锁
            • 为什么会诞生非互斥同步锁、互斥同步锁(悲观锁)的劣势
            • 什么是乐观锁、悲观锁
            • 场景:
          • 可重入锁 不可重入锁
            • 可重入理解
          • 公平锁 非公平锁
            • 什么是公平、非公平
          • 共享锁 排他锁
            • 插队
            • 锁的升降级
            • 场景
          • 自旋锁 阻塞锁
            • 分析总结:
          • 可中断锁 不可中 断锁
          • 锁优化
            • JVM虚拟机对锁的优化
              • 自旋锁和自适应
              • 锁消除
              • 锁粗化
            • 开发写代码的优化
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com