深入学习synchronized

synchronized

并发编程中的三个问题:

可见性(Visibility)

是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。

代码演示:

public class Test01Visibility {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {

            }
        }).start();

        Thread.sleep(2000);

        new Thread(() -> {
            flag = false;
            System.out.println("修改了flag");
        }).start();
    }
}

小结:并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改

后的最新值。

原子性(Atomicity)

在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行

代码演示:

public class Test02Atomicity {
    public static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        // 创建任务
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                num++;
            }
        };
        ArrayList<Thread> threads = new ArrayList<>();
        //创建线程
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            threads.add(t);
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println(num);
    }
}

通过 javap -p -v Test02Atomicity对class 文件进行反汇编:发现++ 操作是由4条字节码指令组成,并不是原子操作

小结:并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作

有序性(Ordering)

是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

代码演示:

@JCStressTest
@Outcome(id={"1","4"},expect=Expect.ACCEPTABLE,desc="ok")
@Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger")
@State
public class Test03Orderliness {
    int num=0;
    boolean ready=false;
    //线程一执行的代码
    @Actor
    public void actor1(I_Resultr){
        if(ready){
            r.r1=num+num;
        }else{
            r.r1=1;
        }
    }
    //线程2执行的代码
    @Actor
    public void actor2(I_Resultr){
        num=2;
        ready=true;
    }
}

运行的结果有:0、1、4

小结:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必

就是开发者编写代码时的顺序。

Java内存模型(JMM)

计算机结构简介

根据冯诺依曼体系结构,计算机由五大组成部分,输入设备,输出设备,存储器,控制器,运算器。

CPU:

中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中的数据。

内存:

我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。

缓存:

CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。于是就有了在

CPU和主内存之间增加缓存的设计。CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。

Java内存模型

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

工作内存

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

小结

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

主内存与工作内存之间的交互

注意:1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值

  1. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

synchronized保证三大特性

synchronized保证可见性

while(flag){
    //增加对象共享数据的打印,println是同步方法
    System.out.println("run="+run);
}

小结:

synchronized保证可见性的原理,执行synchronized时,lock原子操作会刷新工作内存中共享变量的值。

synchronized保证原子性

for(int i = 0; i < 1000; i++){
    synchronized(Test01Atomicity.class){
        number++;
    }
}

小结:

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

synchronized保证有序性

synchronized(Test01Atomicity.class){
    num=2;
ready=true;
}

小结

synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性。

synchronized的特性

可重入特性

public class Demo01 {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(Thread.currentThread().getName() + "获取了锁1");
            synchronized (MyThread.class) {
                System.out.println(Thread.currentThread().getName() + "获取了锁2");
            }
        }
    }
}

可重入原理:

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。

可重入的好处:

  1. 可以避免死锁

  2. 可以让我们更好的来封装代码

小结:

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,获取一次锁加+1,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

不可中断特性

什么是不可中断?

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

public class Uninterruptible {
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "执行同步代码块");
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

        Thread.sleep(1000);
        System.out.println("停止线程2前");
        System.out.println(t2.getState());
        t2.interrupt();
        System.out.println("停止线程2后");
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

synchronized是不可中断,处于阻塞状态的线程会一直等待锁。

ReentrantLock可中断演示

public class Interruptible {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        test01();
    }

    private static void test01() throws InterruptedException {
        Runnable run = () -> {
            boolean flag = false;
            String  name = Thread.currentThread().getName();
            try {
                flag = lock.tryLock(3, TimeUnit.SECONDS);
                if (flag) {
                    System.out.println(name + "获得锁,进入锁执行");
                    Thread.sleep(888888);
                } else {
                    System.out.println(name + "没有获得锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (flag) {
                    lock.unlock();
                    System.out.println(name + "释放锁");
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();
    }
}

小结:

synchronized属于不可被中断

Lock的lock方法是不可中断的

Lock的tryLock方法是可中断的

synchronized 的原理

monitorenter:

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)

  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1

  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorenter小结:

synchronized的锁对象会关联一个monitor, 这个monitor不是我们主动创建的, 是JVM的线程执行到这个同步代码块,发现锁对象

有monitor就会创建monitor, monitor内部有两个重要的成员变量owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,

当一个线程拥有monitor后其他线程只能等待。

monitorexit:

  1. 能执行monitorexit 指令的线程一定是拥有当前对象的monitor的所有权的线程。

  2. 执行monitorexit 时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

monitorexit释放锁。

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

面试题synchroznied出现异常会释放锁吗?

:会释放锁。

同步方法

同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰。会隐式调用monitorenter 和monitorexit。在执行同步方法前会调用

monitorenter,在执行完同步方法后会调用monitorexit 。

小结:

通过javap反汇编可以看到synchronized 使用了monitorentor和monitorexit两个指令。每个锁对象都会关联一个monitor(监视

器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数, 当执行到

monitorexit时, recursions会-1, 当计数器减到0时这个线程就会释放锁。

面试题:synchronized与Lock的区别

1、synchronized 是关键字,lock 是一个接口

2、synchronized 会自动释放锁,lock 需要手动释放锁。

3、synchronized 是不可中断的,lock 可以中断也可以不中断。

4、通过lock 可以知道线程有没有拿到锁,而synchronized 不能。

5、synchronized 能锁住方法和代码块,而lock 只能锁住代码块。

6、lock 可以使用读锁提高多线程读效率。

7、synchronized 是非公平锁,ReentrantLock 可以控制是否是公平锁。

CAS

cas的概述和作用:

compare and swap,可以将比较和交换转为原子操作,这个原子操作直接由cpu保证,cas可以保证共享变量赋值时的原子操作,cas依赖3个值:内存中的值v,旧的预估值x,要修改的新值b。根据atomicInteger的地址加上偏移量offset的值可以得到内存中的值,将内存中的值和旧的预估值进行比较,如果相同,就将新值保存到内存中。不相同就进行重试。

Java对象的布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc。

_mark表示对象标记、属于markOop类型,也就是Mark World,它记录了对象和锁有关的信息

_metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、compressed_klass表示压缩类指针。

Mark Word

锁状态 存储内容 锁标志位
无锁 对象的hashcode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程id、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

klass pointer

用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。通过-XX:+UseCompressedOops开启指针压缩,

在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

实例数据

就是类中定义的成员变量。

对齐填充

由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象的大小必须是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对齐填来补全。

查看Java对象布局

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.9</version>
</dependency>

小结

Java对象由3部分组成,对象头,实例数据,对齐数据,对象头分成两部分:Mark World + Klass pointer

偏向锁

什么是偏向锁?

锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

偏向锁原理

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点

  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态

  3. 撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态

偏向锁是自适应的

小结:

偏向锁的原理是什么?

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的MarkWord之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的好处是什么?

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

轻量级锁

什么是轻量级锁?

轻量级锁是JDK 6之中加入的新型锁机制,轻量级锁并不是用来代替重量级锁的。

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁,所以轻量级锁的出现并非是要代替重量级锁。

轻量级锁原理

当关闭偏向锁或多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  1. 判断当前对象是否处于无锁状态,如果是,则JVM 首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝,将对象的Mark Word 复制到栈帧中的Lock Record 中,将Lock Record中的owner指向当前对象。
  2. JVM 利用CAS 操作尝试将对象的Mark Word 更新为指向Lock Record 的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word 是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

轻量级锁的释放:

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁时保存在Mark Word 中的数据;
  2. 用CAS 操作将取出的数据替换当前对象的Mark Word 中,如果成功,则说明释放锁成功。
  3. 如果CAS 操作替换失败,说明有其他线程获取该锁,则需要将轻量级锁膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS 操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁好处:

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

自旋锁

monitor 实现锁的时候, monitor 会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU 从用户态转为核心态,频繁的阻塞和唤醒对CPU 来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。同时,共享数据的锁定状态可能只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果有一个以上的处理器,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍微等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否释放了锁。为了让线程等待,我们只需让线程执行一个循环(即自旋),这就是自旋锁。

自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改。

适应性自旋锁

在JDK 6 中引入了自适应的自旋锁。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

平时写代码如何对synchronized优化

减少synchronized的范围:

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

synchronized(Demo01.class){
    System.out.println("aaa");
}

降低synchronized锁的粒度:

将一个锁拆分为多个锁提高并发度,如HashTable:锁定整个哈希表,一个操作正在进行时,其他操作也同时锁定,效率低下。ConcurrentHashMap:局部锁定,只锁定桶。

读写分离:

读取时不加锁,写入和删除时加锁

ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

(0)

相关推荐

  • Java之synchronized的JVM底层实现原理精简理解

    Java之synchronized的JVM底层实现原理精简理解

  • 17张图带你秒杀synchronized关键字

    来自公众号:一只自动编码机 引子 小艾和小牛在路上相遇,小艾一脸沮丧. 小牛:小艾小艾,发生甚么事了? 小艾:别提了,昨天有个面试官问了我好几个关于 synchronized 关键字的问题,没答上来. ...

  • Synchronized用法原理和锁优化升级过程(面试)

    简介 多线程一直是面试中的重点和难点,无论你现在处于啥级别段位,对synchronized关键字的学习避免不了,这是我的心得体会.下面咱们以面试的思维来对synchronized做一个系统的描述,如果 ...

  • Java中synchronized的实现原理与应用

    转自:https://blog.csdn.net/u012465296/article/details/53022317 Java中的每一个对象都可以作为锁,而在Synchronized实现同步的几种 ...

  • 自旋、偏向锁、轻量级锁、重量级锁区别

    java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁.偏向锁.轻量级锁.重量级锁.并且锁只能升级不能降级. 在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念. 自旋 现在假设有这 ...

  • synchronized底层揭秘

    前言 上篇文章我们从硬件级别探索,对可见性和有序性的认识上升了一个高度,却迟迟没有介绍原子性的解决方案. 今天我们就来聊一聊原子性的解决方案,锁. 引入锁机制,除了可以保证原子性,同时也可以保证可见性 ...

  • 【干货】连肝7个晚上,总结了关于Java基础的16个问题!

    说说进程和线程的区别? 进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率. 由于进程是资源分配和调度的基本单位,因为进程的创建.销毁.切换产 ...

  • java开发技术之synchronized的使用浅析

    synchronized 这个关键字的重要性不言而喻,几乎可以说是并发.多线程必须会问到的关键字了.synchronized 会涉及到锁.升级降级操作.锁的撤销.对象头等.所以理解 synchroni ...

  • 深入理解Java里的各种锁(上)

    不知道你有没有被Java里各种锁搞晕过, 轻量级锁 重量级锁 公平锁 非公平锁  lock 锁,synchronized锁 都有什么区别呢? 先看图再一个一个说: 1.悲观锁 VS 乐观锁 悲观锁:对 ...

  • S50学习资料讲解

    S50学习资料讲解

  • “取类比象”——学习中医的秘法

    万物皆有"象",按同象同类的原则,由一般到个别,从已知推导未知,以类万物,中国古代圣贤即是以这种演绎方法来认识世界,建立了整个传统文化体系.中医是传统文化中的瑰宝,中医学中亦有&q ...

  • 整理明朝山水画100幅供大家学习收藏

    整理明朝山水画100幅供大家学习收藏

  • 初中数学19类最值问题全覆盖,收藏学习!

    春熙初中数学 25篇原创内容 公众号 初中数学解题思路 本号致力于初中数学学习的钻研和探索.全面覆盖初中数学典型题集.解题模型.动点最值.思路方法.超级易错.几何辅助线.压轴破解等方面,欢迎关注! 1 ...

  • 一则公报案例学习笔记:对修改股东出资期限应否适用资本多数决规则的思考|审判研究

    一.问题的提出 2021年第3期<最高人民法院公报案例>刊登了鸿大(上海)投资管理有限公司与姚锦城公司决议纠纷上诉案,裁判要旨为:"公司股东滥用控股地位,以多数决方式通过修改出资 ...

  • “经方就是好”、“高手在民间”,是中医学习之道

    导读:提到中医的最大优势及特点,很多朋友首先想的是辨证论治,并把其奉为中医的瑰宝.然而事实是这样的,现在很多人倍加推崇的辨证论治,并不是真正的辨证论治,而是一种畸形的中医发展模式,因为目前的辨证论治, ...

  • 孩子学习不积极没动力, 家长要时刻注意, 可能是缺乏“内驱力”

    我们都知道兴趣是孩子最好的老师,孩子在兴趣状态下,非常愿意学习,不用家长催促,孩子就有足够的内驱力,并且孩子也不觉得学习是一件枯燥的事情.所以想要让孩子不再被家长催促着学习,要帮助孩子找到内驱力. 只 ...

  • 迁移学习——入门简介

    一.简介 背景:现如今数据爆炸: 对机器学习模型来说要求快速构建,强泛化 对于数据来说,大部分数据没有标签 所以收集标签数据和从头开始构建一个模型都是代价高昂的,需要对模型和带有标签的数据进行重用 传 ...

  • 真正能拉开孩子未来差距的不是学习成绩, 而是这3个方面的习惯

    李玫瑾教授曾经说过,她的女儿有一次晚上写作业,由于作业很多,已经写到很晚了还没有写完.而老师又要求每个家长都要给孩子检查作业,并且家长签字.于是李教授让女儿不要再写作业了,并且在女儿的作业本上写了一行 ...