Java进阶(锁)——锁的升级,synchronized与lock锁区别_sync和lock区别,简述锁升级过程

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

引出

Java进阶(锁)——锁的升级,synchronized与lock锁区别


Java中锁升级

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看一段代码:

public class App {
    public static void main(String[] args) throws InterruptedException {
        Calculate cal = new Calculate();
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 1000\_0000; i++) {
                cal.increase();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 1000\_0000; i++) {
                cal.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("time = " + (System.currentTimeMillis()-start));
        System.out.println("cal.getNum() = " + cal.getNum());
    }
}
public class Calculate {
    private int num;
    public int getNum() {
        return num;
    }
    // 多线程执行:public void increase() 会有线程安全问题
    // synchronized 锁解决,锁的是什么?
    public void increase(){
        synchronized (this) {
            num++;
        }
    }
}

分析:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对象组成:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是markOop。源码位于markOop.hpp中。在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

对象头在64位虚拟机 8个字节

在这里插入图片描述

在这里插入图片描述

package cn.wn.juc;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class App {
    // 锁升级演示
    public static void main(String[] args) throws InterruptedException {
        User user01 = new User();
        System.out.println("无状态(001):" + ClassLayout.parseInstance(user01).toPrintable());
        // 从jdk6开始,jvm默认延迟4s自动开启开启偏向锁。通过-XX:BiasedLockingStartupDelay=0设置取消延迟
        // 如果不要偏向锁:-XX:-UseBiasedLocking=false
        TimeUnit.SECONDS.sleep(5);
        User user02 = new User();
        System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user02).toPrintable());
        for (int i = 0; i < 2; i++) {
            synchronized (user02) {
                System.out.println("偏向锁(101)带线程ID:" + ClassLayout.parseInstance(user02).toPrintable());
            }
            // 偏向锁释放,对象头不会变化,一直存在, (偏向线程id) 下次执行判断是否同一个线程如果是直接执行
            System.out.println("偏向锁(101)释放线程ID:" + ClassLayout.parseInstance(user02).toPrintable());
        }
        // 多个线程加锁,升级为轻量级锁
        new Thread(() -> {
            synchronized (user02) {
                System.out.println("轻量级锁(00):" + ClassLayout.parseInstance(user02).toPrintable());
                try {
                    System.out.println("=====休眠3秒======");
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("轻量-->重量(10):" + ClassLayout.parseInstance(user02).toPrintable());
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            synchronized (user02) {
                System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user02).toPrintable());
            }
        }).start();
    }
}

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,这四种锁状态分别代表什么,为什么会有锁升级?其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

重量级锁

重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资

synchronized与lock锁区别

总结如下:

1️⃣ lock 是一个接口,而 synchronized 是 Java 的一个关键字,synchronized 是内置的语言实现。
2️⃣ 异常是否释放锁:synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁;而 lock 发生异常时候,不会主动释放占有的锁,必须手动 unlock 来释放锁,可能引起死锁的发生。(所以最好将同步代码块用 try catch 包起来,finally 中写入 unlock,避免死锁的发生。)
3️⃣ 是否响应中断
lock 等待锁过程中可以用 interrupt 来中断等待,而 synchronized 只能等待锁的释放,不能响应中断。
4️⃣ 是否知道获取锁:Lock 可以通过 trylock 来知道有没有获取锁,而 synchronized 不能。
5️⃣ Lock 可以提高多个线程进行读操作的效率。(可以通过 ReadWriteLock 实现读写分离)
6️⃣ 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择。
7️⃣ synchronized 使用 Object 对象本身的 wait 、notify、notifyAll 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度。

性能区别

synchronized 和 lock 性能区别
synchronized 是托管给 JVM 执行的,而 lock 是 Java 写的控制锁的代码。在 Java1.5 中,synchronized 是性能低效的。因为这是一个重量级操作,但是到了 Java1.6,发生了变化,进行了很多优化,有适应自旋,轻量级锁,偏向锁等等。
synchronized 原始采用的是 CPU悲观锁机制,即线程获得的是排他锁。排他锁意味着其他线程只能依靠阻塞来等待线程释放锁。
而 Lock 用的是乐观锁机制。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。进一步研究 ReentrantLock 的源码,会发现其中比较重要的获得锁的一个方法是 compareAndSetState。这里其实就是调用的 CPU 提供的特殊指令。

用途区别

synchronized 和 lock 用途区别
synchronized 原语和 ReentrantLock 在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用 ReentrantLock ,特别是遇到下面两种需求的时候。
1️⃣ 某个线程在等待一个锁的控制权的这段时间需要中断。
2️⃣ 分开处理一些 wait-notify,ReentrantLock 里面的 Condition 应用,能够控制 notify 哪个线程。
3️⃣ 公平锁功能,每个到来的线程都将排队等候。

先说第一种情况,ReentrantLock 的 lock 机制有 2 种,忽略中断锁和响应中断锁,这带来了很大的灵活性。比如:如果 A、B 两个线程去竞争锁,A 线程得到了锁,B 线程等待,但是 A 线程这个时候实在有太多事情要处理,就是一直不返回,B 线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。

这个时候 ReentrantLock 就提供了两种机制:可中断/可不中断:
①B 线程中断自己(或者别的线程中断它),但是 ReentrantLock 不去响应,继续让 B 线程等待,你再怎么中断,我全当耳边风(synchronized 原语就是如此);
②B 线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,完全放弃。

缓存三兄弟:缓存击穿、穿透、雪崩

缓存击穿

缓存击穿:redis中没有,但是数据库有

顺序:先查缓存,判断缓存是否存在;如果缓存存在,直接返回数据;如果缓存不存在,則查询数据库,将数据库的数据存入到缓存

在这里插入图片描述

解决方案:将热点数据设置过期时间长一点;针对数据库的热点访问方法上分布式锁;

缓存穿透

缓存穿透:redis中没有,数据库也没有

在这里插入图片描述

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

6097)]
[外链图片转存中…(img-joVKhe38-1715792196098)]
[外链图片转存中…(img-POcULZlg-1715792196098)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值