并发编程带来的安全性同步锁(synchronized)
1.他的背景
当多个线程同时访问,公共共享资源的时候,这时候就会出现线程安全,代码如:
public class AtomicDemo {
int i=0;
//排他锁、互斥锁
public void incr(){ //synchronized
i++; //i++最终3条指令 [线程安全问题中原子性]
}
public static void main(String[] args) throws InterruptedException {
AtomicDemo ad=new AtomicDemo();
Thread[] thread=new Thread[2];
for (int i = 0; i <2 ; i++) {
thread[i]=new Thread(()->{ for(int k=0;k<10000;k++) { ad.incr(); } });
thread[i].start();
}
thread[0].join();
thread[1].join();
System.out.println("Result:"+ad.i);
}
}
//执行结果:17986,如果加上synchronized同步锁后结果为20000.
Result:17986
图片解析过程:
2.基本使用
synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
public class SynchronizedDemo {
//修饰实例方法
public synchronized void m1(){ }
Object lock=new Object(); //在内存中会分配一个地址来存储
public void m2(){
//代码块
synchronized (lock){ } //lock为锁对象,也表示控制锁的范围
}
//静态方法
public synchronized static void m3(){}
}
3.注意事项
锁的范围: synchronized中的锁对象如果是,普通对象这为当前对象锁,如果是静态类为全局锁。
4.底层原理
4.1 synchronized是如何实现锁的,以及锁的信息是存储在哪里?锁的信息是存储在锁对象下Markword对象头里的。
4.2 在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
4.3 mark-word:对象标记字段占4个字节,用于存储一些列的标记位,比如:哈希值、轻量级锁的标记位,偏向锁标记位、分代年龄等
4.5 偏向锁状态[默认情况下,偏向锁的开启是有个延迟,默认是4秒 -XX:BiasedLockingStartupDelay=0] 为什么这么设计呢?
因为JVM虚拟机自己有一些默认启动的线程,这些线程里面有很多的Synchronized代码,这些
Synchronized代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和撤销,效率较低.
5.技术关联性
关于Synchronized锁的升级
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问
题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现。
如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁 [也就是把markword的线程ID改为当前抢占锁的线程ID的过程] ----》
如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁 --》
如有线程超过自旋,升级到重量级锁 [有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过
CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争的情况来自动控制自旋的时间]
轻量级锁的获取及原理
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
//-------------------------------
public class Demo {
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());
}
}
}
结果:
00 00 (00000001 00000000 00000000 00000000) (1) 无锁
d5 02 (11011[000] 11110000 11010101 00000010) (47575256) 轻量锁
它的锁的标记是轻量级锁呢?
默认情况下,偏向锁的开启是有个延迟,默认是4秒。为什么这么设计呢?
因为JVM虚拟机自己有一些默认启动的线程,这些线程里面有很多的Synchronized代码,这些
Synchronized代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和
撤销,效率较低
偏向锁的获取及原理
通过下面这个JVM参数可以讲延迟设置为0.
-XX:BiasedLockingStartupDelay=0
public class Demo {
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());
}
}
}
结果:
00 00 (00000101 00000000 00000000 00000000) (5) 偏向锁
4a 03 (00000101 00110000 01001010 00000011) (55193605) 偏向锁
重量级锁的获取
public static void main(String[] args) {
Demo testDemo = new Demo();
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());
}
}
结果:
8a 20 5e 26 (10001010 00100000 01011110 00100110) (643702922) 重量锁
8a 20 5e 26 (10001010 00100000 01011110 00100110) (643702922) 重量锁
6.CAS
就是比较并交换的意思。它可以保证在多线程环境下对于一个变量修改的原子性。
CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(E)以及期待更新的值(N)。