【Java】关于synchronized的深入理解

  近日在Java编程中使用到了synchronized这个关键字,对此不是特别了解想深挖以下内部原理,就在网上搜视频,看的是马士兵老师的讲解,听完之后有许多感想就此写出。

CAS(无锁/乐观锁)

  要讲清synchronized之前我们首先要了解什么是CAS。CAS(compare and swap)比较并且交换。

CAS包括三个操作数:需要读写的内存位置的值E,进行比较的预期原值N和经过线程完后拟写入的新值V。首先读取值E,然后放到线程计算结果得V,计算结果后往回写的时候比较E与N,如果相同说明没人动过你,那就直接更新值。这种情况下,不需要上锁的。如果不相同,则它会又进行上述一系列操作直到值相等时。

在这里插入图片描述
ABA解决方案:加版本号
AtomicMarkableReference 可以解决,使用boolean变量——表示引用变量是否被更改过,不关心中间变量变化了几次
AtomicStampedReference 也可以解决,其中的构造方法中initialStamp(时间戳)用来唯一标识引用变量,引用变量中途被更改了几次

Java对CAS的支持

在JDK1.5中增加了java.util.concurrent就是建立在CAS之上的。相比较于synchronized这种阻塞算法,CAS在性能上有了很大提升。

AtomicInteger类的一些方法为例

public class AtomicInteger extends Number implements java.io.Serializable {
	private static final long serialVersionUID = 6214790243416807050L;
	private volatile int value;

	public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }

	public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }
}

追踪incrementAndGet()方法,可得到在Unsafe类中

    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

Java中CAS最底层的实现是一条汇编语言lock cmpxchg,当是多核(Multi-Processor)的时候才会有lock,单核无lock。

加lock原因:但是问题是这条指令不是原子性的,那是必然啊,你看英文cmp和xchg,很明显两步操作比较和交换,如果两操作之间被另外一个打断就不行了。所以要加lock,正在执行操作的CPU在cmpxchg操作时会把总线锁住(实际上是锁定一个北桥信号),其他CPU就一定访问不到这块内存。

对象内存布局(Java Object Layout)

Java对象在堆中的内存结构以及对象大小的计算。

在这里插入图片描述

我们new出来的对象在堆里面分为四个部分。

  1. MarkWord:标志位,记录着锁信息(8字节)
  2. Class Pointer:指针,指向类.class的内存地址(8字节)
  3. instance data:所有成员变量的大小(视具体情况定)
  4. padding:补充(8字节对齐,不够这里补)

我们来用个例子实际观察下它的分布吧。

需要一个jar包:
https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/

public class Demo {

	public static void main(String[] args) {

		Object o = new Object();
		System.out.println(ClassLayout.parseInstance(o).toPrintable());
	}

}

在这里插入图片描述

结论:我们所谓的锁(synchronized)某个对象,实际上是修改了这个对象的markword内容。
因此markword里面一定记录着锁信息。l另外还有GC信息和hashcode。

锁升级

synchronized这个关键字在jdk1.2之前效率十分低下,早期叫重量级锁,申请锁资源必须经过内核,1.6之后有好的优化,这个优化就是锁升级。

为什么要优化?效率低下的原因是什么?答案需要在一个知识点用户态内核态

用户态和内核态

在这里插入图片描述
  最早操作系统,这两层不分,这就导致一些APP可以直接访问硬件,很容易损坏硬件。两层分开的安全之处在于:所有用户态程序,你要进行十分危险的操作时,必须经过OS的控制。内核可以调用所有指令,而用户态有些指令不可以直接执行,要经过内核来调用。用户态需要调用内核的某个指令,要经过一个软中断0x80。

  早期synchronized叫重量级锁,不管你几个线程,一个或者多个都需要申请锁资源必须通过Kernel,其效率低下。
  优化:在某些特定情况下,我不需要经过OS允许,只需要在用户空间就可以解决。比如CAS,只需要比较并且进行交换,不需要锁资源。

markword最低两位用来标志哪种状态的锁。
在这里插入图片描述
00轻量级锁,01偏向锁,10重量级锁,11GC垃圾回收。

锁升级的大致过程就是下面这幅图。

在这里插入图片描述

偏向锁(Biased Lock)

  上锁的过程可以用生活中的上厕所例子很容易解释清楚。synchronized包裹的就是要在厕所里做的事,某个人(线程)来,先要看厕所有没有上锁,如果有,就等在外面,没有则自己进厕所,并且上锁,告知其他人(线程)不能进来(打断我要做的事)。

  所谓偏向锁就是往markword里面写入线程的id号(当前线程指针),它就代表这个厕所我占了。偏向锁实际上没有上锁,他只是修改了markword的信息。

为什么会有偏向锁?多数synchronized方法,在很多情况下,只有一个线程在运行。例如:StringBuffer append方法。只有一个线程在运行,我根本没有必要去OS申请锁资源(浪费时间),我直接把我自己的id号往门上一贴标识是我现在占着厕所开始做事就行了。

自旋锁(spin Lock)

  又称轻量级锁;自旋锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
  用例子来说明就是:这个本来是一个人占着上的是偏向锁,一旦有少量竞争,其他竞争者就认为不合理了,凭什么你一个人占着厕所?就把标签撕下来(偏向锁markword的线程id号),上自旋锁(修改markword信息)。开始抢,谁抢到了把自己信息(lock record)写到markword上,谁就获得了这把锁。
其他线程怎么抢啊?用CAS来抢,看门上还是线程A的id号不,然后改成我自己id号,往回改的过程看门上依然是否是A的id号,依然是说明还没完事呢,就继续用CAS,如果不是说明完事了,我赶紧抓紧机会把自己写上去,只要我在上面写上信息了,就说明这个厕所归我了。

通俗理解:线程1在里面上厕所,其他线程哥们在外面转悠(CAS过程)一直等待,到我了没有啊,终于线程1结束了,一在厕所外面的哥们抢到了,好,这个厕所归你了。这就是为什么称为自旋锁,是其他竞争者一直在CAS的过程。

重量级锁

  其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程,进行竞争。
  没什么好解释的,就是厕所所长OS所拥有的那把锁资源。

偏向锁升级为轻量级锁:只要有不是只有一个线程要做这个事偏向锁就会升级为轻量级锁。偏向:肯定是偏向一个线程的,来一个新的线程我肯定不能偏向一个人所以就要升级。
轻量级锁升级为重量级锁:JDK1.6之前,如果有的线程自旋超过10次和超过CPU核数的二分之一的线程等待就升级为重量级锁。现在是自适应升级,JVM自己做计算,合适的时候升级。

Q:为什么有了轻量级锁还需要重量级锁?
A:考虑这么一种情况,某个线程占厕所时间太长了,外面的人等了好久(都在外面自旋,while消耗CPU资源),这样不公平,都让你一个人占去了,上个大锁,谁抢到了是谁的,重量级锁不消耗CPU资源,重量级锁是通过队列来排队进行控制的。
自旋锁:会占用CPU资源,while循环(排在厕所外等待的人)
重量级锁:不占用CPU资源,等待队列实现,OS进行调度

关于匿名偏向这条路:其实偏向锁默认是启动的,但是它会延迟4s。也就是说JVM启动4s后,偏向锁才起作用。

看Java-xx参数命令:java -XX:+PrintFlagsFinal -version

Q:偏向锁默认打开一定效率高吗?偏向锁效率一定大于自旋锁吗?为什么?
A:不一定。如果明确知道某个资源会有多个线程进行竞争,那么有必要上偏向锁吗?因为升级过程有撕下id号(修改markword),锁撤销这一过程的。明确知道有竞争就不用偏向锁了,直接用轻量级锁更快。
Q:为什么延迟4s?
A:默认延时。JVM启动时,明确有些资源(例如:某段内存)被多个线程使用。为了不着急上偏向锁,看是否要转换成轻/重锁。

各个锁的优缺点。
在这里插入图片描述

synchronized底层实现

字节码层面上

  对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

monitorenter
monitorexit  
monitorexit  

汇编层面上

synchronized的最底层实现还是一条指令。lock cmpxchg

为什么lock cmpxchg。锁的本质:我必须经过这条代码,才能执行下面代码。多核(Multi-Processor)的时候才会有lock,单核无lock。

加lock原因:但是问题是这条指令不是原子性的,那是必然啊,你看英文cmp和xchg,很明显两步操作比较和交换,如果两操作之间被另外一个打断就不行了。所以要加lock,正在执行操作的CPU在cmpxchg操作时会把总线锁住(实际上是锁定一个北桥信号),其他CPU就一定访问不到这块内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值