synchronized锁升级过程以及原理分析
synchronized 能保证什么
线程同步
线程同步,就是采用加锁的方式实现。 synchronized 是java提供的一种锁机制。
synchronized 是一个排他锁,统一时间,只能有一个线程申请到锁,其他线程想要获得锁,只能进入等待,只有会的锁的线程是否了锁,其他线程才会重新竞争,并且只能有一个线程竞争到锁。
当然还有其他锁,如可重入锁,读写锁等等。
synchronized 能保障并发编程下的线程执行的可见性和顺序性 ,但不能保证原子性(synchronized 不能限制指令重排序)。
synchronized 是能保证共享资源同一时刻只能被一个线程访问,其他线程拿不到锁,但是在这个线程的内部语句,指令依然是可以重排序的,as-if-serial 方式执行的。
jdk 1.6 是重量级锁,1.7开始是轻量级锁,会有一个锁升级的过程。
多个线程竞争同一个共享资源时,线程状态的变化
场景说明: 一个共享资源,3个线程t1,t2,t3。共享资源的访问被 synchronized 修饰。
public static int idx=0;
public static void m(){
synchronized (idx) {
// 省略代码
}
}
// t1 t2 t3 ... 都访问m()
1、当3个线程都被创建了,它们的状态都是 NEW ,调用 start 方法后,状态都是 RUNNABLE。
2、此时3个线程要竞争同一把锁,而且只能有一个线程拿到锁,假设是t1线程拿到了,t1线程的状态依然是 RUNNABLE ,而没有拿到锁的2个线程t2和t3,线程状态是BLOCKED 。t2,t3线程会进入 synchronized 机制下的线程等待队列(waiting queue),等待t1线程释放锁。
3、当t1线程是否了锁,此时t1线程的状态可能是 runnable ,线程也可能结束了,也可能自己进入 waiting 状态,但这都都不是重点,重点是t1线程是否了锁。这含义就是,处在线程等待队列中的线程,会开始重新竞争锁。假设是t3线程竞争到了锁,t3线程会从线程等待队列中移除,t3线程的状态从 blocked 切换为 running。而没有竞争到锁的线程状态依然是 blocked 。只能等到下一次竞争锁的机会。
ps: synchronized 的锁指的是jvm机制下的监视器锁 monitor 。实现原理是基于内存屏障。 jvm 语义是 monitorenter 和 monitorexit 。
synchronized 使用姿态
synchronized 使用姿态,首先要弄清楚2个问题。锁,用来锁谁?锁,作用在什么上。
锁,用来锁谁
锁对象实例 : 当是同一个实例,则是顺序访问;如果是不同的对象实例,则是同时访问(前提是多核CPU)。
锁类 class :所有实例都是顺序访问。
总结:
1、多核CPU,并发访问的情况下
2、一个类是可以创建多个实例对象的(单例除外),当锁是加在了对象上,只能保证单个实例对象里涉及的线程并发访问时候,要遵循顺序。多个对象之间互不影响。
3、类的class,是会被加载到方法区中的,只有一个。所以在整个JVM中,锁加在 class 上,所有的线程涉及这块访问都要顺序化。
1、锁实例对象
1)锁非静态对象
我们都知道一个基本概念,类是可以创建多个对象的(单例模式除外)。
演示代码如下:
public class LockNonStaticObject {
private int idx;
private long start;
private Object o = new Object();
public LockNonStaticObject(int idx,long start){
this.idx = idx;
this.start=start;
}
public void m(){
synchronized (o){
try{
idx++;
Thread.sleep(1000);
System.out.println(idx+"--------------耗时:"+(System.currentTimeMillis()-start)/1000);
}catch (InterruptedException e){}
}
}
public static void main(String[] args) throws InterruptedException {
long start =System.currentTimeMillis();
LockNonStaticObject ins1 = new LockNonStaticObject(10,start);
LockNonStaticObject ins2 = new LockNonStaticObject(1000,start);
Thread[] ts = new Thread[10];
Thread[] ts2 = new Thread[10];
for(int i =0 ;i<10;i++){
ts [i] = new Thread(){
@Override
public void run() {
ins1.m();
}
};
ts2 [i] = new Thread(){
@Override
public void run() {
ins2.m();
}
};
}
for(int i =0 ;i<10;i++){
ts[i].start();
ts2[i].start();
}
}
}
运行结果:
1001--------------耗时:1 s
11--------------耗时:1 s
12--------------耗时:2 s
1002--------------耗时:2 s
13--------------耗时:3 s
1003--------------耗时:3 s
1004--------------耗时:4 s
... 省略其他输出
1010--------------耗时:10 s
20--------------耗时:10 s
2)锁 this 对象
synchronized (this){}
- 锁普通方法
public synchronized m(){}
2、锁 类的 class 对象
- 锁类的class
public class Obj{}
// 省略其他
synchronized (Obj.class){}
2)锁静态对象
static Object o = new Object();
// 省略其他
synchronized (o){}
- 锁静态方法
public static synchronized m(){}
锁,作用在哪
1、方法上
-
普通方法上,锁的是对象实例
-
静态方法上,锁的是类的Class
2、代码块上
1)、 锁了 非静态变量
2)、锁了 静态变量
3)、锁了 this 对象
4)、锁了 类的 Class对象
synchronized 锁升级过程
锁分类
1、无锁
2、自旋锁
3、偏向锁
4、轻量级锁
5、重量级锁
升级过程图解
图解如下:
1、jvm刚启动时候(预热阶段),jdk1.6开始,默认是4s,线程都是无锁状态
2、超过预热,第一个线程T1进来,锁状态会升级为偏向匿名锁
3、第二个线程T2进来,T1线程持有的锁会升级为偏向锁
4、第三个线程T3进来,T1线程会升级会轻量级锁
5、T1线程的同步代码块中,有出现调用wait() 、hashCode()等方法,T1线程持有的锁状态会升级为重量级锁
6、T1线程执行完成,才会释放锁,其他线程才可以竞争锁。
触发锁升级的条件
synchronized 底层原理
markword
jvm中对象的结构,有一块对象头。记录对象信息,如锁状态、GC年代。有一块叫mark word 。锁的状态就是记录在mark word 中。
用户态和内核态
用户态,用户线程可以操作的,在用户空间执行操作。
内核态,内核线程可以操作的,在内核空间执行操作。
用户态和内核态的切换是消耗CPU的,线程的创建和销毁就是一个典型例子,所以推荐使用线程池,减少线程的频繁创建和销毁的开销,线程起到复用的效果。