Java并发编程知识点总结(四)——Synchronized实现原理以及优化

1.引入

由之前的知识我们了解到,Java中是存在线程并发安全性问题的,主要原因是内存可见性和指令重排序。而synchronized关键字可以使得线程之间以此排队去操作共享变量,保证线程的安全性。但是这种方式也会导致效率比较低,并发程度低。

2.synchronized作用范围

2.1 作用域为方法

静态方法: 当synchronized关键字修饰静态方法时,保证了同一个类的所有对象中中,只能有一个对象的一个线程同时访问该静态方法。

public class Thread1 extends Thread{
    public synchronized static void test1(){
        for (int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }

    public static void main(String[] args){
        Thread1 thread1 = new Thread1(){
            @Override
            public void run(){
                Thread1.test1();
            }
        };
        Thread1 thread2 = new Thread1(){
            @Override
            public void run(){
                Thread1 curThread = new Thread1();
                curThread.test1();
            }
        };
        thread1.start();
        thread2.start();
    }
}

输出结果:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第1张图片

非静态方法: 当synchronized关键字修饰非静态方法时,保证了同一个对象中,只能有一个线程同时访问该非静态方法。

public class Thread2 {
    public synchronized void test1(){
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }

    public static void main(String[] args) {
        final Thread2 thread = new Thread2();
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                thread.test1();
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                thread.test1();
            }
        };
        thread1.start();
        thread2.start();
    }
}

输出结果:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第2张图片

2.2 作用域为代码块

对象锁: 当synchronized使用this关键字锁住代码块,只能保证同一个对象中,只能由一个线程同时访问代码块的内容。

public class Thread3 {
    public void test(){
        synchronized (this){
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }

    public static void main(String[] args) {
        final Thread3 thread = new Thread3();
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                thread.test();
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                thread.test();
            }
        };
        thread1.start();
        thread2.start();
    }
}

输出结果:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第3张图片

类锁: 当synchronized使用类名.class锁住代码块,保证了同一个类的所有对象, 只能有一个对象的一个线程访问代码块。

public class Thread4 {
    public void test(){
        synchronized (Thread4.class){
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }

    public static void main(String[] args) {
        final Thread4 thread1 = new Thread4();
        final Thread4 thread2 = new Thread4();
        Thread thread3 = new Thread(){
            @Override
            public void run() {
                thread1.test();
            }
        };
        Thread thread4 = new Thread(){
            @Override
            public void run() {
                thread2.test();
            }
        };
        thread3.start();
        thread4.start();
    }
}

输出结果:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第4张图片

对于synchronized作用域的总结梳理如下表格:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第5张图片

3.synchronized实现原理

public class SynchronizedDemo1 {
    public SynchronizedDemo1() {
    }

    public static void main(String[] args) {
        method();
        Class var1 = SynchronizedDemo1.class;
        synchronized(SynchronizedDemo1.class) {
            ;
        }
    }

    public static synchronized void method() {
        System.out.println("ok");
    }
}

我们根据上面的例子来分析synchronized的实现原理,使用javap -v SynchronizedDemo1来对字节码文件进行分析:分析结果如下图所示:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第6张图片

对于上图中我们有几点需要注意:

  1. 对于第一个标号,可以看到是invokestatic,调用静态方法。同时在第五个标号中,我们也看到了ACC_SYNCHRONIZED,也表明method这个方法是一个同步方法。(注意:调用静态方法不需要monitorenter和monitorexit语句,因为已经在方法上注明了)。
  2. 对于第二个标号和第三个标号,这是synchronized关键字独有的。执行同步代码块首先要执行monitorenter命令,离开同步代码要执行monitorexit命令。
  3. 对于第四个标号,为什么会多出来一个monitorexit呢?这个主要是因为编译器为了确保执行的每条monitorenter都一定会有对应的monitorexit被执行。为了保证方法在出现异常的时候,monitorenter和monitorexit指令也能正常配对执行

此外,synchronized关键字具有重入性,如果多层嵌套synchronized关键字,那就会有多个monitorenter,也会有对应的monitorexit。这是因为每个对象都拥有一个计数器,当线程获取该对象锁之后,计数器就会加一,释放的时候就会减一。如下所示:

public class SynchronizedDemo2 {
    public static void main(String[] args) {
        method();
        synchronized (SynchronizedDemo2.class){
            synchronized (SynchronizedDemo2.class){

            }
        }
    }
    public synchronized static void method(){
        System.out.println("ok");
    }
}

Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第7张图片

最后,我们来总结下synchronized的工作流程,如下图所示:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第8张图片

使用synchronized进行同步,其关键就是要获得对象监视器monitor,当线程获取了monitor才能继续往下执行,否则就只能进行等待。获取的过程是互斥的,即同一时刻只能由一个线程获取到monitor。

4.synchronized和Happens-Before原则

我们来看下面这段代码:

public class Thread5 {
    private int answer = 0;

    public synchronized void A() throws InterruptedException {  // 1
        System.out.println("AAAA");
        Thread.sleep(2000);
        answer++;  // 2
        System.out.println("A"+answer);
    }  // 3

    public synchronized void B(){  // 4
        System.out.println("BBBB");
        int i = ++answer;  // 5
        System.out.println("B"+i);
    }  // 6

    public static void main(String[] args) throws InterruptedException {
        final Thread5 thread = new Thread5();
        Thread threadA = new Thread(){
            @Override
            public void run() {
                try {
                    thread.A();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        Thread threadB = new Thread(){
            @Override
            public void run() {
                thread.B();
            }
        };

        threadA.start();
        threadB.start();
    }
}

执行结果如图:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第9张图片

由于synchronized作用于同一个类的不同方法中,同一个对象只会有一个monitor,所以当一个线程进入synchronized修饰的方法中时,其他线程也无法进入其他synchronized修饰的方法中。 因此,根据以上的代码中的序号,我们可以推断出下面的图:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第10张图片

其中黑色的箭头是通过程序顺序执行推导的,蓝色箭头则是通过监视器规则推导出来的happens-before原则。

5.监视器锁获取和释放与内存的关系

Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第11张图片

监视器锁的获取和释放的流程如下:

  1. 首先线程A获得监视器锁,然后从主内存将变量a读取到工作内存中,更新完成后写回到主内存中,然后释放监视器锁。
  2. 线程B尝试获取监视器锁之前,会强制从主内存中获取最新的变量值,这样保证了共享变量a的可见性。然后再获取监视器锁,读取到工作内存,写回到主内存中,然后释放监视器锁。

6.CAS

乐观锁和悲观锁

锁实际上是可以分为两大类的:乐观锁和悲观锁

悲观锁:假设每一次执行临界区代码都会发生冲突,所以在每一个时刻都只能有一个线程获取锁,其余线程只能等待。
乐观锁:假设所有线程访问临界区的资源都不会发生冲突,所以不会添加锁,只在更新数据的时候去判断有没有别的线程更新了这个数据。如果这个数据没有被更新,那就成功写入数据。如果发现被其它线程更新过,则进行重试活报错。

CAS操作

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS的全称是compare and swap(比较与交换),是一种无锁算法,也就是乐观锁,在不使用锁的情况下实现多线程之间的变量同步。

CAS(V,O,N)操作主要包括:V是内存地址中存放的实际值;O是预期值(旧值);N是将要更新的值。当V和O相同时,说明该值没有被其它线程修改过,是安全的,所以可以将值N赋值给V。如果V和O不同,说明该值已经被其它线程修改过,就不能将N赋值给V。

CAS和synchronized的主要区别:如果使用synchronized关键字,当有多个线程竞争临界区资源的时候,当一个线程获得监视器锁,其它线程就会被挂起。而对于CAS操作,当CAS操作失败,并不是立即挂起,而是会进行重试。

CAS操作也会存在一些问题,主要如下:

  1. ABA问题:比如一个旧值从A变到B,然后又变回A。CAS操作则会认为始终都是A,没有被其它线程修改过。这里的解决办法是可以采用版本号的方式,即1A->2B->3C
  2. 只能保证一个共享变量的原子性:不能同时保证多个共享变量的原子性。解决办法是可以通过一个类包含多个变量。
  3. 自旋时间过长:自旋操作也是会消耗计算机资源。解决办法是设置超时等待。

7.synchronized的优化

因为原始的synchronized会造成并发程度低的问题,所以对此做了些优化,也就是我们常说的四种锁的状态:无锁,偏向锁,轻量级锁,重量级锁。
在这里插入图片描述

Java对象头

在同步的时候是获取对象的monitor,即获取到对象的锁。对象的锁的本质其实就是对象中的一个标志,这个标志就是存放在对象头里面,对象头里面的Mark Word中就存放有锁状态的标志位:
在这里插入图片描述

四种锁分别对应四个等级,也就是四个标志位:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第12张图片

无锁

无锁就是没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能够修改成功。

无锁的特点就是修改操作在循环内执行,线程会不断尝试修改共享资源,如果没有冲突就修改成功并退出,否则就进行循环尝试。上面讲到CAS就是无锁的具体实现。

偏向锁

偏向锁顾名思义,它会偏向于第一个访问锁的进程。 如果在代码运行过程中,同步代码只有一个线程访问,不存在线程争用的情况,则不需要触发同步机制,那么就会给该线程偏向锁。

偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销需要等待全局安全点,也就是需要stop the world,变为无锁的状态。
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第13张图片

偏向锁适用于只有一个线程在执行同步代码块,不存在锁争用问题。如果存在锁竞争,则需要禁用偏向锁,否则stop the world很消耗资源。下图时偏向锁获得和撤销的过程:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第14张图片

轻量级锁

轻量级锁是在偏向锁的时候,被另外的线程访问,偏向锁就会升级轻量级锁,其它线程会通过自旋的方式来获取锁,不会阻塞,从而提高性能。

轻量级锁的加锁过程:JVM会在当前线程栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录空间中,官方称为Displaced Mark Word。然后尝试使用CAS将对象头中的Mark Word替换为锁记录空间的地址。如果成功,则当前线程获得锁。如果失败,则通过自旋来尝试获得锁。

轻量级锁的解锁过程:会用CAS操作将锁记录的空间地址替换为Mark Word。如果成功,则释放锁。如果失败,则表示在它持有锁之前有其它线程尝试获取过锁,并且对Mark Word进行了修改,两者不一致,则切换重量级锁。

具体过程如下图:
Java并发编程知识点总结(四)——Synchronized实现原理以及优化_第15张图片

重量级锁

若当前只有一个等待线程,则该线程通过自旋操作进行等待。但是当自旋超过一定次数,或者一个线程持有锁,一个在自旋,又有第三个线程来访,则会升级为重量级锁。 重量级锁则是只有一个线程能够进入同步代码块。和原始的synchronized一样。

这篇文章参考了:
彻底理解synchronized
java中的各种锁详细介绍

你可能感兴趣的:(Java高并发)