1. 线程状态转换
java并发编程状态基础,和锁没什么关系,但是很重要,就放这里吧。
2. synchronized 关键字
2.1 通过 ReentrantLock 查看锁的代码层原理
因为synchronized
底层是由c/c++
来实现的,不容易查看,所以先用 reentrantLock
来看看锁住一个对象的时候究竟是怎样实现线程互斥的。
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 业务代码
Thread.sleep(1000);
lock.unlock();
lock()
方法是进行加锁操作,我们去看lock()
的源码可以看到以下代码(以公平锁为例)。
// 第一步
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//
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()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 加锁成功时,需要判断当前锁的状态
c == 0
, 并且不需要排队!hasQueuedPredecessors()
,就会使用CAS
改变c
值, 然后返回true
,则acquire()
函数正常返回。 - 加锁失败(比如锁被其他线程持有), 此时
c!=0
, 会先判断是否是当前线程持有锁current == getExclusiveOwnerThread()
, 如果不是直接返回false
,返回false
之后,acquire()
会在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
调用LockSupport.park(this);
方法挂起线程。
所以总结锁的本质:
- 加锁成功 ----
lock()
正常返回 - 加锁失败 ----
lock()
阻塞 - 加锁的本质就是改变一个变量的值
2.2 synchronized锁原理
2.2.1 问题引起
由reentrantLock
可知,锁的本质就是改变一个变量的值,对于synchronized
而言,这个变量就是存储在对象头信息中。
在工程中引起openjdk
打印出对象的头信息。
引入下面依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
以下面对象为例子:
public class Tiger {
boolean name;
}
打印对象头:
Tiger tiger = new Tiger();
System.out.println(ClassLayout.parseInstance(tiger).toPrintable());
对象头信息如下:
可以看到一个new
一个对象时,在堆先会放12
个字节的对象头信息(object header
),然后才是对象的成员变量所占字节数,最后是java
内存对齐相关的东西,一个java
对象所在的字节大小必须是8
的倍数。
2.2.2 对象头
java
的对象头(object header
)分为两个部分,前8
个字节表示标记字段Mark Word
,后面4
个字节表示类型指针Klass Point
。其中Klass Point
是对象指向它所属类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word
用于存储对象自身的运行时数据,锁标记就存在Mark Word
中,下面看一下Mark Word
每个bit
在不同状态下的含义。
-
一个对象不做任何处理,就是上图的无锁可偏向状态
-
一个对象如果计算了
hashcode
之后就会进入到无锁不可偏向状态,因为前面那些位需要存储hashcode
无法再存储需要偏向的线程 ID. 注意lombok.Data
注解重写了hashcode
,hashcode
将不会存在对象头中,因此计算了hashcode
之后还是可偏向状态。- 没有
@Data
注解
@NoArgsConstructor //@Data public class Tiger { boolean name; char ch; int age; } Tiger tiger = new Tiger(); System.out.println(ClassLayout.parseInstance(tiger).toPrintable()); System.out.println("Hashcode: " + Integer.toHexString(tiger.hashCode())); System.out.println(ClassLayout.parseInstance(tiger).toPrintable()); synchronized (tiger){ System.out.println(Thread.currentThread().getName()); System.out.println(ClassLayout.parseInstance(tiger).toPrintable()); }
- 没有
运行结果:
计算了hashcode
之后变成不可偏向状态,加锁时直接变成轻量锁。
- 有
@Data
注解
代码相同,就是加上@Data
注解,可以看到hashcode
并没有存在对象头中,对象锁也是可偏向的,因此加锁时是偏向锁。
- 锁的膨胀过程
2.2.3 jvm 参数
jvm
打印所有的 jvm 参数:-XX:+PrintFlagsInitial
jvm
偏向锁延迟时间:-XX:BiasedLockingStartupDelay=0
, 默认是4000ms
。
jvm
轻量锁默认自旋次数 -XX:PreInflateSpin=10
. – 自旋10次。轻量锁自旋到指定次数如果还拿不到锁就会膨胀为重量锁。