大数据Java

「Java多线程」内置锁(Synchronized)的前世今生

2021-11-27  本文已影响0人  Java弟中弟

什么互斥和同步

什么是互斥量

互斥量mutex

线程安全三大特性

【Java多线程】重温并发BUG的源头之可见性、原子性、有序性

二.为什么要用锁?

三.什么是内置锁

Java内置锁不需要显式的获取锁和释放锁,由JVM内部来实现锁的获取与释放。而且任何一个对象都能作为一把内置锁。在 JDK1.4及之前就是使用 内置锁Synchronized来进行线程同步控制的

上文说,任何一个对象都能作为一把内置锁”,意味着synchronized关键字出现的地方,都有一个对象与之关联 ,具体表现为:

原理

优缺点:

四.synchronized使用

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程场景下不存在的问题就来了: 如果 多个线程时读写共享变量 ,会出现数据不一致的问题。

1.线程安全问题产生

public class SyncTest1 {

    public static void main(String[] args) throws Exception {

        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

//计数器
class Counter {

    public static int count = 0;
}
//自增线程
class AddThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {
  Counter.count += 1; }
    }
}
//自减线程
class DecThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {
  Counter.count -= 1; }
    }
}

上面的代码 两个线程同时对一个int变量进行操作 ,一个加10000次,一个减10000次,最后结果应该是0,但是, 每次运行,结果实际上都是不一样的。

连续执行三次结果

「Java多线程」内置锁(Synchronized)的前世今生 「Java多线程」内置锁(Synchronized)的前世今生 「Java多线程」内置锁(Synchronized)的前世今生

这是因为对变量进行读取和写入时,结果要正确,必须保证是 原子操作 。原子操作是指不能被中断的一个或一系列操作。

实际上执行 n = n + 1 并不是一个原子操作,它的执行过程如下:

  1. 从主存中读取变量x副本到工作内存
  2. 给x加1
  3. 将x加1后的值写回主存

我们假设 n 的值是 100 ,如果 两个线程同时执行n = n + 1 ,得到的结果很可能不是 102,而是101 ,

原因在于: 多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程

这说明多线程场景下,要保证逻辑正确, 即某一个线程对共享变量进行读写时,其他线程必须等待

2.初识Synchronized

如何使用Synchronized

synchronized(lockObject) { }.

public void add(int m) {

    synchronized (obj) {

        if (m < 0) {

            throw new RuntimeException();
        }
        this.value += m;
    } // 无论有无异常,都会在此释放锁
}

使用synchronized优化SyncTest1案例中线程不安全问题

public class SyncTest2 {

    public static void main(String[] args) throws Exception {

        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}
//计数器
class Counter {

    public static final Object lock = new Object();
    public static int count = 0;
}
//自增线程
class AddThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {
     synchronized(Counter.lock) {
  Counter.count += 1;} }
    }
}
//自减线程
class DecThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {
   synchronized(Counter.lock){
  Counter.count -= 1;} }
    }
}

执行结果

「Java多线程」内置锁(Synchronized)的前世今生

代码

synchronized(Counter.lock) {
 //获取锁

  }//释放锁

synchronized解决了多线程同步访问共享变量的有序性问题。但它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外 加锁和解锁需要消耗一定的时间,所以, synchronized会降低程序的执行效率。

3.错误使用Synchronized的案例

3.1.案例1

public class Main {

    public static void main(String[] args) throws Exception {

        AddThread add = new AddThread();
        DecThread dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {

    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
    public static int count = 0;
}

class AddThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock1) {

                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock2) {

                Counter.count -= 1;
            }
        }
    }
}

执行结果

「Java多线程」内置锁(Synchronized)的前世今生

结果并不是0,这是因为 2个线程各自的synchronized锁住的不是同一个对象! 这使得2个线程各自都可以同时获得锁: 因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被2个线程分别获取 。 使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

3.2.案例2

public class SyncTest3 {

    public static void main(String[] args) throws Exception {

        Thread [] ts = new Thread[] {
  new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
        for (Thread t : ts) {

            t.start();
        }
        for (Thread t : ts) {

            t.join();
        }
        System.out.println(Counter.studentCount);
        System.out.println(Counter.teacherCount);
    }
}

class Counter {

    public static final Object lock = new Object();
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

class AddStudentThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock) {

                Counter.studentCount += 1;
            }
        }
    }
}

class DecStudentThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock) {

                Counter.studentCount -= 1;
            }
        }
    }
}

class AddTeacherThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock) {

                Counter.teacherCount += 1;
            }
        }
    }
}

class DecTeacherThread extends Thread {

    public void run() {

        for (int i=0; i<10000; i++) {

            synchronized(Counter.lock) {

                Counter.teacherCount -= 1;
            }
        }
    }
}

执行结果

「Java多线程」内置锁(Synchronized)的前世今生
public class SyncMultiTest3 {

    public static void main(String[] args) throws Exception {

        //创建线程
        Thread[] ts = new Thread[]{
 new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()};
        //启动线程
        for (Thread t : ts) {

            t.start();
        }
        //优先子线程先执行
        for (Thread t : ts) {

            t.join();
        }
        //最后打印执行结果
        System.out.println(Counter.studentCount);
        System.out.println(Counter.teacherCount);
    }
}

//计数器
class Counter {

    public static final Object lockTeacher = new Object();//学生线程锁对象
    public static final Object lockStudent = new Object();//老师线程锁对象
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

//增加学生数量线程
class AddStudentThread extends Thread {

    public void run() {

        for (int i = 0; i < 10000; i++) {

            synchronized (Counter.lockStudent) {

                Counter.studentCount += 1;
            }
        }
    }
}

//减少学生数量线程
class DecStudentThread extends Thread {

    public void run() {

        for (int i = 0; i < 10000; i++) {

            synchronized (Counter.lockStudent) {

                Counter.studentCount -= 1;
            }
        }
    }
}

//增加老师数量线程
class AddTeacherThread extends Thread {

    public void run() {

        for (int i = 0; i < 10000; i++) {

            synchronized (Counter.lockTeacher) {

                Counter.teacherCount += 1;
            }
        }
    }
}

//减少老师数量线程
class DecTeacherThread extends Thread {

    public void run() {

        for (int i = 0; i < 10000; i++) {

            synchronized (Counter.lockTeacher) {

                Counter.teacherCount -= 1;
            }
        }
    }
}

执行结果

「Java多线程」内置锁(Synchronized)的前世今生

3.3.案例3

JVM规范定义了几种原子操作:

  1. 单条原子操作的语句不需要同步。例如:
public void set(int m) {

    synchronized(lock) {

        this.value = m;
    }
}

就不需要同步

//引用类型赋值
public void set(String s) {

    this.value = s;
}
  1. 如果是多行赋值语句,就必须保证是同步操作
class Pair {

    int first;
    int last;
    public void set(int first, int last) {

        synchronized(this) {

            this.first = first;
            this.last = last;
        }
    }
}

有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:

class Pair {

    int[] pair;
    public void set(int first, int last) {

        int[] ps = new int[] {
  first, last };
        this.pair = ps;
    }
}

就不再需要同步,因为 this.pair = ps 是 引用赋值的原子操作 。而语句: int[] ps = new int[] { first, last }; ,这里的 ps是方法内部定义的局部变量 , 每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。

3.4.小结

  1. 多线程 同时读写共享变量时 ,会造成逻辑错误,因此需要通过 synchronized 同步;
  2. 同步的本质就是给指定对象加锁 ,加锁后才能继续执行后续代码
  3. 注意 加锁对象必须是同一个实例
  4. JVM定义的单个原子操作不需要同步

五.Jvm对synchronized的优化

在 JDK1.6 之前, syncronized 是一把 重量级锁,在 JDK 1.6之后为了 减少获得锁和释放锁带来的性能消耗,会有一个 锁升级的过程,给Synchronized加入了 "偏向锁、自旋锁、轻量级锁"的特性,这些优化使得Synchronized的性能在某些场景下与ReentrantLock的性能持平

  1. syncronized一共有 4 种锁状态,级别从低到高依次是: 无锁->偏向锁->轻量级锁->重量级锁,这几个状态会 随着竞争情况逐渐升级。
  2. 锁可以升级但不能降级 ,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略, 目的是为了提高获得锁和释放锁的效率。

1.Java对象内存结构

对象在堆内存中存储的布局分为3块

「Java多线程」内置锁(Synchronized)的前世今生

如上图所示, 以Hotspot虚拟机为例, 在实例化一个对象后,在 Java内存中的布局 可分为 3 块:

1.对象头包括2部分(ObjectHeader):

2.实例数据区域(InstanceData)

3.对齐填充区域(Padding)

2.JDK1.6中JVM对Synchronized的优化

锁消除和锁粗化,适应性自旋是虚拟机对低效的锁操作而进行的一个优化。

2.1.锁消除(Lock Elimination)

锁削除是指JVM编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

如:使用StringBuffer的append方法,因为append方法需要判断对象是否被占用,而如果代码不存在锁的竞争,那么这部分的性能消耗是无意义的。于是虚拟机在即时编译的时候就会将上面代码进行优化,也就是锁消除。

那么虚拟机如何判断不存在同步情况呢?通过 逃逸分析 。可见下面伪代码

public static String createStringBuffer(String str1, String str2) {

        StringBuffer sb= new StringBuffer();
        sb.append(str1);// append方法是同步操作
        sb.append(str2);
        return sBuf.toString();// toString方法是同步操作
}

2.2.锁粗化(Lock Coarsening)

若有一系列操作,反复地对同一把锁进行加锁和解锁操作,编译器会扩大这部分代码的同步块的边界,从而只使用一次上锁和解锁操作。。

public static StringBuffer createStringBuffer(String str1, String str2) {

    StringBuffer sBuf = new StringBuffer();
    sBuf.append(str1);// append方法是同步操作
    sBuf.append(str2);// append方法是同步操作
    sBuf.append("abc");// append方法是同步操作
    return sBuf;
}
for(int i=0;i<100000;i++){

    synchronized(this){

        do();  
    }
}  

//在锁粗化之后运行逻辑如下列代码
synchronized(this){

    for(int i=0;i<100000;i++){

        do();
    }    
}

2.3.适应性自旋锁(Adaptive Spinning)

背景:在许多场景中,同步资源的锁定时间很短,为了这一小段时间去阻塞或唤醒一个线程的时间可能比用户代码执行的时间还要长。为了让当前线程“稍等一下”,我们可以让线程进行自旋,如果在自旋过程中占用同步资源的线程已经释放了锁,那么当前线程就可以不进入阻塞而直接获取同步资源,从而避免切换线程的开销。

自旋锁(spinlock):即当一个线程在获取锁的时候,如果锁已经被其它线程获取,不是立即阻塞线程。那么该线程将 循环等待,然后 不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。尝试获取锁的线程不会立即 阻塞(放弃CPU时间片),采用 循环的方式尝试获取锁!

「Java多线程」内置锁(Synchronized)的前世今生

什么是自适应自旋锁:即: 自旋的次数不再固定 , 由前一次在 同一个锁上的自旋时间 及 锁的拥有者的状态来决定 。 来计算出一个较为合理的本次自旋等待时间。

如果 线程1 自旋等待刚刚成功获得过锁,并且占有锁的 线程2 正在运行中,那么JVM就会认为线程1 这次自旋也很有可能再次成功,进而允许线程1进行更多次的自旋等待。反过来说,如果某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费CPU资源。

2.4.简述偏向锁

2.5.简述轻量级锁

2.6.简述重量级锁

重量级锁是一种 悲观锁,它认为总是有多个线程要竞争锁,所以它每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用 互斥同步(阻塞)来保证线程的安全;

3.锁升级

3.1.什么是锁升级

锁升级的过程其实就是对象头中的 Mark Word 数据结构改变的过程。 是不可逆转的。

「Java多线程」内置锁(Synchronized)的前世今生 「Java多线程」内置锁(Synchronized)的前世今生

1.默认是无锁状态

2.偏向锁的判断

3.升级到轻量级锁的判断

4.升级到重量级锁的判断

3.2.锁升级的四种锁状态的思路及特点

无锁、偏向锁 、 轻量级锁、 重量级锁都是指 synchronized在某种场景下的状态 ,整体的锁状态升级流程如下:

「Java多线程」内置锁(Synchronized)的前世今生

Mark Word在不同锁状态下的结构

「Java多线程」内置锁(Synchronized)的前世今生

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级

「Java多线程」内置锁(Synchronized)的前世今生

1.无锁状态

无锁没有对共享资源进行加锁,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

2.偏向锁状态

偏向锁是指一段同步代码一直被 同一个线程所访问,那么该线程会自动获取锁, 减少同一线程获取锁的代价,省去了大量有关 锁申请的操作。

3.轻量级锁状态

是指 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过 自旋的形式尝试获取锁,不会阻塞,从而提高性能。

4.重量级锁状态

升级为重量级锁时,锁标志的状态值变为 “10” ,此时Mark Word中存储的是 指向重量级锁的指针 ,此时 等待锁的线程 都会进入 阻塞状态 。

4.加锁和解锁的过程

4.1.加锁的过程

主要分为 3 步:

  1. 在线程进入同步块的时候,如果同步对象状态为 无锁状态 (锁标志为 01 ),虚拟机首先将在 当前线程 的栈帧中建立一个名为 锁记录( Lock Record) 的空间,用来存储锁对象目前的 Mark Word 的拷贝 。拷贝成功后,虚拟机将使用 CAS 操作尝试 将对象的 Mark Word 更新为指向 Lock Record 的指针 ,并将 Lock Record 里的 owner 指针 指向锁对象的 Mark Word 。如果更新成功,则执行 2,否则执行 3。
「Java多线程」内置锁(Synchronized)的前世今生
  1. 如果这个 更新动作成功 了,那么这个线程就拥有了该对象的锁,并且锁对象的 Mark Word 中的锁标志位设置为 "00" ,即表示此对象处于 轻量级锁定状态 ,这时候虚拟机 线程栈与堆中锁对象的对象头的状态 如图所示。
「Java多线程」内置锁(Synchronized)的前世今生

3. 如果这个 更新操作失败 了,虚拟机首先会检查锁对象的 Mark Word 是否指向 当前线程的栈帧, 如果是 就说明当前线程已经拥有了这个对象的锁,那就可以 直接进入同步块继续执行 。 否则说明多个线程竞争锁 ,轻量级锁就要膨胀为 重要量级锁 ,锁标志的状态值变为 "10" , Mark Word 中存储的就是指向重量级锁的指针 ,后面等待锁的线程也要进入 阻塞状态。 而当前线程便尝试使用自旋来获取锁。 自旋失败后膨胀为重量级锁,被阻塞。

4.2.解锁的过程

5.锁的优缺点

综上所述:

Java中常见使用 synchroinzed 的地方有:

上一篇 下一篇

猜你喜欢

热点阅读