Java并发编程系列- Synchronized(值得学习)

5,490 阅读11分钟

觉得不错请按下图操作,掘友们,哈哈哈!!! image.png

这是这个系列的第三篇啦jym,前边两篇也同样精彩哦!!!

Java并发编程系列-基础知识(非常详细哦)

Java并发编程系列- volatile关键字

目录及前言

synchronized 是我们Java程序员在日常开发中常用的同步工具,但是很多人却对它的用法和实现原理一知半解。

  • 怎么合理的运用synchronized ?
  • 什么场景运用 synchronized ?
  • synchronized怎么用?

一. synchronized 的原理

同样 synchronized 保证了  【原子性、有序性、可见性】

1.1 对象的构成

要想了解 synchronized 的原理,首先我们要了解下,对象的构成,因为 synchronized 作用的目标就是在对象上。

image.png

image.png Java对象在JVM内存中由三块区域组成:对象头、实例数据和对齐填充。

对象头又包含:Mark Word(标记字段)、Class Pointer(类型指针)数组长度【如果是数组】

  • 实例数据:是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。 这部分的存储顺序会受到虚拟机分配策略参数FieldsAllocationStyle和字段在Java源码中定义顺序的影响。HotSpot虚拟机 默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

  • 对齐填充没有特殊含义,由于虚拟机要求 对象起始地址必须是8字节的整数倍,作用仅是字节对齐。

  • Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

1.1 对象监视器

JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。 具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。 对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

image.png

image.png 可以看到在同步块的入口和出口分别有monitorenter和monitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

1.3 特点:

image.png

二:什么场景运用 synchronized

synchronized 支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则该对象变量的所有读取或写入都是通过同步方法完成的,保证同一时间只有一个线程操作该对象,以保证并发安全的效果。

三. synchronized怎么用

synchronized可以修饰静态方法、实例方法,同时还可以直接定义代码块,但是最总加锁的资源只有两类:一个是对象,一个是,那么这三种有什么区别呢?

  • 实例方法,普通成员属性、成员方法是归实例化的对象所有,锁是当前实例对象
  • 静态同步方法,static修饰的静态方法、静态属性都是归类所有 ,锁是当前类的 class 对象
  • 同步方法块,锁是括号里面的对象

3.1 synchronized 修饰 实例方法

synchronized 如果修饰的是实例方法,实例方法是属于类的实例。synchronized 修饰的实例方法相当于是对象锁。下面是一个 synchronized 修饰实例方法的例子。

public class TestSynchronized implements Runnable{
    static int i = 0;

    // ⾮静态,访问时锁不⼀样不会发⽣互斥
    public synchronized  void increase(){
        i++;
        System.out.println(Thread.currentThread().getName());
    }
    @Override
    public void run() {
        for(int i = 0;i < 20;i++) {
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized testSynchronized = new TestSynchronized();
        Thread t1 = new Thread(testSynchronized);
        Thread t2 = new Thread(testSynchronized);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i = " + i);
    }
}

结果展示:

image.png

上面输出的结果 i = 40 ,每次都会打印当前执行线程的名字

解释一下上面代码,代码中的 i 是一个静态变量,静态变量也是全局变量,静态变量存储在方法区中。increase 方法由 synchronized 关键字修饰,但是没有使用 static 关键字修饰,表示 increase 方法是一个实例方法,每次创建一个 TestSynchronized 类的同时都会创建一个 increase 方法,increase 方法中只是打印出来了当前访问的线程名称。Synchronized 类实现了 Runnable 接口,重写了 run 方法,run 方法里面就是一个 0 - 20 的计数器。在 main 方法中,new 出了两个线程,分别是 aThread 和 bThread,Thread.join 表示等待这个线程处理结束。这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性。

那么如果不加synchronized 会有什么效果呢,会不会结果不是40 ??? 直接上代码和运行结果吧:

public class TestSynchronized implements Runnable {
    static int i = 0;

    // ⾮静态,访问时锁不⼀样不会发⽣互斥
    public synchronized void increase() {
        i++;
        System.out.println(Thread.currentThread().getName());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 同一个对象的不同实例
        Thread t1 = new Thread(new TestSynchronized());
        Thread t2 = new Thread(new TestSynchronized());
        t1.start();
        t2.start();
        /**
         * join(): 等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
         *  确保t1和t2先执行完
         */
        t1.join();
        t2.join();
        System.out.println("i = " + i);
    }
}

/** 这个地方 循环 10000次  输出 18986

3.2 synchronized 修饰静态方法

synchronized 修饰静态方法就是 synchronized 和 static 关键字一起使用,就是上述方法中在 increase() 前加static这种方式,

// 作⽤于静态⽅法,锁是当前class对象,也就是 TestSynchronized.class
public static synchronized void increase() {
   i++;
   System.out.println(Thread.currentThread().getName());
}

当 synchronized 作用于静态方法时,表示的就是当前类的锁,因为静态方法是属于类的,它不属于任何一个实例成员,因此可以通过 class 对象控制并发访问.

这里需要注意一点,因为 synchronized 修饰的实例方法是属于实例对象,而 synchronized 修饰的静态方法是属于类对象,所以调用 synchronized 的实例方法并不会阻止访问 synchronized 的静态方法。

3.3 synchronized 修饰代码块

synchronized 除了修饰实例方法和静态方法外,synchronized 还可可以修饰代码块,同时代码块可以嵌套在方法体的内部使用。

public class TestSynchronized implements Runnable {
    TestSynchronized instanceObj = new TestSynchronized();
    static int i = 0;
    public  void increase() {
        i++;
        System.out.println(Thread.currentThread().getName());
    }


    @Override
    public void run() {
        synchronized (instanceObj) {
            for (int i = 0; i < 100000; i++) {
                increase();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 同一个对象的不同实例
        Thread t1 = new Thread(new TestSynchronized());
        Thread t2 = new Thread(new TestSynchronized());
        t1.start();
        t2.start();
        /**
         * join(): 等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
         *  确保t1和t2先执行完
         */
        t1.join();
        t2.join();
        System.out.println("i = " + i);
    }
}

上面代码中将 instanceObj 作为锁对象对其加锁,每次当线程进入 synchronized 修饰的代码块时就会要求当前线程持有obj 实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。

synchronized 修饰的代码块,除了可以锁定对象之外,也可以对当前实例对象锁、class 对象锁进行锁定

    // 实例对象锁
    synchronized(this){
        for(int j = 0;j < 1000;j++){
            i++;
        }
    }

   //class对象锁
    synchronized(TestSynchronized.class){
        for(int j = 0;j < 1000;j++){
            i++;
        }
    }

反编译后的代码:

image.png

3.5 总结多线程访问同步方法的7中情况

  • 1.两个线程同时访问一个对象的同步方法这里它们是同一把锁(this),争抢同一把锁的时候必然要相互等待只能有一个人持有

  • 2.两个线程访问的是两个对象(实例)的同步方法这种情况下synchronized是不起作用的,它们之间是不受干扰的原因是它们的锁对象不是同一个,所以不干扰并行执行

  • 3.两个线程访问的是synchronized的静态方法虽然它们是不同的实例,但是只要它们是静态的,那么对应的锁就是同一把,它们会一个一个执行.

  • 4.同时访问同步方法与非同步方法(被synchronized修饰和没有被synchronized修饰的方法)同时访问同步方法与非同步方法,synchronized只作用与加了同步的方法中,没有加synchronized的方法不会受到影响

  • 5.访问同一个对象的不同的普通同步方法(不是static非静态的方法,那么它们是串行还是并行) 这两个都加了Synchronized的方法是同一个实例拿到的都是一样this,所以这两个方法没有办法同时运行,它们是串行执行的

  • 6.同时访问静态synchronized和非静态synchronized方法(都是被synchronized所修饰的,不同的 是一个是静态的一个不是静态的,这个时候有多线程去并发执行,会带来怎样的同步效果)静态synchronized修饰的方法是类锁,非静态synchronized方法是方法锁形式,一个是.class一个是对象实例的this虽然都加了Synchronized但是它们还是会同时执行同时结束

  • 7.如果方法抛出异常后,会释放锁吗? 方法抛出异常之后JVM会帮它释放锁,其它方法就会获得这把锁接着往下执行 (跟lock接口比较,lock即便抛出异常,你没有显式的去释放锁,lock是不会释放的,synchronized一旦抛出异常它会自动的释放的)

  • 8 . 被synchronized修饰的方法调用了另个一个没有被synchronized修饰的方法,还是线程安全吗?答案不是,一旦出了本方法而另一个方法没有被synchronized修饰那它是可以同时被多个线程访问的

四:synchronized 锁升级过程

先上图:

sychronized锁升级过程1.png

sychronized锁的四种状态1.png

1)无锁 : 没有资源锁定 每个线程都能访问修改同一个资源 ,只能有一个线程修改成功 其他线程不断重试修改 直到成功为止。

升级时机: 一个线程访问时 升级偏向锁,  Mark Word(上边对象的构成中有解释) 里存储锁偏向的线程 ID 在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁 偏向锁: 是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能【一直有一个线程拿着 省去加锁解锁的消耗】

(2)偏向锁 :  竞争压力小 不会主动释放锁 加锁解锁不额外消耗 当有其他线程尝试竞争时 释放锁
关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头(下面有解释)设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

升级时机: 有锁竞争时 升级为轻量级锁(其他线程尝试获取锁时)

(3)轻量级锁: 其他线程会通过自旋方式 一直尝试获取锁

升级时机: 自旋失败 十次后 (锁膨胀 )升级为重量级锁

(4)重量级锁:  其他线程发现 锁是重量级锁时 将自己挂起 等待唤醒
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

五:synchronized 锁和其他锁对比

5.1 我们常用的Java中的锁有:CAS机制、synchronized、ReentrantLock,ReentrantReadWriteLock。

5.2根据锁的性质分类:根据重入和排它性分析:共享锁、可重入锁、排它锁

  • 共享锁:线程可以同时获取锁。ReentrantReadWriteLock对于读锁是共享的。在读多写少的情况下使用共享锁会非常高效。
  • 重入锁:线程获取锁后可以重复执行锁区域。Java提供的锁都是可重入锁。不可重入锁非常容易导致死锁。
  • 排它锁:多线程不可同时获取的锁,与共享锁对立。与重入锁不矛盾可以是并存属性。

5.3根据获取锁的方式:乐观锁、悲观锁:

  • 乐观锁:其实是一种采用具有原子性的CAS非加锁机制,保证当前线程原子性执行。

  • 悲观锁:直接加锁进行线程隔离。synchronized、ReentrantLock、ReentrantReadWriteLock的写锁都属于悲观锁

  • 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

5.4 根据获取锁时是否先参与排队:公平锁、非公平锁

  • 公平锁:线程试图获取锁时,先按尝试获取锁的时间顺序排队
  • 非公平锁:线程试图获取锁时,如果当前锁没有线程占有,则跟排队获取锁的线程一起竞争锁而无序按顺序排队,则为非公平锁。如果竞选失败,依然要排队。
  • 非公平锁比较高效,因为公平锁需要有先线程唤起

5.5 根据锁的状态划分:偏向锁、轻量级锁、重量级锁

  • 偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。类似于乐观锁。
  • 轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
  • 重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

5.6 根据锁粒度划分:分段锁等

分段锁:分段锁是一种锁思想,对数据分段加锁已提高并发效率,比如jdk8之前的ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一 些。整个ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的 意思,所以很多地方都会将其描述为分段锁,当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

5.6 Sychronized 与 Lock 锁的区别

  • Lock 是显式锁,synchronized 是隐式锁
  • Lock只有代码块锁,而 synchronized 有代码块锁和方法锁
  • Lock锁会让JVM花较少的时间调度线程,性能更好,子类多扩展性好
  • 优先顺序 Lock > synchronized (obj) {...} > synchronized func()

image.png

六:Java相关锁性质总结

Java相关锁1.png

本文正在参加「金石计划」