synchronized原理

前提

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字节的整数,可能会选一个合适长度的字段填充到头部。

填充

对象填充是虚拟机提升性能的一个优化。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值