synchronized为何能解决原子性?
何为原子性?
如果多个线程在做同一件事情的时候
1.如何产生
public class Demo { private int i =0; private void incr(){ i++; } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); Thread[] threads = new Thread[2]; for (int i = 0; i <2; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { demo.incr(); } }); //线程启动 threads[i].start(); } //join是主进程等待线程执行完毕 threads[0].join(); threads[1].join(); System.out.println("计算的结果为------>"+ demo.i); } }
结果:由于 i++ 并不具备原子性则计算结果不正确
为何呢?那让我们来看一下class字节码的运行过程
将java文件编译成class文件,然后javap看一下class字节码他是如何的一种运行过程?
javap -v xxx.class
过程理解一下:
i++在class字节码中分为几个步骤?
- 先是通过getfield获取变量的值
- 然后将其放在操作数栈中
- 然后将其相加之后放回去
- 将计算好了的值在给i这个字段进行赋值
好了那么万一这个四步的其中一个环节线程切换到线程2去了。线程2执行完成回来再去拿i这个时候i还是线程1的i 线程2等于没有作用。这个就会发生问题
这就是在多线程环境下,存在的原子性问题,那么,怎么解决这个问题呢?
2.如何解决?
认真观察上面这个图,表面上是多个线程对于同一个变量的操作,实际上是i++这行代码,它不是原子的。所以才导致在多线程环境下出现这样一个问题。
也就是说,我们只需要保证,i++这个指令在运行期间,在同一时刻只能由一个线程来访问,就可以解决问题,那么接下来的重点就是同步锁Synchronized
Synchronized
Synchronized最终要实现互斥性不让让他们都去访问同一个对象
1.作用范围
synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
public static synchronized void one() { }
2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
public synchronized void two() { }
3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public synchronized void three() {synchronized (ThreadOneDemo.class){}}
这里里面看他传递进来的是什么对象 如果是static就是全局锁
可以控制锁的范围,影响锁的作用范围 其实就是对象的生命周期
2.锁的实现模型
3.如何实现?
Synchronized是如何实现锁的,以及锁的信息是存储在哪里?就拿上面分析的图来说,线程A抢到锁了,线程B怎么知道当前锁被抢占了,这个地方一定会有一个标记来实现,而且这个标记一定是存储在某个地方。
其实就是对象头里面存储了锁的信息
4.Markword对象头
这就要引出Markword对象头这个概念了,它是对象头的意思,简单理解,就是一个对象,在JVM内存中的布局或者存储的形式。
在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
解释:
也就是说 每次线程进来的时候就会从 lock中获取这个对象 。从而知道这个对象的锁标记。争对是否有锁做对应的处理
可以用过ClassLayout打印对象头
添加依赖
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
例子
加上锁之后对象头是什么样的?
Object o = new Object(); public static void main(String[] args) { Demo demo = new Demo(); //o这个对象,在内存中是如何存储和布局的。 System.out.println(ClassLayout.parseInstance(demo).toPrintable()); synchronized (demo) { System.out.println(ClassLayout.parseInstance(demo).toPrintable()); } }
输出
上面了解到对象头上储存了锁的类型,那么具体有哪些呢?往下看看
Synchronized锁升级
jdk6对锁的实现引入了大量的优化,比如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销
1.Synchronized锁分类
- 无锁
- 偏量锁(默认延迟开启 4s)
当前线程进入Synchronized的范围内但是没有其他线程进行竞争的前提下 他会偏向当前线程,当他下次再进来的时候就不需要再抢占锁 它能直接进来
- 轻量级锁
当之前偏向锁指向A,B来抢占锁,锁进行升级。会升级为轻量级锁通过自旋锁 用来保证 既不能让线程阻塞 还得让线程尽快获取
- 重量级锁
- 用户得到内核态的交换需要用户空间到内核指令的发生
- 没有获得锁的线程会阻塞等待唤醒
这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现
2.锁的升级过程
- 去抢占锁查看一下偏量锁是否开启。
- 如果开启了就标记偏量锁 如果没有或者发送竞争就会锁升级为轻量级锁
- 如果2个线程同时去抢占锁,这个时候 他是不会去直接标记偏量锁的他是会直接去标记为轻量级锁(因为偏量锁有延迟)
- 一个线程抢占到了锁,那个线程在执行中 另外一个线程也不会一直等着。他会不停的循环重试。大概是10次这个过程他是不会阻塞的。(也可以将其称之为自旋锁)
- 如果还是没等到的话 那么就把锁标记为重量级锁 第二个线程也就进入阻塞队列等待唤醒
- 如果其他线程都一直循环等待不到的话 就会把锁升级为重量级锁
2.0 锁的升级过程的具体解释:
1.默认情况下是偏向锁是开启状态,偏向的线程ID是0,偏向一个Anonymo0us BiasedLock 2.如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把markword的线程ID改为当前抢占锁的线程ID的过程 3.如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程在自己的线程栈帧中会创建一个LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢占到锁。 4.如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争的情况来自动控制自旋的时间。
升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列。
以这张图的锁标志为参考下面用代码来看一下
2.轻量锁
public class ThreadTwoDemo3 { Object o = new Object(); public static void main(String[] args) { Person person = new Person(); System.out.println(ClassLayout.parseInstance(person).toPrintable()); synchronized (person){ System.out.println("--------------------加锁之后-------------------"); System.out.println(ClassLayout.parseInstance(person).toPrintable()); } } public static class Person{ } }
3.偏量锁
默认情况下,偏向锁的开启是有个延迟,默认是4秒。为什么这么设计呢? 因为JVM虚拟机自己有一些默认启动的线程,这些线程里面有很多的Synchronized代码,这些Synchronized代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和撤销,效率较低。 通过下面这个JVM参数可以讲延迟设置为0.
-XX:BiasedLockingStartupDelay=0
设置成不延迟加载的话 就会默认使用偏量锁了
当前main获得了偏向锁
这里第一个对象和第二个对象都是偏向锁,因为打开的偏向锁 默认会有匿名对象去获取偏向锁
4.重量锁
Monitor监视器
在竞争比较激烈的情况下,线程一直无法获得锁的时候,就会升级到重量级锁。
public class ClassLayoutWeightDemo { public static void main(String[] args) { ClassLayoutWeightDemo testDemo = new ClassLayoutWeightDemo(); Thread t1 = new Thread(() -> { synchronized (testDemo){ System.out.println("t1 lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } }); t1.start(); synchronized (testDemo){ System.out.println("main lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }
从结果可以看出,在竞争的情况下锁的标记为 [010] ,其中所标记 [10]表示重量级锁
main lock ing com.example.gupao_thread_v1.synchron02.ClassLayoutWeightDemo object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) ca c9 e4 02 (11001010 11001001 11100100 00000010) (48548298) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total t1 lock ing com.example.gupao_thread_v1.synchron02.ClassLayoutWeightDemo object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) ca c9 e4 02 (11001010 11001001 11100100 00000010) (48548298) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total