并发编程-原子性
并发编程-原子性
我们都清楚当多个线程去同时做一件事情的时候,我们需要考虑原子性、可见性、和有序性这几个问题,本章主要说原子性,以下是阐述内容
- 原子性:主要用原子性问题进行展开讨论
- 同步锁(synchronize):使用同步锁解决问题
- MarkWord对象头:锁的状态存在哪里
- synchronize的锁升级机制:多个线程抢占资源的时候,锁的底层状态是如何改变的
- CAS机制:当无锁化时候(例如自旋锁的时候,cas起到的作用)
线程的原子性
【原子性】:指的是一个操作一旦进行就不能被别的操作进行干扰。我们创建两个线程对一个数字进行增加,两个线程都增加数字10000,按照常理来讲,结果应该是20000,实则不然
public class AtomicDemo { private int i =0; private void mock(){ i++; } public static void main(String[] args) throws InterruptedException { AtomicDemo atomicDemo=new AtomicDemo(); Thread[] thread =new Thread[2]; for (int i = 0; i < thread.length; i++) { thread[i]=new Thread(()->{ for (int j = 0; j <10000 ; j++) { atomicDemo.mock(); } }); thread[i].start(); } thread[0].join(); thread[1].join(); System.out.println(atomicDemo.i); } }
运行结果为(证明肯定有一个线程被打扰到,否则结果肯定是20000,这就是一个典型的原子性问题)
分析为什么导致
以上述demo为例,实际上当线程对 i 进行 ++ 的时候,在底层分三步
- 加载 内存中 i
- 对i++
- 把++后的数据写入内存
那我们可以想象一下,当线程a正在对i++
- ->此时线程cpu切换到线程B
- ->当线程b把i=0变成i=1
- ->这个时候cpu又切换到线程a
- ->那么线程a按照之前执行的位置再次进行相加,但是之前的位置是i=0,所以两个线程都循环了一次结果却只是相加了1
- 这就是为什么最终结果不等于20000 的原因了
进行验证:打开terminal 输入 [javap -v AtomicDemo.class] 查看字节指令
所以很有可能在某个过程中被别的线程打断,从而得到我们预期外的结果
如何解决这一问题?
实质上有很多方法解决,我们今天只围绕synchronized进行展开,我们只用给这个方法加上synchronized就可以了。
synchronize是什么、如何使用?
synchronize就是一种排他锁,换句话说是一种互斥锁,他可以同一时间只让一个线程对你的逻辑进行访问,那么我我们的逻辑受到怎么样的保护,或者说这个关键字的范围是什么呢?如何使用呢?
可以修饰:实例方法(对象范围)、代码块(取决于括号中你所放置的内容this/对象.class)、静态方法(对象范围)
实例方法:
如果两个线程同时访问不同对象的同一个方法,那么他们不是互斥关系,就是这个关键字不会保护你的逻辑
如果访问的是同一个对象,则会对你的逻辑进行保护,比如你的method1中写了一个逻辑,必须等线程1执行完成线程而才能执行
代码块:如果你的括号中写的是this那和实例方法的作用域是一样的
但是如果括号中写的是**.class那作用域就是不管多少线程想执行必须一个一个进行排队
静态方法(因为静态方法是一个类产生就产生的,所以他的作用也是全局的)
tips: 细细想来,其实是对象决定synchronized 的范围,我们想想,如果有多个线程抢占同一个资源,那其中一个抢占到怎么通知别的线程这个坑位已经被抢占了呢?是否有一个全局的位置去存储锁的标记呢,那既然对象决定关键字的范围,那么一个资源是否被抢占到,【是否抢占标记】是否会存储在每个对象中呢?对的,现在就来说一下MarkWord对象头
MarkWord(就是对象在内存中布局的三大部分中的2/1部分,这一部分存储着锁的标记)
对象在内存中的布局分为:
- 对象头(Header):这一部分由两部分组成
- Mark Word:这一部分就是存储锁的标记
- class 对象指针(类元信息):对象对应的原数据对象的内存地址
- 实例数据(Instance Data):这里存储的就是对象的成员变量等
对齐填充(Padding):这一部分属于一个优化部分,由于HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍,所以如果不是8的倍数,那就就会自动填充为8的倍数,否则进行读取就会消耗多余的性能,仅此而已
tips: 由下图可以看出,当线程对资源进行获取的时候,看一下这个锁的状态,在决定是否进行抢占,这个是否可以抢占在你规定的对象中存储,这也就是为什么线程对两个不同的对象的资源进行抢占时,其资源不受保护的原因了,(因为他们有不同的锁的状态)
我们来看看是否锁的状态被存储了起来
增加这个jar,这是用来查询对象的大小和布局的工具,由 openjdk 提供
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>我们用这张图片作为参考去查询锁的状态
对对象的布局进行打印
结果如下:
tip:我们已经知道了锁的状态被存储起来,那么那些轻量级、重量级、等等,锁的状态都代表了什么呢,锁的状态是怎么流转的呢,来让我们继续剖析,以及来模拟状态并且看一下状态markword中的状态是否变了...
synchronize的锁升级机制
【在jdk1.6之前只有无锁->重量级锁,使用‘synchronize’的流程是,如果一个线程没有抢占到资源,那就直接是一个重量级锁的状态,为了提高性能,才有了后面的锁的状态】
synchronize锁的类型有这几种:
- 无锁:
- 偏向锁:就是当没有线程对资源竞争的时候,
- 此时线程a得到了资源,
- 那这个锁就会存储一个线程a的指向地址,
- 下次线程a再次进入资源,就不需要抢占锁,直接可以进行执行
- 轻量级锁:避免线程阻塞(因为如何线程阻塞,我们再次对线程进行唤醒,那就增加了性能开销,也就是从用户态到内核态) -》从偏向锁升级而来
- 线程b也来抢占资源,但是发现锁已经偏向了线程a,那么他就要升级为轻量级锁,轻量级锁,使用自旋锁进行实现
- 自旋锁:实际上就是用一个for循环不断询问时候别的线程已经执行完成,当前正在抢占线程的线程就可以执行,然而在循环的时候肯定会牵扯到线程问题,那么我们用CAS进行实现
- 重量级锁:比较消耗性能,为了优化,所以在1.6后引入了偏向锁和轻量级锁
- 牵扯用户态到内核态的交换(用户态就是在java中的操作,而内核态就牵扯到cpu层的操作,所以更消耗性能),
- 没有获得锁的线程会阻塞,当这个线程获得锁的时候在进行唤醒
我们来验证是否锁的流程是像我们说的一样
public class LockDemo { Object o=new Object(); public static void main(String[] args) { LockDemo lockDemo=new LockDemo(); System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable()); // 加锁后的状态 System.out.println("加锁之后--------"); synchronized (lockDemo){ System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable()); } } }我们观察markword中的状态,参考查询锁的状态那张图
我们发现,按照上面的画的流程来讲,应该首先是无锁,之后是偏向锁啊,为什么这里是自旋锁的?因为偏向锁是默认关闭的,因为在程序启动的时候已经有一些我们不知道的线程在底层运行了,那么下个线程来的话直接就会执行重量级锁了。
public class HeightLockDemo { public static void main(String[] args) { HeightLockDemo heightLockDemo = new HeightLockDemo(); Thread thread = new Thread(() -> { synchronized (heightLockDemo) { System.out.println("线程1"); System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable()); } }); thread.start(); synchronized (heightLockDemo) { System.out.println("主线程"); System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable()); } } }
CAS机制:
【CAS(Compare And Set)】:其实就是在操作逻辑之前,拿之前的数值,和你的预期 值进行对比,如果相同那就修改你传入的新的数值。实际上有点像数据库的乐观锁
那在轻量级锁中大概的流程就是这样的:首先不断循环->判断cas,如果cas返回ture则对锁的状态进行修改,当然除了cas的判断外可以进行breadk,肯定还有自旋次数的限制,否则循环就无终止了
在java中有使用cas的例子【sun.misc.Unsafe#compareAndSwapObject】
那么CAS的底层又是如何实现的,因为在底层也是多个线程来抢占这个CAS的,其实CAS的底层也是用锁来实现的,只不过是CPU层面的锁。