一、原子性定义
原子性的本质是互斥访问,同一时刻只有一个线程对它进行访问操作
二、原子性问题的简述
public class AutomicDemo {
int count = 0;
public static void main(String[] args) throws InterruptedException {
AutomicDemo automicDemo = new AutomicDemo();
Thread thread1 = new Thread(() ->
{
for (int j = 0; j < 1000; j++) {
automicDemo.incr();
}
});
Thread thread2 = new Thread(() ->
{
for (int j = 0; j < 1000; j++) {
automicDemo.incr();
}
});
thread1.start();
thread2.start();
// join方法保证线程执行完毕
thread1.join();
thread2.join();
System.out.println("i的结果值为:"+automicDemo.count);
}
public void incr(){
count++;
}
}
运行结果:
线程1线程2各循环一千次执行i++操作,正常情况下应该得到的值是2000,那么为什么会得到1412呢?这是由于当前代码中的i++操作是非原子性的。
其实一个count++操作是分为3步的:1.加载2.计算3.写入内存。由上图可以看出来,在同一时刻,线程A与线程B同时运行,当线程A刚把count为0的值加载到寄存器的时候,此时线程进行了切换,线程B完成了整个count++操作并把结果写入了内存,此时线程A接着执行,那么线程A加载到寄存器的count还是0,因此它计算后也把count=1存入了内存,这无形中就少了一次计算。思考:那么应该怎么解决这个问题呢?加同步锁synchronized关键字,保证在同一时刻,只有一个线程能够访问并操作count++
思考: 为什么加了synchronized关键字 系统就会认定它为一个同步锁呢,从而避免多线程对该方法 的一个操作呢?(见3.3)synchronized的作用范围是什么呢?(见3.1)锁的本质又是什么呢?(见3.2)
三、Synchronized关键字
3.1 作用范围
- 修饰实例方法 创建不同的对象,都可以访问该实例
- 修饰静态方法 全局锁
- 修饰代码块
synchronized() 括号中可以存储任何一个对象, 影响锁的作用范围,其实就是括号中对象的生命周期
3.2 抢占锁的本质
抢占锁的本质就是如何实现互斥,那么必定有两个条件
- 共享资源
- 锁标记 可以假设 0代表无锁 1 代表有锁
也就是说,抢占锁,一定是共同要访问一个共享的资源,当一个线程先占有了这个资源,就变为有锁状态,阻塞其它的线程。
3.3 锁信息的存储
加了synchronized关键字,系统为何就能识别这是一个同步锁呢,其实,系统根据该关键字,保存了一些信息在作用的对象上。
MarkWord对象头
我们可以通过以下代码打印对象头的信息(不加synchronized和加synchronized的区别)
public static void main(String[] args) {
Object lock = new Object();
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
打印的信息如下
结合下图可以看出,第一行的前8个字节的最后3位001 则为无锁状态
四、Synchronized锁升级
1. 无锁状态
2. 偏向锁: 假设没有线程竞争的时候,A线程进入到同步代码 就会偏向A线程 有线程竞争的时候 线程B再进来 就会做升级 轻量级锁
3. 轻量级锁 主要作用是避免线程阻塞 采用自旋锁的方式
4 .重量级锁 表示的是用户态到内核态的交换 没有获得锁的线程会阻塞,再被唤醒
思考: 线程A已经抢占到了锁,当线程B来竞争的时候,如果是重量级锁,则线程A执行完还需要唤醒线程B, 比较消耗性能,那么有没有一种办法可以避免或者优化这种阻塞呢?于是就引入了轻量级锁,线程B会不断的去重试,如果此时正好线程A结束了,那么线程B就可以执行了,不需要再去唤醒了。举个例子,就比如你去找老王,会先敲几下门,然后如果门没开,则会再去一边等着,等着老王来唤醒。
分析: 当有两个线程的时候,线程A先进入了,则线程B抢占锁的时候,则会先进行自旋,如果抢占到了,则修改lock flag的标记,使用CAS机制保证操作的原子性
五、CAS机制
old: ThreadA
expect: ThreadB
update: ThreadC
CAS机制与乐观锁类似
CAS机制其底层是C++代码,采用了lock指令
举例:
当前线程A获得了偏向锁,线程B来抢占偏向锁
线程B就会来调用CAS,把偏向锁的指针指向自己
CAS(object,线程A的指针,线程B的指针(带更新的值))