一、synchronized

线程安全问题的主要诱因

1.存在共享数据(也称临界资源)
2.存在多条线程共同操作这些共享数据

解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作;

互斥锁的特性

互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也被称为操作的原子性。

可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

synchronized锁的不是代码,锁的都是对象

对于synchronized锁有两类,分别是对象锁和类锁;

获取对象锁的两种用法

1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号()中的实例对象。
2.同步非静态方法(synchronized method),锁是当前对象的实例对象。

获取类锁的两种用法

1.同步代码块(synchronized(类.class)),锁是小括号()中的类对象(Class对象)。
2.同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)。

对象锁和类锁的总结

1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
3.若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
5.同一个类的不同对象的对象锁互不干扰;
6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
7.类锁和对象锁互不干扰;

二、synchronized底层实现原理

synchronized实现的基础是Java对象头和Monitor;

Java对象头

对象头的主要结构是由Mark Word和Class Metadata Address组成:

Mark Word:默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息;
Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定对象是哪个类的数据;

Monitor

每个Java对象天生自带了一把看不见的锁,就是Monitor;Monitor会存在于每个对象的对象头中,这也是为什么在Java中所有对象都可以当做一个把锁的原因;

咱们先来看一段编译前的代码:

public class MonitorTest {public void synTest(){synchronized (this){System.out.println("Hello World");}}public synchronized void synTestWithSynchronized(){System.out.println("Hello World");}}

接着来看编译后的代码,看看monitor是如何上锁的:

咱们先找到synTest方法,发现在进入代码块时会先执行monitorenter方法,也就是获取锁,之后在运行完代码块时第13句执行了monitorexit方法,该方法是指将锁释放,但是我们发现在释放完锁之后在19句又一次的执行了monitorexit方法,这是因为这个monitorexit是在出现异常时才会执行的,因为如果线程出现了异常就需要主动释放当前锁,否则会出现死锁;

  public void synTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc           #3                  // String Hello World9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_113: monitorexit14: goto          2217: astore_218: aload_119: monitorexit20: aload_221: athrow22: return

最后我们接着看一下synTestWithSynchronized方法:

可以发现使用synchronized修饰的方法和代码块不太一样,因为synchronized修饰的方法在编译后只会在flags中添加ACC_SYNCHRONIZED标志,表示在该方法执行前调用monitorenter方法,在方法执行结束了调用monitorexit方法,虽然编译后的代码不一致,但是效果是一样的

  public synchronized void synTestWithSynchronized();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String Hello World5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 12: 0line 13: 8

什么是锁的重入

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入,也就是说父亲的锁可以给儿子使用;

为什么有些人对synchronized嗤之以鼻

1.早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现
2.线程之间的切换需要从用户态转换到核心态,开销较大

Java6以后,synchronized性能得到了很大的提升;

自旋锁

许多情况下,共享数据的锁定状态持续时间较短,这是如果使用切换线程的方式去获取锁有点不值得;

通过让线程执行忙循环等待锁的释放,不让出CPU,也就是说让线程在门外等着,不要走开,这样就不用去消耗切换线程所带来的开销的了;

缺点:若锁被其他线程长时间占用,就会带来许多线程上的开销,因为等待锁的线程此时其实是没有在干活的,只是在干等,如果等待的时间过长,就会大量浪费CPU资源,所以可以通过设置PreBlockSpin来设计需要获取锁的线程在尝试获取锁失败几次后就不再尝试;

自适应自旋锁

由于线程每次等待的次数和时间都不是固定的,所以PreBlockSpin的设置就比较困难,这时,自适应自旋锁就诞生了,它有以下两点特性:

1.自旋的次数不再固定;
2.由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定;

锁消除

JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁;

这么解释比较抽象,大家直接看下面一段代码就能理解了:

public class StringBufferTest {public void add(String s){//StringBuffer是线程安全的,由于sb只会在append中使用,不可能被其他线程引用//所以sb属于不可能共享的资源,JVM会自动消除内部的锁StringBuffer sb = new StringBuffer();sb.append(s);}public static void main(String[] args) {StringBufferTest stringBufferTest = new StringBufferTest();for (int i=0;i<100;i++){stringBufferTest.add("Hello");}}}

锁粗化

如果一个对象在循环中疯狂的执行加锁,释放锁的操作,那么JVM就会通过扩大加锁的范围,避免反复加锁和解锁;

synchronized的四种状态

无锁、偏向锁、轻量级锁、重量级锁;

锁膨胀方向:无锁 > 偏向锁 > 轻量级锁 > 重量级锁

偏向锁

大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,所以偏向锁的意义就是为了减少同一线程获取锁的代价;

核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为了偏向锁结构,当该线程再次请求锁时,无需在做任何同步操作(比如CAS),即获取锁的过程只需要检查Mark Word的锁标记为为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作;

不使用于锁竞争比较激烈的多线程场合;

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

适应的场景:线程交替执行同步块。

若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级加锁过程:


解锁过程:

锁的内存语义

当线程释放锁时,Java内存模型会把线程对应的本地内存中的共享变量刷新到主内存中;

而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

偏向锁、轻量级锁、重量级锁的汇总

三、synchronized和ReentrantLock

ReentrantLock(再入锁)

1.位于java.util.concurrent.locks包;
2.和CountDownLatch、FutureTask、Semaphore一样基于AQS实现;
3.能够实现比synchronized更细粒度的控制,如控制fairness(公平性);
4.调用lock()之后,必须调用unlock()释放锁;
5.性能未必比synchronized高,并且也是可重入的;

ReentrantLock公平性的设置

1.ReentrantLock reentrantLock = new ReentrantLock(true);
2.参数为true时,倾向于将锁赋予等待时间最久的线程;
3.公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用),因为会有额外消耗;
4.非公平锁:抢占的顺序不一定,看运气;
5.synchronized是非公平锁;

synchronized和ReentrantLock的区别

1.synchronized是关键字,ReentrantLock是类;
2.ReentrantLock可以对获取锁的等待时间进行设置,避免死锁;
3.ReentrantLock可以获取各种锁的信息;
4.ReentrantLock可以灵活地实现多路通知;
5.机制:sync操作Mark Word,lock调用Unsafe类的park()方法;

四、JMM的内存可见性

Java内存模型JMM

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

Java中的主内存

1.存储Java实例对象,包括成员变量、类信息、常量、静态变量等;
2.属于数据共享的区域,多线程并发操作时会引发线程安全问题;

Java中的工作内存

1.存储当前方法的所有本地变量信息,本地变量对其他线程不可见,即使是不同线程执行相同代码,那么这两个线程之间的本地变量也是不可见的;
2.还存储着字节码行号指示器和Native方法信息;
3.属于线程私有数据区域,不存在线程安全问题;

JMM与Java内存区域划分是同步的概念层次

1.JMM描述的是一组规则,围绕原子性、有序性、可见性展开;
2.相似点就是都存在共享区域和私有区域;

主内存与工作内存的数据存储类型以及操作方式归纳

1.方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中;
2.引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中,所以工作内存中的只是一个副本;
3.成员变量、static变量、类信息均会被存储在主内存中;
4.主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新会主内存;

指令重排序需要满足的条件

1.在单线程环境下不能改变程序运行的结果;
2.存在数据依赖关系的不允许重排序;
总结一下就是无法通过happens-before原推导出来的,才能进行指令的重排序;

happens-before的八大原则

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start()方法先行发生于线程的每一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

happens-before的概念

如果两个操作不满足上述任意一个happens-before原则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;
如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

volatile的可见性

1.当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中;
2.当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,所以volatile变量立即可见。

volatile如何禁止重排优化

内存保障(Memory Barrier)
1.保证特定操作的执行顺序
2.保证某些变量的内存可见性

通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化;

强制刷新出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本;

volatile和synchronized的区别

1.volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止;
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性;
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化;

五、CAS

一种高效实现线程安全性的方法

1.支持原子更新操作,适用于计数器,序列发生器等场景;
2.属于乐观锁机制,号称lock-free;
3.CAS操作失败时由开发者决定是继续尝试,还是执行别的操作;

CAS思想

包含三个操作数——内存位置(V)、预期原值(A)和新值(B)

CAS多数情况下对开发者来说是透明的

1.J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选;
2.Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患;
3.Java9以后,可以使用Variable Handle API来替代Unsafe;

CAS的缺点

1.若循环时间长,则开销很大;
2.只能保证一个共享变量的原子操作;
3.ABA问题, 可通过AtomicStampedReference来解决;

六、Java线程池

利用Executors创建不同的线程池满足不同场景的需求

1.newFixedThreadPool(int nThreads)
创建指定工作线程数量的线程池,如果线程池满了,则会先将该线程添加到池队列中,等到有线程退出后,再加入到线程池中运行;

2.newCachedThreadPool()
用于处理大量短时间工作任务的线程池,有以下几个特点:
(1)、试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
(2)、如果线程闲置的时间超过阈值,则会被终止并移出缓存;
(3)、系统长时间闲置的时候,不会消耗什么资源;

3.newSingleThreadExecutor()
创建唯一的工作者线程来执行任务,如果线程异常结束,会有另外一个线程取代它;

4.newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)
定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程,前者和newSingleThreadExecutor()一致,如果线程异常结束,会有另外一个线程取代它;

5.newWorkStealingPool()
jdk8中才引入的,内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序;

Fork/Join框架

把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架,充分使用计算机的计算能力;

Fork/Join框架采用Work-Stealing算法分配任务,该算法可以让某个线程从其他队列里窃取任务来执行,比如说当前有2个线程A和B,A的任务完成了,但是发现B的队列中还有好多任务待处理,那么A就会去把B的任务拿过来处理,为了获取任务的效率,采用了双端策略,也就是说被窃取的线程(B)从队列的头部获取任务,而窃取的线程(A)从队列的尾部获取任务,这样就大大提高了性能;

为什么要使用线程池

1.降低资源消耗,重复利用已创建的线程来降低创建和销毁线程所带来的消耗;
2.提高线程的可管理性,线程是一种稀缺资源,如果无限制的创建线程,会给服务器带来大量的消耗,使用线程池可以统一的分配,调优和监控;

Executor的框架

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框
架,目的是将任务如何提交,如何运行分离开来的机制;

J.U.C的三个Executor接口

1.Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;
2.ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善;
3.ScheduledExecutorService:支持Future和定期执行任务;

ThreadPoolExecutor的设计与实现

因为ThreadPoolExecutor是最基础的一种线程池,其他的线程池都是基于ThreadPoolExecutor上扩展的,所以这里直接介绍ThreadPoolExecutor就可以;

从下图可以清晰的了解到应用提交任务后,先是提交到WorkQueue队列中去等待,之后提交到内部线程池即工作线程集合,该集合管理线程的创建和销毁,当线程压力较大时,会新增线程,如果线程压力较小,则会闲置一段时间后,结束线程;ThreadFactory提供了线程池所需的创建逻辑;如果任务提交被拒绝,比如线程池出去shutdown状态,此时先来的线程需要有机制去处理,Java提供了许多的机制,如果需要自定义机制,则需实现RejectExecutorHandler即可;

ThreadPoolExecutor的构造函数

1.corePoolSize:核心线程数量;
2.maximumPoolSize:线程不够用时能够创建的最大线程数;
3.workQueue:任务等待队列;
4.keepAliveTime:闲置线程被销毁的时间;
5.threadFactory:创建新线程的工厂,默认是Executors.defaultThreadFactory();
6.handler:线程池的饱和策略,Java提供了4中策略,分别是:
(1).AbortPolicy:直接抛出异常,这是默认策略;
(2).CallerRunsPolicy:用调用者所在的线程来执行任务;
(3).DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
(5)实现RejectedExecutionHandler接口自定义handler;

新任务调剂execute执行后的判断逻辑

1.如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
2.如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
3.如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
4.如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;

以下是一个大致的逻辑图:

线程池的状态

1.RUNNING:能接收新提交的任务,并且也能处理阻塞队列中的任务;
2.SHUTDOWN:不能接受新提交的任务,但可以处理存量任务;
3.STOP:不再接收新提交的任务,也不处理存量任务;
4.TIDYING:所有的任务都已终止;
5.TERMINATED:terminated()方法执行完后进入该状态,什么不做的,只是一个标识;

下图为线程池的状态转换图:

下图为工作线程的生命周期逻辑图:

线程池的大小如何选定

1.CPU密集型:线程数=按照CPU核数或者核数+1设定
2.I/O密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)

Java多线程与并发-原理相关推荐

  1. java多线程与并发原理

    三.java多线程与并发原理 1.进程和线程的区别: 进程和线程的由来: (1)串行:初期的计算机只能串行执行任务,并且需要长时间等待用户输入: (2)批处理:预先将用户的指令集集中成清单,批量串行处 ...

  2. Java多线程与并发相关 — 原理

    Java多线程与并发相关 - 原理 一 synchronized同步 1. 线程安全问题的主要诱因? 存在共享资源(也称临界资源); 存在多条线程共同操作这些共享数据; 2. 解决办法. 同一时刻有且 ...

  3. Java多线程与并发系列从0到1全部合集,强烈建议收藏!

    在过去的时间中,我写过Java多线程与并发的整个系列. 为了方便大家的阅读,也为了让知识更系统化,这里我单独把Java多线程与并发的整个系列一并罗列于此,希望对有用的人有用,也希望能帮助到更多的人. ...

  4. java书籍_还搞不定Java多线程和并发编程面试题?你可能需要这一份书单!

    点击蓝色"程序员书单"关注我哟 加个"星标",每天带你读好书! ​ 在介绍本书单之前,我想先问一下各位读者,你们之前对于Java并发编程的了解有多少呢.经过了1 ...

  5. JAVA Java多线程与并发库

    Java多线程与并发库 同步方式 import javax.xml.stream.events.StartDocument;public class TestSynchronized {public ...

  6. JAVA多线程和并发面试问题

    转载自   JAVA多线程和并发面试问题 Java多线程面试问题 1.进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程 ...

  7. JAVA多线程和并发基础面试问答(转载)

    JAVA多线程和并发基础面试问答 原文链接:http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-ans ...

  8. Java多线程与并发库高级应用--18_传智播客_张孝祥_java5阻塞队列的应用

    Java多线程与并发库高级应用--18_传智播客_张孝祥_java5阻塞队列的应用 原创:徐工 2018-5- 5 17.10 package cn.itcast.heima2; import jav ...

  9. java多线程与并发_漫画 | Java多线程与并发(一)

    1.什么是线程? 2.线程和进程有什么区别? 3.如何在Java中实现线程? 4.Java关键字volatile与synchronized作用与区别? volatile修饰的变量不保留拷贝,直接访问主 ...

最新文章

  1. if for switch语句
  2. Eclipse常用功能键
  3. 外部编辑Infopath的表单模板(xsn)
  4. ITK:二进制图像的最小和最大曲率流
  5. html文本设置float,css怎么float(浮动)?
  6. mysql数据库老是被锁怎么解决_Mysql数据库全局锁是如何引起的,如何解决?
  7. ORACLE添加字段、删除字段
  8. c++输入错误重新输入_管家婆提示本单据的日期太大,请确认后重新输入
  9. 如何在Ubuntu 16.04中创建GIF动图
  10. RDIFramework.NET(.NET快速信息化系统开发框架) Web版介绍
  11. Spring Boot中的事务管理与手把手实战
  12. DXGI中的flip显示模型
  13. 更改访问局域网win7计算机的用户,win7局域网共享设置 win7局域网共享设置方法...
  14. Java 8 获取本(天周月季度年)的开始、结束时间
  15. 极客日报:腾讯下一步或减持美团和拼多多的股份;iPhone 13连续6周成中国最畅销智能手机;Linux 5.16 开发者统计
  16. 接入百家号流量的方法
  17. 心里的那块大石终于定下来了
  18. 【web前端】H5图片制作
  19. dorado获取用户登录信息
  20. 景安服务器密码修改,[景安网络]提示页

热门文章

  1. 电脑微信关闭自动保存_微信自动保存图片功能怎么取消掉 微信自动保存图片关闭设置方法...
  2. 如何正确地连接PLC与7种设备的输入输出线路
  3. Linux学习——awk
  4. 量子计算机 郭光灿,郭光灿(中国科学院院士、量子信息学家)_百度百科
  5. 大电流对电池电压的影响
  6. 旧电脑变废为宝成为nas
  7. 龙猫数据:服务AI产业,筑基智慧生态
  8. 【cocos2d-x 仙凡奇缘-网游研发(1) 登录注册】
  9. 轻量级秀恩爱网站源码 – 我们的小窝(星益云)
  10. 微信开发者工具 不想要警告提示怎么设置