前提
CAS的概念,如果这个概念不理解将很难搞明白synchronized的原理。关于CAS的概念和原理,可以参考文章https://www.cnblogs.com/javalyy/p/8882172.html
概念和使用
synchronized是由JVM实现,java语言规范规定,要理解synchronized关键词的原理,首先理解它能用来干啥?
Oracle官方文档,Java语言规范规定了synchronized的语义:https://docs.oracle.com/javase/specs/jls/se8/html/index.html
简单的讲,保证多线程操作共享资源的互斥,达到保护共享资源数据,实现线程安全的操作的目的。
synchronized用法简介
1、直接修饰方法(静态or非静态),官方文档第8章。
2、作为同步块使用,修饰需要加锁的对象,官方文档第14章。
上面两种方式的本质是一样的,其实都是给某一对象加互斥锁,加在方法上实质是给ClassName.class或者this对象加锁
synchronized(this) {
// TODO
}
所以,要理清楚synchronized的原理,由使用来看(因为它传入的参数就是一个对象),不难看出我们需要从它修饰的对象出发,搞清楚对象里面究竟保存了什么样的数据,即对象的结构,通过对象里的数据如何帮助我们实现Java语言规范规定的语义。
Java对象的结构
先推理一下
1、平时我们定义一个类,然后通过new关键字新建一个对象,不难想象,对象中肯定开辟了空间用于保存我们在class类中定义的字段
2、另一方面,我们必须知道这个对象的结构,即它是由哪个class抽象的,熟悉jvm运行时数据区的,我们可以知道class的定义在方法区。那么对象中需要保存一个指向该方法区定义的该class的指针。
3、既然我们同步关键字需要来操作对象,那么可以推测,对象中还保存有锁相关的一些数据。
4、其它可能需要的信息
Hotspot虚拟机内的对象结构
先上图:
对象结构主要包含3部分:
1、对象头,图中黄低背景(这里面就有我们刚才推理出来的锁相关、类型指针等数据)
2、实例数据,我们自己定义的字段数据或者引用存储,图中蓝底背景
3、对齐填充,灰色部分。
对象头
不难看出,我们同步关键字synchronized的原理的关键就在对象头部分,这里以32位虚拟机举例(64位差不多,区别是多余的内存可能就浪费了,所以虚拟机参数提供压缩选项,开启后,可以压缩对象),由上面的图从右至左为低位到高位的顺序。
1、Markword
markword是对象头中一个32位长度的存储区,用来存储锁状态,gc状态、hashcode等对象关键数据。为了让Markword存储更多的信息,最低的2位为标志位,不同的标志位对应不同的状态。第3位(从低到高)为偏向锁状态。
a、无锁状态(标志位=01)
剩余bit位,从低到高依次为:偏向锁状态=0(1位)、gc年龄(4位)、hashcode(25位)
b、偏向锁(标志位=01)
如果虚拟机开启了偏向锁优化,当有线程第一次来到synchronized同步块时,会直接获取到偏向锁,对象会进入到偏向锁状态,此时除最低两位为01外,剩余bit位,从低到高依次为:偏向锁状态=1(1位)、gc年龄(4位)、epoch偏向锁时间戳(2位)、偏向锁持有线程ID(23位)。这里高位的23位如果为空,则代表当前对象可偏向,但是未锁定也未偏向;如果高位23位保存了某个线程的ID,则表示当前对象处于锁定且偏向状态,此时,如果线程自己释放了偏向锁,它不会发生任何变化,而如果该线程再次来获取锁,也不会有CAS操作,只需要判断这里的线程id是否是自己即可(这是JVM做的优化);而如果有其它线程来获取锁,当判断到这里的线程ID不是自己,然后进行CAS抢锁,因为这里已经被别的线程占有了,肯定会失败,于是会进行锁升级;
偏向锁升级过程:
1)、先进行偏向锁撤销
2)、等待占有偏向锁线程进入到安全点后,暂停原线程
3)、再次检查偏向锁状态,锁已释放,则进入不可偏向对象无锁状态、唤醒原线程继续执行。锁未释放,升级为轻量级锁的状态(这里就是轻量级锁机制、在原线程生成lock record,保存锁对象的mark word和owner,而对象的mark word则用lock record指针替换,标志位修改等工作)、唤醒原线程继续执行。
c、轻量级锁(标志位=00)
轻量级锁采用CAS实现,进入轻量级锁状态的对象,剩余bit位,执行持有锁线程执行栈帧中的lock record地址。这个lock record是线程再抢轻量级锁时创建,里面保存有用于释放锁时恢复锁对象的mark word,owner指向持有锁的对象地址。
如果没有开启偏向锁优化(JDK1.6以后默认开启),则线程来抢锁,直接进入抢轻量级锁的流程,抢轻量级锁的流程实质就是采用CAS操作修改对象头Markword的过程,首先线程执行到synchronized的临界区时,在线程堆栈创建lock record信息,把synchronized修饰的对象的对象头中的markword(前提是没有别的线程获取到锁)复制到lock record中,然后采用CAS操作将lock record + 末位00,这样一个32位的数据替换到对象头的markword位置,如果成功,代表抢到了锁,则记录lock record中的owner=对象的地址。
d、重量级锁(标志位=10)
轻量级锁有一个缺陷,如果同时很多线程通过CAS自旋抢锁,那么可能存在有线程一直在自旋占用CPU而抢不到锁,会浪费大量的cpu时间,严重影响程序性能,那么虚拟机有机制将轻量级锁升级为重量级锁,重量级锁的状态为,剩余bit为,指向每个对象都会有一个与之对象的monitor,重量级锁不会存在抢不到锁一直占用cpu资源的情况,它的实现原理类似Java的ReentrantLock,可以参见我简单参照Java源码实现的一个ReentrantLock。
```java
package com.study.lock;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
/**
* 利用CommonMash实现
* @author Administrator
*
*/
public class YbjReentrantLock implements Lock
{
private boolean isfair;
public YbjReentrantLock(boolean isfair) {
this.isfair = isfair;
}
/**
* 模板方法模式,实现锁的公共逻辑
* @author Administrator
*
*/
public static class CommonMash
{
protected AtomicInteger readCount = new AtomicInteger(0);
protected AtomicInteger writeCount = new AtomicInteger(0);
//只有写线程能成为owner
protected AtomicReference<Thread> owner = new AtomicReference<>();
protected volatile LinkedBlockingQueue<WaitNode> lockWaitors = new LinkedBlockingQueue<>();
public static class WaitNode {
Thread thread;
boolean write;
int arg;
public WaitNode(Thread thread, boolean write, int arg) {
this.thread = thread;
this.write = write;
this.arg = arg;
}
}
public void lock()
{
int acqurie = 1;
if(!tryLock(acqurie)) {
//放入队列,用什么方法?
WaitNode node = new WaitNode(Thread.currentThread(), true, acqurie);
lockWaitors.offer(node);
while(true) {
node = lockWaitors.peek();
if (node != null && node.thread == Thread.currentThread()) {//为什么必须判断头部是当前线程本身?
//因为,程序代码这里当前是在为执行到这里的线程本身抢锁,抢到锁之后,应该移除队列的也必须是当前线程,否则不是本身的话
//就相当于我线程抢到了锁,但是我把你从队列里移除了
if(tryLock(acqurie)) {
lockWaitors.poll();
return;
} else {
LockSupport.park();//因为park和unpark不分先后,即先unpark,再park不会导致卡死,所以及时没有获取到锁,但是在park之前又有线程释放了锁,导致先unpark了,不会存在卡死,没有问题
}
} else {
LockSupport.park();
}
}
}
}
public boolean tryLock(int acqurie)
{
int rc = readCount.get();
if (rc != 0) {
return false;//为什么直接只判断写锁不为0就返回,这和jdk的读写锁实现是一致的,不允许同一个线程读锁,升级写锁//如果rc==1,是否能判断这个获取了唯一读锁的线程是否是来抢锁的线程,貌似判断不了
}
int count = writeCount.get();
if (count == 0) {
//利用原子操作,去抢写锁(设置writeCount=1)但是这里与上面readCount的判断会有原子性问题,可能此时readCount被别的线程修改了
//所以需要一个判断read,write,和设置write的原子操作,JDK是将readCount和WriteCount用一个整形的高半位和低半位分别来表示实现的。
//这里为了简单,先不管
//抢锁
//bug1,不要把参数传反了,否则不会成功,bug2,应该设置为获取的count + acqurie
//bug2 boolean success = writeCount.compareAndSet(0, acqurie);
boolean success = writeCount.compareAndSet(count, count + acqurie);
//成功则设置当前线程为owner
if (success) {
owner.set(Thread.currentThread());//bug,这里抢成功了没有返回true,那么会一直抢不成功
return true;
}
} else {
//能直接返回吗,不能
if(owner.get() == Thread.currentThread()) {//写锁重入
writeCount.set(count + acqurie);//这里可以直接修改值
}
return false;
}
return false;
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
throw new UnsupportedOperationException();
}
public void unlock()
{
int acquire = 1;
if (tryUnlock(acquire)) {
WaitNode next = lockWaitors.peek();
if (next != null) {
Thread t = next.thread;
LockSupport.unpark(t);
}
}
System.out.println("writeCount"+writeCount);
System.out.println("readCount"+readCount);
}
public boolean tryUnlock(int acquire) {
if (Thread.currentThread() != owner.get()) {
throw new IllegalMonitorStateException();
} else {
int count = writeCount.get();
writeCount.set(count - acquire);
if (writeCount.get() == 0) {
//为什么要用原子操作
//按理说只有获得到锁的线程才能走到这里,owner也不会被获取锁的地方改变
//1。不会被释放锁的改变,2、抢锁的线程呢?其它线程此时能抢锁吗,能,因为writeCount==0
//因为writeCount先被修改为0,此时其它线程可以去抢写锁,抢到后owner被修改为其它线程,若不采用CAS操作,可能会覆盖成功抢锁的owner为空,但是此时锁确实另外一个线程的
//所以要用原子操作,防止覆盖
owner.compareAndSet(Thread.currentThread(), null);
return true;
}
return false;
}
}
public void lockShared()
{
throw new UnsupportedOperationException();
}
public boolean tryLockShared(int acqurie)
{
throw new UnsupportedOperationException();
}
public boolean tryLockShared(long time, TimeUnit unit) throws InterruptedException
{
throw new UnsupportedOperationException();
}
public void unlockSharedBadPratice()
{
throw new UnsupportedOperationException();
}
public void unlockShared()
{
throw new UnsupportedOperationException();
}
public boolean tryUnlockShared(int acquire) {
throw new UnsupportedOperationException();
}
}
private CommonMash common = new CommonMash(){
public boolean tryLock(int acquire)
{
return tryLock(acquire, isfair);
}
private boolean tryLock(int acqurie,boolean isfair)
{
int rc = readCount.get();
if (rc != 0) {
return false;//为什么直接只判断写锁不为0就返回,这和jdk的读写锁实现是一致的,不允许同一个线程读锁,升级写锁//如果rc==1,是否能判断这个获取了唯一读锁的线程是否是来抢锁的线程,貌似判断不了
}
int count = writeCount.get();
if (count == 0) {
CommonMash.WaitNode node = null;
if (isfair) {
return tryLock0(count, count+acqurie);
} else if((node = lockWaitors.peek()) !=null && Thread.currentThread() == node.thread) {
return tryLock0(count, count+acqurie);
}
} else if(owner.get() == Thread.currentThread()) {
writeCount.set(count + acqurie);//这里可以直接修改值
return true;
}
return false;
}
private boolean tryLock0(int expect, int update) {
if (writeCount.compareAndSet(expect, update)) {
owner.set(Thread.currentThread());
return true;
}
return false;
}
};
@Override
public void lock()
{
common.lock();
}
@Override
public void lockInterruptibly() throws InterruptedException
{
// TODO Auto-generated method stub
}
@Override
public boolean tryLock()
{
return common.tryLock(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
// TODO Auto-generated method stub
return false;
}
@Override
public void unlock()
{
common.unlock();
}
@Override
public Condition newCondition()
{
// TODO Auto-generated method stub
return null;
}
}
e、gc(标志位=11)
该对象可以被gc啦
2、类型指针
对象头第二部分,通过类型指针,对象可以知道该对象的抽象类,可以知道对象是什么类型以及对象的结构。
3、数组长度
如果对象是数组,那么对象头中还存储了数组的长度。
数据区
伪共享
Jvm编译时,会对成员变量进行优化排序,基本的排序规则是越长的类型在月前面,如果64位开启了对象头压缩,对象头长度不是8字节的整数,可能会选一个合适长度的字段填充到头部。
填充
对象填充是虚拟机提升性能的一个优化。