一、Synchronized和ReentrantLock是怎么实现的,他们有什么区别
synchronized属于独占式悲观锁,通过jvm隐式实现,只允许同一时刻只有一个线程操作资源。
java中,每个对象都隐式包含一个monitor(监视器)对象
加锁的过程其实就是竞争monitor的过程
当线程进入字节码monitorenter指令之后
线程将持有monitor对象,执行monitorexit时释放monitor对象
当其他线程没有拿到monitor对象时,则需阻塞等待,获取该对象
ReentrantLock是Lock的默认实现方式之一
是基于AQS(Abstract Queued Synchronizer,队列同步器)实现的,默认是通过非公平锁实现的
内部有一个state的状态字段,用于表示锁是否被占用
如果是0则表示锁未被占用,此时线程就可以把state改成1,并成功获得锁
而其他未获得锁的线程只能排队等待获取锁的资源
区别如下:
synchronized是jvm隐式实现的,而ReentrantLock是Java语言提供的API;
ReentrantLock可设置成公平锁,而synchronized不行
ReentrantLock只能修饰代码块,而synchronized可以修饰方法,代码块等
ReentrantLock需要手动加锁和释放锁,如果忘了释放就会造成资源永久使用
synchronized则无需手动释放锁
ReentrantLock可以知道是否获得了锁,而synchronized不行
两者都提供了锁的功能,具备互斥性和不可见性,在jdk5中,synchronized的性能远远低于ReentrantLock,但在jdk6之后synchronized的性能只是略低于ReentrantLock
MarkWord的字节码:
公平锁与非公平锁:
线程需要按照请求的顺序来获得锁,
非公平锁则允许“插队”的情况存在
插队:线程在发送请求的同时,该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程,直接拥有锁。
频繁的挂起和恢复会造成一定的开销,所以公平锁的性能不如非公平锁,所以ReentrantLock和Synchronized默认都是非公平锁来实现的。
二、大厂高频面试题
ReentrantLock的实现细节是什么
先解释waitStatus的值有哪些,后面用得到
(源码里的注释太长,就不放了)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock是通过lock来加锁,通过unlock来释放锁。
1、lock:
public void lock() {
sync.acquire(1); // 调用AbstractQueuedSynchronizer.acquire
}
1.1、AbstractQueuedSynchronizer.acquire:
public final void acquire(int arg) {
// 调用ReentrantLock.NonfairSync或FairSync.tryAcquire
if (!tryAcquire(arg) &&
// 独占锁中,获取锁失败:// 将当前线程包装进Node
// 若当前线程是head节点的后置节点,且head状态为cancelled
// 则尝试获取锁,若成功,则将head节点设置为null,帮助原来的head节点gc(此时等待队列没有Node了)
// 否则尝试挂起当前线程
// 如果前驱节点的状态为Signal,则挂起当前线程
// 如果前驱节点被cancelled,则尝试找到非cancelled状态的节点,并将他的next设置成当前节点
// 否则(前驱节点的waitStatus<0),则将前驱节点的状态设置成0
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 调用Thread.currentThread().interrupt();停止线程
selfInterrupt();
}
NonfairSync.tryAcquire调用的是Sync的nonfairTryAcquire,所以我们直接分析nonfairTryAcquire和Fair.tryAcquire,公平锁比非公平锁多了一行!hasQueuedPredecessors() ,用来查看等待队列是否有已经在排队的线程
1.1.1、Sync.nonfairTryAcquire
// 保护被注解的方法,通过添加一些额外的空间,防止在多线程运行的时候出现栈溢出
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前锁状态
int c = getState();
// 没有线程持有锁
if (c == 0) {
// cas尝试将锁状态换成1
if (compareAndSetState(0, acquires)) {
// 如果cas成功,则将当前线程设置成当前锁的持有线程
setExclusiveOwnerThread(current);
return true; // 返回上锁成功
}
}
// 如果有线程持有锁,且持有线程是当前线程
else if (current == getExclusiveOwnerThread()) {
// 锁状态 + 1 可重入锁--可重入的含义
int nextc = c + acquires;
// 重入次数溢出,超过Integer.MAX_VALUE导致变成负数
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置锁状态
setState(nextc);
// 返回获取锁成功
return true;
}
// 已经有线程获取锁,且非当前线程,返回获取锁失败
return false;
}
1.1.2、Fair.tryAcquire
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前锁状态
int c = getState();
// 没有线程持有锁
if (c == 0) {
// 等待队列没有待唤醒且仍存活的线程
if (!hasQueuedPredecessors() &&
// 尝试获取当前锁
compareAndSetState(0, acquires)) {
// 获取成功,将当前线程设置为当前锁所有线程
setExclusiveOwnerThread(current);
// 返回获取锁成功
return true;
}
}
// 如果有线程持有锁,且持有线程是当前线程
else if (current == getExclusiveOwnerThread()) {
// 锁状态 + 1
int nextc = c + acquires;
// 重入次数溢出,超过Integer.MAX_VALUE导致变成负数
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置锁状态
setState(nextc);
// 返回获取成功
return true;
}
// 返回获取失败
return false;
}
2、unlock:
public void unlock() {
sync.release(1); // 调用AbstractQueuedSynchronizer.release
}
2.1、AbstractQueuedSynchronizer.release:
public final boolean release(int arg) {
// 调用ReentrantLock.Sync.tryRelease(1)
if (tryRelease(arg)) {
// 获取等待队列的head
Node h = head;
// 如果等待队列有node,且线程并不是处于初始状态
if (h != null && h.waitStatus != 0)
//调用AbstractQueuedSynchronizer.unparkSuccessor,尝试唤醒等待队列头部线程
unparkSuccessor(h);
// 返回解锁成功
return true;
}
// 返回失败
return false;
}
2.2、ReentrantLock.Sync.tryRelease
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 持有锁状态 - 1(放一次锁,但不一定保证是把锁直接放掉,有可能重入了)
int c = getState() - releases;
// 如果当前线程不是锁持有者,则报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 设置返回的状态,标识着锁是否完全释放
boolean free = false;
// 锁状态为0(完全不持有)
if (c == 0) {
// 返回值设置为已释放锁,且锁持有线程设置为null
free = true;
setExclusiveOwnerThread(null);
}
// 设置锁状态
setState(c);
// 返回锁是否已经没有线程持有
return free;
}
2.3、AbstractQueuedSynchronizer.unparkSuccessor
private void unparkSuccessor(Node node) {
// 获取等待状态
int ws = node.waitStatus;
// 等待状态小于0,则设置为初始状态
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
// 获取下一个node(队列中下一个线程)
Node s = node.next;
// node为空或者状态为cancelled
if (s == null || s.waitStatus > 0) {
s = null;
// 循环找到下一条能执行的线程
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
// 如果有下一个能执行的线程
if (s != null)
// 给node增加调用凭证
// 调用线程的时候会判断,如果凭证不为0则挂起
// 凭证只能有1个,所以unpark多次也是一样的效果
LockSupport.unpark(s.thread);
}
JDK1.6时锁做了哪些优化
自适应式自旋锁,锁升级
JDK1.6引入自适应自旋锁,意味着自旋时间不再固定
比如在同一个锁对象上,如果通过自旋等待成功获取了锁,那么虚拟机就会认为,它下一次很有可能也会成功(通过自旋获取到锁),因此自旋等待的时间会比较长,相反,则比较短,甚至直接忽略自旋,避免浪费cpu资源。
锁升级就是从偏向锁,到轻量级锁,再到重量级锁的升级过程。是JDK1.6提供的优化功能,也称为锁膨胀。
偏向锁是指在无竞争的情况下设置的一种锁状态,意思是他会偏向第一个获取它的线程,当锁对象第一次被获取到之后,会在此对象头中设置01表示偏向锁模式,并且在对象头中记录此线程ID。偏向锁可以提高带有同步,但无竞争的程序性能。但如果在多数锁总会被不同线程访问时,偏向锁模式就比较多余。可以通过-XX:-UseBiasedLocking来禁用偏向锁以提高性能。
轻量锁是相对重量锁而言的。
在JDK1.6之前,Synchronized是通过操作系统的互斥量(mutex Lock)实现的,这种实现方式需要在用户态和核心态之间做转换,有很大的性能消耗,这种传统实现锁的方式被称为重量锁。
轻量锁是通过比较并交换(CAS,Compare and Swap)来实现的,它对比的是线程和对象的Mark Word,如果更新成功,则表示当前线程成功拥有此锁,如果失败,虚拟机会先检查对象的MarkWord是否指向当前线程的栈帧。如果是,则说明当前线程已经拥有了此锁,否则,则说明此锁已经被其他线程占用,当有两个以上的线程竞争锁时,锁就会膨胀,升级成重量锁。
有两个锁写的不错的相关博客,可以参考
https://www.cnblogs.com/deveypf/p/11406932.html
https://www.jianshu.com/p/73b9a8466b9c