深刻学习synchronized

synchronized

并发编程中的三个问题:

可见性(Visibility)

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

代码演示:面试

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在编译时和运行时会对代码进行优化,会致使程序最终的执行顺序不必定就是咱们编写代码时的顺序。ide

代码演示:高并发

@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、一、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的区别

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

二、synchronized 会自动释放锁,lock 须要手动释放锁。

三、synchronized 是不可中断的,lock 能够中断也能够不中断。

四、经过lock 能够知道线程有没有拿到锁,而synchronized 不能。

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

六、lock 可使用读锁提升多线程读效率。

七、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

相关文章
相关标签/搜索