关于并发编程:关于并发编程与线程安全的思考与实践-京东云技术团队

作者:京东衰弱 张娜 一、并发编程的意义与挑战并发编程的意义是充沛的利用处理器的每一个核,以达到最高的解决性能,能够让程序运行的更快。而处理器也为了进步计算速率,作出了一系列优化,比方: 1、硬件降级:为均衡CPU 内高速存储器和内存之间数量级的速率差,晋升整体性能,引入了多级高速缓存的传统硬件内存架构来解决,带来的问题是,数据同时存在于高速缓存和主内存中,须要解决缓存一致性问题。 2、处理器优化:次要蕴含,编译器重排序、指令级重排序、内存零碎重排序。通过单线程语义、指令级并行重叠执行、缓存区加载存储3种级别的重排序,缩小执行指令,从而进步整体运行速度。带来的问题是,多线程环境里,编译器和CPU指令无奈辨认多个线程之间存在的数据依赖性,影响程序执行后果。 并发编程的益处是微小的,然而要编写一个线程平安并且执行高效的代码,须要治理可变共享状态的操作拜访,思考内存一致性、处理器优化、指令重排序问题。比方咱们应用多线程对同一个对象的值进行操作时会呈现值被更改、值不同步的状况,失去的后果和理论值可能会天差地别,此时该对象就不是线程平安的。而当多个线程拜访某个数据时,不论运行时环境采纳何种调度形式或者这些线程如何交替执行,这个计算逻辑始终都体现出正确的行为,那么称这个对象是线程平安的。因而如何在并发编程中保障线程平安是一个容易疏忽的问题,也是一个不小的挑战。 所以,为什么会有线程平安的问题,首先要明确两个关键问题: 1、线程之间是如何通信的,即线程之间以何种机制来替换信息。 2、线程之间是如何同步的,即程序如何管制不同线程间的产生程序。 二、Java并发编程Java并发采纳了共享内存模型,Java线程之间的通信总是隐式进行的,整个通信过程对程序员齐全通明。 2.1 Java内存模型为了均衡程序员对内存可见性尽可能高(对编译器和解决的束缚就多)和进步计算性能(尽可能少束缚编译器处理器)之间的关系,JAVA定义了Java内存模型(Java Memory Model,JMM),约定只有不扭转程序执行后果,编译器和处理器怎么优化都行。所以,JMM次要解决的问题是,通过制订线程间通信标准,提供内存可见性保障。 JMM构造如下图所示: 以此看来,线程内创立的局部变量、办法定义参数等只在线程内应用不会有并发问题,对于共享变量,JMM规定了一个线程如何和何时能够看到由其余线程批改过后的共享变量的值,以及在必须时如何同步的访问共享变量。 为管制工作内存和主内存的交互,定义了以下标准: •所有的变量都存储在主内存(Main Memory)中。 •每个线程都有一个公有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝正本。 •线程对变量的所有操作都必须在本地内存中进行,而不能间接读写主内存。 •不同的线程之间无奈间接拜访对方本地内存中的变量。 具体实现上定义了八种操作: 1.lock:作用于主内存,把变量标识为线程独占状态。 2.unlock:作用于主内存,解除独占状态。 3.read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。 4.load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量正本中。 5.use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。 6.assign:作用工作内存,把一个从执行引擎接管到的值赋值给工作内存的变量。 7.store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。 8.write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。 这些操作都满足以下准则: •不容许read和load、store和write操作之一独自呈现。 •对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。 2.2 Java中的并发关键字Java基于以上规定提供了volatile、synchronized等关键字来保障线程平安,基本原理是从限度处理器优化和应用内存屏障两方面解决并发问题。如果是变量级别,应用volatile申明任何类型变量,同根本数据类型变量、援用类型变量一样具备原子性;如果利用场景须要一个更大范畴的原子性保障,须要应用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需要。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地应用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字。 这两个字的作用:volatile仅保障对单个volatile变量的读/写具备原子性,而锁的互斥执行的个性能够确保整个临界区代码的执行具备原子性。在性能上,锁比volatile更弱小,在可伸缩性和执行性能上,volatile更有劣势。 2.3 Java中的并发容器与工具类2.3.1 CopyOnWriteArrayListCopyOnWriteArrayList在操作元素时会加可重入锁,一次来保障写操作是线程平安的,然而每次增加删除元素就须要复制一份新数组,对空间有较大的节约。 public E get(int index) { return get(getArray(), index); } public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }2.3.2 Collections.synchronizedList(new ArrayList<>());这种形式是在 List的操作外包加了一层synchronize同步控制。须要留神的是在遍历List是还得再手动做整体的同步控制。 ...

May 9, 2023 · 3 min · jiezi

关于并发编程:并行获取机票信息高并发场景微服务实战七

你好,我是程序员Alan。 在《 需要剖析—高并发场景微服务实战(二)》一文的最初,我提了一个问题 “你会用什么形式获取和聚合机票信息?”,明天我会具体地解说解决这类问题的几种罕用办法。 问题回顾在开始解说问题的解决办法之前,咱们再来看一下问题的具体形容。搭建一个订票零碎常常会有这样的需要 ,那就是同时获取多家航空公司的航班信息。比方,从深圳到三亚的机票钱是多少?有很多家航空公司都有这样的航班信息,所以应该把所有航空公司的航班、票价等信息都获取到,而后再聚合。因为每个航空公司都有本人的服务器,所以须要别离去申请它们的服务器,如下图所示: 解决办法1. 串行 咱们想获取所有航空公司某个航班信息,要先去拜访东航,而后再去拜访南航,以此类推。每一个申请收回去之后,等它响应回来当前,咱们能力去申请下一个航空公司,这就是串行的形式。 这样做的效率十分低下,如果航空公司比拟多,假如每个航空公司都须要 1 秒钟的话,那么用户必定等不及,所以这种形式是不可取的。 2. 并行 既然串行的办法很慢,那么咱们能够并行地去获取这些机票信息,而后再把机票信息给聚合起来,这样的话,效率会成倍的进步。 这种并行尽管进步了效率,但也有一个毛病,那就是会“始终等到所有申请都返回”。如果有一个航空公司的响应特地慢,那么咱们的整个服务就会被连累。所以咱们须要再改良一下,减少超时获取的性能。 3. 有超时的并行获取 上图的这种状况,就属于有超时的并行获取,同样也在并行的去申请各个公司的机票信息。然而咱们规定了一个超时工夫,如果没能在指定工夫内响应信息,咱们就把这些申请给疏忽掉,这样用户体验就比拟好了,它最多只须要等固定的工夫就能取得机票信息,尽管拿到的信息可能是不全的,然而总比始终等更好。 实现这个指标有多种实现计划,咱们一个个的来看看。 3.1 线程池的实现第一个实现计划是用线程池,咱们来看一下代码。 /** * @author alan * @create 2022 - 10 - 05 15:17 */public class ThreadPoolDemo { ExecutorService threadPool = Executors.newFixedThreadPool(3); public static void main(String[] args) throws InterruptedException { ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo(); System.out.println(threadPoolDemo.getPrices()); } private Set<Integer> getPrices() throws InterruptedException { Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>()); threadPool.submit(new Task(1, prices)); threadPool.submit(new Task(2, prices)); threadPool.submit(new Task(3, prices)); Thread.sleep(3000); return prices; } private class Task implements Runnable { Integer productId; Set<Integer> prices; public Task(Integer productId, Set<Integer> prices) { this.productId = productId; this.prices = prices; } @Override public void run() { int price=0; try { Thread.sleep((long) (Math.random() * 6000)); price= productId; }catch (Exception e){ e.printStackTrace(); } prices.add(price); } }}在代码中,新建了一个线程平安的 Set,命名为Prices 用它来存储价格信息,而后往线程池中去放工作。线程池是在类的最开始时创立的,是一个固定 3 线程的线程池。 ...

November 1, 2022 · 3 min · jiezi

关于并发编程:JDK数组阻塞队列源码深入剖析

JDK数组阻塞队列源码深刻分析前言在后面一篇文章从零开始本人入手写阻塞队列当中咱们认真介绍了阻塞队列提供给咱们的性能,以及他的实现原理,并且基于谈到的内容咱们本人实现了一个低配版的数组阻塞队列。在这篇文章当中咱们将认真介绍JDK具体是如何实现数组阻塞队列的。 阻塞队列的性能而在本篇文章所谈到的阻塞队列当中,是在并发的状况下应用的,下面所谈到的是队列是并发不平安的,然而阻塞队列在并发下状况是平安的。阻塞队列的次要的需要如下: 队列根底的性能须要有,往队列当中放数据,从队列当中取数据。所有的队列操作都要是并发平安的。当队列满了之后再往队列当中放数据的时候,线程须要被挂起,当队列当中的数据被取出,让队列当中有空间的时候线程须要被唤醒。当队列空了之后再往队列当中取数据的时候,线程须要被挂起,当有线程往队列当中退出数据的时候被挂起的线程须要被唤醒。在咱们实现的队列当中咱们应用数组去存储数据,因而在构造函数当中须要提供数组的初始大小,设置用多大的数组。下面就是数组阻塞队列给咱们提供的最外围的性能,其中将线程挂起和唤醒就是阻塞队列的外围,挂起和唤醒体现了“阻塞”这一核心思想。 数组阻塞队列设计浏览这部分内容你须要相熟可重入锁ReentrantLock和条件变量Condition的应用。 数组的循环应用因为咱们是应用数组存储队列当中的数据,从下表为0的地位开始,当咱们往队列当中退出一些数据之后,队列的状况可能如下,其中head示意队头,tail示意队尾。 在上图的根底之上咱们再进行四次出队操作,后果如下: 在下面的状态下,咱们持续退出8个数据,那么布局状况如下: 咱们晓得上图在退出数据的时候不仅将数组后半局部的空间应用完了,而且能够持续应用前半部分没有应用过的空间,也就是说在队列外部实现了一个循环应用的过程。 字段设计在JDK当中数组阻塞队列的实现是ArrayBlockingQueue类,在他的外部是应用数组实现的,咱们当初来看一下它的次要的字段,为了不便浏览将所有的解释阐明都写在的正文当中: /** The queued items */ final Object[] items; // 这个就是具体存储数据的数组 /** items index for next take, poll, peek or remove */ int takeIndex; // 因为是队列 因而咱们须要晓得下一个出队的数据的下标 这个就是示意下一个将要出队的数据的下标 /** items index for next put, offer, or add */ int putIndex; // 咱们同时也须要下一个入队的数据的下标 /** Number of elements in the queue */ int count; // 统计队列当中一共有多少个数据 /* * Concurrency control uses the classic two-condition algorithm * found in any textbook. */ /** Main lock guarding all access */ final ReentrantLock lock; // 因为阻塞队列是一种能够并发应用的数据结构 /** Condition for waiting takes */ private final Condition notEmpty; // 这个条件变量次要用于唤醒被 take 函数阻塞的线程 也就是从队列当中取数据的线程 /** Condition for waiting puts */ private final Condition notFull; // 这个条件变量次要用于唤醒被 put 函数阻塞的线程 也就是从队列当中放数据的线程构造函数构造函数的次要性能是申请指定大小的内存空间,并且对类的成员变量进行赋值操作。 ...

August 14, 2022 · 3 min · jiezi

关于并发编程:并发程序的噩梦数据竞争

并发程序的噩梦——数据竞争前言在本文当中我次要通过不同线程对同一个数据进行加法操作的例子,层层递进,应用忙期待、synchronized和锁去解决咱们的问题,切实领会为什么数据竞争是并发程序的噩梦。 问题介绍在本文当中会有一个贯通全文的例子:不同的线程会对一个全局变量一直的进行加的操作!而后比拟后果,具体来说咱们设置一个动态类变量data,而后应用两个线程循环10万次对data进行加一操作!!!像这种多个线程会存在同时对同一个数据进行批改操作的景象就叫做数据竞争。数据竞争会给程序造成很多不可意料的后果,让程序存在许多破绽。而咱们下面的工作就是一个典型的数据竞争的问题。 并发不平安版本在这一大节咱们先写一个上述问题的并发不平安的版本: public class Sum { public static int data; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) data++; }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) data++; }); t1.start(); t2.start(); // 让主线程期待 t1 和 t2 // 直到 t1 和 t2 执行实现 t1.join(); t2.join(); System.out.println(data); }}// 输入后果131888下面两个线程执行的后果最终都会小于200000,为什么会呈现这种状况呢? ...

July 21, 2022 · 4 min · jiezi

关于并发编程:并发编程CAS与Volatile

第一、并发编程三大个性原子性:指一系列操作是不可分割的,一旦执行则整个过程将会一次性全副执行实现,不会停留在中间状态。(有点相似于事务的概念)。举例:例如A向B汇款1000元,那么就须要有两个操作,一个是A账户减1000元,另一个是B账户减少1000元,如果这个过程中任何一个操作呈现故障,都是不合乎规矩的也是不能保障汇款人和收款人的财产平安。换句话说,如果想要保障每次转账都不会造成单方任何一方的财产损失,咱们必须要保障操作的原子性。要么都做,要么都不做。 可见性:多个线程拜访同一共享数据的时候,如果某一个线程批改了此共享数据,那么其余线程可能立刻看到此数据的扭转。即批改可见。 有序性:代码执行时的程序与语句程序统一。也就是说执行前不重排。指令重排序不会影响单个线程的执行,然而会影响到线程并发执行的正确性 要想并发程序正确地执行,必须要保障原子性、可见性以及有序性。只有有一个没有被保障,就有可能会导致程序运行不正确。第二、CAS定义一个账户接口:Account .java import java.util.ArrayList;import java.util.List;/** * 定义一个账户接口 * @author shixiangcheng * 2019-12-17 */public interface Account { //获取余额 Integer getBalance(); //取款 void withdraw(Integer amount); //办法内启动1000个线程,每个线程做-10元的操作。若初始余额为10000,那么正确后果该当为0 static void demo(Account account) { List<Thread> ts=new ArrayList<Thread> (); long start=System.currentTimeMillis(); for(int i=0;i<1000;i++) { ts.add(new Thread(()->{ account.withdraw(10); })); } ts.forEach(Thread::start); ts.forEach(t->{ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end=System.currentTimeMillis(); System.out.println(account.getBalance()+" cost: "+(end-start)+"ms"); }}2.接口实现类AccountImpl.java import java.util.concurrent.atomic.AtomicInteger;/** * 接口实现类 * @author shixiangcheng * 2019-12-17 */public class AccountImpl implements Account { private AtomicInteger balance;//账户余额 public AccountImpl(int balance) {//通过构造方法给一个默认的余额 this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } @Override public void withdraw(Integer amount) { //一直尝试,直到胜利为止 while(true) { int prev=balance.get(); int next=prev-amount; /**比拟替换:在set前先比拟prev和以后值? * 不统一,next作废,cas返回false标识失败 * 统一,以next设置为新值,返回true标识胜利 */ if(balance.compareAndSet(prev, next)) { break; } } }}测试类Test.java ...

May 6, 2022 · 1 min · jiezi

关于并发编程:高并发深入解析Callable接口

大家好,我是冰河~~ 本文纯干货,从源码角度深刻解析Callable接口,心愿大家踏下心来,关上你的IDE,跟着文章看源码,置信你肯定播种不小。 1.Callable接口介绍Callable接口是JDK1.5新增的泛型接口,在JDK1.8中,被申明为函数式接口,如下所示。 @FunctionalInterfacepublic interface Callable<V> { V call() throws Exception;}在JDK 1.8中只申明有一个办法的接口为函数式接口,函数式接口能够应用@FunctionalInterface注解润饰,也能够不应用@FunctionalInterface注解润饰。只有一个接口中只蕴含有一个办法,那么,这个接口就是函数式接口。 在JDK中,实现Callable接口的子类如下图所示。 默认的子类层级关系图看不清,这里,能够通过IDEA右键Callable接口,抉择“Layout”来指定Callable接口的实现类图的不同构造,如下所示。 这里,能够抉择“Organic Layout”选项,抉择后的Callable接口的子类的构造如下图所示。 在实现Callable接口的子类中,有几个比拟重要的类,如下图所示。 别离是:Executors类中的动态外部类:PrivilegedCallable、PrivilegedCallableUsingCurrentClassLoader、RunnableAdapter和Task类下的TaskCallable。 2.实现Callable接口的重要类剖析接下来,剖析的类次要有:PrivilegedCallable、PrivilegedCallableUsingCurrentClassLoader、RunnableAdapter和Task类下的TaskCallable。尽管这些类在理论工作中很少被间接用到,然而作为一名合格的开发工程师,设置是秃顶的资深专家来说,理解并把握这些类的实现有助你进一步了解Callable接口,并进步专业技能(头发再掉一批,哇哈哈哈。。。)。 PrivilegedCallablePrivilegedCallable类是Callable接口的一个非凡实现类,它表明Callable对象有某种特权来拜访零碎的某种资源,PrivilegedCallable类的源代码如下所示。 /** * A callable that runs under established access control settings */static final class PrivilegedCallable<T> implements Callable<T> { private final Callable<T> task; private final AccessControlContext acc; PrivilegedCallable(Callable<T> task) { this.task = task; this.acc = AccessController.getContext(); } public T call() throws Exception { try { return AccessController.doPrivileged( new PrivilegedExceptionAction<T>() { public T run() throws Exception { return task.call(); } }, acc); } catch (PrivilegedActionException e) { throw e.getException(); } }}从PrivilegedCallable类的源代码来看,能够将PrivilegedCallable看成是对Callable接口的封装,并且这个类也继承了Callable接口。 ...

February 16, 2022 · 3 min · jiezi

关于并发编程:Java-并发编程AQS-的互斥锁与共享锁

咱们晓得古代机器处理器简直都是多核多线程的,引入多核多线程机制是为了尽可能晋升机器整体解决性能。然而多核多线程也会带来很多并发问题,其中很重要的一个问题是数据竞争,数据竞争即多个线程同时访问共享数据而导致了数据抵触(不正确)。数据竞争如果没解决好则意味着整个业务逻辑可能出错,所以在高并发环境中咱们要特地留神这点。 数据竞争产生的条件存在数据竞争的场景必须满足以下几个条件: 多个线程对某个共享数据进行拜访。这些线程同时地进行拜访。拜访即是读或写数据操作。至多有一个线程是执行写数据操作。数据竞争例子为更好了解数据竞争问题,上面咱们举一个数据竞争的例子。上面两张图,下面的是不存在数据竞争时正确的后果。刚开始内存中i=0,线程一读取后将i加5。批改完后线程二才读取内存中的i并将其加6,最终i=11。而上面的状况则不同,线程二在线程一还没批改完就读取内存中i,此时导致最终的后果为i=6。 同步与锁既然多个线程并发执行常常会波及数据竞争问题,那么咱们该如何解决这个问题呢?答案就是引入同步机制,通过同步机制来管制共享数据的拜访,就可能解决数据竞争问题。实现同步机制能够通过锁来实现,所以AQS框架也形象出了锁的获取操作和开释操作。而且还提供了包含独占锁和共享锁两种模式,这样对于下层的各种同步器的实现就不便很多了 独占锁独占锁是指该锁一次只能由一个线程持有,其它线程则无奈取得,除非已持有锁的线程开释了该锁。一个线程只有在胜利获取锁后能力持续往下执行,当来到竞争区域时则开释锁,开释的锁供其余行将进入数据竞争区域的线程获取。 获取独占锁和开释独占锁别离对应acquire办法和release办法。获取独占锁的次要逻辑为:先尝试获取锁,胜利则往下执行,否则把线程放到期待队列中并可能将线程挂起。开释独占锁的次要逻辑为:唤醒期待队列中一个或多个线程去尝试获取锁。在AQS中能够用以下伪代码示意独占锁的获取与开释 获取独占锁的伪代码 if(尝试获取锁失败){ 创立Node 应用CAS把Node增加到队列尾部 while(true){ if(尝试获取锁 && Node的前驱节点为头节点){ 把以后节点设置为头 跳出循环 }else{ 应用CAS形式批改Node前驱节点的waitStatus 为Singal if(批改胜利){ 挂起以后线程 } } }}开释独占锁的伪代码 if(尝试开释锁胜利){ 唤醒后续节点蕴含的线程}共享锁获取共享锁和开释共享锁别离对应acquireShared办法和releaseShared办法。获取共享锁的次要逻辑为:先尝试获取锁,胜利则往下执行,否则把线程放到期待队列中并可能将线程挂起。开释共享锁的次要逻辑为:唤醒期待队列中一个或多个线程去尝试获取锁。在AQS中能够用以下伪代码示意共享锁的获取与开释。 公众号:码农架构Java 并发编程Java并发编程:Java 序列化的工作机制Java并发编程:并发中死锁的造成条件及解决Java并发编程:过程、线程、并行与并发Java并发编程:工作执行器Executor接口Java 并发编程:AQS 的互斥锁与共享锁Java并发编程:什么是JDK内置并发框架AQSJava并发编程:AQS的原子性如何保障Java并发编程:如何避免在线程阻塞与唤醒时死锁Java并发编程:多线程如何实现阻塞与唤醒

December 22, 2020 · 1 min · jiezi

关于并发编程:JAVA并发编程核心方法与框架pdf

为什么要编写并发程序? 想要充分发挥多处理器零碎的弱小计算能力,最简略的形式就是应用线程。随着处理器数量的持续增长,如何高效地应用并发正变得越来越重要! Java并发编程无处不在,服务器、数据库、利用,Java并发是永远不可跳过的坎。 想要深刻学习java,就必须要把握并发编程,尤其是在进行大数据、分布式、高并发类的专项攻坚克难时,并发编程的学习必不可少。 但学习并发编程的过程并不轻松,常常会遇到很多的坑。最近也是很多小伙伴问我要一些 算法 相干的材料,于是我翻箱倒柜,找到了这本十分经典的电子书——《JAVA并发编程外围办法与框架》。 材料介绍 《JAVA并发编程外围办法与框架》根本齐全笼罩了Java并发包中外围类、API与并发框架,最大水平介绍了每个罕用类的应用,以案例的形式进行解说,以使读者疾速学习,迅速把握。全书尽量应用Demo式案例来解说技术点的实现,使读者看到代码及运行后果后就能够晓得此我的项目要解决的是什么问题。 如何获取? 1.辨认二维码并关注公众号「Java后端技术全栈」; 2.在公众号后盾回复关键字「926」。

December 20, 2020 · 1 min · jiezi

关于并发编程:Java-并发编程如何防止在线程阻塞与唤醒时死锁

Java并发编程:多线程如何实现阻塞与唤醒 说到suspend与resume组合有死锁偏向,一不小心将导致很多问题,甚至导致整个零碎解体。接着看另外一种解决方案,咱们能够应用以对象为指标的阻塞,即利用Object类的wait()和notify()办法实现线程阻塞。当线程达到监控对象时,通过wait办法会使线程进入到期待队列中。而当其它线程调用notify时则能够使线程从新回到执行队列中,得以继续执行  思维不同针对对象的阻塞编程思维须要咱们略微转变下思维,它与面向线程阻塞思维有较大差别。如后面的suspend与resume只需在线程内间接调用就能实现挂起复原操作,这个很好了解。而如果改用wait与notify模式则是通过一个object作为信号,能够将其看成是一堵门。object的wait()办法是锁门的动作,notify()是开门的动作。某一线程一旦关上门后其余线程都将阻塞,直到别的线程打开门。 如图所示,一个对象object调用wait()办法则像是堵了一扇门。线程一、线程二都将阻塞,而后线程三调用object的notify()办法打开门,精确地说是调用了notifyAll()办法,notify()仅仅能让线程一或线程二其中一条线程通过)。最终线程一、线程二得以通过。  死锁问题解决了吗?应用wait与notify能在肯定水平上防止死锁问题,但并不能完全避免,它要求咱们必须在编程过程中防止死锁。在应用过程中须要留神的几点是: 首先,wait与notify办法是针对对象的,调用任意对象的wait()办法都将导致线程阻塞,阻塞的同时也将开释该对象的锁。相应地,调用任意对象的notify()办法则将随机解除该对象阻塞的线程,但它须要从新获取改对象的锁,直到获取胜利能力往下执行。其次,wait与notify办法必须在synchronized块或办法中被调用,并且要保障同步块或办法的锁对象与调用wait与notify办法的对象是同一个。如此一来在调用wait之前以后线程就曾经胜利获取某对象的锁,执行wait阻塞后以后线程就将之前获取的对象锁开释。当然如果你不依照下面规定束缚编写,程序一样能通过编译,但运行时将抛出IllegalMonitorStateException异样,必须在编写时保障用法正确。最初,notify是随机唤醒一条阻塞中的线程并让之获取对象锁,进而往下执行,而notifyAll则是唤醒阻塞中的所有线程,让他们去竞争该对象锁,获取到锁的那条线程能力往下执行。 改良例子咱们通过wait与notify革新后面的例子,代码如下。革新的思维就是在MyThread中增加一个标识变量,一旦变量扭转就相应地调用wait和notify阻塞唤醒线程。因为在执行wait后将开释synchronized(this)锁住的对象锁,此时System.out.println("running….");早已执行结束,System类out对象不存在死锁问题。  Park与UnParkwait与notify组合的形式看起来是个不错的解决形式,但其面向的主体是对象object,阻塞的是以后线程,而唤醒的是随机的某个线程或所有线程,偏重于线程之间的通信交互。如果换个角度,面向的主体是线程的话,我就能轻而易举地对指定的线程进行阻塞唤醒,这个时候就须要LockSupport,它提供的park与unpark办法别离用于阻塞和唤醒.而且它提供防止死锁和竞态条件,很好地代替suspend和resume组合。 用park与unpark革新上述例子,代码如下。把主体换成线程进行的阻塞看起来貌似比拟悦目,而且因为park与unpark办法管制的颗粒度更加细小,能精确决定线程在某个点进行,进而防止死锁的产生。例如此例中在执行System.out.println火线程就被阻塞了,于是不存在因竞争System类out对象而产生死锁,即使在执行System.out.println后线程才阻塞也不存在死锁问题,因为锁已开释。  LockSupport 劣势LockSupport类为线程阻塞唤醒提供了根底,同时,在竞争条件问题上具备wait和notify无可比拟的劣势。应用wait和notify组合时,某一线程在被另一线程notify之前必须要保障此线程曾经执行到wait期待点,错过notify则可能永远都在期待,另外notify也不能保障唤醒指定的某线程。反观LockSupport,因为park与unpark引入了许可机制,许可逻辑为:   park将许可在等于0的时候阻塞,等于1的时候返回并将许可减为0。 unpark尝试唤醒线程,许可加1。依据这两个逻辑,对于同一条线程,park与unpark先后操作的程序仿佛并不影响程序正确地执行。如果先执行unpark操作,许可则为1,之后再执行park操作,此时因为许可等于1间接返回往下执行,并不执行阻塞操作。 最初,LockSupport的park与unpark组合真正解耦了线程之间的同步,不再须要另外的对象变量存储状态,并且也不须要思考同步锁,wait与notify要保障必须有锁能力执行,而且执行notify操作开释锁后还要将以后线程扔进该对象锁的期待队列,LockSupport则齐全不必思考对象、锁、期待队列等问题。 Java 并发编程Java并发编程:如何避免在线程阻塞与唤醒时死锁Java并发编程:多线程如何实现阻塞与唤醒Java并发编程:工作执行器Executor接口Java并发编程:并发中死锁的造成条件及解决Java并发编程:Java 序列化的工作机制Java并发编程:过程、线程、并行与并发

December 17, 2020 · 1 min · jiezi

关于并发编程:Java-并发编程并发中死锁的形成条件及处理

死锁是一种有限的相互期待的状态,两个或两个以上的线程或过程形成一个相互期待的环状。以两个线程为例,线程一持有A锁同时在期待B锁,而线程二持有B锁同时在期待A锁,这就导致两个线程相互期待无奈往下执行。现实生活中一个经典的死锁情景就是四辆汽车通过没有红绿灯的十字路口,如果四辆车同时达到核心的,那么它们将造成一个死锁状态。每辆车领有本人车道上的使用权,但同时也在等另外一辆汽车让出另外一条道的使用权 死锁的例子该例子中一共有lock1和lock2两个锁。线程一启动后先尝试获取lock1锁,胜利获取lock1后再持续尝试获取lock2锁。而线程二则是先尝试获取lock2锁,胜利获取lock2锁后再持续尝试获取lock1锁。当咱们某次启动程序后可能的输入状况如下,也就进入了死锁状态,但并非每次都肯定会进入死锁状态,每个线程睡眠100毫秒是为了减少死锁的可能。最终两个线程处于相互无线期待状态,取得lock1的线程一在等lock2,而取得lock2的锁却在等lock1。 死锁的解决因为死锁的检测波及到很多简单的场景,而且它还是运行时才会产生的,所以编程语言编译器个别也不会提供死锁的检测性能,包含Java也不提供死锁检测性能。这其实就叫做鸵鸟算法,对于某件事如果咱们没有很好的解决办法,那么就学鸵鸟一样把头埋入沙中伪装什么都看不见。死锁的场景解决就交给了理论编程的开发者,开发者须要本人去防止死锁的产生,或者制订某些措施去解决死锁产生时的场景。常见的死锁解决形式大抵分为两类:一种是事先的预防措施,包含锁的程序化、资源合并、防止锁嵌套等等。另一种是预先的解决措施,包含锁超时机制、抢占资源机制、撤销线程等等。上面咱们具体看看每种措施的状况。 锁的程序变动后面说到的死锁造成的条件中环形条件,咱们能够毁坏这个条件来防止死锁的产生。具体就是将锁的获取进行程序化,所有线程和过程对锁的获取都按指定的程序进行,比方下图中P1、P2、P3三个线程它们都先尝试持有R1锁,再尝试持有R2锁,最初尝试持有R3锁。当然也能够看成是要获取R3锁就必须先获取R2锁,而要获取R2锁就必须先获取R1锁。这样就能毁坏环形条件,从而防止死锁。 资源合并资源合并的做法就是将多个资源合并当成一个资源来对待,这样就能将对多个资源的获取变成只对一个资源的获取,从而防止了死锁的产生。如下图,将资源R1、资源R2和资源R3合并成一个资源R,而后三个线程对其进行获取操作。 防止锁嵌套锁获取操作的嵌套行为才可能导致死锁产生,所以咱们能够通过去除锁嵌套来防止死锁。每个线程都是应用完某个资源就开释,而后能力再获取另外一个资源,而且应用完又进行开释,这就是去除锁嵌套。如下图中线程P1持有R1锁后开释,而后持有R2锁后开释,最初持有R3锁并开释,其它线程也是相似地操作。 锁的超时机制预先解决的第一种措施是锁超时机制,外围就在于对锁的期待并非永恒的而是有超时的,某个线程对某个锁的期待如果超过了指定的工夫则做超时解决,间接完结掉该线程。比方下图中,三个线程曾经进入死锁状态了,如果线程P1期待R2锁的工夫超过了超时工夫,此时P1将完结并且开释对R1锁的占有权。这时线程P3则可能获取到R1锁,于是可能解除期待,自此解除了死锁状态。 总结本文次要介绍了死锁相干内容,除了介绍死锁概念外咱们还提供了死锁的例子,还有死锁造成的条件,以及死锁的解决形式。死锁的解决次要包含锁的程序化、资源合并、防止锁嵌套等事先预防措施和超时机制、抢占资源机制、撤销线程机制等事中的解决措施

December 13, 2020 · 1 min · jiezi

关于并发编程:并发AQS原理及应用

JUC包下有很多的工具类都是基于 AQS(AbstractQueuedSynchronizer) 实现. 故深刻理解这部分内容十分重要. 尽管从代码角度AQS只是一个模板类,但波及的概念和细节特地多,防止忘记,做个总结. 会继续补充AQS 实现原理AQS 是由一个双端链表形成, 每个节点(Node)蕴含指向前后两个节点的指针(pre,next),一个代表以后竞争的资源状态(state),一个代表期待队列外面节点的期待状态(waitStatus)对于期待状态的含意值,形容如下: waitStatus: CANCEL(1): 在队列中期待的线程被中断或勾销. 这类节点会被移除队列SIGNAL(-1): 示意以后节点的后继节点处于阻塞状态,须要被我唤醒CONDITION(-2): 示意自身再 期待队列 中, 期待一个condition, 当其余线程调用condition.signal 办法时,才会将这样的节点搁置在同步队列中PROPAGATE(-3): 共享模式下应用,示意在可运行状态.0: 初始化状态 重点剖析以下几个办法 public final void acquire(int arg) { //调用子类尝试获取资源 if (!tryAcquire(arg) && //没获取到的话,放入队列, 并且再队列里自旋获取锁资源(acquireQueued) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果期待途中被中断,则复原中断 selfInterrupt(); } //入期待队列 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { //调整新入队节点的前置指针 node.prev = pred; //调整尾指针指向新入队的节点, 并发故用cas if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //自旋入队 enq(node); return node; } **重要** /** * 在队列里自旋查看可能获取资源,然而也不是始终自旋, 如果线程很多,始终自旋会耗费cpu资源, 对于前置节点 的waitStatus是 Signal 的话,就意味着我须要parking(parking操作会将上下文由用户态转化为内核态,频繁park/unpark会增多上下文切换) */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //自旋 判断前驱节点是不是头结点, 即判断 是不是轮到我来竞争资源了 for (;;) { final Node p = node.predecessor(); //如果是并且胜利获取了资源, 调整指针, 设置队头是以后节点 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } //依据前置节点的waitStatus是不是 signal 判断是否须要park if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } //如果前置节点 waitStatus 是 signal 状态,则以后节点park 期待. // 否则向前查问,将以后节点排到最近的signal状态节点的前面 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ //设置之前失常的节点,状态为SIGNAL. compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } //从头结点开始查找下一个须要唤醒的就"非勾销" 节点 private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; // 第一次没找到的话, 从队尾开始找 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }AQS 实现原理再总结同步队列调用tryAcquire可重写办法来判断是否曾经获取竞争资源,如果没有获取,就将以后线程包装成节点入队列,而后再自旋获取资源.是否自旋取决于前置节点的waitStatus,如果前置节点的waitStatus的状态是signal,则代表,以后节点须要parking期待,parkding期待的目标是为了缩小cpu空转,但会减少线程上下文切换,因为parking的原理是将用户态数据转为内核态. 前面unpark的操作则是将线程状态数据由内核态转为用户态. 等到前置节点release开释掉竞争状态后,前面的自旋判断就会竞争获取状态反复以上过程 ...

December 11, 2020 · 5 min · jiezi

关于并发编程:一文让你彻底明白ThreadLocal

前言:ThreadLocal在JDK中是一个十分重要的工具类,通过浏览源码,能够在各大框架都能发现它的踪影。它最经典的利用就是 事务管理 ,同时它也是面试中的常客。明天就来聊聊这个ThreadLocal;本文主线: ①、ThreadLocal 介绍 ②、ThreadLocal 实现原理 ③、ThreadLocal 内存透露剖析 ④、ThreadLocal 利用场景及示例 注:本文源码基于 JDK1.8ThreadLocal 介绍:正如 JDK 正文中所说的那样: ThreadLocal 类提供线程局部变量,它通常是公有类中心愿将状态与线程关联的动态字段。简而言之,就是 ThreadLocal 提供了线程间数据隔离的性能,从它的命名上也能晓得这是属于一个线程的本地变量。也就是说,每个线程都会在 ThreadLocal 中保留一份该线程独有的数据,所以它是线程平安的。 相熟 Spring 的同学可能晓得 Bean 的作用域(Scope),而 ThreadLocal 的作用域就是线程。 上面通过一个简略示例来展现一下 ThreadLocal 的个性: public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); // 创立一个有2个外围线程数的线程池 ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10)); // 线程池提交一个工作,将工作序号及执行该工作的子线程的线程名放到 ThreadLocal 中 threadPool.execute(() -> threadLocal.set("工作1: " + Thread.currentThread().getName())); threadPool.execute(() -> threadLocal.set("工作2: " + Thread.currentThread().getName())); threadPool.execute(() -> threadLocal.set("工作3: " + Thread.currentThread().getName())); // 输入 ThreadLocal 中的内容 for (int i = 0; i < 10; i++) { threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get())); } // 线程池记得敞开 threadPool.shutdown();}下面代码首先创立了一个有2个外围线程数的一般线程池,随后提交一个工作,将工作序号及执行该工作的子线程的线程名放到 ThreadLocal 中,最初在一个 for 循环中输入线程池中各个线程存储在 ThreadLocal 中的值。这个程序的输入后果是: ...

December 2, 2020 · 8 min · jiezi

关于并发编程:Java并发编程并发操作原子类Atomic以及CAS的ABA问题

本文基于JDK1.8Atomic原子类原子类是具备原子操作特色的类。 原子类存在于java.util.concurrent.atmic包下。 依据操作的数据类型,原子类能够分为以下几类。 根本类型AtomicInteger:整型原子类AtomicLong:长整型原子类AtomicBoolean:布尔型原子类AtomicInteger的罕用办法public final int get() //获取以后的值public final int getAndSet(int newValue)//获取以后的值,并设置新的值public final int getAndIncrement()//获取以后的值,并自增public final int getAndDecrement() //获取以后的值,并自减public final int getAndAdd(int delta) //加上给定的值,并返回之前的值public final int addAndGet(int delta) //加上给定的值,并返回最终后果boolean compareAndSet(int expect, int update) //如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update)public final void lazySet(int newValue)//最终设置为newValue,应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。 AtomicInteger常见办法的应用@Testpublic void AtomicIntegerT() { AtomicInteger c = new AtomicInteger(); c.set(10); System.out.println("初始设置的值 ==>" + c.get()); int andAdd = c.getAndAdd(10); System.out.println("为原先的值加上10,并返回原先的值,原先的值是 ==> " + andAdd + "加上之后的值是 ==> " + c.get()); int finalVal = c.addAndGet(5); System.out.println("加上5, 之后的值是 ==> " + finalVal); int i = c.incrementAndGet(); System.out.println("++1,之后的值为 ==> " + i); int result = c.updateAndGet(e -> e + 3); System.out.println("能够应用函数式更新 + 3 计算后的后果为 ==> "+ result); int res = c.accumulateAndGet(10, (x, y) -> x + y); System.out.println("应用指定函数计算后的后果为 ==>" + res);}初始设置的值 ==>10为原先的值加上10,并返回原先的值,原先的值是 ==> 10 加上之后的值是 ==> 20加上5, 之后的值是 ==> 25++1,之后的值为 ==> 26能够应用函数式更新 + 3 计算后的后果为 ==> 29应用指定函数计算后的后果为 ==>39 AtomicInteger保障原子性咱们晓得,volatile能够保障可见性和有序性,然而不能保障原子性,因而,以下的代码在并发环境下的后果会不正确:最终的后果可能会小于10000。 ...

September 26, 2020 · 5 min · jiezi

关于并发编程:Java并发编程从CPU缓存模型到JMM来理解volatile关键字

并发编程三大个性原子性一个操作或者屡次操作,要么所有的操作全副都失去执行并且不会受到任何因素的烦扰而中断,要么所有的操作都执行,要么都不执行。 对于根本数据类型的拜访,读写都是原子性的【long和double可能例外】。 如果须要更大范畴的原子性保障,能够应用synchronized关键字满足。 可见性当一个变量对共享变量进行了批改,另外的线程都能立刻看到批改后的最新值。 volatile保障共享变量可见性,除此之外,synchronized和final都能够 实现可见性。 synchronized:对一个变量执行unclock之前,必须先把此变量同步回主内存中。 final:被final润饰的字段在结构器中一旦被初始化实现,并且结构器没有把this的援用传递进来,其余线程中就可能看见final字段的值。 有序性即程序执行的程序依照代码的先后顺序执行【因为指令重排序的存在,Java 在编译器以及运行期间对输出代码进行优化,代码的执行程序未必就是编写代码时候的程序】,volatile通过禁止指令重排序保障有序性,除此之外,synchronized关键字也能够保障有序性,由【一个变量在同一时刻只容许一条线程对其进行lock操作】这条规定取得。 CPU缓存模型是什么高速缓存为何呈现?计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必波及到数据的读取和写入。因为程序运行过程中的长期数据是寄存在主存(物理内存)当中的,这时就存在一个问题,因为CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因而如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。 为了解决CPU处理速度和内存不匹配的问题,CPU Cache呈现了。 图源:JavaGuide 缓存一致性问题当程序在运行过程中,会将运算须要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就能够间接从它的高速缓存读取数据和向其中写入数据,当运算完结之后,再将高速缓存中的数据刷新到主存当中。 在单线程中运行是没有任何问题的,然而在多线程环境下问题就会浮现。举个简略的例子,如上面这段代码: i = i + 1; 依照下面剖析,次要分为如下几步: 从主存读取i的值,复制一份到高速缓存中。CPU执行执行执行对i进行加1操作,将数据写入高速缓存。运算完结后,将高速缓存中的数据刷新到内存中。多线程环境下,可能呈现什么景象呢? 初始时,两个线程别离读取i的值,存入各自所在的CPU高速缓存中。线程T1进行加1操作,将i的最新值1写入内存。此时线程T2的高速缓存中i的值还是0,进行加1操作,并将i的最新值1写入内存。最终的后果i = 1而不是i = 2,得出结论:如果一个变量在多个CPU中都存在缓存(个别在多线程编程时才会呈现),那么就可能存在缓存不统一的问题。如何解决缓存不统一解决缓存不统一的问题,通常来说有如下两种解决方案【都是在硬件层面上提供的形式】: 通过在总线加LOCK#锁的形式 在晚期的CPU当中,是通过在总线上加LOCK#锁的模式来解决缓存不统一的问题。因为CPU和其余部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其余CPU对其余部件拜访(如内存),从而使得只能有一个CPU能应用这个变量的内存。比方下面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上收回了LCOK#锁的信号,那么只有期待这段代码齐全执行结束之后,其余CPU能力从变量i所在的内存读取变量,而后进行相应的操作。这样就解决了缓存不统一的问题。 但,有一个问题,在锁住总线期间,其余CPU无法访问内存,导致效率低下,于是就呈现了上面的缓存一致性协定。 通过缓存一致性协定 较驰名的就是Intel的MESI协定,MESI协定保S证了每个缓存中应用的共享变量的正本是统一的。 当CPU写数据时,如果发现操作的变量是共享变量,即在其余CPU中也存在该变量的正本,会发出信号告诉其余CPU将该变量的缓存行置为有效状态,因而当其余CPU须要读取这个变量时,发现自己缓存中缓存该变量的缓存行是有效的【嗅探机制:每个处理器通过嗅探在总线上流传的数据来查看本人的缓存的值是否过期】,那么它就会从内存从新读取。 基于MESI一致性协定,每个处理器须要一直从主内存嗅探和CAS一直循环,有效交互会导致总线带宽达到峰值,呈现总线风暴。图源:JavaFamily 敖丙三太子 JMM内存模型是什么JMM【Java Memory Model】:Java内存模型,是java虚拟机标准中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别,以实现让Java程序在各种平台下都能达到统一的内存拜访成果。 它形容了Java程序中各种变量【线程共享变量】的拜访规定,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。 留神,为了取得较好的执行性能,Java内存模型并没有限度执行引擎应用处理器的寄存器或者高速缓存来晋升指令执行速度,也没有限度编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。JMM的规定所有的共享变量都存储于主内存,这里所说的变量指的是【实例变量和类变量】,不蕴含局部变量,因为局部变量是线程公有的,因而不存在竞争问题。 每个线程都有本人的工作内存(相似于后面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能间接对主存进行操作。 每个线程不能拜访其余线程的工作内存。 Java对三大个性的保障原子性在Java中,对根本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 为了更好地了解下面这句话,能够看看上面这四个例子: x = 10; //1y = x; //2x ++; //3x = x + 1; //4 只有语句1是原子性操作:间接将数值10赋值给x,也就是说线程执行这个语句的会间接将数值10写入到工作内存中。语句2理论蕴含两个操作:先去读取x的值,再将x的值写入工作内存,尽管两步别离都是原子操作,然而合起来就不能算作原子操作了。语句3和4示意:先读取x的值,进行加1操作,写入新的值。须要留神的点: ...

September 21, 2020 · 1 min · jiezi