面试宝典——Java 锁

Java锁

1、锁的大致分类

image.png

2、乐观锁和悲观锁

悲观锁:对于同一个数据的使用上,悲观锁会认为在使用过程中一定会有其他线程来访问,因此会提前加上一把锁。Java中synchronized 和 Lock锁 都属于悲观锁。

乐观锁:在使用数据的过程中,只有需要修改数据时,才会去比较内存中的最新数据是否是有没有被修改。属于一种无锁编程的方式实现,Java中的CAS就是一种乐观锁。

Java乐观锁最直观的就是Atomic原子操作类,如AtomicInteger、AtomicLong等,在自增函数里面,就用到了CAS

可以看一下AtomicInteger的源码,其中static静态类里面,使用了unsafe类,这个是直接取了atomicinteger对象中value的偏移地址,因此通过valueoffset可以直接从内存里面,取出我们存的数据。

image.png

  • unsafe: 获取并操作内存的数据。
  • valueOffset: 存储value在AtomicInteger中的偏移量。
  • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}


当我们调用自增的方法时,会调用getAndAddInt的方法,这个方法里面有一个do{} while(),do是先从内存中获取value的值,也就是我们所说的CAS的旧值。

compareAndSwapInt 会借助一个CPU指令,cmpxchg 来完成 比较+交换 的原子操作。

CAS存在的问题

  1. ABA问题:在Java1.5之后,引入了AtomicStampedReference类,可以在每个值上面加上一个版本号
  2. 循环时间长:这个在JVM的参数里面,可以设置自旋次数,默认是10次,1.6版本之后,引入了自适应自旋锁,自旋次数,可以由上一次自旋的时间和持有锁的情况,进行动态的调整。
  3. 只能保证一个变量的原子操作。CAS的原子操作,只能针对一个变量,如果是多个,则无法使用。

3、Java对象模型

image.png

以64位操作系统来说,原生对象包含 对象头、实际数据、对象填充3部分:

  • Java对象头:包含 Mark Word(标记字段)、Klass Pointer(类型指针),数组长度只有在对象是数组是才有用,其他时候没有这个字段。有压缩模式和非压缩模式两种,压缩模式下指针只占4个字节,一共占12个字节,非压缩模式下16个字节。
  • 实际数据
  • 对象填充

Java对象头

Java对象头Mark Word的组成部分:

image.png

Java对象有4中锁的状态、无锁、偏向锁、轻量级锁、重量级锁

无锁:锁标记位为01,偏向标记为0

偏向锁:锁标记位为01,偏向标记为1,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。优点:偏向锁的加锁和解锁更容易,而且更快,接近于纳秒级别,缺点:一旦产生竞争,锁膨胀过程中会对锁进行撤销,带来一定的损耗。

轻量级锁:锁标记位为00,偏向锁一旦产生竞争,会通过CAS自旋加锁,如果加锁成功,则会升级为轻量级锁,同时不短的通过自旋的方式,抢占锁。优点:通过自旋的方式加锁,不会阻塞;缺点:自旋会带来CPU的损耗,同时容易进入长时间的自旋;适应场景:同步代码块短小,且执行快,适合使用自旋的方式加锁。

重量级锁:锁标记为10,在自旋加锁过程中,如果自旋次数超过10次无法加锁成功,则会膨胀为重量级锁,重量级锁会依赖于操作系统的系统调用,需要像内核申请资源,需要进行内核态和用户态的转换,因此会消耗很多的CPU资源。优点:不需要自旋,缺点:线程是阻塞的, 适用场景:高吞吐量(等待过程中,线程的调度是由操作系统完成挂起和唤醒,无需CPU自旋等待,CPU可以去做其他事情,整体的吞吐量会比其他锁高一些),或者执行时间长的同步代码块。

4、公平锁和非公平锁

公平锁和非公平锁一般是在ReentrantLock里面才会用到,synchronized是非公平锁,获取锁时,所有的线程会有竞争,而且是随机唤醒一个,参考notify和notifyAll,只有一个线程能获得锁,但并一定是先来的那一个。

ReentrantLock里面,实现公平锁和非公平锁的方式,是在获取锁的时候,需要判断当前线程是不是等待队列的第一个线程。可以参考源码

5、可重入锁和非可重入锁

可重入锁:在同一个对象里面,同一把锁,且已经获取了锁的前提下,同一个线程,如果执行其他的需要获取这把锁的方法或者同步代码段,无需排队等待,直接可以拿到锁。

非可重入锁:必须每次都得等待获取锁,非可重入锁,容易引发死锁,举例说明:Class A 里面有两个加锁的方法,方法1 和 方法2


class A {
    public synchronized void fun1(){
        fun2();
    }
    
    public synchronized void fun2(){
        doSomething();
    }
    
}

复制代码

线程1获取到A的锁之后,如果是非可重入锁,在执行fun2时,需要继续等待获取锁,但是这个时候,如果拿不到,那么fun1就无法释放锁,所以会有可能引发死锁。

可重入锁的实现,就是每次加锁的时候,会比较线程ID跟当前持有锁的线程是不是相同,如果相同,则不用获取锁。

ReentrantLock里面,如果是重入锁,会对state进行加1。

image.png

6、共享锁和排他锁

ReentrantReadWriteLock 读写锁。

写锁:可重入的排他锁。

读锁:可重入的共享锁。

锁降级:写锁可以降级为读锁。这块不是太了解,感觉很难理解,而且不知道在什么场景下使用,有知道的大佬可以指点一下。

7、synchronized和ReentrantLock的相同点和区别

  • ReentrantLock是一种可中断锁,如果等待时间较长,等待中的线程可以放弃锁去执行其他的事情

  • ReentrantLock和synchronized都是非公平锁,但是reentrantlock可以通过构造方法,设置为公平锁。

  • ReentrantLock可以有设置多个条件。synchronized 一个锁会维护一个等待队列,reentrantlock的条件等待则会每个条件都维护一个队列,这样的话,就可以实现按照条件唤醒。

8、ReentrantLock的实现机制——AQS

image.png

 

最后


如果你想要学习Java的话,我给你分享一些Java的学习资料,你不用浪费时间到处搜了,从Java入门到精通的资料我都给你整理好了,这些资料都是我做Java这几年整理的Java最新学习路线,Java笔试题,Java面试题,Java零基础到精通视频课程,Java开发工具,Java练手项目,Java电子书,Java学习笔记,PDF文档教程,Java程序员面经,Java求职简历模板等,这些资料对你接下来学习Java一定会带来非常大的帮助,每个Java初学者都必备,请你进我的Java技术qq交流群自行下载,所有资料都在群文件里,进去要跟大家多交流学习哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值