前沿
面试服务端开发岗位的时候,经常会问到java锁,以前是单体应用,用到java锁频率很高,现在是多实例,因为不是处于同一个jvm,所以java基础锁用的地方少了很多,大多数情况都在用分布式锁,关于分布式锁可以看我另外一片文章,但是在面试中也是经常出现的。下面我们来一环一环的讨论问答
正文
java中常用的锁包含哪些?
synchronized、ReentrantLock、CountDownLatch(倒计时门栓)、Semaphore(信号量)、CyclicBarrier(栅栏)
synchronized锁可以用在方面?
可以用在普通方法上、静态方法上、方法内部
都有啥区别
用在普通方法上,只能锁同一个对象下的该方法;
用在静态方法上,可以锁整个方法;
用在方法内部,只会在synchronized内部的方法块上阻塞,缩小了阻塞的范围,提高了并发量。
synchronized锁能够实现原子性、可见性、有序性吗?
原子性
在进入加锁代码块的时候加一个monitorEnter的指令,然后针对锁对象关联的monitor对象,累加加锁计数器,同时标识自己这个线程加了锁。通过monitor里的加锁计数器可以实现可重入的加锁,在出锁代码块的时候,加一个monitorExit的指令,然后递减锁计数器,如果锁计数为0,就会标志当前线程不持有锁,就会释放锁。wait和notify关键字的实现也是依托于monitor实现的,有线程执行wait之后,自己会加入一个waitset中等待唤醒获取锁,notifyall操作会从monitor的waitset中唤醒所有的线程,让他们竞争获取锁。
可见性、有序性
synchronized是依托于各种内存屏障来保证可见性和有序性的,按照可见性来划分,内存屏障可以分为Load屏障、Store屏障Load屏障的作用:执行refresh处理器缓冲的操作,也就是对别的处理器更新过的变量,从其他处理器的高速缓冲或者主存加载数据到自己的高速缓冲来,保证自己看到的是最新的数据Store屏障作用:执行flush处理器缓冲的操作,也就是把自己当前处理器更新的变量的值,都刷新到高速缓冲或者主存中。
按照有序性保障可以分为,Acquire屏障、Release屏障
例如下面代码块:
int b= 1,c = 1;
synchronized(this){->monitorenter
Load内存屏障
Acquired内存屏障
int a = b;
c = 2; ->synchronized代码块里面还是可能发生指令重排序,但是不会出现代码块中的指令与外部的指令发生重排序的情况
Release内存屏障
}->monitorexit
Store内存屏障
在monitorenter指令之后加一个Load屏障,执行refresh处理器缓冲的操作,把别的处理器修改过的最新值加载到自己的高速缓冲中,
在monitorexit指令之后,会有一个Store屏障,让线程把自己在同步代码块里修改的变量的值都执行flush处理器缓冲操作,刷到高速缓冲中,所以通过Load屏障和Store屏障,可以保障synchronized的可见性。
在monitorenter指令之后,Load屏障之后,会加一个Acqure屏障,这个屏障的作用禁止读操作和读写操作之间发生指令重排序。在monitorexit指令之前会加一个Release屏障,这个屏障的作用是禁止写操作和读写操作之间发生指令重排序。
1)原子性:通过ObjectMonitor实现加锁和释放锁
2)可见性:通过加Load屏障、Store屏障,加锁会refresh数据,释放锁会flush数据
3)有序性:Acqure屏障和Release屏障,保证同步代码块内部指令可以重排序,但是同步代码块与外部的指令不能重排序
synchronized锁底层实现逻辑
MyObject lock = new MyObject();
synchronized(lock) {}
java对象都是分为对象头和实例变量两块的,其中实例变量就是对象里的那些变量数据。然后对象头包含了两块东西,一个是Mark Word(包含hashCode、锁数据、GC数据,等等),另一个是Class Metadata Address(包含了指向类的元数据的指针) 在Mark Word里就有一个指针,是指向了这个对象实例关联的monitor的地址,这个monitor是c++实现的,不是java实现的。这个monitor实际上是c++实现的一个ObjectMonitor对象,里面包含了一个_owner指针,指向了持有锁的线程。 ObjectMonitor里还有一个entrylist,想要加锁的线程全部先进入这个entrylist等待获取机会尝试加锁,实际有机会加锁的线程,就会设置_owner指针指向自己,然后对_count计数器累加1次。 各个线程尝试竞争进行加锁,此时竞争加锁是在JDK 1.6以后优化成了基于CAS来进行加锁,操作_count计数器,比如说将_count值尝试从0变为1,如果成功了,那么加锁成功了;如果失败了,那么加锁失败了。 然后释放锁的时候,先是对_count计数器递减1,如果为0了就会设置_owner为null,不再指向自己,代表自己彻底释放锁 如果获取锁的线程执行wait,就会将计数器递减,同时_owner设置为null,然后自己进入waitset中等待唤醒,别人获取了锁执行notify的时候就会唤醒waitset中的线程竞争尝试获取锁。
ReentrantLock的底层原理
聊ReentrantLock底层原理,离不开AbstractQueuedSynchronizer。
ReentrantLock与AbstractQueuedSynchronizer的关联:ReentrantLock中有个静态内部类FairSync,而FairSync继承本类中另外一个抽象静态内部类Sync,而Sync继承AbstractQueuedSynchronizer,AbstractQueuedSynchronizer类:简称AQS,AQS内部结构如下:
private transient volatile Node head; // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node tail; // 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个隐视的链表
private volatile int state; // 当前锁的状态,0代表没有被占用,大于0代表有线程持有当前锁,之所以说大于0,而不是等于1,是因为锁可以重入嘛,每次重入都加上1
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁 //if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer 代表当前持有独占锁的线程,因为锁可以重入
Node节点的数据结构:Thread + 前驱节点+后继节点 + 线程状态(0、1、-1、-2、-3)+ nextWaiter(用于实现条件队列的单向链表)
ReentrantLock如何使用
//创建全局变量
private static ReentrantLock reentrantLock = new ReentrantLock(true);
//方法内部使用
try{
reentrantLock.lock();
//锁住的代码块
} finally{
reentrantLock.unLock();
}
ReentrantLock是否存在公平锁与非公平锁,如果存在,默认是哪种?公平锁与非公平锁有啥区别
ReentrantLock存在公平锁与非公平锁,默认是非公平锁。
公平锁和非公平锁的区别:
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
CountDownLatch原理
CountDownLatch(倒计时门栓)基于锁AQS共享模式:是通过一个计数器来实现的,计数器的初始化值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已完成任务
countDownLatch.await();
countDownLatch.countDown();
CountDownLatch与AQS关系:CountDownLatch类中有个静态内部类Sync,Sync继承了AQS类
CountDownLatch latch = new CountDownLatch(2)构造方法,最终会将2赋值给AQS中state变量(锁状态变量)。
功能:让一些线程阻塞直到另一些线程完成一系列操作后才唤醒。它通过调用await方法让线程进入阻塞状态等待倒计时0时唤醒。它通过线程调用countDown方法让倒计时中的计数器减去1,当计数器为0时,会唤醒哪些因为调用了await而阻塞的线程
Semaphore原理
Semaphore(信号量)也是基于AQS共享锁模式:
semaphore.acquire();
semaphore.release();
其相当于一个资源池,需要的时候从池中获取,state - 1 ,其他线程释放后 state + 1 功能:用于多个共享资源互斥使用以及用于控制并发线程数存在公平模式和非公平模式的区别,默认是非公平
CyclicBarrier原理
CyclicBarrier(栅栏) 基于 ReetrantLock锁 来实现:数量通过count来保存cyclicBarrier.await() count--功能:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障,所有被屏障拦截的线程才会继续执行。它通过调用await方法让线程进入屏障
CountDownLatch、Semaphore、CyclicBarrier之间的区别
CountDownLatch和CyclicBarrier不能控制并发线程的数量
Semaphor可以实现控制并发线程的数量
分布式锁的问题
分布式相关问题 参考另一篇文章
锁升级
升级过程从无锁 -》 偏向锁 -》轻量级锁 -》重量级锁
在锁对象的对象头中,有一个mark work字段,该字段标示了当前锁是否被占用以及锁的类型:
当锁对象刚被创建的时候,没有线程来占有锁,所以这个时候是无锁的状态,mark word 由下面几个部分构成:
当有一个线程来竞争的时候,由无锁状态升级为偏向锁,会将偏向锁标志设置为1,线程id设置为当前竞争锁的线程id,设置时间,由下面几部分构成:
当有2个线程来竞争的时候,升级为轻量级锁,这个时候需要记录当前抢锁线程桢栈中锁的记录,如下
当有多个线程来竞争的时候,会将轻量级锁升级为重量级锁,这个时候需要一个链表来保存这些竞争锁的线程,但是又不能在mark work 上新建一个链表,所以要创建一个监视器对象,而mark work 可以引用该监视器对象,如下:
如果你都能对答如流,并且有在自己的思考,恭喜你,离成功又近了一步