synchronized关键字

425 阅读8分钟

前言

java锁的概念

锁特性概念
自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待
然后不断地判断锁是否能够被成功获取,直到获取到锁才会退出循环
乐观锁假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读取最新数据后重试修改
悲观锁假定会发生冲突,同步所有对数据的相关操作,从读数据就开始上锁
独享锁(写锁)给资源加上写锁,线程可以修改资源,其他线程不能再加锁 (单写)
共享锁(读锁)给字段加上读锁后只能读不能改,其他线程也只能加读锁 (多读)
可重入锁、不可重入锁指的是当一个线程拿到一把锁之后可以自由进入同一把锁所同步的其他代码
公平锁、非公平锁争抢锁的顺序是先来先得则为公平

可重入锁和不可重入锁的区别

  • 可重入锁互斥其他线程但不互斥它自己

  • 不可重入锁互斥其他线程也包括它自己

公平锁和非公平锁的区别

  • 公平锁讲究的是获取锁的顺序是先到先得
  • 非公平锁讲究的是没有获取锁的顺序,而是直接竞争
常用锁的实现方式简介
synchronizedjvm同步关键字
ReentrantLocklock接口标准实现类
ReentrantReadWriteLock读写锁

今天咋们重点来聊一聊synchronized关键字,从使用到原理去揭开synchronized背后的面纱

synchronized的概念和使用

synchronized仅限当前jvm进程不能实现多个进程之间加锁

synchronized关键字,不仅能实现同步.JMM中规定synchronized要保证可见性(不能够被缓存)

用于实例方法、静态方法隐式指定锁对象:

  • 实例方法锁的是对象实例
  • 静态方法锁的是类对象

用于代码块时显示指定锁对象

锁的作用域:对象锁、类锁

特性:可重入、独享、悲观

synchronized使用示例

/**
 * <p>
 * synchronized使用示例
 * </p>
 *
 * @author 昊天锤
 * @date 2020/11/2 0029 15:17
 */
public class Demo {

    private static int count = 0;

    /**
     * synchronized用于实例方法:锁的是当前对象实例
     */
    public synchronized void addLockMethod() {
        count++;
    }


    /**
     * synchronized用于代码块:显示指定this锁的是当前对象实例
     */
    public void addLockCodePiece() {
        synchronized (this) {
            count++;
        }
    }

    /**
     * synchronized用于静态方法:锁的是当前对象的class
     */
    public synchronized static void staticAddLockMethod() {
        count++;
    }

    /**
     * synchronized用于代码块:显示指定class锁的是当前对象的class
     */
    public static void staticAddLockCodePiece() {
        synchronized (Demo.class) {
            count++;
        }
    }
}

对于synchronized的使用无非就这几种方式

但是值得注意的是:静态变量属于类变量对象变量属于实例变量

在使用synchronized进行加锁的时候需要考虑清楚共享资源是属于实例还是类的

锁优化

  • 锁消除 (开启锁消除的参数: -XX:+DoEscapeAnalysis -XX:EliminateLocks)

    JIT优化消除了锁.锁消除要满足两个条件: 1.单线程内部的锁 2.反复执行相同代码

    /**
     * <p>
     * 锁消除案例
     * </p>
     *
     * @author 昊天锤
     * @date 2020/11/2 0029 15:17
     */
    public class Demo {
    
        public static void main(String[] args) {
            // StringBuffer使用了synchronized关键字是线程安全的
            // 但是由于当前的stringBuffer对象属于线程内部变量又循环反复执行相同的代码 所以会被JIT优化进行锁消除
            StringBuffer stringBuffer = new StringBuffer();
            for (int i = 9999; i > 0; i--) {
                stringBuffer.append(1);
                stringBuffer.append(2);
                stringBuffer.append(3);
            }
    
            System.out.println(stringBuffer.toString());
        }
    }
    
  • 锁粗化 (JDK做了锁粗化的优化,但是建议可以从代码层面进行优化)

    JIT锁粗化的条件: 在一段代码中连续出现多个synchronized且每个synchronized相隔的执行时间较短

    /**
     * <p>
     * 锁粗化案例
     * </p>
     *
     * @author 昊天锤
     * @date 2020/11/2 0029 15:17
     */
    public class Demo {
        private static int count;
    
        public static void main(String[] args) {
            synchronized (Demo.class) {
                count++;
            }
    
            synchronized (Demo.class) {
                count--;
            }
    
            synchronized (Demo.class) {
                count += 1;
            }
    
            synchronized (Demo.class) {
                count += 2;
            }
    
            synchronized (Demo.class) {
                count -= 5;
                System.out.println(count);
            }
    
            // 经过JIT编译以后它会认为这里加那么多synchronized没有什么意义,所以会进行锁粗化
            synchronized (Demo.class) {
                count++;
                count--;
                count += 1;
                count += 2;
                count -= 5;
                System.out.println(count);
            }
        }
    }
    

    如果不想让它锁粗化优化可以在synchronized相隔的代码中间加一段耗时的操作

小结

synchronized的概念和使用已经介绍完了

进入到同步块中说明该线程获得了锁

执行完同步块代码后该线程释放了锁

而同步块是需要一个实例对象或者是类对象才能使用

这个加锁和释放锁的过程完全是由JVM自动实现的,所以我们很难通过代码分析出来它是怎么做的同步

现在能观察得到的是synchronized是通过对象来进行加锁的,所以要搞清楚synchronized之前先要知道java对象长什么样

java对象在JVM中的布局

/**
 * <p>
 * 用户对象
 * </p>
 *
 * @author 昊天锤
 * @date 2020/11/2 0029 15:17
 */
@Data
public class User {

    private String id = "1";
    private int age = 18;
    private boolean sex = true;
}

该User对象在堆中的样貌如下图

一个完整的java对象分为三部分对象头、实例数据、padding补位

实例数据:该实例对象拥有的引用类型、值

padding补位:JVM要求对象的大小必须是8的整数倍否则需要补位对齐

对象头

对象头组成部分

Mark word

我们先看一张表这张表告诉了我们Mark word代表的含义

是否看完这张表心中有点小激动似乎好像明白了什么,嗯!是的!那么我们再往下看另一张表,它描述了State状态的含义

锁状态

无锁态

当一个对象创建完没有线程进入synchronized时锁的状态位为无锁态,锁标志位为01

这里挺好理解的,都没有线程执行到锁定的代码,状态自然也就没被修改过

偏向锁

在JDK1.6以后,默认已经开启了偏向锁这个优化,通过JVM参数 -XX:-UseBiasedLocking来禁用偏向锁

如偏向锁开启,只有一个线程抢锁可获取到偏向锁

线程ID指的是当前锁的拥有者

Epoch指的是偏向锁的有效性

偏向锁中有个概念叫重偏向一般发生在批量处理的时候

比如说有一个class A new出了10个对象 A1到A10

假设有一个T1线程先来操作然后A1到A10全部偏向于该线程

接着又有个T2线程来对这10个对象进行抢锁操作,按理说这时候应该会进行锁升级

可是class A突然能感知到刚new出来的所有实例都偏向于一个线程

所以会将A6到A10实例原本指向T1的偏向锁ID指向T2线程

轻量级锁

当有两个线程争抢的时候.在未锁定的状态下通过CAS来抢锁抢到的是轻量级锁,锁标志位为00

cas操作进行抢锁那么就只会有一个线程抢锁成功

抢锁成功的线程会将Lock Record Address指向为它自己,并且在虚拟机栈中会有个owner指向对象头的Lock Record Address

抢锁失败的线程则自旋不断尝试再次去争抢锁,当自旋到一定的次数或是出现第三个线程来争抢时将升级为重量级锁

重量级锁

自旋对于cpu消耗是挺大的,所以轻量级锁中的自旋有一定的次数限制超过了次数限制轻量级锁升级为重量级锁

当出现第三个线程抢锁时,如果发现对象已经被其他线程锁定并且还有其他线程在自旋抢锁,也会升级为重量级锁

重量级锁锁标志位为10

升级为重量级锁之后会新创建一个Monito(对象监视器),并且将Monitor address指向这个对象监视器

对象监视器中包含的内容有:

  • owner

    标识着当前锁的拥有者,如果正常结束则将owner置空释放锁

    如果调用了wait方法则将owner置空释放锁后进入waitSet 等待池

  • entryList 锁池

    抢锁未成功的线程列表根据抢锁的顺序进行排队,等待锁的拥有者释放后调度

    在这里的线程都是BLOCKED状态

  • waitSet 等待池

    当锁的拥有者调用wait方法以后会进入等待池中直到有其他线程调用notify唤醒

    在等待池中的线程被唤醒后会看一下owner如果为空则将owner指向自己,owner如果不为空则进入锁池排队

    在这里的线程都是WAITING状态

之所以称为重量级锁是相比较于轻量级锁多了太多的东西,比如说对象监视器、锁池、等待池

总结

再来总结一下synchronized锁升级的过程

一个对象开始时肯定是一个非锁定状态

当有一个线程来操作的时候将升级为偏向锁

当再来一个线程来操作时发现偏向锁ID并不是自己就升级为轻量级锁

当有一个线程自旋到一定的次数或者是多个线程抢锁时又升级为重量级锁

注意的两个点是

  • synchronized也是非公平锁因为当新的线程进行抢锁时会先去抢锁,抢失败后才会到锁池进行等待

  • 这个锁升级是不可逆的,也就是说一旦升到轻量级锁以后就不能降下来了