一、写作议题:
- 一个问题:为什么要使用锁机制,锁机制是解决了一个什么问题,这里我在维基百科上拷贝了一下线程安全的一个基本概论;
Thread safety is a computer programming concept applicable to multi-threaded code.
Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction. There are various strategies for making thread-safe data structures.
A program may execute code in several threads simultaneously in a shared address space where each of those threads has access to virtually all of the memory of every other thread. Thread safety is a property that allows code to run in multithreaded environments by re-establishing some of the correspondences between the actual flow of control and the text of the program, by means of synchronization
- 由于cpu的串行特点,在访问共享数据和共享内存的时候,势必会造成一些数据竞争,导致程序不能按照我们预想的顺序进行,所以java利用锁机制来实现多线程情况下的同步。
二、java中的锁机制
- java内置的锁,同步原语synchronized的关键字
- 在jDK1.5以后引入的lock接口。
本篇主要介绍一下我对synchronized关键字的一些理解,查阅了一些资料,后续我也会分享出来。
三、深入理解synchronized关键字。
- synchronized关键字是java并发同步开发的基本技术,是java语言层面上提供的同步技术;
- 下面一段代码来了解一下,java对于synchronized关键字,在编译以后会发生什么操作。
public class Syn {
private static final Object lock=new Object();
private static int a=10;
public static void main(String[] args) {
int a=0;
synchronized (lock){
a++;
}
System.out.println("Result"+a);
}
public static synchronized int getNum(int increse){
a=increse+a;
return a;
}
现在使用javap命令了解一下,针对同步的部分生成的字节码;
7: **monitorenter**
8: iinc 1, 1
11: aload_2
12: **monitorexit**
13: goto 21
16: astore_3
17: aload_2
18: **monitorexit**
具体monitorenter和monitorexit到底干了什么事?jvm需要保证monitorenter和monitorexit一一配对,任何一个对象都有一个monitor与之关联,当且一个monitor被持有以后,它将处于锁定的状态,线程执行到monitorenter指令以后,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁,有兴趣的同学可以了解一下jdk的源码。找到这几个函数主要干了什么工作。
- 这里直接给出答案,java在jdk1.6引入了偏向锁,大大的提升了synchronized关键字的性能,这里synchronized不在是我们所说的重锁,性能底下,说它鸟枪换炮也不为过。
这里贴出java官方文档openJdkwiki
第一次看这个图的时候,也是懵逼状态,查阅了一些资料和一些讲解后有了一点自己的理解。 - 知识补充
想要了解这张图可能需要了解一些其他的java知识;
1、java对象的内存布局:java对象在内存中分为对象头和对象体,对象头中存储对象的特征数据,对象体中存储对象的数据部分,那我们如何查看对象的内存布局了,其实java提供了一个工具包交叫jol,可以帮组我们了解java对象的内存布局。
step1:
//首先引入jol的maven依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
step2:
//编写一个简单的java类,调用jol的方法,打印对象的内存分布
public class JavaObject {
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(A.class).toPrintable());
}
public static class A{
public boolean f;
public Double d;
}
}
step3:
分析运行结果:
运行结果截图:
4种锁,对象内存的分布情况。
通过截图可以看出,JavaObejct对象的对象头占12个字节,f字段为boolen类型字段,占一个字节,因为java是8字节对齐(Objects are 8 bytes aligned),所以需要3字节的填充字段,Double类型占占4个字节,加上填充字段4个字节,所以这个对象的整体长度为24字节。
- 现在就来分析一下openJdk的那一幅图,首先需要一个名次解释,jdk1.6引入的偏向锁,也可以叫做偏置锁是什么意思?所谓偏置就是就是偏向某一个线程的意思。
- 偏向锁的由来:java的编写者发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获取,偏向锁的目的是在某个线程获取锁之后,消除这个线程重入的开销,看起来是对这个线程又一个偏心的操作。
传统的锁过程是一个重量级锁,所谓重量级锁,其实是一个悲观锁的表现,会导致线程状态的改变,而线程状态的改变其实是很重量级的,下来我们就可以分析一个jdk1.6以后引入的锁优化技术,默认的使用了偏向锁。 - 上图分析,从左边开始,java1.6+就按照以上的逻辑进行锁优化,首先
java在分配对象的时候,会有下面两种情况,但是默认jdk1.6以后开启了优化技术,所以我们就从左边开始讲起。
1、if biased locking is enable for the class;(如果偏向锁可用)
无锁状态:如果一个对象处于未锁定,未偏置,但是可以偏置的状态,对象头中会存储
1 |01 1表示可以偏置(默认开启)。01表示未锁定;
2、unlocked and unbiased but biasable object
初始化锁定:(偏向锁的一个获取)
这个时候如果有一个线程来获取锁此时这个对象就马上变成了偏置状态,其实对比上一个状态,改变的就是在对象头的栈帧中多了线程的线程ID,其实也就是官宣了,说明我这个对象目前已经是名花有主的人了。当相同的线程再次来获取这个锁的时候,其实这里就不需要在执行CAS操作,只需要一次指针的对比,而指针的对比是很轻量级的,如果对比成功,就继续往下执行,并该线程持有这个锁,如果这个时候有另外一根线程来竞争这个锁,指针的对比就失败那么就发生以下两种情况。结合上面的图我们可以分析;
一、假如持有锁的为线程1,线程2此时来竞争锁,当线程2到达的时候,要么线程1已经释放了锁,处于一个解锁的状态,此时对象的状态就是偏置1,未锁定。但是偏向锁还是存在的,并且线程的id可能还是线程1。
二、如果此时线程1正好持有这个锁,那么此时对象的状态为偏置1,已锁定。
我们结合上图分析一个这两种情况,情况一,如果当前锁对象的状态为不可偏置,未锁定,虚拟机将会执行CAS操作,尝试更新指向线程2,也就是撤销偏向的操作。如果成功的话,那么线程2就持有这个锁对象。
并且后面的标志位变为00,表示轻量级锁定。
3、情况二、说明目前的锁是处于锁定的状态,这时候仍然会升级为轻量级锁,但是此时线程2会获取锁失败,因此这个锁会进行膨胀操作,变为一个重量级锁。
总结:在一个线程轻量级锁定某个对象的时候,如果出现竞争也会导致锁定膨胀,变成重量级锁,也就是没有竞争的偏向锁,没有竞争或者少量竞争就是轻量级锁,大量竞争就会变成重量级锁。
轻量级锁升级为重量级锁的流程图:
三、锁的分类
1、概念上的锁分类:乐观锁和悲观锁我常常听见,现在来个名次解释说明一下,也是记录一下自己对此的理解:
- 悲观锁:就是持有一种悲观的心态,就是认为如果我们不考虑线程安全,不做充分的同步工作就肯定会出问题,举个例子,就是我们不管等会会不会下雨,我们都穿上雨衣,雨鞋,雨伞。java传统的同步手段,包括synchronized关键字,可重入锁。
- 乐观锁:会乐观的估计系统当前的状态,认为状态是符合预期的,如果发生竞争,就退避,然后进行一定的策略,比如我们先看天气预报,万一天气预报不准,我们可以选择planB。
java的CAS操作,其实就是一种乐观的表现。
四、总结:
上述介绍的锁,不是说越轻量级就性能越好,对于不同的锁场景,选择其有点的锁,会大大增加效率。
1、对于偏向锁,加速和解锁不需要额外的消耗,但是如果线程间存在锁的竞争,会带来额外的锁撤销消耗:
2、对于轻量级锁,竞争的线程不会阻塞,但是一个线程始终得不到锁,会一直自旋消耗cpu;
3、重量级锁:线程竞争不会使用自旋,但是线程阻塞,相应时间很慢。