synchronized主要是用于解决线程安全问题的,而线程安全问题的主要诱因有以下两点:java
解决线程安全问题的根本方法:算法
因此互斥锁是解决问题的办法之一,互斥锁的特性以下:数组
互斥性:即在同一时间只容许一个线程持有某个对象锁,经过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对须要同步的代码块(复合操做)进行访问。互斥性也称为操做的原子性
可见性:必须确保在锁被释放以前,对共享变量所作的修改,对于随后得到该锁的另外一个线程是可见的(即在得到锁时应得到最新共享变量的值),不然另外一个线程多是在本地缓存的某个副本上继续操做,从而引发数据不一致问题缓存
而synchronized就能够实现互斥锁的特性,不过须要注意的是synchronized锁的不是代码,而是对象。安全
根据获取的锁能够分为两类:数据结构
对象锁和类锁的总结:多线程
实现synchronized须要依赖两个基础概念:并发
Java对象在内存中的布局主要分为三块区域:app
synchronized使用的锁对象是存储在Java对象头里的,对象头结构以下:框架
因为对象头信息是与对象自身定义的数据没有关系的额外存储成本,考虑到JVM的空间效率,Mark Word被设计为非固定的数据结构以便存储更多有效的数据,它会根据对象自身的状态赋予本身的存储空间:
简单介绍了对象头,接着咱们来了解一下Monitor,每一个Java对象天生自带了一把看不见的锁,它叫作内部锁或Monitor锁。Monitor的主要实现代码在ObjectMonitor.hpp中:
Monitor锁的竞争、获取与释放:
而后咱们从字节码层面上看一下synchronized,将以下代码经过javac编译成class文件:
package com.example.demo.thread; /** * @author 01 * @date 2019-07-20 **/ public class SyncBlockAndMethod { public void syncsTask() { synchronized (this) { System.out.println("Hello syncsTask"); } } public synchronized void syncTask() { System.out.println("Hello syncTask"); } }
而后经过 javap -verbose 将class文件反编译成可阅读的字节码内容,以下:
Classfile /E:/Java_IDEA/demo/src/main/java/com/example/demo/thread/SyncBlockAndMethod.class Last modified 2019年7月20日; size 637 bytes MD5 checksum 7600723349daa088a5353acd84c80fa5 Compiled from "SyncBlockAndMethod.java" public class com.example.demo.thread.SyncBlockAndMethod minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #6 // com/example/demo/thread/SyncBlockAndMethod super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 1 Constant pool: #1 = Methodref #7.#18 // java/lang/Object."<init>":()V #2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #21 // Hello syncsTask #4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = String #24 // Hello syncTask #6 = Class #25 // com/example/demo/thread/SyncBlockAndMethod #7 = Class #26 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 syncsTask #13 = Utf8 StackMapTable #14 = Class #27 // java/lang/Throwable #15 = Utf8 syncTask #16 = Utf8 SourceFile #17 = Utf8 SyncBlockAndMethod.java #18 = NameAndType #8:#9 // "<init>":()V #19 = Class #28 // java/lang/System #20 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #21 = Utf8 Hello syncsTask #22 = Class #31 // java/io/PrintStream #23 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #24 = Utf8 Hello syncTask #25 = Utf8 com/example/demo/thread/SyncBlockAndMethod #26 = Utf8 java/lang/Object #27 = Utf8 java/lang/Throwable #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V { public com.example.demo.thread.SyncBlockAndMethod(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 public void syncsTask(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter // 指向同步代码块的开始位置 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3 // String Hello syncsTask 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit // 指向同步代码块的结束位置,monitorenter和monitorexit之间就是同步代码块 14: goto 22 17: astore_2 18: aload_1 19: monitorexit // 若代码发生异常时就会执行这句指令释放锁 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 10: 0 line 11: 4 line 12: 12 line 13: 22 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class com/example/demo/thread/SyncBlockAndMethod, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public synchronized void syncTask(); descriptor: ()V flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // 用于标识是一个同步方法,不须要像同步块那样须要经过显式的字节码指令去标识哪里须要获取锁,哪里须要释放锁。同步方法不管是正常执行仍是发生异常都会释放锁 Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String Hello syncTask 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 16: 0 line 17: 8 } SourceFile: "SyncBlockAndMethod.java"
什么是重入:
从互斥锁的设计上来讲,当一个线程试图操做一个由其余线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入
为何会对synchronized嗤之以鼻:
锁优化之自旋锁:
许多状况下,共享数据的锁定状态持续时间较短,切换线程不值得。因而自旋锁应运而生,所谓自旋就是经过让线程执行忙循环等待锁的释放,从而不让出CPU时间片,例如while某个标识变量
缺点:若锁被其余线程长时间占用,将会带来许多性能上的开销,因此通常超过指定的自旋次数就会将线程挂起处于阻塞状态
锁优化之自适应自旋锁:
自适应自旋锁与普通自旋锁不一样的就是能够自适应自旋次数,即自旋次数再也不固定。而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁优化之锁消除,锁消除是JVM另外一种锁优化,这种优化更完全:
在JIT编译时,对运行上下文进行扫描,去除不可能存在资源竞争的锁。这种方式能够消除没必要要的锁,能够减小毫无心义的请求锁时间
关于锁消除,咱们能够看一个例子,代码以下:
public class StringBufferWithoutSync { public void add(String str1, String str2) { //StringBuffer是线程安全,因为sb只会在append方法中使用,不可能被其余线程引用 //所以sb属于不可能共享的资源,JVM会自动消除内部的锁 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferWithoutSync withoutSync = new StringBufferWithoutSync(); for (int i = 0; i < 1000; i++) { withoutSync.add("aaa", "bbb"); } } }
锁优化之锁粗化,咱们再来了解锁粗化的概念,有些状况下可能会须要频繁且重复进行加锁和解锁操做,例如同步代码写在循环语句里,此时JVM会有锁粗化的机制,即经过扩大加锁的范围,以免反复加锁和解锁操做。代码示例:
public class CoarseSync { public static String copyString100Times(String target){ int i = 0; // JVM会将锁粗化到外部,使得重复的加解锁操做只须要进行一次 StringBuffer sb = new StringBuffer(); while (i < 100){ sb.append(target); } return sb.toString(); } }
synchronized锁存在四种状态:
偏向锁:
大多数状况下,锁不存在多线程竞争,老是由同一线程屡次得到,为了减小同一线程获取锁的代价,就会使用偏向锁
核心思想:
若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再作任何同步操做,即获取锁的过程只须要检查Mark Word的锁标记位为偏向锁以及当前线程id等于Mark Word的ThreadID便可,这样就省去了大量有关锁申请的操做,那么这个锁也就偏向于该线程了偏向锁不适用于锁竞争比较激烈的多线程场合
轻量级锁:
轻量级锁是由偏向锁升级而来,偏向锁运行在一个线程进入同步块的状况下,当有第二个线程加入锁竞争时,偏向锁就会升级为轻量级锁
适用场景:线程交替执行同步块
若存在线程同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁
轻量级锁的加锁过程:
在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态以下图所示:
若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00",即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态以下图所示:
轻量级锁的解锁过程:
锁的内存语义:
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
偏向锁、轻量级锁、重量级锁的汇总:
在JDK1.5以前,synchronized是Java惟一的同步手段,而在1.5以后则有了ReentrantLock类(重入锁):
ReentrantLock公平性的设置:
ReentrantLock fairLock = new ReentrantLock(true);
ReentrantLock的好处在于将锁对象化了,所以能够实现synchronized难以实现的逻辑,例如:
若是说ReentrantLock将synchronized转变为了可控的对象,那么是否能将wait、notify及notifyall等方法对象化,答案是有的,即Condition:
synchronized和ReentrantLock的区别:
Java内存模型(JMM):
Java内存模型(Java Memory Model,简称JMM)自己是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,经过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM中的主内存(即堆空间):
JMM中的工做内存(即本地内存,或线程栈):
JMM与Java内存区域划分(即Java内存结构)是不一样的概念层次:
主内存与工做内存的数据存储类型以及操做方式概括:
JMM如何解决可见性问题:
指令重排序须要知足的条件:
什么是Java内存模型中的happens-before:
happens-before的八大原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做
- 锁定规则:一个unLock操做先行发生于后面对同一个锁的lock操做
- volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做(保证了可见性)
- 传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个动做
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
volatile:
volatile变量为什么当即可见?简单来讲:
volatile变量如何禁止重排序优化:
volatile和synchronized的区别:
- volatile本质是在告诉JVM当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取;synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住直到该线程完成变量操做为止
- volatile仅能使用在变量级别;synchronized则可使用在变量、方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则能够保证变量修改的可见性和原子性
- volatile不会形成线程的阻塞;synchronized可能会形成线程的阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化
CAS(Compare and Swap)是一种线程安全性的方法:
CAS思想:
CAS多数状况下对开发者来讲是透明的:
缺点:
利用Executors建立不一样的线程池知足不一样场景的需求:
- newFixedThreadPool(int nThreads):指定工做线程数量的线程池
- newCachedThreadPool():处理大量短期工做任务的线程池,特色:
- 试图缓存线程并重用,当无缓存线程可用时,就会建立新的工做线程
- 若是线程闲置的时间超过阈值,则会被终止并移出缓存
- 系统长时间闲置的时候,不会消耗什么资源
- newSingleThreadExecutor():建立惟一的工做者线程来执行任务,若是线程异常结束,会有另外一个线程取代它
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize):定时或者周期性的工做调度,二者的区别在于单一工做线程仍是多个线程
- JDK8新增的newWorkStealingPool():内部会构建ForkJoinPool ,利用working-stealing算法,并行地处理任务,不保证处理顺序
- working-stealing算法:某个线程从其余线程的任务队列里窃取任务来执行
Fork/Join框架(JDK7提供):
为何要使用线程池:
Executor的框架:
J.U.C的三个Executor接口:
线程池执行任务流程图:
ThreadPoolExecutor的七个构造器参数:
int corePoolSize
:核心线程数int maximumPoolSize
:最大线程数long keepAliveTime
:线程空闲存活时间TimeUnit unit
:存活时间的单位BlockingQueue<Runnable> workQueue
:任务等待队列ThreadFactory threadFactory
:线程建立工厂,用于建立新线程RejectedExecutionHandler handler
:任务拒绝策略
新任务提交execute执行后的判断:
execute执行流程图:
线程池的状态:
线程池状态转换图:
线程池中工做线程的生命周期:
关于线程池大小如何选定参考: