synchronized锁升级过程以及原理分析

89 阅读5分钟

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个问题。锁,用来锁谁?锁,作用在什么上。

锁,用来锁谁

image.png

锁对象实例 : 当是同一个实例,则是顺序访问;如果是不同的对象实例,则是同时访问(前提是多核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){}
  1. 锁普通方法
public synchronized m(){}

2、锁 类的 class 对象

  1. 锁类的class
public class Obj{}

// 省略其他
synchronized (Obj.class){}

2)锁静态对象

     static Object o = new Object();
     // 省略其他
     synchronized (o){}
  1. 锁静态方法

public static synchronized m(){}

锁,作用在哪

1、方法上

  1. 普通方法上,锁的是对象实例

  2. 静态方法上,锁的是类的Class

2、代码块上

1)、 锁了 非静态变量

2)、锁了 静态变量

3)、锁了 this 对象

4)、锁了 类的 Class对象

synchronized 锁升级过程

锁分类

1、无锁

2、自旋锁

3、偏向锁

4、轻量级锁

5、重量级锁

升级过程图解

图解如下:

image.png

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的,线程的创建和销毁就是一个典型例子,所以推荐使用线程池,减少线程的频繁创建和销毁的开销,线程起到复用的效果。

实战下什么条件下选择synchronized,什么时候选择CAS