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

作者:京东衰弱 张娜 一、并发编程的意义与挑战并发编程的意义是充沛的利用处理器的每一个核,以达到最高的解决性能,能够让程序运行的更快。而处理器也为了进步计算速率,作出了一系列优化,比方: 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

并发编程之线程共享和协作一

更多Android架构进阶视频学习请点击:https://space.bilibili.com/47...本篇文章将从以下几个内容来阐述线程共享和协作: [基础概念之CPU核心数、线程数,时间片轮转机制解读][线程之间的共享][线程间的协作] 一、基础概念CPU核心数、线程数两者的关系:cpu的核心数与线程数是1:1的关系,例如一个8核的cpu,支持8个线程同时运行。但在intel引入超线程技术以后,cpu与线程数的关系就变成了1:2。此外在开发过程中并没感觉到线程的限制,那是因为cpu时间片轮转机制(RR调度)的算法的作用。什么是cpu时间片轮转机制看下面1.2. CPU时间片轮转机制含义就是:cpu给每个进程分配一个“时间段”,这个时间段就叫做这个进程的“时间片”,这个时间片就是这个进程允许运行的时间,如果当这个进程的时间片段结束,操作系统就会把分配给这个进程的cpu剥夺,分配给另外一个进程。如果进程在时间片还没结束的情况下阻塞了,或者说进程跑完了,cpu就会进行切换。cpu在两个进程之间的切换称为“上下文切换”,上下文切换是需要时间的,大约需要花费5000~20000(5毫秒到20毫秒,这个花费的时间是由操作系统决定)个时钟周期,尽管我们平时感觉不到。所以在开发过程中要注意上下文切换(两个进程之间的切换)对我们程序性能的影响。 二、 线程之间的共享synchronized内置锁线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。 Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。volatile 关键字volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 private volatile static boolean ready;private static int number;不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环,而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。但是volatile不能保证数据在多个线程下同时写时的线程安全,参见代码:thread-platformsrccomchjthreadcapt01volatilesNotSafe.javavolatile最适用的场景:一个线程写,多个线程读。线程私有变量 ThreadLocal + get() 获取每个线程自己的threadLocals中的本地变量副本。+ set() 设置每个线程自己的threadLocals中的线程本地变量副本。ThreadLocal有一个内部类ThreadLocalMap: public T get() { Thread t = Thread.currentThread(); //根据当前的线程返回一个ThreadLocalMap.点进去getMap ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } //点击去getMap(t)方法发现其实返回的是当前线程t的一个内部变量ThreadLocal.ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //由此可以知道,当调用ThreadLocal的get方法是,其实返回的是当前线程的threadLocals(类型是ThreadLocal.ThreadLocalMap)中的变量。调用set方法也类似。 //举例一个使用场景 /** * ThreadLocal使用场景:把数据库连接对象存放在ThreadLocal当中. * 优点:减少了每次获取Connection需要创建Connection * 缺点:因为每个线程本地会存放一份变量,需要考虑内存的消耗问题。 * @author luke Lin * */ public class ConnectionThreadLocal { private final static String DB_URL = "jdbc:mysql://localhost:3306:test"; private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ protected Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { e.printStackTrace(); } return null; }; }; /** * 获取连接 * @return */ public Connection getConnection(){ return connectionHolder.get(); } /** * 释放连接 */ public void releaseConnection(){ connectionHolder.remove(); } } //解决ThreadLocal中弱引用导致内存泄露的问题的建议 + 声明ThreadLoal时,使用private static修饰 + 线程中如果本地变量不再使用,即使使用remove()三、 线程间的协作wait() notify() notifyAll() ...

November 4, 2019 · 2 min · jiezi

并发编程系列之深入理解Synchronized

synchronized的底层是使用操作系统的mutex lock实现的 内存可见性:同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。 操作原子性:持有同一个锁的两个同步块只能串行地进入 锁的内存语义当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中当线程获取锁时,JMM会把线程对应的本地内存置为无效。从而使得监视器保护的临界区代码必须从主内存中读取共享变量锁释放和锁获取的内存语义线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息synchronized锁 synchronized锁的是对象的头。 JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。 根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经有用了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放 Mutex Lock 监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。 synchronized的使用场景分类具体分类被锁的对象伪代码方法实例方法类的实例对象

October 22, 2019 · 1 min · jiezi

Java程序员面试跳槽涨薪必备多线程和并发面个阿里P6轻轻松

一. ThreadLocal 的原理ThreadLocal 相当于一个容器, 用于存放每个线程的局部变量。 ThreadLocal 实例通常来说都是 private static 类型的。 ThreadLocal 可以 给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证 不同的线程都有一份拷贝。 一般情况下,通过 ThreadLocal.set() 到线程中的对象是该线程自己使用 的对象,其他线程是访问不到的,各个线程中访问的是不同的对象。如果 ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个 线程的 ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问 题。 向 ThreadLocal 中 set 的变量是由 Thread 线程对象自身保存的,当用户 调 用 ThreadLocal 对象的 set(Object o) 时 , 该方法则通过 Thread.currentThread() 获取当前线程, 将变量存入线程中的 ThreadLocalMap 类的对象内,Map 中元素的键为当前的 threadlocal 对象, 而值对应线程的变量副本。 public T get() { Thread t = Thread.currentThread(); //每个 Thread 对象内都保存一个 ThreadLocalMap 对象。 ThreadLocalMap map = getMap(t); //map 中 元 素 的 键 为 共 用 的 threadlocal 对象,而值为对应线程的变量副本。 if (map != null) return (T)map.get(this); } T value = initialValue(); createMap(t, value); return value;}public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}ThreadLocalMap getMap(Thread t) { return t.threadLocals;}void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}二. Collections.synchronizedXX 方法的原理。public E get(int index) { synchronized (mutex) { return list.get(index); }}public E set(int index, E element) { synchronized (mutex) { return list.set(index,element); }}public void add(int index, E element) { synchronized (mutex) { list.add(index, element); }}public E remove(int index) { synchronized (mutex) { return list.remove(index); }}在返回的列表上进行迭代时,用户必须手工在返回的列表上进行同步: ...

October 15, 2019 · 3 min · jiezi

面试必问的并发编程知识点你知道多少

前言众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java开发者是必须对并发编程这块有所了解的。而如何在并发场景中写出优良的代码,是一道绕不开的坎,也是考量一个 Java 开发者功底的关键技术。因此,不难发现 Java并发问题一直是各个大厂面试的重点之一,然而我发现很多候选人在面试时,常常表示对各种并发原理一脸懵逼,好像知道一些却又讲不清楚,最终导致面试失败。于是发奋学习,啃大部头书又发现理论太多,头疼。其实Java 的并发问题虽然内容繁杂,然而整个脉络还是很清晰的。那么接下来,一起来看看面试中会问到哪些并发的知识点。 线程池相关问题问题一:Java 中的线程池是如何实现的?问题二:创建线程池的几个核心构造参数?问题三:线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?问题四:既然提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同。问题五:如何在 Java 线程池中提交线程? 线程安全和线程同步器相关问题问题一:java如何实现多线程之间的通讯和协作?问题二:什么叫线程安全?servlet是线程安全吗?问题三:同步有几种实现方法?问题四:volatile有什么用?能否用一句话说明下volatile的应用场景?问题五:请说明下java的内存模型及其工作流程。问题六:为什么代码会重排序?问题七:分析下JUC 中倒数计数器 CountDownLatch 的使用与原理?问题八:CountDownLatch 与线程的 Join 方法区别是什么?问题九:讲讲对JUC 中回环屏障 CyclicBarrier 的使用?问题十:CyclicBarrier内部的实现与 CountDownLatch 有何不同?问题十一:Semaphore 的内部实现是怎样的?问题十二:简单对比同步器实现,谈谈你的看法?问题十三:并发组件CopyOnWriteArrayList 是如何通过写时拷贝实现并发安全的 List? 内存模型相关问题问题一:什么是 Java 的内存模型,Java 中各个线程是怎么彼此看到对方的变量的?问题二:请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?问题三:既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?问题四:请对比下 volatile 对比 Synchronized 的异同。问题五:请谈谈 ThreadLocal 是怎么解决并发安全的?问题六:很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么? 锁相关问题问题一:什么是可重入锁、乐观锁、悲观锁、公平锁、非公平锁、独占锁、共享锁?问题二:当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?问题三:synchronized和java.util.concurrent.locks.Lock的异同?问题四:什么是锁消除和锁粗化?问题五:乐观锁和悲观锁的理解及如何实现,有哪些实现方式?问题六:如何实现乐观锁(CAS)?如何避免ABA问题?问题七:读写锁可以用于什么应用场景?问题八:什么是可重入性,为什么说 Synchronized 是可重入锁?问题九:什么时候应该使用可重入锁?问题十:什么场景下可以使用volatile替换synchronized? 并发框架和并发队列相关问题问题一:SynchronizedMap和ConcurrentHashMap有什么区别?问题二:CopyOnWriteArrayList可以用于什么应用场景?问题三:如何让一段程序并发的执行,并最终汇总结果?问题四:任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?问题五:如何使用阻塞队列实现一个生产者和消费者模型?问题六:多读少写的场景应该使用哪个并发容器,为什么使用它?问题七:谈下对基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理的理解?问题八:ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程安全?问题九:基于链表的阻塞队列 LinkedBlockingQueue 原理。问题十:阻塞队列LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线程先入队出队操作的线程安全?问题十一:为什么不使用一把锁,使用两把为何能提高并发度?问题十二:基于数组的阻塞队列 ArrayBlockingQueue 原理。问题十三:ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全?问题十四:谈谈对无界优先级队列 PriorityBlockingQueue 原理?问题十五:PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? CountDownLatch相关问题问题一:介绍一下 CountDownLatch 工作原理?问题二:CountDownLatch 和 CyclicBarrier 的区别?问题三:CountDownLatch 的使用场景?问题四:CountDownLatch 类中主要的方法? ...

September 6, 2019 · 1 min · jiezi

Go-译文之竞态检测器-race

作者:Dmitry Vyukov,Andrew Gerrand | Introducing the Go Race Detector 译者前言第三篇 Go 官方博客译文,主要是关于 Go 内置的竞态条件检测工具。它可以有效地帮助我们检测并发程序的正确性。使用非常简单,只需在 go 命令加上 -race 选项即可。 本文最后介绍了两个真实场景下的竞态案例,第一个案例相对比较简单。重点在于第二个案例,这个案例比较难以理解,在原文的基础上,我也简单做了些补充,不知道是否把问题讲的足够清楚。同时,这个案例也告诉我们,任何时候我们都需要重视检测器给我们的提示,因为一不小心,你就可能为自己留下一个大坑。 概要在程序世界中,竞态条件是一种潜伏深且很难发现的错误,如果将这样的代码部署线上,常会产生各种谜一般的结果。Go 对并发的支持让我们能非常简单就写出支持并发的代码,但它并不能阻止竞态条件的发生。 本文将会介绍一个工具帮助我们实现它。 Go 1.1 加入了一个新的工具,竞态检测器,它可用于检测 Go 程序中的竞态条件。当前,运行在 x86_64 处理器的 Linux、Mac 或 Windows 下可用。 竞态检测器的实现基于 C/C++ 的 ThreadSanitizer 运行时库,ThreadSanitier 在 Googgle 已经被用在一些内部基础库以及 Chromium上,并且帮助发现了很多有问题的代码。 ThreadSanitier 这项技术在 2012 年 9 月被集成到了 Go 上,它帮助检测出了标准库中的 42 个竞态问题。它现在已经是 Go 构建流程中的一部分,当竞态条件出现,将会被它捕获。 如何工作竞态检测器集成在 Go 工具链,当命令行设置了 -race 标志,编译器将会通过代码记录所有的内存访问,何时以及如何被访问,运行时库也会负责监视共享变量的非同步访问。当检测到竞态行为,警告信息会把打印出来。(具体详情阅读 文章) 这样的设计导致竞态检测只能在运行时触发,这也意味着,真实环境下运行 race-enabled 的程序就变得非常重要,但 race-enabled 程序耗费的 CPU 和内存通常是正常程序的十倍,在真实环境下一直启用竞态检测是非常不切合实际的。 ...

August 18, 2019 · 3 min · jiezi

Java并发24并发设计模式-生产者消费者模式并发提高效率

生产者 - 消费者模式在编程领域的应用非常广泛,前面我们曾经提到,Java 线程池本质上就是用生产者 - 消费者模式实现的,所以每当使用线程池的时候,其实就是在应用生产者 - 消费者模式。 当然,除了在线程池中的应用,为了提升性能,并发编程领域很多地方也都用到了生产者 - 消费者模式,例如 Log4j2 中异步 Appender 内部也用到了生产者 - 消费者模式。所以我们就来深入地聊聊生产者 - 消费者模式,看看它具体有哪些优点,以及如何提升系统的性能。 生产者 - 消费者模式的优点生产者 - 消费者模式的核心是一个任务队列,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行。下面是生产者 - 消费者模式的一个示意图,你可以结合它来理解。 生产者 - 消费者模式示意图#### 从架构设计的角度来看,生产者 - 消费者模式有一个很重要的优点,就是解耦。解耦对于大型系统的设计非常重要,而解耦的一个关键就是组件之间的依赖关系和通信方式必须受限。在生产者 - 消费者模式中,生产者和消费者没有任何依赖关系,它们彼此之间的通信只能通过任务队列,所以生产者 - 消费者模式是一个不错的解耦方案 除了架构设计上的优点之外,生产者 - 消费者模式还有一个重要的优点就是支持异步,并且能够平衡生产者和消费者的速度差异。在生产者 - 消费者模式中,生产者线程只需要将任务添加到任务队列而无需等待任务被消费者线程执行完,也就是说任务的生产和消费是异步的,这是与传统的方法之间调用的本质区别。 异步化处理最简单的方式就是创建一个新的线程去处理,那中间增加一个任务队列”究竟有什么用呢?主要还是用于平衡生产者和消费者的速度差异。我们假设生产者的速率很慢,而消费者的速率很高,比如是 1:3,如果生产者有 3 个线程,采用创建新的线程的方式,那么会创建 3 个子线程,而采用生产者 - 消费者模式,消费线程只需要 1 个就可以了。 Java 语言里,Java 线程和操作系统线程是一一对应的,线程创建得太多,会增加上下文切换的成本,所以 Java 线程不是越多越好,适量即可。 支持批量执行以提升性能在两阶段终止模式:优雅地终止线程中,我们提到一个监控系统动态采集的案例,其实最终回传的监控数据还是要存入数据库的(如下图)。但被监控系统往往有很多,如果每一条回传数据都直接 INSERT 到数据库,那DB压力就非常大了。很显然,更好的方案是批量执行 SQL,那如何实现呢?这就要用到生产者 - 消费者模式了。 动态采集功能示意图 #### 利用生产者 - 消费者模式实现批量执行 SQL 非常简单:将原来直接 INSERT 数据到数据库的线程作为生产者线程,生产者线程只需将数据添加到任务队列,然后消费者线程负责将任务从任务队列中批量取出并批量执行。 ...

July 15, 2019 · 2 min · jiezi

从入门到放弃Java并发编程线程安全

概述并发编程,即多条线程在同一时间段内“同时”运行。 在多处理器系统已经普及的今天,多线程能发挥出其优势,如:一个8核cpu的服务器,如果只使用单线程的话,将有7个处理器被闲置,只能发挥出服务器八分之一的能力(忽略其它资源占用情况)。同时,使用多线程,可以简化我们对复杂任务的处理逻辑,降低业务模型的复杂程度。 因此并发编程对于提高服务器的资源利用率、提高系统吞吐量、降低编码难度等方面起着至关重要的作用。 以上是并发编程的优点,但是它同样引入了一个很重要的问题:线程安全。 什么是线程安全问题线程在并发执行时,因为cpu的调度等原因,线程会交替执行。如下图例子所示 public class SelfIncremental { private static int count; public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i< 10000; i++) { count++; System.out.println(count); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i< 10000; i++) { count++; System.out.println(count); } }); thread1.start(); thread2.start(); }}执行完毕后count的值并不是每次都能等于20000,会出现小于20000的情况,原因是thread1和thread2可能会交替执行。 如图所示: t1时刻: thread1 读取到count=100t2时刻: thread2 读取到count=100t3时刻: thread1 对count+1t4时刻: thread2 对count+1t5时刻: thread1 将101写入countt5时刻: thread2 将101写入count因为count++ 不是一个原子操作,实际上会执行三步: ...

July 15, 2019 · 1 min · jiezi

Java并发21并发设计模式-Balking模式线程安全的单例模式

上一篇文章中,我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式,不同于单线程中的 if,这个“多线程版本的 if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。 需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了,很显然 AutoSaveEditor 这个类不是线程安全的,因为对共享变量 changed 的读写没有使用同步,那如何保证 AutoSaveEditor 的线程安全性呢? class AutoSaveEditor{ // 文件是否被修改过 boolean changed=false; // 定时任务线程池 ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); // 定时执行自动保存 void startAutoSave(){ ses.scheduleWithFixedDelay(()->{ autoSave(); }, 5, 5, TimeUnit.SECONDS); } // 自动存盘操作 void autoSave(){ if (!changed) { return; } changed = false; // 执行存盘操作 // 省略且实现 this.execSave(); } // 编辑操作 void edit(){ // 省略编辑逻辑 ...... changed = true; }}读写共享变量 changed 的方法 autoSave() 和 edit() 都加互斥锁就可以了。这样做虽然简单,但是性能很差,原因是锁的范围太大了。那我们可以将锁的范围缩小,只在读写共享变量 changed 的地方加锁,实现代码如下所示。 ...

July 12, 2019 · 2 min · jiezi

Java并发20并发设计模式-Guarded-Suspension模式等待唤醒机制的规范实现

在开发中我们或许回遇到这样的情况:有一个Web 版的文件浏览器,通过它用户可以在浏览器里查看服务器上的目录和文件。这个项目依赖运维部门提供的文件浏览服务,而这个文件浏览服务只支持消息队列(MQ)方式接入。消息队列在互联网大厂中用的非常多,主要用作流量削峰和系统解耦。在这种接入方式中,发送消息和消费结果这两个操作之间是异步的,你可以参考下面的示意图来理解。 消息队列(MQ)示意图 在这个 Web 项目中,用户通过浏览器发过来一个请求,会被转换成一个异步消息发送给 MQ,等 MQ 返回结果后,再将这个结果返回至浏览器。问题来了:给 MQ 发送消息的线程是处理 Web 请求的线程 T1,但消费 MQ 结果的线程并不是线程 T1,那线程 T1 如何等待 MQ 的返回结果呢?示例代码如下。 class Message{ String id; String content;}// 该方法可以发送消息void send(Message msg){ // 省略相关代码}//MQ 消息返回后会调用该方法// 该方法的执行线程不同于// 发送消息的线程void onMessage(Message msg){ // 省略相关代码}// 处理浏览器发来的请求Respond handleWebReq(){ // 创建一消息 Message msg1 = new Message("1","{...}"); // 发送消息 send(msg1); // 如何等待 MQ 返回的消息呢? String result = ...;}Guarded Suspension 模式上面遇到的问题,在现实世界里比比皆是,只是我们一不小心就忽略了。比如,项目组团建要外出聚餐,我们提前预订了一个包间,然后兴冲冲地奔过去,到那儿后大堂经理看了一眼包间,发现服务员正在收拾,就会告诉我们:“您预订的包间服务员正在收拾,请您稍等片刻。”过了一会,大堂经理发现包间已经收拾完了,于是马上带我们去包间就餐。 我们等待包间收拾完的这个过程和前面的等待 MQ 返回消息本质上是一样的,都是等待一个条件满足:就餐需要等待包间收拾完,程序里要等待 MQ 返回消息。 ...

July 9, 2019 · 3 min · jiezi

Java并发19并发设计模式-ThreadLocal-线程本地存储模式

我们曾经重复说到,多个线程同时读写同一共享变量存在并发问题。前面两篇文章我们突破的是写,没有写操作自然没有并发问题了。其实还可以突破共享变量,没有共享变量也不会有并发问题。 那如何避免共享呢?思路其实很简单,并发编程领域,就是每个线程都拥有自己的变量,彼此之间不共享,也就没有并发问题了。 我们知道局部变量可以做到避免共享, 即线程封闭,其本质上就是避免共享。那还有没有其他方法可以做到呢?有的Java 语言提供的线程本地存储(ThreadLocal)就能够做到 ThreadLocal 的使用方法下面这个静态类 ThreadId 会为每个线程分配一个唯一的线程 Id,如果一个线程前后两次调用 ThreadId 的 get() 方法,两次 get() 方法的返回值是相同的。但如果是两个线程分别调用 ThreadId 的 get() 方法,那么两个线程看到的 get() 方法的返回值是不同的。若你是初次接触 ThreadLocal,可能会觉得奇怪,为什么相同线程调用 get() 方法结果就相同,而不同线程调用 get() 方法结果就不同呢? static class ThreadId { static final AtomicLong nextId=new AtomicLong(0); // 定义 ThreadLocal 变量 static final ThreadLocal<Long> tl=ThreadLocal.withInitial( ()->nextId.getAndIncrement()); // 此方法会为每个线程分配一个唯一的 Id static long get(){ return tl.get(); }}在详细解释 ThreadLocal 的工作原理之前,我们再看一个实际工作中可能遇到的例子来加深一下对 ThreadLocal 的理解。你可能知道 SimpleDateFormat 不是线程安全的,那如果需要在并发场景下使用它。 其实有一个办法就是用 ThreadLocal 来解决,下面的示例代码就是 ThreadLocal 解决方案的具体实现,这段代码与前面 ThreadId 的代码高度相似,同样地,不同线程调用 SafeDateFormat 的 get() 方法将返回不同的 SimpleDateFormat 对象实例,由于不同线程并不共享 SimpleDateFormat,所以就像局部变量一样,是线程安全的。 ...

July 9, 2019 · 2 min · jiezi

从入门到放弃Java并发编程NIOBuffer

前言上篇【从入门到放弃-Java】并发编程-NIO-Channel中我们学习到channel是双向通道,数据通过channel在实体(文件、socket)和缓冲区(buffer)中可以双向传输。 本文我们就来学习下buffer 简介buffer即缓冲区,实际上是一块内存,可以用来写入、读取数据。是一个线性的、大小有限的、顺序承载基础数据类型的内存块。 buffer有三个重要的属性: capacity:缓冲池大小,是不可变的。当buffer写满时,需要先清空才能继续写入。limit:是buffer中不可以被读或者写的第一个元素的位置,limit的大小永远不会超过capacity(在写模式下,limit等于capacity)position:是buffer中可以被读或者写的第一个元素的位置,position的大小永远不会超过limit除了boolean外,每一个基础数据类型都有对应的buffer。如:ByteBuffer、CharBuffer、LongBuffer等 buffer不是线程安全的,如果要在多线程中使用 需要加锁控制 接下来以ByteBuffer为例开始学习。 ByteBufferallocateDirectpublic static ByteBuffer allocateDirect(int capacity) { //会创建一个容量大小为capacity的DirectByteBuffer(ByteBuffer的子类) return new DirectByteBuffer(capacity);}allocatepublic static ByteBuffer allocate(int capacity) { if (capacity < 0) throw createCapacityException(capacity); //会创建一个容量大小为capacity的HeapByteBuffer(ByteBuffer的子类) return new HeapByteBuffer(capacity, capacity);}HeapByteBuffer和DirectByteBuffer的区别: DirectByteBuffer是直接调用native方法在本机os::malloc()创建堆外内存;HeapByteBuffer是直接在jvm的堆中分配内存。当buffer中的数据和磁盘、网络等的交互都在操作系统的内核中发生时,使用DirectByteBuffer能避免从内核态->用户态->内核态的切换开销,所有的处理都在内核中进行,性能会比较好当频繁创建操作数据量比较小的buffer时,使用HeapByteBuffer在jvm堆中分配内存能抵消掉使用DirectByteBuffer带来的好处。wrappublic static ByteBuffer wrap(byte[] array, int offset, int length){ try { return new HeapByteBuffer(array, offset, length); } catch (IllegalArgumentException x) { throw new IndexOutOfBoundsException(); }}public static ByteBuffer wrap(byte[] array) { return wrap(array, 0, array.length); }将byte数组包装成一个ByteBuffer ...

July 8, 2019 · 2 min · jiezi

从入门到放弃Java并发编程NIOChannel

前言上篇[【从入门到放弃-Java】并发编程-NIO使用]()简单介绍了nio的基础使用,本篇将深入源码分析nio中channel的实现。 简介channel即通道,可以用来读、写数据,它是全双工的可以同时用来读写操作。这也是它与stream流的最大区别。 channel需要与buffer配合使用,channel通道的一端是buffer,一端是数据源实体,如文件、socket等。在nio中,通过channel的不同实现来处理 不同实体与数据buffer中的数据传输。 channel接口: package java.nio.channels;import java.io.IOException;import java.io.Closeable;/** * A nexus for I/O operations. * * <p> A channel represents an open connection to an entity such as a hardware * device, a file, a network socket, or a program component that is capable of * performing one or more distinct I/O operations, for example reading or * writing. * * <p> A channel is either open or closed. A channel is open upon creation, * and once closed it remains closed. Once a channel is closed, any attempt to * invoke an I/O operation upon it will cause a {@link ClosedChannelException} * to be thrown. Whether or not a channel is open may be tested by invoking * its {@link #isOpen isOpen} method. * * <p> Channels are, in general, intended to be safe for multithreaded access * as described in the specifications of the interfaces and classes that extend * and implement this interface. * * * @author Mark Reinhold * @author JSR-51 Expert Group * @since 1.4 */public interface Channel extends Closeable { /** * Tells whether or not this channel is open. * * @return <tt>true</tt> if, and only if, this channel is open */ public boolean isOpen(); /** * Closes this channel. * * <p> After a channel is closed, any further attempt to invoke I/O * operations upon it will cause a {@link ClosedChannelException} to be * thrown. * * <p> If this channel is already closed then invoking this method has no * effect. * * <p> This method may be invoked at any time. If some other thread has * already invoked it, however, then another invocation will block until * the first invocation is complete, after which it will return without * effect. </p> * * @throws IOException If an I/O error occurs */ public void close() throws IOException;}常见的channel实现有: ...

July 8, 2019 · 12 min · jiezi

Java并发18并发设计模式-COW模式CopyonWrite模式的应用领域

在上一篇文章中我们讲到 Java 里 String 这个类在实现 replace() 方法的时候,并没有更改原字符串里面 value[] 数组的内容,而是创建了一个新字符串,这种方法在解决不可变对象的修改问题时经常用到。如果你深入地思考这个方法,你会发现它本质上是一种Copy-on-Write 方法。所谓 Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是写时复制。 不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的,当然 Copy-on-Write 的应用领域并不局限于 Immutability 模式。下面我们先简单介绍一下 Copy-on-Write 的应用领域,让你对它有个更全面的认识。 Copy-on-Write 模式的应用领域我们知道 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。 除了上面我们说的 Java 领域,很多其他领域也都能看到 Copy-on-Write 的身影:Docker 容器镜像的设计是 Copy-on-Write,甚至分布式源码管理系统 Git 背后的设计思想都有 Copy-on-Write。 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。 一个真实案例RPC 框架中有个基本的核心功能就是负载均衡。服务提供方是多实例分布式部署的,所以服务的客户端在调用 RPC 的时候,会选定一个服务实例来调用,这个选定的过程本质上就是在做负载均衡,而做负载均衡的前提是客户端要有全部的路由信息。 例如在下图中,A 服务的提供方有 3 个实例,分别是 192.168.1.1、192.168.1.2 和 192.168.1.3,客户端在调用目标服务 A 前,首先需要做的是负载均衡,也就是从这 3 个实例中选出 1 个来,然后再通过 RPC 把请求发送选中的目标实例。 ...

July 4, 2019 · 2 min · jiezi

Java并发17并发设计模式-Immutability模式如何利用不变性解决并发问题

解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。 快速实现具备不可变性的类实现一个具备不可变性的类,还是挺简单的。将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。 Java SDK 里很多类都具备不可变性,只是由于它们的使用太简单,最后反而被忽略了。例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的 我们结合 String 的源代码来解释一下这个问题,下面的示例代码源自 Java 1.8 SDK,我略做了修改,仅保留了关键属性 value[] 和 replace() 方法,你会发现:String 这个类以及它的属性 value[] 都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串作为返回值返回了。 public final class String { private final char value[]; // 字符替换 String replace(char oldChar, char newChar) { // 无需替换,直接返回 this if (oldChar == newChar){ return this; } int len = value.length; int i = -1; /* avoid getfield opcode */ char[] val = value; // 定位到需要替换的字符位置 while (++i < len) { if (val[i] == oldChar) { break; } } // 未找到 oldChar,无需替换 if (i >= len) { return this; } // 创建一个 buf[],这是关键 // 用来保存替换后的字符串 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } // 创建一个新的字符串返回 // 原字符串不会发生任何变化 return new String(buf, true); }}通过分析 String 的实现,你可能已经发现了,如果具备不可变性的类,需要提供类似修改的功能,具体该怎么操作呢?做法很简单,那就是创建一个新的不可变对象,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。 ...

July 4, 2019 · 2 min · jiezi

Golang-并发编程与同步原语

浅谈 Go 语言实现原理原文链接:https://draveness.me/golang/c... 当提到并发编程、多线程编程时,我们往往都离不开『锁』这一概念,Go 语言作为一个原生支持用户态进程 Goroutine 的语言,也一定会为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goroutine 在访问同一片内存时不会出现混乱的问题,锁其实是一种并发编程中的同步原语(Synchronization Primitives)。 在这一节中我们就会介绍 Go 语言中常见的同步原语 Mutex、RWMutex、WaitGroup、Once 和 Cond 以及扩展原语 ErrGroup、Semaphore和 SingleFlight 的实现原理,同时也会涉及互斥锁、信号量等并发编程中的常见概念。 基本原语Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的互斥锁 Mutex 与读写互斥锁 RWMutex 以及 Once、WaitGroup。 这些基本原语的主要作用是提供较为基础的同步功能,我们应该使用 Channel 和通信来实现更加高级的同步机制,我们在这一节中并不会介绍标准库中全部的原语,而是会介绍其中比较常见的 Mutex、RWMutex、Once、WaitGroup 和 Cond,我们并不会涉及剩下两个用于存取数据的结构体 Map 和 Pool。 MutexGo 语言中的互斥锁在 sync 包中,它由两个字段 state 和 sema 组成,state 表示当前互斥锁的状态,而 sema 真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。 type Mutex struct { state int32 sema uint32}状态互斥锁的状态是用 int32 来表示的,但是锁的状态并不是互斥的,它的最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置都用来表示当前有多少个 Goroutine 等待互斥锁被释放: ...

July 4, 2019 · 13 min · jiezi

Java并发16-CompletionService批量执行异步任务

我们思考下这个场景:从三个电商询价,然后保存在自己的数据库里。通过之前所学,我们可能这么实现。 // 创建线程池ExecutorService executor = Executors.newFixedThreadPool(3);// 异步向电商 S1 询价Future<Integer> f1 = executor.submit( ()->getPriceByS1());// 异步向电商 S2 询价Future<Integer> f2 = executor.submit( ()->getPriceByS2());// 异步向电商 S3 询价Future<Integer> f3 = executor.submit( ()->getPriceByS3()); // 获取电商 S1 报价并保存r=f1.get();executor.execute(()->save(r)); // 获取电商 S2 报价并保存r=f2.get();executor.execute(()->save(r)); // 获取电商 S3 报价并保存 r=f3.get();executor.execute(()->save(r));上面的这个方案本身没有太大问题,但是有个地方的处理需要你注意,那就是如果获取电商 S1 报价的耗时很长,那么即便获取电商 S2 报价的耗时很短,也无法让保存 S2 报价的操作先执行,因为这个主线程都阻塞在了 f1.get(),那我们如何解决了? 我们可以增加一个阻塞队列,获取到 S1、S2、S3 的报价都进入阻塞队列,然后在主线程中消费阻塞队列,这样就能保证先获取到的报价先保存到数据库了。下面的示例代码展示了如何利用阻塞队列实现先获取到的报价先保存到数据库。 // 创建阻塞队列BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();// 电商 S1 报价异步进入阻塞队列 executor.execute(()-> bq.put(f1.get()));// 电商 S2 报价异步进入阻塞队列 executor.execute(()-> bq.put(f2.get()));// 电商 S3 报价异步进入阻塞队列 executor.execute(()-> bq.put(f3.get()));// 异步保存所有报价 for (int i=0; i<3; i++) { Integer r = bq.take(); executor.execute(()->save(r));} 利用 CompletionService 实现询价系统不过在实际项目中,并不建议你这样做,因为 Java SDK 并发包里已经提供了设计精良的 CompletionService。利用 CompletionService 能让代码更简练。 ...

June 27, 2019 · 2 min · jiezi

并发编程安全问题可见性原子性和有序性

缓存导致的可见性问题一个线程对共享变量的修改,另外一个线程能够立刻看到,称为可见性在多核下,多个线程同时修改一个共享变量时,如++操作,每个线程操作的CPU缓存写入内存的时机是不确定的。除非你调用CPU相关指令强刷。 线程切换带来的原子性问题我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。高级语言里一条语句往往需要多条CPU指令完成。例如count += 1,至少需要三条CPU指令: 指令1:首先,需要把变量count从内存加载到CPU的寄存器;指令2:之后,在寄存器中执行+1操作;指令3:最后,将结果写入内存(缓存机制导致可能写入的是CPU缓存而不是内存)。操作系统做任务切换,可以发生在任何一条CPU指令执行完,而不是高级语言里的一条语句。 编译优化带来的有序性问题顾名思义,有序性指的是程序按照代码的先后顺序执行。public class Singleton { static Singleton instance; static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }}在new操作上,我们以为 的new操作应该是: 分配一块内存M;在内存M上初始化Singleton对象;然后M的地址赋值给instance变量。但是实际上优化后的执行路径却是这样的: 分配一块内存M;将M的地址赋值给instance变量;最后在内存M上初始化Singleton对象。优化后会导致什么问题呢?我们假设线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换到了线程B 上;如果此时线程B也执行getInstance()方法,那么线程B在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的instance是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

June 25, 2019 · 1 min · jiezi

Java并发14-Future-优雅的使用多线程

上一篇,我们详细介绍了如何创建正确的线程池,那创建完线程池,我们该如何使用呢?在上一篇文章中,我们仅仅介绍了 ThreadPoolExecutor 的 void execute(Runnable command) 利用这个方法虽然可以提交任务,但是却没有办法获取任务的执行结果(execute() 方法没有返回值)。而很多场景下,我们又都是需要获取任务的执行结果的。 下面我们就来介绍一下使用 ThreadPoolExecutor 的时候,如何获取任务执行结果。 如何获取任务执行结果Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。下面我们先来介绍这 3 个 submit() 方法,这 3 个方法的方法签名如下。 // 提交 Runnable 任务Future<?> submit(Runnable task);// 提交 Callable 任务<T> Future<T> submit(Callable<T> task);// 提交 Runnable 任务及结果引用 <T> Future<T> submit(Runnable task, T result);你会发现它们的返回值都是 Future 接口,Future 接口有 5 个方法,我都列在下面了,它们分别是取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone()以及2 个获得任务执行结果的 get() 和 get(timeout, unit),其中最后一个 get(timeout, unit) 支持超时机制。通过 Future 接口的这 5 个方法你会发现,我们提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是:这两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。 ...

June 23, 2019 · 3 min · jiezi

Java并发12-原子类无锁工具类的典范

前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都是采用的互斥锁方案。 public class Test { long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } }}其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。 在下面的代码中,我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使 add10K() 方法变成线程安全的,原子类的使用还是挺简单的。 public class Test { AtomicLong count = new AtomicLong(0); void add10K() { int idx = 0; while(idx++ < 10000) { count.getAndIncrement(); } }}无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。那它是如何做到的呢? ...

June 16, 2019 · 3 min · jiezi

Java-并发设计模式

Java 并发设计模式一、Thread Local Storage 模式1. ThreadLocal 的使用Thread Local Storage 表示线程本地存储模式。 大多数并发问题都是由于变量的共享导致的,多个线程同时读写同一变量便会出现原子性,可见性等问题。局部变量是线程安全的,本质上也是由于各个线程各自拥有自己的变量,避免了变量的共享。 Java 中使用了 ThreadLocal 来实现避免变量共享的方案。ThreadLocal 保证在线程访问变量时,会创建一个这个变量的副本,这样每个线程都有自己的变量值,没有共享,从而避免了线程不安全的问题。 下面是 ThreadLocal 的一个简单使用示例: public class ThreadLocalTest { private static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static SimpleDateFormat safeDateFormat() { return threadLocal.get(); } public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<SimpleDateFormat> task1 = new FutureTask<>(ThreadLocalTest::safeDateFormat); FutureTask<SimpleDateFormat> task2 = new FutureTask<>(ThreadLocalTest::safeDateFormat); Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); t2.start(); System.out.println(task1.get() == task2.get());//返回false,表示两个对象不相等 }}程序中构造了一个线程安全的 SimpleDateFormat ,两个线程取到的是不同的示例对象,这样就保证了线程安全。 ...

June 5, 2019 · 5 min · jiezi

JUC读写锁ReentrantReadWriteLock

一、写在前面在上篇我们聊到了可重入锁(排它锁)ReentrantLcok ,具体参见《J.U.C|可重入锁ReentrantLock》 ReentrantLcok 属于排它锁,本章我们再来一起聊聊另一个我们工作中出镜率很高的读-写锁。 二、简介重入锁ReentrantLock是排他锁(互斥锁),排他锁在同一时刻仅有一个线程可访问,但是在大多数场景下,大部分时间都是提供读服务的,而写服务占用极少的时间,然而读服务不存在数据竞争的问题,如果一个线程在读时禁止其他线程读势必会降低性能。所以就有了读写锁。 读写锁内部维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般排他锁有着显著的提升。 读写锁在同一时间可以允许多个读线程同时访问,但是写线程访问时,所有的读线程和写线程都会阻塞。 主要有以下特征: 公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。重进入:该锁支持重进入,以读写线程为列,读线程在获取到读锁之后,能再次获取读锁。而写线程在获取写锁后能够再次获取写锁,同时也可以获取读锁。锁降级:遵循获取写锁、读锁再释放写锁的次序,写锁能够降级成为读锁。读写锁最多支持65535个递归写入锁和65535个递归读取锁。 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁 读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。 三、主要方法介绍读写锁ReentrantReadWriteLock 实现了ReadWriteLock 接口,该接口维护一对相关的锁即读锁和写锁。 public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock();}ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。ReentrantReadWriteLock定义如下: /** 内部类 读锁*/private final ReentrantReadWriteLock.ReadLock readerLock; /** 内部类 写锁*/private final ReentrantReadWriteLock.WriteLock writerLock;/** 执行所有同步机制 */final Sync sync;// 默认实现非公平锁public ReentrantReadWriteLock() { this(false);}// 利用给定的公平策略初始化ReentrantReadWriteLockpublic ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } // 返回写锁 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } //返回读锁 public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } // 实现同步器,也是实现锁的核心 abstract static class Sync extends AbstractQueuedSynchronizer { // 省略实现代码 } // 公平锁的实现 static final class FairSync extends Sync { // 省略实现代码 } // 非公平锁的实现 static final class NonfairSync extends Sync { // 省略实现代码 } // 读锁实现 public static class ReadLock implements Lock, java.io.Serializable { // 省略实现代码 } // 写锁实现 public static class WriteLock implements Lock, java.io.Serializable { // 省略实现代码 }ReentrantReadWriteLock 和 ReentrantLock 其实都一样,锁核心都是Sync, 读锁和写锁都是基于Sync来实现的。从这分析其实ReentrantReadWriteLock就是一个锁,只不过内部根据不同的场景设计了两个不同的实现方式。其读写锁为两个内部类: ReadLock、WriteLock 都实现了Lock 接口。 ...

April 30, 2019 · 6 min · jiezi

Java并发之线程组ThreadGroup介绍

线程组介绍线程组的构造ThreadGroup方法介绍 查看线程组信息终止线程组中的所有线程总结Links 作者资源相关资源线程组介绍线程组(ThreadGroup)简单来说就是一个线程集合。线程组的出现是为了更方便地管理线程。 线程组是父子结构的,一个线程组可以集成其他线程组,同时也可以拥有其他子线程组。从结构上看,线程组是一个树形结构,每个线程都隶属于一个线程组,线程组又有父线程组,这样追溯下去,可以追溯到一个根线程组——System线程组。 <div align=center> </div> 下面介绍一下线程组树的结构: JVM创建的system线程组是用来处理JVM的系统任务的线程组,例如对象的销毁等。system线程组的直接子线程组是main线程组,这个线程组至少包含一个main线程,用于执行main方法。main线程组的子线程组就是应用程序创建的线程组。你可以在main方法中看到JVM创建的system线程组和main线程组: public static void main(String[] args) { ThreadGroup mainThreadGroup=Thread.currentThread().getThreadGroup(); ThreadGroup systenThreadGroup=mainThreadGroup.getParent(); System.out.println("systenThreadGroup name = "+systenThreadGroup.getName()); System.out.println("mainThreadGroup name = "+mainThreadGroup.getName()); }console输出: systenThreadGroup name = systemmainThreadGroup name = main一个线程可以访问其所属线程组的信息,但不能访问其所属线程组的父线程组或者其他线程组的信息。 线程组的构造java.lang.ThreadGroup提供了两个构造函数: ConstructorDescriptionThreadGroup(String name)根据线程组名称创建线程组,其父线程组为main线程组ThreadGroup(ThreadGroup parent, String name)根据线程组名称创建线程组,其父线程组为指定的parent线程组下面演示一下这两个构造函数的用法: public static void main(String[] args) { ThreadGroup subThreadGroup1 = new ThreadGroup("subThreadGroup1"); ThreadGroup subThreadGroup2 = new ThreadGroup(subThreadGroup1, "subThreadGroup2"); System.out.println("subThreadGroup1 parent name = " + subThreadGroup1.getParent().getName()); System.out.println("subThreadGroup2 parent name = " + subThreadGroup2.getParent().getName());}console输出: ...

April 29, 2019 · 3 min · jiezi

线程池系列-1-让多线程不再坑爹的线程池

背景线程池的来由服务端的程序,例如数据库服务器和Web服务器,每次收到客户端的请求,都会创建一个线程来处理这些请求。 创建线程的方式又很多,例如继承Thread类、实现Runnable或者Callable接口等。 通过创建新的线程来处理客户端的请求,这种看起来很容易的方法,其实是有很大弊端且有很高的风险的。 俗话说,简单的路越走越困难,困难的路越走越简单,就是这个道理。 创建和销毁线程,会消耗大量的服务器资源,甚至创建和销毁线程消耗的时间比线程本身处理任务的时间还要长。 由于启动线程需要消耗大量的服务器资源,如果创建过多的线程会造成系统内存不足(run out of memory),因此限制线程创建的数量十分必要。 什么是线程池线程池通俗来讲就是一个取出和放回提前创建好的线程的池子,概念上,类似数据库的连接池。 那么线程池是如何发挥作用的呢? 实际上,线程池是通过重用之前创建好线程来处理当前任务,来达到大大降低线程频繁创建和销毁导致的资源消耗的目的。 A thread pool reuses previously created threads to execute current tasks and offers a solution to the problem of thread cycle overhead and resource thrashing. Since the thread is already existing when the request arrives, the delay introduced by thread creation is eliminated, making the application more responsive. 背景总结下面总结一下开篇对于线程池的一些介绍。 线程是程序的组成部分,可以帮助我们搞事情。多个线程同时帮我们搞事情,可以通过更大限度地利用服务器资源,用来大大提高我们搞事情的效率。我们创建的每个线程都不是省油的灯,线程越多就会占用越多的系统资源,因此小弟虽好使但不要贪多哦,在有限的系统资源下,线程并不是“韩信点兵,多多益善”的,要限制线程的数量。请记住这一条,因为下面“批判”Java提供的线程池创建解决方案的时候,这就是“罪魁祸首”。创建和销毁线程会耗费大量系统资源,就像大佬招募和遣散小弟,都是要大费周章的。因此聪明的大佬就想到了“池”,把线程缓存起来,用的时候拿出来不用的时候还放回去,这就可以既享受多线程的乐趣,又可以避免使用多线程的痛苦了。但到底怎么使用线程池呢?线程池真的这么简单好用吗?线程池使用的过程中有没有什么坑? 不要着急,下面就结合具体的示例,跟你讲解各种使用线程池的姿势,以及这些姿势爽在哪里,痛在哪里。 准备好纸巾,咳咳...,是笔记本,涛哥要跟你开讲啦! ...

April 29, 2019 · 3 min · jiezi

JUC可重入锁ReentrantLock

一、写在前面前几篇我们具体的聊了AQS原理以及底层源码的实现,具体参见 《J.U.C|一文搞懂AQS》《J.U.C|同步队列(CLH)》《J.U.C|AQS独占式源码分析》《J.U.C|AQS共享式源码分析》 本章我们来聊一聊其实现之一 可重入锁ReentrantLock的实现原理以及源码分析。 注 :本章主要讲解非公平锁的实现流程和源码解析,其中涉及到AQS底层的实现因在前面几章都已经详细聊过在这会一笔带过。 二、什么是重入锁可重入锁 ReentrantLock ,顾名思义,支持重新进入的锁,其表示该锁能支持一个线程对资源的重复加锁。 Java API 描述 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ReentrantLock还提供了公平锁和非公平锁的选择, 其构造方法接受一个公平参数(默认是非公平方式),当传入ture时表示公平锁, 否则为非公平锁。其两者的主要区别在于公平锁获取锁是有顺序的。但是其效率往往没有非公平锁的效率高,在多线程的访问时往往表现很低的吞吐量(即速度慢,常常急慢)。 来张图缓解下 三、源码分析我们先来看一段代码 ReentrantLock lock = new ReentrantLock(); try { lock.lock(); // 业务代码 } finally { lock.unlock(); }这一段代码相信学过Java的同学都非常熟悉了,今天我们就以此为入口一步一步的带你深入其底层世界。 共享状态的获取(锁的获取)lock()方法// ReentrantLock --> lokc() 实现Lock 接口的方法public void lock() { // 调用内部类sync 的lock方法, 这里有两种实现,公平锁(FairSync)非公平锁(NonfairSync)这里我们来主要说 NonfairSync sync.lock(); }ReentrantLock 的lock 方法, sync 为ReentrantLock的一个内部类,其继承了AbstractQueuedSynchronizer(AQS), 他有两个子类公平锁FairSync 和非公平锁NonfairSync ReentrantLock 中其中大部分的功能的实现都是委托给内部类Sync实现的,在Sync 中定义了abstract void lock() 留给子类去实现, 默认实现了final boolean nonfairTryAcquire(int acquires) 方法,可以看出其为非公平锁默认实现方式,下面我讲下给看下非公平锁lock方法。 ...

April 26, 2019 · 2 min · jiezi

JUCAQS共享式源码分析

一、写在前面上篇给大家聊了独占式的源码,具体参见《J.U.C|AQS独占式源码分析》 这一章我们继续在AQS的源码世界中遨游,解读共享式同步状态的获取和释放。 二、什么是共享式共享式与独占式唯一的区别是在于同一时刻可以有多个线程获取到同步状态。 我们以读写锁为例来看两者,一个线程在对一个资源文件进行读操作时,那么这一时刻对于文件的写操作均被阻塞,而其它线程的读操作可以同时进行。当写操作要求对资源独占操作,而读操作可以是共享的,两种不同的操作对同一资源进行操作会是什么样的?看下图 共享式访问资源,其他共享时均被允许,而独占式被阻塞。 独占式访问资源时,其它访问均被阻塞。 通过读写锁给大家一起温故下独占式和共享式概念,上一节我们已经聊过独占式,本章我们主要聊共享式。 主要讲解方法 protected int tryAcquireShared(int arg);共享式获取同步状态,返回值 >= 0 表示获取成功,反之则失败。protected boolean tryReleaseShared(int arg): 共享式释放同步状态。三、核心方法分析*3.1 同步状态的获取public final void acquireShared(int arg) 共享式获取同步状态的顶级入口,如果当前线程未获取到同步状态,将会加入到同步队列中等待,与独占式唯一的区别是在于同一时刻可以有多个线程获取到同步状态。方法源码 public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }方法函数解析 tryAcquireShared(arg):获取同步状态,返回值大于等于0表示获取成功,否则失败。doAcquireShared(arg):共享式获取共享状态,包含构建节点,加入队列等待,唤醒节点等操作。源码分析 同步器的 acquireShared 和 doAcquireShared 方法 //请求共享锁的入口public final void acquireShared(int arg) { // 当state != 0 并且tryAcquireShared(arg) < 0 时才去才获取资源 if (tryAcquireShared(arg) < 0) // 获取锁 doAcquireShared(arg); }// 以共享不可中断模式获取锁private void doAcquireShared(int arg) { // 将当前线程一共享方式构建成 node 节点并将其加入到同步队列的尾部。这里addWaiter(Node.SHARED)操作和独占式基本一样, final Node node = addWaiter(Node.SHARED); // 是否成功标记 boolean failed = true; try { // 等待过程是否被中断标记 boolean interrupted = false; 自旋 for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); // 判断前驱节点是否是head节点,也就是看自己是不是老二节点 if (p == head) { // 如果自己是老二节点,尝试获取资源锁,返回三种状态 // state < 0 : 表示获取资源失败 // state = 0: 表示当前正好线程获取到资源, 此时不需要进行向后继节点传播。 // state > 0: 表示当前线程获取资源锁后,还有多余的资源,需要向后继节点继续传播,获取资源。 int r = tryAcquireShared(arg); // 获取资源成功 if (r >= 0) { // 当前节点线程获取资源成功后,对后继节点进行逻辑操作 setHeadAndPropagate(node, r); // setHeadAndPropagate(node, r) 已经对node.prev = null,在这有对p.next = null; 等待GC进行垃圾收集。 p.next = null; // help GC // 如果等待过程被中断了, 将中断给补上。 if (interrupted) selfInterrupt(); failed = false; return; } } // 判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt() if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }在acquireShared(int arg)方法中,同步器调用tryAcquireShared(arg)方法获取同步状态,返回同步状态有两种。 ...

April 24, 2019 · 4 min · jiezi

锁开销优化以及-CAS-简单说明

锁开销优化以及 CAS 简单说明[TOC] 锁互斥锁是用来保护一个临界区,即保护一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。 在谈及锁的性能开销,一般都会说锁的开销很大,那锁的开销有多大,主要耗在哪,怎么提高锁的性能。 锁的开销现在锁的机制一般使用 futex(fast Userspace mutexes),内核态和用户态的混合机制。还没有futex的时候,内核是如何维护同步与互斥的呢?系统内核维护一个对象,这个对象对所有进程可见,这个对象是用来管理互斥锁并且通知阻塞的进程。如果进程A要进入临界区,先去内核查看这个对象,有没有别的进程在占用这个临界区,出临界区的时候,也去内核查看这个对象,有没有别的进程在等待进入临界区,然后根据一定的策略唤醒等待的进程。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生。 Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。 mutex 是在 futex 的基础上用的内存共享变量来实现的,如果共享变量建立在进程内,它就是一个线程锁,如果它建立在进程间共享内存上,那么它是一个进程锁。pthread_mutex_t 中的 _lock 字段用于标记占用情况,先使用CAS判断_lock是否占用,若未占用,直接返回。否则,通过__lll_lock_wait_private 调用SYS_futex 系统调用迫使线程进入沉睡。 CAS是用户态的 CPU 指令,若无竞争,简单修改锁状态即返回,非常高效,只有发现竞争,才通过系统调用陷入内核态。所以,FUTEX是一种用户态和内核态混合的同步机制,它保证了低竞争情况下的锁获取效率。 所以如果锁不存在冲突,每次获得锁和释放锁的处理器开销仅仅是CAS指令的开销。 确定一件事情最好的方法是实际测试和观测它,让我们写一段代码来测试无冲突时锁的开销: #include <pthread.h>#include <stdlib.h>#include <stdio.h>#include <time.h>static inline long long unsigned time_ns(struct timespec* const ts) { if (clock_gettime(CLOCK_REALTIME, ts)) { exit(1); } return ((long long unsigned) ts->tv_sec) * 1000000000LLU + (long long unsigned) ts->tv_nsec;}int main(){ int res = -1; pthread_mutex_t mutex; //初始化互斥量,使用默认的互斥量属性 res = pthread_mutex_init(&mutex, NULL); if(res != 0) { perror("pthread_mutex_init failed\n"); exit(EXIT_FAILURE); } long MAX = 1000000000; long c = 0; struct timespec ts; const long long unsigned start_ns = time_ns(&ts); while(c < MAX) { pthread_mutex_lock(&mutex); c = c + 1; pthread_mutex_unlock(&mutex); } const long long unsigned delta = time_ns(&ts) - start_ns; printf("%f\n", delta/(double)MAX); return 0;}说明:以下性能测试在腾讯云 Intel(R) Xeon(R) CPU E5-26xx v4 1核 2399.996MHz 下进行。 ...

April 24, 2019 · 6 min · jiezi

再次认识ReentrantReadWriteLock读写锁

前言最近研究了一下juc包的源码。在研究ReentrantReadWriteLock读写锁的时候,对于其中一些细节的思考和处理以及关于提升效率的设计感到十分的震撼,难以遏制想要分享这份心得的念头,因此在这里写一篇小文章作为记录。 本片文章建立在已经了解并发相关基础概念的基础上,可能不会涉及很多源码,以思路为主。如果文章有什么纰漏或者错误,还请务必指正,预谢。 1. 从零开始考虑如何实现读写锁首先我们需要知道独占锁(RenentractLock)这种基础的锁,在juc中是如何实现的:它基于java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)这个抽象类所搭建的同步框架,利用AQS中的一个volatile int类型变量的CAS操作来表示锁的占用情况,以及一个双向链表的数据结构来存储排队中的线程。 简单地说:一个线程如果想要获取锁,就需要尝试对AQS中的这个volatile int变量(下面简称state)执行类似comapre 0 and swap 1的操作,如果不成功就进入同步队列排队(自旋和重入之类的细节不展开说了)同时休眠自己(LockSupport.park),等待占有锁的线程释放锁时再唤醒它。 那么,如果不考虑重入,state就是一个简单的状态标识:0表示锁未被占用,1表示锁被占用,同步性由volatile和CAS来保证。 上面说的是独占锁,state可以不严谨地认为只有两个状态位。 但是如果是读写锁,那这个锁的基本逻辑应该是:读和读共享、读和写互斥、写和写互斥 如何实现锁的共享呢?如果我们不再把state当做一个状态,而是当做一个计数器,那仿佛就可以说得通了:获取锁时compare n and swap n+1,释放锁时compare n and swap n-1,这样就可以让锁不被独占了。 因此,要实现读写锁,我们可能需要两个锁,一个共享锁(读锁),一个独占锁(写锁),而且这两个锁还需要协作,写锁需要知道读锁的占用情况,读锁需要知道写锁的占用情况。 设想中的简单流程大概如下: 设想中的流程很简单,然而存在一些问题: 关于读写互斥: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?如果允许,会有什么问题?如果不允许,当真的存在这种需求时怎么办?关于写写互斥是否也存在上面两条提到的情况和问题呢?我们知道一般而言,读的操作数量要远远大于写的操作,那么很有可能读锁一旦被获取就长时间处于被占有的情况(因为新来的读操作只需要进去+1就好了,不需要等待state回到0,这样的话state可能永远不会有回到0的一天),这会导致极端情况下写锁根本没有机会上位,该如何解决这种情况?对于上面用计数器来实现共享锁的假设,当任意一个线程想要释放锁(即使它并未获取锁,因为解锁的方法是开放的,任何获取锁对象的线程都可以执行lock.unlock())时,如何判断它是否有权限执行compare n and swap n-1? 是否应该使用ThreadLocal来实现这种权限控制?如果使用ThreadLocal来控制,如何保证性能和效率?2. 带着问题研究ReentrantReadWriteLock在开始研究ReentrantReadWriteLock之前,应当先了解两个概念: 重入性一个很现实的问题是:我们时常需要在锁中加锁。这可能是由代码复用产生的需求,也可能业务的逻辑就是这样。但是不管怎样,在一个线程已经获取锁后,在释放前再次获取锁是一个合理的需求,而且并不生硬。上文在说独占锁时说到如果不考虑重入的情况,state会像boolean一样只有两个状态位。那么如果考虑重入,也很简单,在加锁时将state的值累加即可,表示同一个线程重入此锁的次数,当state归零,即表示释放完毕。 公平、非公平这里的公平和非公平是指线程在获取锁时的机会是否公平。我们知道AQS中有一个FIFO的线程排队队列,那么如果所有线程想要获取锁时都来排队,大家先来后到井然有序,这就是公平;而如果每个线程都不守秩序,选择插队,而且还插队成功了,那这就是不公平。 但是为什么需要不公平呢?因为效率。有两个因素会制约公平机制下的效率: 上下文切换带来的消耗依赖同步队列造成的消耗我们之所以会使用锁、使用并发,可能很大一部分原因是想要挖掘程序的效率,那么相应的,对于性能和效率的影响需要更加敏感。简单地说,上述的两点由于公平带来的性能损耗很可能让你的并发失去高效的初衷。当然这也是和场景密切关联的,比如说你非常需要避免ABA问题,那么公平模式很适合你。具体的不再展开,可以参考这篇文章:深入剖析ReentrantLock公平锁与非公平锁源码实现 回到我们之前提的问题: 对于某个线程,当它先获得读锁,然后在执行代码的过程中,又想获得写锁,这种情况是否应该允许?我们先考虑这种情况是否实际存在:假设我们有一个对象,它有两个实例变量a和b,我们需要在实现:if a = 1 then set b = 2,或者换个例子,如果有个用户名字叫张三就给他打钱。 这看上去仿佛是个CAS操作,然而它并不是,因为它涉及了两个变量,CAS操作并不支持这种针对多个变量的疑似CAS的操作。为什么不支持呢?因为cpu不提供这种针对多个变量的CAS操作指令(至少x86不提供),代码层面的CAS只是对cpu指令的封装而已。为什么cpu不支持呢?可以,但没必要鄙人也不是特别清楚(逃)。 总而言之这种情况是存在的,但是在并发情况下如果不加锁就会有问题:比如先判断得到这个用户确实名叫张三,正准备打钱,突然中途有人把他的名字改了,那再打这笔钱就有问题了,我们判断名字和打钱这两个行为中间应当是没有空隙的。那么为了保证这个操作的正确性,我们或许可以在读之前加一个读锁,在我释放锁之前,其他人不得改变任何内容,这就是之前所说的读写互斥:读的期间不准写。但是如果照着这个想法,就产生了自相矛盾的地方:都说了读期间不能写,那你自己怎么在写(打钱)呢? 如果我们顺着这个思路去尝试解释“自己读的期间还在写”的行为的正当性,我们也许可以设立一个规则:如果读锁是我自己持有,则我可以写。然而这会出现其他的问题:因为读锁是共享的,你加了读锁,其他人仍然可以读,这是否会有问题呢?假如我们的打钱操作涉及更多的值的改变,只有这些值全部改变完毕,才能说此时的整体状态正确,否则在改变完毕之前,读到的东西都有可能是错的。再去延伸这个思路似乎会变得非常艰难,也许会陷入耦合的地狱。 但是实际上我们不需要这样做,我们只需要反过来使用读写互斥的概念即可:因为写写互斥(写锁是独占锁),所以我们在执行这个先读后写的行为之前,加一个写锁,这同样能防止其他人来写,同时还可以阻止其他人来读,从而实现我们在单线程中读写并存的需求。 这就是ReentrantReadWriteLock中一个重要的概念:锁降级 对于另一个子问题:如果在已经获取写锁的期间还要再获取写锁的时候怎么办?这种情况还是很常见的,多数是由于代码的复用导致,不过相应的处理也很简单:对写锁这个独占锁增加允许单线程重入的规则即可。 极端情况下写操作根本没有机会上位,该如何解决这种情况?如果我们有两把锁,一把读锁,一把写锁,它们之间想要互通各自加锁的情况很简单——只要去get对方的state就行了。但是只知道state是不够的,对于读的操作来说,它如果只看到写锁没被占用,也不管有多少个写操作还在排队,就去在读锁上+1,那很可能发展成为问题所说的场景:写操作永远没机会上位。 那么我们理想的情况应该是:读操作如果发现写锁空闲,最好再看看写操作的排队情况如何,酌情考虑放弃这一次竞争,让写操作有机会上位。这也是我理解的,为什么ReentrantReadWriteLock不设计成两个互相沟通的、独立的锁,而是公用一个锁(class Sync extends AbstractQueuedSynchronizer)——因为它们看似独立,实际上对于耦合的需求很大,它们不仅需要沟通锁的情况,还要沟通队列的情况。 公用一个锁的具体实现是:使用int state的高16位表示读锁的state,低16位表示写锁的state,而队列公用的方式是给每个节点增加一个标记,表明该节点是一个共享锁的节点(读操作)还是一个独占锁的节点(写操作)。 上面说到的“酌情放弃这一次竞争”,ReentrantReadWriteLock中体现在boolean readerShouldBlock()这个方法里,这个方法有两个模式:公平和非公平,我们来稍微看一点源码先看公平模式的实现: final boolean readerShouldBlock() { return hasQueuedPredecessors();}当线程发现自己可以获取读锁时(写锁未被占用),会调用这个方法,来判断自己是否应该放弃此次获取。hasQueuedPredecessors()这个方法我们不去看源码,因为它的意思很显而易见(实际代码也是):是否存在排队中的线程(Predecessor先驱者可以理解为先来的)。如果有,那就放弃竞争去排队。在公平模式下,无论读写操作,只需要大家都遵守FIFO的秩序,就不会出现问题描述的情况 ...

April 23, 2019 · 3 min · jiezi

JUCAQS独占式源码分析

一、写在前面上篇文章通过ReentrantLock 的加锁和释放锁过程给大家聊了聊AQS架构以及实现原理,具体参见《J.U.C|AQS的原理》。 理解了原理,我们在来看看再来一步一步的聊聊其源码是如何实现的。 本章给大家聊聊AQS中独占式获取和释放共享状态的流程,主要根据tryAcquire(int arg) -- > tryRelease(int arg)来讲。 二、什么是独占式AQS的同步队列提供两种模式即独占式(EXCLUSIVE) 和 共享式(SHARED)。 本章我们主要聊独占式: 即同一时刻只能有一个线程获取同步状态,其它获取同步状态失败的线程则会加入到同步队列中进行等待。 主要讲解方法: tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。有对同步队列不明白的请看《J.U.C|同步队列(CLH)》 三、核心方法分析3.1 共享状态的获取acquire(int arg) 独占式获取同步状态的顶级入口acquire(int arg)方法,如果线程获取到共享这状态则直接返回, 否则把当前线程构造成独占式(node.EXCLUSIVE)模式节点并添加到同步队列尾部,直到获取到资源为止,整个过程忽略中断。方法源码 public final void acquire(int arg) {         if (!tryAcquire(arg) &&             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))             selfInterrupt();     } }方法函数: tryAcquire(arg):尝试获取同步状态、获取成功则直接返回。addWaiter(Node.EXCLUSIVE):当同步状态获取失败时,构建一个独占式节点并将其加入到同步队列的尾部。acquireQueued(Node, arg)) : 获取该节点指定数量的资源,通过自旋的方式直到获取成功,返回是该节点线程的中断状态。selfInterrupt(): 将中断补上(因其获取资源的整个过程是忽略中断的所以最后手动将中断补上)源码分析 tryAcquire(arg) protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }??? 什么鬼? 直接抛出异常? AQS 中对共享状态的获取没有提供具体的实现,等待子类根据自己的场景去实现。有没有人疑惑,那为什么不是 abstract 的尼? 因为AQS不止是独占模式的锁需要继承它还有别人也需要继承它,总不能让别人也来实现一个无关的方法吧。 ...

April 23, 2019 · 3 min · jiezi

并发编程导论

并发编程导论是对于分布式计算-并发编程 https://url.wx-coder.cn/Yagu8 系列的总结与归纳。欢迎关注公众号:某熊的技术之路。 并发编程导论随着硬件性能的迅猛发展与大数据时代的来临,并发编程日益成为编程中不可忽略的重要组成部分。简单定义来看,如果执行单元的逻辑控制流在时间上重叠,那它们就是并发(Concurrent)的。并发编程复兴的主要驱动力来自于所谓的“多核危机”。正如摩尔定律所预言的那样,芯片性能仍在不断提高,但相比加快 CPU 的速度,计算机正在向多核化方向发展。正如 Herb Sutter 所说,“免费午餐的时代已然终结”。为了让代码运行得更快,单纯依靠更快的硬件已无法满足要求,并行和分布式计算是现代应用程序的主要内容,我们需要利用多个核心或多台机器来加速应用程序或大规模运行它们。 并发编程是非常广泛的概念,其向下依赖于操作系统、存储等,与分布式系统、微服务等,而又会具体落地于 Java 并发编程、Go 并发编程、JavaScript 异步编程等领域。云计算承诺在所有维度上(内存、计算、存储等)实现无限的可扩展性,并发编程及其相关理论也是我们构建大规模分布式应用的基础。 本节主要讨论并发编程理论相关的内容,可以参阅 [Java 并发编程 https://url.wx-coder.cn/72vCj 、Go 并发编程 https://url.wx-coder.cn/FO9EX 等了解具体编程语言中并发编程的实践,可以参阅微服务实战 https://url.wx-coder.cn/7KZ2i 或者关系型数据库理论 https://url.wx-coder.cn/DJNQn 了解并发编程在实际系统中的应用。 并发与并行并发就是可同时发起执行的程序,指程序的逻辑结构;并行就是可以在支持并行的硬件上执行的并发程序,指程序的运⾏状态。换句话说,并发程序代表了所有可以实现并发行为的程序,这是一个比较宽泛的概念,并行程序也只是他的一个子集。并发是并⾏的必要条件;但并发不是并⾏的充分条件。并发只是更符合现实问题本质的表达,目的是简化代码逻辑,⽽不是使程序运⾏更快。要是程序运⾏更快必是并发程序加多核并⾏。 简言之,并发是同一时间应对(dealing with)多件事情的能力;并行是同一时间动手做(doing)多件事情的能力。 并发是问题域中的概念——程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件;一个并发程序含有多个逻辑上的独立执行块,它们可以独立地并行执行,也可以串行执行。而并行则是方法域中的概念——通过将问题中的多个部分并行执行,来加速解决问题。一个并行程序解决问题的速度往往比一个串行程序快得多,因为其可以同时执行整个任务的多个部分。并行程序可能有多个独立执行块,也可能仅有一个。 具体而言,Redis 会是一个很好地区分并发和并行的例子。Redis 本身是一个单线程的数据库,但是可以通过多路复用与事件循环的方式来提供并发地 IO 服务。这是因为多核并行本质上会有很大的一个同步的代价,特别是在锁或者信号量的情况下。因此,Redis 利用了单线程的事件循环来保证一系列的原子操作,从而保证了即使在高并发的情况下也能达到几乎零消耗的同步。再引用下 Rob Pike 的描述: A single-threaded program can definitely provides concurrency at the IO level by using an IO (de)multiplexing mechanism and an event loop (which is what Redis does).线程级并发从 20 世纪 60 年代初期出现时间共享以来,计算机系统中就开始有了对并发执行的支持;传统意义上,这种并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换的方式实现的,这种配置称为单处理器系统。从 20 世纪 80 年代开始,多处理器系统,即由单操作系统内核控制的多处理器组成的系统采用了多核处理器与超线程(HyperThreading)等技术允许我们实现真正的并行。多核处理器是将多个 CPU 集成到一个集成电路芯片上: ...

April 22, 2019 · 6 min · jiezi

Java并发编程之线程间通讯(下)-生产者与消费者

前文回顾上一篇文章重点唠叨了java中协调线程间通信的wait/notify机制,它有力的保证了线程间通信的安全性以及便利性。本篇将介绍wait/notify机制的一个应用以及更多线程间通信的内容。生产者-消费者模式目光从厕所转到饭馆,一个饭馆里通常都有好多厨师以及好多服务员,这里我们把厨师称为生产者,把服务员称为消费者,厨师和服务员是不直接打交道的,而是在厨师做好菜之后放到窗口,服务员从窗口直接把菜端走给客人就好了,这样会极大的提升工作效率,因为省去了生产者和消费者之间的沟通成本。从java的角度看这个事情,每一个厨师就相当于一个生产者线程,每一个服务员都相当于一个消费者线程,而放菜的窗口就相当于一个缓冲队列,生产者线程不断把生产好的东西放到缓冲队列里,消费者线程不断从缓冲队列里取东西,画个图就像是这样:现实中放菜的窗口能放的菜数量是有限的,我们假设这个窗口只能放5个菜。那么厨师在做完菜之后需要看一下窗口是不是满了,如果窗口已经满了的话,就在一旁抽根烟等待,直到有服务员来取菜的时候通知一下厨师窗口有了空闲,可以放菜了,这时厨师再把自己做的菜放到窗口上去炒下一个菜。从服务员的角度来说,如果窗口是空的,那么也去一旁抽根烟等待,直到有厨师把菜做好了放到窗口上,并且通知他们一下,然后再把菜端走。我们先用java抽象一下菜:public class Food { private static int counter = 0; private int i; //代表生产的第几个菜 public Food() { i = ++counter; } @Override public String toString() { return “第” + i + “个菜”; }}每次创建Food对象,字段i的值都会加1,代表这是创建的第几道菜。为了故事的顺利进行,我们首先定义一个工具类:class SleepUtil { private static Random random = new Random(); public static void randomSleep() { try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } }}SleepUtil的静态方法randomSleep代表当前线程随机休眠一秒内的时间。然后我们再用java定义一下厨师:public class Cook extends Thread { private Queue<Food> queue; public Cook(Queue<Food> queue, String name) { super(name); this.queue = queue; } @Override public void run() { while (true) { SleepUtil.randomSleep(); //模拟厨师炒菜时间 Food food = new Food(); System.out.println(getName() + " 生产了" + food); synchronized (queue) { while (queue.size() > 4) { try { System.out.println(“队列元素超过5个,为:” + queue.size() + " " + getName() + “抽根烟等待中”); queue.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } queue.add(food); queue.notifyAll(); } } }}我们说每一个厨师Cook都是一个线程,内部维护了一个名叫queue的队列。在run方法中是一个死循环,代表不断的生产Food。他每生产一个Food后,都要判断queue队列中元素的个数是不是大于4,如果大于4的话,就调用queue.wait()等待,如果不大于4的话,就把创建号的Food对象放到queue队列中,由于可能多个线程同时访问queue的各个方法,所以对这段代码用queue对象来加锁保护。当向队列添加完刚创建的Food对象之后,就可以通知queue这个锁对象关联的等待队列中的服务员线程们可以继续端菜了。然后我们再用java定义一下服务员:class Waiter extends Thread { private Queue<Food> queue; public Waiter(Queue<Food> queue, String name) { super(name); this.queue = queue; } @Override public void run() { while (true) { Food food; synchronized (queue) { while (queue.size() < 1) { try { System.out.println(“队列元素个数为: " + queue.size() + “,” + getName() + “抽根烟等待中”); queue.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } food = queue.remove(); System.out.println(getName() + " 获取到:” + food); queue.notifyAll(); } SleepUtil.randomSleep(); //模拟服务员端菜时间 } }}每个服务员也是一个线程,和厨师一样,都在内部维护了一个名叫queue的队列。在run方法中是一个死循环,代表不断的从队列中取走Food。每次在从queue队列中取Food对象的时候,都需要判断一下队列中的元素是否小于1,如果小于1的话,就调用queue.wait()等待,如果不小于1的话,也就是队列里有元素,就从队列里取走一个Food对象,并且通知与queue这个锁对象关联的等待队列中的厨师线程们可以继续向队列里放入Food对象了。在厨师和服务员线程类都定义好了之后,我们再创建一个Restaurant类,来看看在餐馆里真实发生的事情:public class Restaurant { public static void main(String[] args) { Queue<Food> queue = new LinkedList<>(); new Cook(queue, “1号厨师”).start(); new Cook(queue, “2号厨师”).start(); new Cook(queue, “3号厨师”).start(); new Waiter(queue, “1号服务员”).start(); new Waiter(queue, “2号服务员”).start(); new Waiter(queue, “3号服务员”).start(); }}我们在Restaurant中安排了3个厨师和3个服务员,大家执行一下这个程序,会发现在如果厨师生产的过快,厨师就会等待,如果服务员端菜速度过快,服务员就会等待。但是整个过程厨师和服务员是没有任何关系的,它们是通过队列queue实现了所谓的解耦。这个过程虽然不是很复杂,但是使用中还是需要注意一些问题:我们这里的厨师和服务员使用同一个锁queue。使用同一个锁是因为对queue的操作只能用同一个锁来保护,假设使用不同的锁,厨师线程调用queue.add方法,服务员线程调用queue.remove方法,这两个方法都不是原子操作,多线程并发执行的时候会出现不可预测的结果,所以我们使用同一个锁来保护对queue这个变量的操作,这一点我们在唠叨设计线程安全类的时候已经强调过了。厨师和服务员线程使用同一个锁queue的后果就是厨师线程和服务员线程使用的是同一个等待队列。但是同一时刻厨师线程和服务员线程不会同时在等待队列中,因为当厨师线程在wait的时候,队列里的元素肯定是5,此时服务员线程肯定是不会wait的,但是消费的过程是被锁对象queue保护的,所以在一个服务员线程消费了一个Food之后,就会调用notifyAll来唤醒等待队列中的厨师线程们;当消费者线程在wait的时候,队列里的元素肯定是0,此时厨师线程肯定是不会wait的,生产的过程是被锁对象queue保护的,所以在一个厨师线程生产了一个Food对象之后,就会调用notifyAll来唤醒等待队列中的服务员线程们。所以同一时刻厨师线程和服务员线程不会同时在等待队列中。在生产和消费过程,我们都调用了SleepUtil.randomSleep();。我们这里的生产者-消费者模型是把实际使用的场景进行了简化,真正的实际场景中生产过程和消费过程一般都会很耗时,这些耗时的操作最好不要放在同步代码块中,这样会造成别的线程的长时间阻塞。如果把生产过程和消费过程都放在同步代码块中,也就是说在一个厨师炒菜的同时不允许别的厨师炒菜,在一个服务员端菜的同时不允许别的程序员端菜,这个显然是不合理的,大家需要注意这一点。以上就是wait/notify机制的一个现实应用:生产者-消费者模式的一个简介。管道输入/输出流还记得在唠叨I/O的时候提到的管道流么,这些管道流就是用于在不同线程之间的数据传输,一共有四种管道流:PipedInputStream:管道输入字节流PipedOutputStream:管道输出字节流PipedReader:管道输入字符流PipedWriter:管道输出字符流字节流和字符流的用法是差不多的,我们下边以字节流为例来唠叨一下管道流的用法。一个线程可以持有一个PipedInputStream对象,这个PipedInputStream对象在内部维护了一个字节数组,默认大小为1024字节。它并不能单独使用,需要与另一个线程持有的一个PipedOutputStream建立关联,PipedOutputStream往该字节数组中写数据,PipedInputStream从该字节数组中读数据,从而实现两个线程的通信。PipedInputStream先看一下它的几个构造方法:它有一个特别重要的方法就是:PipedOutputStream看一下它的构造方法:它也有一个连接到管道输入流的方法:使用示例管道流的通常使用场景就是一个线程持有一个PipedInputStream对象,另一个线程持有一个PipedOutputStream对象,然后把这两个输入输出管道流通过connect方法建立连接,此后从管道输出流写入的数据就可以通过管道输入流读出,从而实现了两个线程间的数据交换,也就是实现了线程间的通信:public class PipedDemo { public static void main(String[] args){ PipedInputStream in = new PipedInputStream(); PipedOutputStream out = new PipedOutputStream(); try { in.connect(out); //将输入流和输出流建立关联 } catch (IOException e) { throw new RuntimeException(e); } new ReadThread(in).start(); new WriteThread(out).start(); }}class ReadThread extends Thread { private PipedInputStream in; public ReadThread(PipedInputStream in) { this.in = in; } @Override public void run() { int i = 0; try { while ((i=in.read()) != -1) { //从输入流读取数据 System.out.println(i); } } catch (IOException e) { throw new RuntimeException(e); } finally { try { in.close(); } catch (IOException e) { throw new RuntimeException(e); } } }}class WriteThread extends Thread { private PipedOutputStream out; public WriteThread(PipedOutputStream out) { this.out = out; } @Override public void run() { byte[] bytes = {1, 2, 3, 4, 5}; try { out.write(bytes); //向输出流写入数据 out.flush(); } catch (IOException e) { throw new RuntimeException(e); } finally { try { out.close(); } catch (IOException e) { throw new RuntimeException(e); } } }}执行结果是:12345join方法我们前边说过这个方法,比如有代码是这样:public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { // … 线程t执行的具体任务 } }, “t”); t.start(); t.join(); System.out.println(“t线程执行完了,继续执行main线程”);}在main线程中调用t.join(),代表main线程需要等待t线程执行完成后才能继续执行。也就是说,这个join方法可以协调各个线程之间的执行顺序。它的实现其实很简单:public final synchronized void join() throws InterruptedException { while (isAlive()) { wait(); }}需要注意的是,join方法是Thread类的成员方法。上边例子中在main线程中调用t.join()的意思就是,使用Thread对象t作为锁对象,如果t线程还活着,就调用wait(),把main线程放到与t对象关联的等待队列里,直到t线程执行结束,系统会主动调用一下t.notifyAll(),把与t对象关联的等待队列中的线程全部移出,从而main线程可以继续执行~当然它还有两个指定等待时间的重载方法:java线程的状态java为了方便的管理线程,对底层的操作系统的线程状态做了一些抽象封装,定义了如下的线程状态:需要注意的是:对于在操作系统中线程的运行/就绪状态,java语言中统一用RUNNABLE状态来表示。对于在操作系统中线程的阻塞状态,java语言中用BLOCKED、WAITING和TIME_WAITING这三个状态分别表示。也就是对阻塞状态进行了进一步细分。对于因为获取不到锁而产生的阻塞称为BLOCKED状态,因为调用wait或者join方法而产生的阻塞称为WAITING状态,因为调用有超时时间的wait、join或者sleep方法而产生的在有限时间内阻塞称为TIME_WAITING状态。大家可以通过这个图来详细的看一下各个状态之间的转换过程:java这么划分线程的状态纯属于方便自己的管理,比如它会给在WAITING和TIMED_WAITING状态的线程分别建立不同的队列,来方便实施不同的恢复策略~所以大家也不用纠结为啥和操作系统中定义的不一样,其实操作系统中对各个状态的线程仍然有各种细分来方便管理,如果是你去设计一个语言或者一个操作系统,你也可以为了自己的方便来定义一下线程的各种状态。我们作为语言的使用者,首先还是把这些状态记住了再说哈????~获取线程执行状态java中定义了一个State枚举类型,来表示线程的状态:public class Thread implements Runnable { // … 为节省篇幅,省略其它方法和字段 public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }}然后又在Thread类里定义了一个成员方法:public State getState() { //省略了具体的实现}我们可以通过这个getState方法来获取到对应的线程状态,下边来举个例子获取上边列举的6种状态,为了故事的顺利发展,我们先定义一个工具类:public class LockUtil { public static void sleep(long mill) { try { Thread.sleep(mill); } catch (InterruptedException e) { throw new RuntimeException(e); } } public static void wait(Object obj) { try { obj.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } }}因为每次调用sleep和wait操作的时候都会有InterruptedException的异常说明,我们都需要try…catch一下,会导致代码结构会很混乱,所以我们写了个工具类来把InterruptedException的异常转为运行时异常。注意,我们这里转为运行时异常只是为了代码结构清晰,真实情况需要认真处理InterruptedException的异常说明,具体怎么使用我们后边会详细唠叨。然后接着写具体的获取状态的代码:public class ThreadStateDemo { private static Object lock = new Object(); //锁对象 public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { double d = 0.1; int i = 0; while (i++ < 100000) { //模仿一个耗时操作 d = d*0.3d; } SleepUtil.sleep(2000L); //休眠2秒钟 synchronized (lock) { LockUtil.wait(lock); } synchronized (lock) { //尝试获取lock锁 } } }, “t”); System.out.println(“初始状态:” + t.getState()); t.start(); System.out.println(“运行一个耗时操作时的状态:” + t.getState()); SleepUtil.sleep(1000L); System.out.println(“休眠时的状态:” + t.getState()); SleepUtil.sleep(2000L); System.out.println(“wait的状态:” + t.getState()); synchronized (lock) { lock.notifyAll(); } System.out.println(“被notify后的状态:” + t.getState()); synchronized (lock) { SleepUtil.sleep(1000L); //调用sleep方法不会释放锁 System.out.println(“因为获取锁而阻塞的状态:” + t.getState()); } }}我们在程序里用了一系列的sleep方法来控制程序的执行顺序,这只是为了简单的说明线程的各个状态的产生原因,在真实环境中是不允许使用sleep方法来控制线程间的执行顺序的,应该使用同步或者我们上边介绍的一系列线程通信的方式。这个程序的执行结果是:初始状态:NEW运行一个耗时操作时的状态:RUNNABLE休眠时的状态:TIMED_WAITINGwait的状态:WAITING被notify后的状态:BLOCKED因为获取锁而阻塞的状态:TERMINATED线程的各个状态都获取到了哈。总结基于wait/notify机制的生产者-消费者模式很重要,务必认真看几遍~一个线程可以持有一个PipedInputStream对象,这个PipedInputStream对象在内部维护了一个字节数组,默认大小为1024字节。它并不能单独使用,需要与另一个线程持有的一个PipedOutputStream建立关联,PipedOutputStream往该字节数组中写数据,PipedInputStream从该字节数组中读数据,从而实现两个线程的通信。使用join方法可以实现一个线程在另一个线程执行完毕后才继续执行的功能。java为了方便的管理线程,对底层的操作系统的线程状态做了一些抽象封装,定义了NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED这些线程状态,与操作系统中的线程有一些区别:对于在操作系统中线程的运行/就绪状态,java语言中统一用RUNNABLE状态来表示。对于在操作系统中线程的阻塞状态,java语言中用BLOCKED、WAITING和TIME_WAITING这三个状态分别表示。题外话写文章挺累的,有时候你觉得阅读挺流畅的,那其实是背后无数次修改的结果。如果你觉得不错请帮忙转发一下,万分感谢~ ...

April 18, 2019 · 3 min · jiezi

Java并发编程之设计线程安全的类

设计线程安全的类前边我们对线程安全性的分析都停留在一两个可变共享变量的基础上,真实并发程序中可变共享变量会非常多,在出现安全性问题的时候很难准确定位是哪块儿出了问题,而且修复问题的难度也会随着程序规模的扩大而提升(因为在程序的各个位置都可以随便使用可变共享变量,每个操作都可能导致安全性问题的发生)。比方说我们设计了一个这样的类:public class Increment { private int i; public void increase() { i++; } public int getI() { return i; }}然后有很多客户端程序员在多线程环境下都使用到了这个类,有的程序员很聪明,他在调用increase方法时使用了适当的同步操作:public class RightUsageOfIncrement { public static void main(String[] args) { Increment increment = new Increment(); Thread[] threads = new Thread[20]; //创建20个线程 for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { synchronized (RightUsageOfIncrement.class) { // 使用Class对象加锁 increment.increase(); } } } }); threads[i] = t; t.start(); } for (int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(increment.getI()); }}在调用Increment的increase方法的时候,使用RightUsageOfIncrement.class这个对象作为锁,有效的对i++操作进行了同步,的确不错,执行之后的结果是:2000000可是并不是每个客户端程序员都会这么聪明,有的客户端程序员压根儿不知道啥叫个同步,所以写成了这样:public class WrongUsageOfIncrement { public static void main(String[] args) { Increment increment = new Increment(); Thread[] threads = new Thread[20]; //创建20个线程 for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { increment.increase(); //没有进行有效的同步 } } }); threads[i] = t; t.start(); } for (int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(increment.getI()); }}没有进行有效同步的执行结果是(每次执行都可能不一样):1815025其实对于Increment这个类的开发者来说,本质上是把对可变共享变量的必要同步操作转嫁给客户端程序员处理。有的情况下我们希望自己设计的类可以让客户端程序员们不需要使用额外的同步操作就可以放心的在多线程环境下使用,我们就把这种类成为线程安全类。其实就是类库设计者把一些在多线程环境下可能导致安全性问题的操作封装到类里边儿,比如Increment的increase方法,我们可以写成这样:public synchronized void increase() { i++;}也就是说把对可变共享变量i可能造成多线程安全性问题的i++操作在Increment类内就封装好,其他人直接调用也不会出现安全性问题。使用封装也是无奈之举:你无法控制其他人对你的代码调用,风险始终存在,封装使无意中破坏设计约束条件变得更难。封装变量访问找出共享、可变的字段设计线程安全类的第一步就是要找出所有的字段,这里的字段包括静态变量也包括成员变量,然后再分析这些字段是否是共享并且可变的。首先辨别一下字段是否是共享的。由于我们无法控制客户端程序员以怎样的方式来使用这个类,所以我们可以通过访问权限,也就是public权限、protected权限、 默认权限以及private权限来控制哪些代码是可以被客户端程序员调用的,哪些是不可以调用的。一般情况下,我们需要把所有字段都声明为 private 的,把对它们的访问都封装到方法中,对这些方法再进行必要的同步控制,也就是说我们只暴露给客户端程序员一些可以调用的方法来间接的访问到字段,因为如果直接把字段暴露给客户端程序员的话,我们无法控制客户端程序员如何使用该字段,比如他可以随意的在多线程环境下对字段进行累加操作,从而不能保证把所有同步逻辑都封装到类中。所以如果一个字段是可以通过对外暴露的方法访问到,那这个字段就是共享的。然后再看一下字段是否是可变的。如果该字段的类型是基本数据类型,可以看一下类所有对外暴露的方法中是否有修改该字段值的操作,如果有,那这个字段就是可变的。如果该字段的类型是非基本数据类型的,那这个字段可变就有两层意思了,第一是在对外暴露的方法中有直接修改引用的操作,第二是在对外暴露的方法中有直接修改该对象中字段的操作。比如一个类长这样:public class MyObj { private List<String> list; public void m1() { list = new ArrayList<>(); //直接修改字段指向的对象 } public void m2() { list[0] = “aa”; //修改该字段指向对象的字段 }}代码中的m1和m2都可以算做是修改字段list,如果类暴露的方法中有这两种修改方式中的任意一种,就可以算作这个字段是可变的。小贴士:是不是把字段声明成final类型,该字段就不可变了呢?如果该字段是基本数据类型,那声明为final的确可以保证在程序运行过程中不可变,但是如果该字段是非基本数据类型,那么需要让该字段代表的对象中的所有字段都是不可变字段才能保证该final字段不可变。所以在使用字段的过程中,应该尽可能的让字段不共享或者不可变,不共享或者不可变的字段才不会引起安全性问题哈哈。这让我想起了一句老话:只有死人才不会说话~用锁来保护访问确定了哪些字段必须是共享、可变的之后,就要分析在哪些对外暴露的方法中访问了这些字段,我们需要在所有的访问位置都进行必要的同步处理,这样才可以保证这个类是一个线程安全类。通常,我们会使用锁来保证多线程在访问共享可变字段时是串行访问的。但是一种常见的错误就是:只有在写入共享可变字段时才需要使用同步,就像这样:public class Test { private int i; public int getI() { return i; } public synchronized void setI(int i) { this.i = i; }}为了使Test类变为线程安全类,也就是需要保证共享可变字段i在所有外界能访问的位置都是线程安全的,而上边getI方法可以访问到字段i,却没有进行有效的同步处理,由于内存可见性问题的存在,在调用getI方法时仍有可能获取的是旧的字段值。所以再次强调一遍:我们需要在所有的访问位置都进行必要的同步处理。使用同一个锁还有一点需要强调的是:如果使用锁来保护共享可变字段的访问的话,对于同一个字段来说,在多个访问位置需要使用同一个锁。我们知道如果多个线程竞争同一个锁的话,在一个线程获取到锁后其他线程将被阻塞,如果是使用多个锁来保护同一个共享可变字段的话,多个线程并不会在一个线程访问的时候阻塞等待,而是会同时访问这个字段,我们的保护措施就变得无效了。一般情况下,在一个线程安全类中,我们使用同步方法,也就是使用this对象作为锁来保护字段的访问就OK了~。封不封装取决于你的心情虽然面向对象技术封装了安全性,但是打破这种封装也没啥不可以,只不过安全性会更脆弱,增加开发成本和风险。也就是说你把字段声明为public访问权限也没人拦得住你,当然你也可能因为某种性能问题而打破封装,不过对于我们实现业务的人来说,还是建议先使代码正确运行,再考虑提高代码执行速度吧~。不变性条件现实中有些字段之间是有实际联系的,比如说下边这个类:public class SquareGetter { private int numberCache; //数字缓存 private int squareCache; //平方值缓存 public int getSquare(int i) { if (i == numberCache) { return squareCache; } int result = ii; numberCache = i; squareCache = result; return result; } public int[] getCache() { return new int[] {numberCache, squareCache}; }}这个类提供了一个很简单的getSquare功能,可以获取指定参数的平方值。但是它的实现过程使用了缓存,就是说如果指定参数和缓存的numberCache的值一样的话,直接返回缓存的squareCache,如果不是的话,计算参数的平方,然后把该参数和计算结果分别缓存到numberCache和squareCache中。从上边的描述中我们可以知道,squareCache不论在任何情况下都是numberCache平方值,这就是SquareGetter类的一个不变性条件,如果违背了这个不变性条件的话,就可能会获得错误的结果。在单线程环境中,getSquare方法并不会有什么问题,但是在多线程环境中,numberCache和squareCache都属于共享的可变字段,而getSquare方法并没有提供任何同步措施,所以可能造成错误的结果。假设现在numberCache的值是2,squareCache的值是3,一个线程调用getSquare(3),另一个线程调用getSquare(4),这两个线程的一个可能的执行时序是:两个线程执行过后,最后numberCache的值是4,而squareCache的值竟然是9,也就意味着多线程会破坏不变性条件。为了保持不变性条件,我们需要把保持不变性条件的多个操作定义为一个原子操作,即用锁给保护起来。我们可以这样修改getSquare方法的代码:public synchronized int getSquare(int i) { if (i == numberCache) { return squareCache; } int result = ii; numberCache = i; squareCache = result; return result;}但是不要忘了将代码都放在同步代码块是会造成阻塞的,能不进行同步,就不进行同步,所以我们修改一下上边的代码:public int getSquare(int i) { synchronized(this) { if (i == numberCache) { // numberCache字段的读取需要进行同步 return squareCache; } } int result = i*i; //计算过程不需要同步 synchronized(this) { // numberCache和squareCache字段的写入需要进行同步 numberCache = i; squareCache = result; } return result;}虽然getSquare方法同步操作已经做好了,但是别忘了SquareGetter类的getCache方法也访问了numberCache和squareCache字段,所以对于每个包含多个字段的不变性条件,其中涉及的所有字段都需要被同一个锁来保护,所以我们再修改一下getCache方法:public synchronized int[] getCache() { return new int[] {numberCache, squareCache};}这样修改后的SquareGetter类才属于一个线程安全类。使用volatile修饰状态使用锁来保护共享可变字段虽然好,但是开销大。使用volatile修饰字段来替换掉锁是一种可能的考虑,但是一定要记住volatile是不能保证一系列操作的原子性的,所以只有我们的业务场景符合下边这两个情况的话,才可以考虑:对变量的写入操作不依赖当前值,或者保证只有单个线程进行更新。该变量不需要和其他共享变量组成不变性条件。比方说下边的这个类:public class VolatileDemo { private volatile int i; public int getI() { return i; } public void setI(int i) { this.i = i; }}VolatileDemo中的字段i并不和其他字段组成不变性条件,而且对于可以访问这个字段的方法getI和setI来说,并不需要以来i的当前值,所以可以使用volatile来修饰字段i,而不用在getI和setI的方法上使用锁。避免this引用逸出我们先来看一段代码:public class ExplicitThisEscape { private final int i; public static ThisEscape INSTANCE; public ThisEscape() { INSTANCE = this; i = 1; }}在构造方法中就把this引用给赋值到了静态变量INSTANCE中,而别的线程是可以随时访问INSTANCE的,我们把这种在对象创建完成之前就把this引用赋值给别的线程可以访问的变量的这种情况称为 this引用逸出,这种方式是极其危险的!,这意味着在ThisEscape对象创建完成之前,别的线程就可以通过访问INSTANCE来获取到i字段的信息,也就是说别的线程可能获取到字段i的值为0,与我们期望的final类型字段值不会改变的结果是相违背的。所以千万不要在对象构造过程中使this引用逸出。上边的this引用逸出是通过显式将this引用赋值的方式导致逸出的,也可能通过内部类的方式神不知鬼不觉的造成this引用逸出:public class ImplicitThisEscape { private final int i; private Thread t; public ThisEscape() { t = new Thread(new Runnable() { @Override public void run() { // … 具体的任务 } }); i = 1; }}虽然在ImplicitThisEscape的构造方法中并没有显式的将this引用赋值,但是由于Runnable内部类的存在,作为外部类的ImplicitThisEscape,内部类对象可以轻松的获取到外部类的引用,这种情况下也算this引用逸出。this引用逸出意味着创建对象的过程是不安全的,在对象尚未创建好的时候别的线程就可以来访问这个对象。虽然我们不确定客户端程序员会怎么使用这个逸出的this引用,但是风险始终存在,所以强烈建议千万不要在对象构造过程中使this引用逸出。总结客户端程序员不靠谱,我们有必要把线程安全性封装到类中,只给客户端程序员提供线程安全的方法。认真找出代码中既共享又可变的变量,并把它们使用锁来保护起来,同一个字段的多个访问位置需要使用同一个锁来保护。对于每个包含多个字段的不变性条件,其中涉及的所有字段都需要被同一个锁来保护。在对变量的写入操作不依赖当前值以及该变量不需要和其他共享变量组成不变性条件的情况下可以考虑使用volatile变量来保证并发安全。千万不要在对象构造过程中使this引用逸出。 ...

April 18, 2019 · 3 min · jiezi

Java并发编程之指令重排序

指令重排序如果说内存可见性问题已经让你抓狂了,那么下边的这个指令重排序的事儿估计就要骂娘了~这事儿还得从一段代码说起:public class Reordering { private static boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { Thread.yield(); } System.out.println(num); } }, “t1”); t1.start(); num = 5; flag = true; }}需要注意到flag并不是一个volatile变量,也就是说它存在内存可见性问题,但是即便如此,num = 5也是写在flag = true的前边的,等到t1线程检测到了flag值的变化,num值的变化应该是早于flag值刷新到主内存的,所以线程t1最后的输出结果肯定是5!!!no!no!no! 输出的结果也可能是0,也就是说flag = true可能先于num = 5执行,有没有亮瞎你的狗眼~ 这些代码最后都会变成机器能识别的二进制指令,我们把这种指令不按书写顺序执行的情况称为指令重排序。大多数现代处理器都会采用将指令乱序执行的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。Within-Thread As-If-Serial Semantics既然存在指令重排序这种现象,为什么我们之前写代码从来没感觉到呢?到了多线程这才发现问题?指令重排序不是随便排,一个一万行的程序直接把最后一行当成第一行就给执行那不就逆天了了么,指令重排序是需要遵循代码依赖情况的。比如下边几行代码:int i = 0, b = 0;i = i + 5; //指令1i = i*2; //指令2b = b + 3; //指令3对于上边标注的3个指令来说,指令2是对指令1有依赖的,所以指令2不能被排到指令1之前执行。但是指令3跟指令1和指令2都没有关系,所以指令3可以被排在指令1之前,或者指令1和指令2中间或者指令2后边执行都可以~ 这样在单线程中执行这段代码的时候,最终结果和没有重排序的执行结果是一样的,所以这种重排序有着Within-Thread As-If-Serial Semantics的含义,翻译过来就是线程内表现为串行的语义。但是这种指令重排序在单线程中没有任何问题的,但是在多线程中,就引发了我们上边在执行flag = true后,num的值仍然不能确定是0还是5~抑制重排序在多线程并发编程的过程中,执行重排序有时候会造成错误的后果,比如一个线程在main线程中调用setFlag(true)的前边修改了某些程序配置项,而在t1线程里需要用到这些配置项,所以会造成配置缺失的错误。但是java给我们提供了一些抑制指令重排序的方式。同步代码抑制指令重排序将需要抑制指令重排序的代码放入同步代码块中:public class Reordering { private static boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!getFlag()) { Thread.yield(); } System.out.println(num); } }, “t1”); t1.start(); num = 5; setFlag(true); } public synchronized static void setFlag(boolean flag) { Reordering.flag = flag; } public synchronized static boolean getFlag() { return flag; }}在获取锁的时候,它前边的操作必须已经执行完成,不能和同步代码块重排序;在释放锁的时候,同步代码块中的代码必须全部执行完成,不能和同步代码块后边的代码重排序。加了锁之后,num=5就不能和flag=true的代码进行重排序了,所以在线程2中看到的num值肯定是5,而不会是0喽~虽然抑制重排序可以保证多线程程序按照我们期望的执行顺序进行执行,但是它抑制了处理器对指令执行的优化,原来能并行执行的指令现在只能串行执行,会导致一定程度的性能下降,所以加锁只能保证在执行同步代码块时,它之前的代码已经执行完成,在同步代码块执行完成之前,代码块后边的代码是不能执行的,也就是只保证加锁前、加锁中、加锁后这三部分的执行时序,但是同步代码块之前的代码可以重排序,同步代码块中的代码可以重排序,同步代码块之后的代码也可以进行重排序,在保证执行顺序的基础上,尽最大可能让性能得到提升,比方说下边这段代码:int i = 1;int j = 2;synchronized (Reordering.class) { int m = 3; int n = 4;}int x = 5;int y = 6;它的一个执行时序可能是:volatile变量抑制指令重排序还是那句老话,加锁会导致竞争同一个锁的线程阻塞,造成线程切换,代价比较大,volatile变量也提供了一些抑制指令重排序的语义,上边的程序可以改成这样:public class Reordering { private static volatile boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { Thread.yield(); } System.out.println(num); } }); t1.start(); num = 5; flag = true; }}也就是把flag声明为volatile变量,这样也能起到抑制重排序的效果,volatile变量具体抑制重排序的规则如下:1. volatile写之前的操作不会被重排序到volatile写之后。2. volatile读之后的操作不会被重排序到volatile读之前。3. 前边是volatile写,后边是volatile读,这两个操作不能重排序。![图片描述][3]除了这三条规定以外,其他的操作可以由处理器按照自己的特性进行重排序,换句话说,就是怎么执行着快,就怎么来。比如说:flag = true;num = 5;在volatile变量之后进行普通变量的写操作,那就可以重排序喽,直到遇到一条volatile读或者有执行依赖的代码才会阻止重排序的过程。final变量抑制指令重排序在java语言中,用final修饰的字段被赋予了一些特殊的语义,它可以阻止某些重排序,具体的规则就这两条:在构造方法内对一个final字段的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。初次读一个包含final字段对象的引用,与随后初次读这个final字段,这两个操作不能重排序。可能大家看的有些懵逼,赶紧写代码理解一下:public class FinalReordering { int i; final int j; static FinalReordering obj; public FinalReordering() { i = 1; j = 2; } public static void write() { obj = new FinalReordering(); } public static void read() { FinalReordering finalReordering = FinalReordering.obj; int a = finalReordering.i; int b = finalReordering.j; }}我们假设有一个线程执行write方法,另一个线程执行read方法。先看一下对final字段进行写操作时,不同线程执行write方法和read方法的一种可能情况是:从上图中可以看出,普通的字段可能在构造方法完成之后才被真正的写入值,所以另一个线程在访问这个普通变量的时候可能读到了0,这显然是不符合我们的预期的。但是final字段的赋值不允许被重排序到构造方法完成之后,所以在把该字段所在对象的引用赋值出去之前,final字段肯定是被赋值过了,也就是说这两个操作不能被重排序。再来看一下初次读取final字段的情况,下边是不同线程执行write方法和read方法的一种可能情况:从上图可以看出,普通字段的读取操作可能被重排序到读取该字段所在对象引用前边,自然会得到NullPointerException异常喽,但是对于final字段,在读final字段之前,必须保证它前边的读操作都执行完成,也就是说必须先进行该字段所在对象的引用的读取,再读取该字段,也就是说这两个操作不能进行重排序。值得注意的是,读取对象引用与读取该对象的字段是存在间接依赖的关系的,对象引用都没有被赋值,还读个锤子对象的字段喽,一般的处理器默认是不会重排序这两个操作的,可是有一些为了性能不顾一切的处理器,比如alpha处理器,这种处理器是可能把这两个操作进行重排序的,所以这个规则就是给这种处理器贴身设计的~ 也就是说对于final字段,不管在什么处理器上,都得先进行对象引用的读取,再进行final字段的读取。但是并不保证在所有处理器上,对于对象引用读取和普通字段读取的顺序是有序的。安全性小结我们上边介绍了原子性操作、内存可见性以及指令重排序三个在多线程执行过程中会影响到安全性的问题。synchronized可以把三个问题都解决掉,但是伴随着这种万能特性,是多线程在竞争同一个锁的时候会造成线程切换,导致线程阻塞,这个对性能的影响是非常大的。volatile不能保证一系列操作的原子性,但是可以保证对于一个变量的读取和写入是原子性的,一个线程对某个volatile变量的写入是可以立即对其他线程可见的,另外,它还可以禁止处理器对一些指令执行的重排序。final变量依靠它的禁止重排序规则,保证在使用过程中的安全性。一旦被赋值成功,它的值在之后程序执行过程中都不会改变,也不存在所谓的内存可见性问题。 ...

April 18, 2019 · 2 min · jiezi

Java并发编程之原子性操作

上头一直在说以线程为基础的并发编程的好处了,什么提高处理器利用率啦,简化编程模型啦。但是砖家们还是认为并发编程是程序开发中最不可捉摸、最诡异、最扯犊子、最麻烦、最恶心、最心烦、最容易出错、最不符合社会主义核心价值观的一个部分~ 造成这么多最的原因其实很简单:进程中的各种资源,比如内存和I/O,在代码里以变量的形式展现,而某些变量在多线程间是共享、可变的,共享意味着这个变量可以被多个线程同时访问,可变意味着变量的值可能被访问它的线程修改。围绕这些共享、可变的变量形成了并发编程的三大杀手:安全性、活跃性、性能,下边我们来详细唠叨这些风险~共享变量的含义并不是所有内存变量都可以被多个线程共享,在一个线程调用一个方法的时候,会在栈内存上为局部变量以及方法参数申请一些内存,在方法调用结束的时候,这些内存便被释放。不同线程调用同一个方法都会为局部变量和方法参数拷贝一个副本(如果你忘了,需要重新学习一下方法的调用过程),所以这个栈内存是线程私有的,也就是说局部变量和方法参数是不可以共享的。但是对象或者数组是在堆内存上创建的,堆内存是所有线程都可以访问的,所以包括成员变量、静态变量和数组元素是可共享的,我们之后讨论的就是这些可以被共享的变量对并发编程造成的风险~ 如果不强调的话,我们下边所说的变量都代表成员变量、静态变量或者数组元素。安全性原子性操作、内存可见性和指令重排序是构成线程安全性的三个主题,下边我们详细看哈~原子性操作我们先拿一个例子开场:public class Increment { private int i; public void increase() { i++; } public int getI() { return i; } public static void test(int threadNum, int loopTimes) { Increment increment = new Increment(); Thread[] threads = new Thread[threadNum]; for (int i = 0; i < threads.length; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < loopTimes; i++) { increment.increase(); } } }); threads[i] = t; t.start(); } for (Thread t : threads) { //main线程等待其他线程都执行完成 try { t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(threadNum + “个线程,循环” + loopTimes + “次结果:” + increment.getI()); } public static void main(String[] args) { test(20, 1); test(20, 10); test(20, 100); test(20, 1000); test(20, 10000); test(20, 100000); }}其中,increase方法的作用是给成员变量i增1,test方法接受两个参数,一个是线程的数量,一个是循环的次数,每个线程中都有一个将成员变量i增1给定循环次数的任务,在所有线程的任务都完成之后,输出成员变量i的值,如果没有什么问题的话,程序执行完成后成员变量i的值都是threadNum*loopTimes。大家看一下执行结果:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:1992620个线程,循环10000次结果:11990320个线程,循环100000次结果:1864988咦,貌似有点儿不对劲唉~再次执行一遍的结果:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:1950220个线程,循环10000次结果:10015720个线程,循环100000次结果:1833170这就更令人奇怪了~~ 当循环次数增加时,执行结果与我们预期不一致,而且每次执行貌似都是不一样的结果,这个是个什么鬼?答:这个就是多线程的非原子性操作导致的一个不确定结果。啥叫个原子性操作呢?就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。java中自带了一些原子性操作,比如给一个非long、double基本数据类型变量或者引用的赋值或者读取操作。为什么强调非long、double类型的变量?我们稍后看哈~那i++这个操作不是一个原子性操作么?答:还真不是,这个操作其实相当于执行了i = i + 1,也就是三个原子性操作:读取变量i的值将变量i的值加1将结果写入i变量中由于线程是基于处理器分配的时间片执行的,在这个过程中,这三个步骤可能让多个线程交叉执行,为简化过程,我们以两个线程交叉执行为例,看下图:这个图的意思就是:线程1执行increase方法先读取变量i的值,发现是5,此时切换到线程2执行increase方法读取变量i的值,发现也是5。线程1执行将变量i的值加1的操作,得到结果是6,线程二也执行这个操作。线程1将结果赋值给变量i,线程2也将结果赋值给变量i。在这两个线程都执行了一次increase方法之后,最后的结果竟然是变量i从5变到了6,而不是我们想象中的7。。。另外,由于CPU的速度非常快,这种交叉执行在执行次数较低的时候体现的并不明显,但是在执行次数多的时候就十分明显了,从我们上边测试的结果上就能看出。在真实编程环境中,我们往往需要某些涉及共享、可变变量的一系列操作具有原子性,我们可以从下边三个角度来保证这些操作具有原子性。从共享性解决如果一个变量变得不可以被多线程共享,不就可以随便访问了呗哈哈,大致有下面这么两种改进方案。尽量使用局部变量解决问题因为方法中的局部变量(包括方法参数和方法体中创建的变量)是线程私有的,所以无论多少线程调用某个不涉及共享变量的方法都是安全的。所以如果能将问题转换为使用局部变量解决问题而不是共享变量解决,那将是极好的哈~。不过我貌似想不出什么案例来说明一下,等想到了再说哈,各位想到了也可以告诉我哈。使用ThreadLocal类为了维护一些线程内可以共享的数据,java提出了一个ThreadLocal类,它提供了下边这些方法:public class ThreadLocal<T> { protected T initialValue() { return null; } public void set(T value) { … } public T get() { … } public void remove() { … }}其中,类型参数T就代表了在同一个线程中共享数据的类型,它的各个方法的含义是:T initialValue():当某个线程初次调用get方法时,就会调用initialValue方法来获取初始值。void set(T value):调用当前线程将指定的value参数与该线程建立一对一关系(会覆盖initialValue的值),以便后续get方法获取该值。T get():获取与当前线程建立一对一关系的值。void remove():将与当前线程建立一对一关系的值移除。我们可以在同一个线程里的任何代码处存取该类型的值:public class ThreadLocalDemo { public static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){ @Override protected String initialValue() { return “调用initialValue方法初始化的值”; } }; public static void main(String[] args) { ThreadLocalDemo.THREAD_LOCAL.set(“与main线程关联的字符串”); new Thread(new Runnable() { @Override public void run() { System.out.println(“t1线程从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); ThreadLocalDemo.THREAD_LOCAL.set(“与t1线程关联的字符串”); System.out.println(“t1线程再次从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); } }, “t1”).start(); System.out.println(“main线程从ThreadLocal中获取的值:” + ThreadLocalDemo.THREAD_LOCAL.get()); }}执行结果是:main线程从ThreadLocal中获取的值:与main线程关联的字符串t1线程从ThreadLocal中获取的值:调用initialValue方法初始化的值t1线程再次从ThreadLocal中获取的值:与t1线程关联的字符串从这个执行结果我们也可以看出来,不同线程操作同一个 ThreadLocal 对象执行各种操作而不会影响其他线程里的值。这一点非常有用,比如对于一个网络程序,通常每一个请求都分配一个线程去处理,可以在ThreadLocal里记录一下这个请求对应的用户信息,比如用户名,登录失效时间什么的,这样就很有用了。虽然ThreadLocal很有用,但是它作为一种线程级别的全局变量,如果某些代码依赖它的话,会造成耦合,从而影响了代码的可重用性,所以设计的时候还是要权衡一下子滴。从可变性解决如果一个变量可以被共享,但是它自打被创建之后就不能被修改,那么随意哪个线程去访问都可以哈,反正又不能改变它的值,随便读啦~再强调一遍,我们写的程序可能不仅我们自己会用,所以我们不能靠猜、靠直觉、靠信任其他使用我们写的代码的客户端程序猿,所以如果我们想通过让对象不可变的方式来保证线程安全,那就把该变量声明为 final 的吧 :public class FinalDemo { private final int finalField; public FinalDemo(int finalField) { this.finalField = finalField; }}然后就可以随便在多线程间共享finalField这个变量喽~加锁解决锁的概念如果我们的需求确实是需要共享并且可变的变量,又想让某些关于这个变量的操作是原子性的,还是以上边的increase方法为例,我们现在面临的困境是increase方法其实是由下边3个原子性操作累积起来的一个操作:读变量i;运算;写变量i;针对同一个变量i,不同线程可能交叉执行上边的三个步骤,导致两个线程读到同样的变量i的值,从而导致结果比预期的小。为了让increase方法里的操作具有原子性,也就是在一个线程执行这一系列操作的同时禁止其他线程执行这些操作,java提出了锁的概念。我们拿上厕所做一个例子,比如我们上厕所需要这几步:脱裤子干正事儿擦屁股提裤子上厕所的时候必须把这些步骤都执行完了,才能圆满的完成上厕所这个事儿,要不然执行到擦屁股环节被别人赶出来岂不是贼尴尬????,所以为了能安全的完成上厕所这个事儿,我们不得不在进入厕所之后,就拿一把锁把厕所门给锁了,等提完裤子走出厕所的时候再把锁给打开,让其他人来上厕所。同步代码块java语言里把锁给做了个抽象,任何一个对象都可以作为一个锁,也称为内置锁,某个线程在进入某个代码块的时候去获取一个锁,在退出该代码块的时候把锁给释放掉,我们来修改一下Increment的代码:public class Increment { private int i; private Object lock = new Object(); public void increase() { synchronized (lock) { i++; } } public int getI() { synchronized (lock) { return i; } } public static void test(int threadNum, int loopTimes) { // … 为节省篇幅,省略此处代码,与上边的一样 } public static void main(String[] args) { test(20, 1); test(20, 10); test(20, 100); test(20, 1000); test(20, 10000); test(20, 100000); }}对i++加锁之后的代码执行结果是:20个线程,循环1次结果:2020个线程,循环10次结果:20020个线程,循环100次结果:200020个线程,循环1000次结果:2000020个线程,循环10000次结果:20000020个线程,循环100000次结果:2000000哈哈,这回就符合预期了,如果你不信可以多执行几遍试试。我们再回过头来看这个加锁的语法:synchronized (锁对象) { 需要保持原子性的一系列代码}如果一个线程获取某个锁之后,就相当于把厕所门儿给锁上了,其他的线程就不能获取该锁了,进不去厕所只能干等着,也就是这些线程处于一种阻塞状态,直到已经获取锁的线程把该锁给释放掉,也就是把厕所门再打开,某个线程就可以再次获得锁了。这样线程们按照获取锁的顺序执行的方式也叫做同步执行(英文名就是synchronized),这个被锁保护的代码块也叫做同步代码块,我们也会说这段代码被这个锁保护。由于如果线程没有获得锁就会阻塞在同步代码块这,所以我们需要格外注意的是,在同步代码块中的代码要尽量的短,不要把不需要同步的代码也加入到同步代码块,在同步代码块中千万不要执行特别耗时或者可能发生阻塞的一些操作,比如I/O操作啥的。为什么一个对象就可以当作一个锁呢?我们知道一个对象会占据一些内存,这些内存地址可是唯一的,也就是说两个对象不能占用相同的内存。真实的对象在内存中的表示其实有对象头和数据区组成的,数据区就是我们声明的各种字段占用的内存部分,而对象头里存储了一系列的有用信息,其中就有几个位代表锁信息,也就是这个对象有没有作为某个线程的锁的信息。详细情况我们会在JVM里详细说明,现在大家看个乐呵,了解用一个对象作为锁是有底层依据的哈~锁的重入我们前边说过,当一个线程请求获得已经被其他线程获得的锁的时候,它就会被阻塞,但是如果一个线程请求一个它已经获得的锁,那么这个请求就会成功。public class SynchronizedDemo { private Object lock = new Object(); public void m1() { synchronized (lock) { System.out.println(“这是第一个方法”); m2(); } } public void m2() { synchronized (lock) { System.out.println(“这是第二个方法”); } } public static void main(String[] args) { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); synchronizedDemo.m1(); }}执行结果是:这是第一个方法这是第二个方法也就是说只要一个线程持有了某个锁,那么它就可以进入任何被这个锁保护的代码块。小贴士:这样的重入锁实现起来也简单,可以给每个锁关联一个持有的线程和获取锁的次数,初始状态下锁的计数值是0,也就是没有被任何线程持有锁,当某个线程获取这个锁的时候,计数值为1,如果继续获取该锁,那么计数值继续递增,每次退出一个同步代码块时,计数值递减,直到递减到0为止。同步方法我们前边说为了创建一个同步代码块,必须显式的指定一个对象作为锁,有时候我们想把整个方法的操作都写入同步代码块,就像我们上边说过的increase方法,这种情况下其实有个偷懒的办法,因为我们的程序中默默的藏着某些对象~对于成员方法来说,我们可以直接用this作为锁。对于静态方法来说,我们可以直接用Class对象作为锁(Class对象可以直接在任何地方访问,如果不知道的话需要重新学一下反射了亲)。就像这样:public class Increment { private int i; public void increase() { synchronized (this) { //使用this作为锁 i++; } } public static void anotherStaticMethod() { synchronized (Increment.class) { //使用Class对象作为锁 // 此处填写需要同步的代码块 } }}为了简便起见,设计java的大叔们规定整个方法的操作都需要被同步,而且使用this作为锁的成员方法,使用Class对象作为锁的静态方法,就可以被简写成这样:public class Increment { private int i; public synchronized increase() { //使用this作为锁 i++; } public synchronized static void anotherStaticMethod() { //使用Class对象作为锁 // 此处填写需要同步的代码块 }}再写一遍通用格式,大家长长记性:public synchronized 返回类型 方法名(参数列表) { 需要被同步执行的代码}public synchronized static 返回类型 方法名(参数列表) { 需要被同步执行的代码}上述的两种方法也被称为同步方法,也就是说整个方法都需要被同步执行,而且使用的锁是this对象或者Class对象。注意,同步方法只不过是同步代码块的另一种写法,没什么稀奇的~。总结共享、可变的变量形成了并发编程的三大杀手:安全性、活跃性、性能,本章详细讨论安全性问题。本文中的共享变量指的是在堆内存上创建的对象或者数组,包括成员变量、静态变量和数组元素。安全性问题包括三个方面,原子性操作、内存可见性和指令重排序,本篇文章主要对原子性操作进行详细讨论。原子性操作就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。为了保证某些操作的原子性,提出了下边几种解决方案:尽量使用局部变量解决问题使用ThreadLocal类解决问题从共享性解决,在编程时,最好使用下边这两种方案解决问题:从可变性解决,最好让某个变量在程序运行过程中不可变,把它使用final修饰。加锁解决任何一个对象都可以作为一个锁,也称为内置锁。某个线程在进入某个同步代码块的时候去获取一个锁,在退出该代码块的时候把锁给释放掉。锁的重入是指只要一个线程持有了某个锁,那么它就可以进入任何被这个锁保护的代码块。同步方法是一种比较特殊的同步代码块,对于成员方法来讲,使用this作为锁对象,对于静态方法来说,使用Class对象作为锁对象。 ...

April 18, 2019 · 2 min · jiezi

最新java并发编程高级面试30题:并发队列+可重入锁+线程池+Synchronized

众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java程序员是必须对并发编程这块有所了解的。然而不论是哪个国家,什么背景的 Java 开发者,都对自己写的并发程序相当自信,但也会在出问题时表现得很诧异甚至一筹莫展。可见,Java 并发编程显然不是一件能速成的能力,基础搭得越好,越全面,在实践中才会有更深刻的理解。因此,大家不难发现 Java 并发问题一直是各个大厂面试的重点之一。我在平时的面试中, 也发现很多候选人对一些基本的并发概念表示没听过,或原理不理解,可能知道一些却又讲不清楚,最终导致面试失败。本文会结合实际中接触到的一些面试题,重点来聊一聊 Java 并发中的相关知识点。Synchronized 相关问题Synchronized 用过吗,其原理是什么?你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?什么是可重入性,为什么说 Synchronized 是可重入锁?JVM 对 Java 的原生锁做了哪些优化?为什么说 Synchronized 是非公平锁?什么是锁消除和锁粗化?为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是乐观锁一定就是好的吗?可重入锁 ReentrantLock 及其他显式锁相关问题跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?那么请谈谈 AQS 框架是怎么回事儿?请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。ReentrantLock 是如何实现可重入性的?除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?请谈谈 ReadWriteLock 和 StampedLock。如何让 Java 的线程彼此同步?你了解过哪些同步器?请分别介绍下。CyclicBarrier 和 CountDownLatch 看起来很相似,请对比下呢?Java 内存模型相关问题什么是 Java 的内存模型,Java 中各个线程是怎么彼此看到对方的变量的?请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?请对比下 volatile 对比 Synchronized 的异同。请谈谈 ThreadLocal 是怎么解决并发安全的?很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?并发队列相关问题谈下对基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理的理解?ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程安全?基于链表的阻塞队列 LinkedBlockingQueue 原理。阻塞队列LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线程先入队出队操作的线程安全?为什么不使用一把锁,使用两把为何能提高并发度?基于数组的阻塞队列 ArrayBlockingQueue 原理。ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全?谈谈对无界优先级队列 PriorityBlockingQueue 原理?PriorityBlockingQueue内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的?如何学习并发编程学习java并发就像进入了另外一个学习领域,就像学习一门新的编程语言,或者是学习一套新的语言概念,要理解并发编程,其难度跟理解面向对象编程难度差不多。你花一点功夫,就可以理解它的基本机制,但是要想真正掌握它的本质,就需要深入的学习与理解。 最后在分享一个并发编程知识的学习导图给大家,为了方便大家能看的清楚我把Xmind图缩略了 ...

April 11, 2019 · 1 min · jiezi

[Java并发-4]关于Java的死锁

[Java并发-4]关于Java的死锁

April 10, 2019 · 1 min · jiezi

[Java并发-3]Java互斥锁,解决原子性问题

在前面的分享中我们提到。一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”思考:在32位的机器上对long型变量进行加减操作存在并发问题,什么原因!?原子性问题如何解决我们已经知道原子性问题是线程切换,如果能够禁用线程切换不就解决了这个问题了嘛?而操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。在单核 CPU 时代,这个方案的确是可行的。这里我们以 32 位 CPU 上执行 long 型变量的写操作为例来说明这个问题,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作(写高 32 位和写低 32 位,如下图所示)。图在单核 CPU 场景下,同一时刻只有一个线程执行,禁止 CPU 中断,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性。但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,还是会出现问题。同一时刻只有一个线程执行这个条件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。简易锁模型互斥的解决方案,锁。大家脑中的模型可能是这样的。图线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。这样理解本身没有问题,但却很容易让我们忽视两个非常非常重要的点:我们锁的是什么?我们保护的又是什么?改进后的锁模型我们知道在现实世界里,锁和锁要保护的资源是有对应关系的,比如我用我家的锁保护我家的东西。在并发编程世界里,锁和资源也应该有这个关系,但这个关系在我们上面的模型中是没有体现的,所以我们需要完善一下我们的模型。图首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,增加了一条连线,这个关联关系非常重要,这里很容易发生BUG,容易出现了类似锁自家门来保护他家资产的事情。Java语言提供的锁锁是一种通用的技术方案,Java 语言提供的synchronized 关键字,就是锁的一种实现。synchronized关键字可以用来修饰方法,也可以用来修饰代码块,基本使用:class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } }} 参考我们上面提到的模型,加锁 lock() 和解锁 unlock() 这两个操作在Java 编译会自动加上。这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的。上面的代码我们看到只有修饰代码块的时候,锁定了一个 obj 对象,那修饰方法的时候锁定的是什么呢?这个也是 Java 的一条隐式规则:当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;当修饰非静态方法的时候,锁定的是当前实例对象 this。class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 }}class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 }}锁解决 count+1 问题我们来尝试下用synchronized解决之前遇到的 count+=1 存在的并发问题,代码如下所示。SafeCalc 这个类有两个方法:一个是 get() 方法,用来获得 value 的值;另一个是 addOne() 方法,用来给 value 加 1,并且 addOne() 方法我们用 synchronized 修饰。那么我们使用的这两个方法有没有并发问题呢?class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; }}我们先来看看 addOne() 方法,首先可以肯定,被 synchronized 修饰后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行 addOne() 方法,所以一定能保证原子操作,那是否有可见性问题呢?让我们回顾下之前讲一条 Happens-Before的规则。管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,就是我们这里的 synchronized.我们知道 synchronized 修饰的临界区是互斥的,也就是说同一时刻只有一个线程执行临界区的代码;而这里指的就是前一个线程的解锁操作对后一个线程的加锁操作可见.我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。按照这个规则,如果多个线程同时执行 addOne() 方法,可见性是可以保证的,也就说如果有 1000 个线程执行 addOne() 方法,最终结果一定是 value 的值增加了 1000。我们在来看下,执行 addOne() 方法后,value 的值对 get() 方法是可见的吗?这个可见性是没法保证的。管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并没有加锁操作,所以可见性没法保证。那如何解决呢?很简单,就是 get() 方法也 synchronized 一下,完整的代码如下所示。class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; }}上面的代码转换为我们提到的锁模型,就是下面图示这个样子。get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。图锁和受保护资源的关系我们前面提到,受保护资源和锁之间的关联关系非常重要,他们的关系是怎样的呢?一个合理的关系是:受保护资源和锁之间的关联关系是 N:1 的关系上面那个例子我稍作改动,把 value 改成静态变量,把 addOne() 方法改成静态方法,此时 get() 方法和 addOne() 方法是否存在并发问题呢?class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; }}如果你仔细观察,就会发现改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。图:小结:互斥锁,在并发领域的知名度极高,只要有了并发问题,大家首先容易想到的就是加锁,加锁能够保证执行临界区代码的互斥性。synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有很多其他类型的锁,但作为互斥锁,原理都是相通的:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情。如何一把锁保护多个资源?保护没有关联关系的多个资源当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。class Account { // 锁:保护账户余额 private final Object balLock = new Object(); // 账户余额 private Integer balance; // 锁:保护账户密码 private final Object pwLock = new Object(); // 账户密码 private String password; // 取款 void withdraw(Integer amt) { synchronized(balLock) { if (this.balance > amt){ this.balance -= amt; } } } // 查看余额 Integer getBalance() { synchronized(balLock) { return balance; } } // 更改密码 void updatePassword(String pw){ synchronized(pwLock) { this.password = pw; } } // 查看密码 String getPassword() { synchronized(pwLock) { return password; } }}当然,我们也可以用一把互斥锁来保护多个资源,例如我们可以用 this 这一把锁来管理账户类里所有的资源:但是用一把锁就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而我们用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能 。这种锁还有个名字,叫 细粒度锁保护有关联关系的多个资源如果多个资源是有关联关系的,那这个问题就有点复杂了。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。那对于像转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题代码化。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?class Account { private int balance; // 转账 void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }相信你的直觉会告诉你这样的解决方案:用户 synchronized 关键字修饰一下 transfer() 方法就可以了,于是你很快就完成了相关的代码,如下所示。class Account { private int balance; // 转账 synchronized void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢?问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。下面我们具体分析一下,假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。我们假设线程 1 执行账户 A 转账户 B 的操作,线程 2 执行账户 B 转账户 C 的操作。这两个线程分别在两颗 CPU 上同时执行,那它们是互斥的吗?我们期望是,但实际上并不是。因为线程 1 锁定的是账户 A 的实例(A.this),而线程 2 锁定的是账户 B 的实例(B.this),所以这两个线程可以同时进入临界区 transfer()。同时进入临界区的结果是什么呢?线程 1 和线程 2 都会读到账户 B 的余额为 200,导致最终账户 B 的余额可能是 300(线程 1 后于线程 2 写 B.balance,线程 2 写的 B.balance 值被线程 1 覆盖),可能是 100(线程 1 先于线程 2 写 B.balance,线程 1 写的 B.balance 值被线程 2 覆盖),就是不可能是 200。使用锁的正确知识在上一篇文章中,我们提到用同一把锁来保护多个资源,也就是现实世界的“包场”,那在编程领域应该怎么“包场”呢?很简单,只要我们的 锁能覆盖所有受保护资源 就可以了。这里我们用 Account.class· 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。class Account { private int balance; // 转账 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }下面这幅图很直观地展示了我们是如何使用共享的锁 Account.class 来保护不同对象的临界区的。图思考下:上面的写法不是最佳实践,锁是可变的。小结对如何保护多个资源已经很有心得了,关键是要分析多个资源之间的关系。如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。思路:在第一个示例程序里,我们用了两把不同的锁来分别保护账户余额、账户密码,创建锁的时候,我们用的是:private final Object xxxLock = new Object();如果账户余额用 this.balance 作为互斥锁,账户密码用 this.password 作为互斥锁,你觉得是否可以呢? ...

April 10, 2019 · 3 min · jiezi

Java线程的生命周期

概要目前CPU的运算速度已经达到了百亿次每秒,甚至更高的量级,家用电脑即使维持操作系统正常运行的进程也会有数十个,线程更是数以百计。线程是CPU的调度和分派的基本单位,为了更充分地利用CPU资源以及提高生产率和高效地完成任务,在现实场景中一般都会采用多线程处理。线程的生命周期线程的生命周期大致可以分为下面五种状态:New(新建状态)、RUNABLE(就绪状态)、RUNNING(运行状态)、休眠状态、DEAD(终止状态)1、新建状态,是线程被创建且未启动的状态;这里的创建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。Thread t1 = new Thread()2、就绪状态,指的是调用start()方法之后,线程等待分配给CPU执行(这时候,线程已经在操作系统层面被创建)t1.start()3、运行状态,当CPU空闲时,线程被分得CPU时间片,执行Run()方法的状态4、休眠状态,运行状态的线程,如果调用一个阻塞的API或者等待某个事件,那么线程的状态就会转换到休眠状态,一般有以下几种情况同步阻塞:锁被其它线程占用主动阻塞:调用Thread的某些方法,主动让出CPU执行权,比如:sleep()、join()等方法等待阻塞:执行了wait()方法5、终止状态,线程执行完(run()方法执行结束)或者出现异常就会进入终止状态对应的是JAVA中Thread类State中的六种状态public class Thread implements Runnable { //……… public enum State { NEW, // 初始化状态 RUNNABLE, // 可运行/运行状态 BLOCKED, // 阻塞状态 WAITING, // 无时限等待 TIMED_WAITING, // 有时限等待 TERMINATED; // 终止状态 } // ……….}休眠状态(BLOCKED、WAITING、TIMED_WAITING)与RUNNING状态的转换1、RUNNING状态与BLOCKED状态的转换线程等待 synchronized 的隐式锁,RUNNING —> BLOCKED线程获得 synchronized 的隐式锁,BLOCKED —> RUNNING2、RUNNING状态与WAITING状态的转换获得 synchronized 隐式锁的线程,调用无参数的Object.wait()方法调用无参数Thread.join()方法调用LockSupport.park()方法,线程阻塞切换到WAITING状态,调用LockSupport.unpark()方法,可唤醒线程,从WAITING状态切换到RUNNING状态3、RUNNING状态与TIMED_WAITING状态的转换调用带超时参数的 Thread..sleep(long millis)方法获得 synchronized 隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法调用带超时参数的Thread.join(long millis)方法调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法调用带超时参数的LockSupport.parkUntil(long deadline)方法废弃掉的线程方法 :stop()、suspend()、resume()stop()方法,会真正的杀死线程,不给线程任何喘息的机会,假设获得 synchronized 隐式锁的线程,此刻执行stop()方法,该锁不会被释放,导致其它线程没有任何机会获得锁,显然这样的结果不是我们想要见到的。suspend() 和 resume()方法同样,因为某种不可预料的原因,已经被建议不在使用不能使用stop()、suspend() 、resume() 这些方法来终止线程或者唤醒线程,那么我们应该使用什么方法来做呢?答案是:优雅的使用Thread.interrupt()方法来做优雅的Thread.interrupt()方法中断线程interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知,这个方法通过修改了调用线程的中断状态来告知那个线程,说它被中断了,线程可以通过isInterrupted() 方法,检测是不是自己被中断。当线程被阻塞的时候,比如被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞时;调用它的interrput()方法,会产生InterruptedException异常。扩展知识点探秘局部变量不会引发并发问题的原因在Java领域,线程可以拥有自己的操作数栈,程序计数器、局部变量表等资源;我们都知道,多个线程同时访问共享变量的时候,会导致数据不一致性等并发问题;但是 Java 方法里面的局部变量是不存在并发问题的,具体为什么呢?我们先来了解一下这些基础知识。局部变量的作用域是方法内部的,当方法执行完了,局部变量也就销毁了,也就是说局部变量应该是和方法同生共死的。Java中的方法是如何调用的?当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。从上面我们可以得出:方法的调用就是压栈和出栈的过程,而在Java中的方法的局部变量又是存储在栈帧中,所以我们用下面的示意图帮助大家理解一下说了那么多,我们还没有解释局部变量为啥不会产生并发问题,以上,我们知道了,方法的调用是压栈和出栈(栈帧)的过程,局部变量又存储在栈帧中。那么我们的线程和调用栈又有什么关系呢,答案是:每个线程都有自己独立的调用栈到现在,相信大家都已经明白了,局部变量之所以不存在并发问题,是因为,每个线程都有自己的调用栈,局部变量都保存在线程各自的调用栈里面,没有共享,自然就不存在并发问题。欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!

April 1, 2019 · 1 min · jiezi

Java并发之原子性、有序性、可见性

原子性 原子性指的是一个或者多个操作在 CPU 执行的过程中不被中断的特性线程切换 带来的原子性问题Java 并发程序都是基于多线程的,操作系统为了充分利用CPU的资源,将CPU分成若干个时间片,在多线程环境下,线程会被操作系统调度进行任务切换。为了直观的了解什么是原子性,我们看下下面哪些操作是原子性操作int count = 1; //1count++; //2int a = count; //3上面展示语句中,除了语句1是原子操作,其它两个语句都不是原子性操作,下面我们来分析一下语句2其实语句2在执行的时候,包含三个指令操作指令 1:首先,需要把变量 count 从内存加载到 CPU的寄存器指令 2:之后,在寄存器中执行 +1 操作;指令 3:最后,将结果写入内存对于上面的三条指令来说,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。操作系统做任务切换,可以发生在任何一条CPU 指令执行完有序性 有序性指的是程序按照代码的先后顺序执行编译优化 带来的有序性问题为了性能优化,编译器和处理器会进行指令重排序,有时候会改变程序中语句的先后顺序,比如程序:a = 5; //1b = 20; //2c = a + b; //3编译器优化后可能变成b = 20; //1a = 5; //2c = a + b; //3在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果synchronized(具有有序性、原子性、可见性)表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }}我们先看 instance = new Singleton() 的未被编译器优化的操作指令 1:分配一块内存 M;指令 2:在内存 M 上初始化 Singleton 对象;指令 3:然后 M 的地址赋值给 instance 变量。编译器优化后的操作指令指令 1:分配一块内存 M;指令 2:将 M 的地址赋值给 instance 变量;指令 3:然后在内存 M 上初始化 Singleton 对象。现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化),这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断 instance==null会发现instance不等于null了,所以直接返回instance,而此时的 instance 是没有初始化过的。现行的比较通用的做法就是采用静态内部类的方式来实现public class SingletonDemo { private SingletonDemo() { } private static class SingletonDemoHandler{ private static SingletonDemo instance = new SingletonDemo(); } public static SingletonDemo getInstance() { return SingletonDemoHandler.instance; }}可见性 可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改缓存 导致的可见性问题首先我们来看一下Java内存模型(JMM)我们定义的所有变量都储存在主内存中每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写(不能越级)不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。(同级不能相互访问)共享变量可见性的实现原理:线程1对共享变量的修改要被线程2及时看到的话,要经过如下步骤:把工作内存1中更新的变量值刷新到主内存把主内存中的变量的值更新到工作内存2中可以使用 synchronized 、volatile 、final 来保证可见性欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 27, 2019 · 1 min · jiezi

[Java并发]2,入门:并发编程Bug的源头

之前我们说了:1,可见性2,原子性3,有序性3个并发BUG的之源,这三个也是编程领域的共性问题。Java诞生之处就支持多线程,所以自然有解决这些问题的办法,而且在编程语言领域处于领先地位。理解Java解决并发问题的方案,对于其他语言的解决方案也有触类旁通的效果。什么是Java内存模型我们已经知道了,导致可见性的原因是缓存,导致有序性的问题是编译优化。那解决问题的办法就是直接禁用 缓存和编译优化。但是直接不去使用这些是不行了,性能无法提升。所以合理的方案是 按需禁用缓存和编译优化。如何做到“按需禁用”,只有编写代码的程序员自己知道,所以程序需要给程序员按需禁用和编译优化的方法才行。Java的内存模型如果站在程序员的角度,可以理解为,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile,synchronized和final三个关键字段。以及六项 Happens-Before 规则。使用volatile的困惑volatile 关键字并不是 Java 语言特有的,C语言也有,它的原始意义就是禁用CPU缓存。例如,我们声明一个volatile变量 ,volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。看起来语义很明确,实际情况比较困惑。看下以下代码:class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里 x 会是多少呢? } }}直觉上看,这里的X应该是42,那实际应该是多少呢?这个要看Java的版本,如果在低于 1.5 版本上运行,x 可能是42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。分析一下,为什么 1.5 以前的版本会出现 x = 0 的情况呢?因为变量 x 可能被 CPU 缓存而导致可见性问题。这个问题在 1.5 版本已经被圆满解决了。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。怎么增强的呢?答案是一项 Happens-Before 规则。Happens-Before 规则这里直接给出定义:Happens-Before :前面一个操作的结果对后续操作是可见的。所以比较正式的说法是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。看一看Java内存模型定义了哪些重要的Happens-Before规则1,程序的顺序性规则这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这还是比较容易理解的,比如刚才那段示例代码,按照程序的顺序,第 6 行代码 “x = 42;” Happens-Before 于第 7 行代码 “v = true;”,这就是规则 1 的内容,也比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。2,volatile 变量规则2. volatile 变量规则这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。3,传递性4,管程中锁的规则5,线程 start() 规则6,线程 join() 规则参考:Java内存模型 ...

March 18, 2019 · 1 min · jiezi

[Java并发]1,入门:并发编程Bug的源头

介绍如何坚决并发问题,首先要理解并发的实际源头怎么发生的。现代大家使用的计算机的不同硬件的运行速度是不一样的,这个大家应该都是知道的。计算机数据传输运行速度上的快慢比较: CPU > 缓存 > I/O如何最大化的让不通速度的硬件可以更好的协调执行,需要做一些“撮合”的工作CUP增加了高速缓存来均衡与缓存间的速度差异操作系统增加了 进程,线程,以分时复用CPU,进而均衡CPU与I/O的速度差异(当等待I/O的时候切换给其他CPU去执行)现代编程语言的编译器优化指令顺序,使得缓存能够合理的利用上面说来并发才生问题的背景,下面说下并发才生的具体原因是什么缓存导致的可见性问题先看下单核CPU和缓存之间的关系:单核情况下,也是最简单的情况,线程A操作写入变量A,这个变量A的值肯定是被线程B所见的。因为2个线程是在一个CPU上操作,所用的也是同一个CPU缓存。这里我们来定义一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为 “可见性”多核CPU时代下,我们在来看下具体情况:很明显,多核情况下每个CPU都有自己的高速缓存,所以变量A的在每个CPU中可能是不同步的,不一致的。结果程A刚好操作来CPU1的缓存,而线程B也刚好只操作了CPU2的缓存。所以这情况下,当线程A操作变量A的时候,变量并不对线程B可见。我们用一段经典的代码说明下可见性的问题: private void add10K() { int idx = 0; while (idx++ < 100000) { count += 1; } } @Test public void demo() { // 创建两个线程,执行 add() 操作 Thread th1 = new Thread(() -> { add10K(); }); Thread th2 = new Thread(() -> { add10K(); }); // 启动两个线程 th1.start(); th2.start(); // 等待两个线程执行结束 try { th1.join(); th2.join(); } catch (Exception exc) { exc.printStackTrace(); } System.out.println(count); }大家应该都知道,答案肯定不是 200000这就是可见性导致的问题,因为2个线程读取变量count时,读取的都是自己CPU下的高速缓存内的缓存值,+1时也是在自己的高速缓存中。线程切换带来的原子性问题进程切换最早是为了提高CPU的使用率而出现的。比如,50毫米操作系统会重新选择一个进程来执行(任务切换),50毫米成为“时间片”早期的操作系统是进程间的切换,进程间的内存空间是不共享的,切换需要切换内存映射地址,切换成本大。而一个进程创建的所有线程,内存空间都是共享的。所以现在的操作系统都是基于更轻量的线程实现切换的,现在我们提到的“任务切换”都是线程切换。任务切换的时机大多数在“时间片”结束的时候。现在我们使用的基本都是高级语言,高级语言的一句对应多条CPU命令,比如 count +=1 至少对应3条CPU命令,指令:1, 从内存加载到CPU的寄存器2, 在寄存器执行 +13, 最后,讲结果写回内存(缓存机制导致可能写入的是CPU缓存而不是内存)操作系统做任务切换,会在 任意一条CPU指令执行完就行切换。所以会导致问题如图所示,线程A当执行完初始化count=0时候,刚好被线程切换给了线程B。线程B执行count+1=1并最后写入值到缓存中,CPU切换回线程A后,继续执行A线程的count+1=1并再次写入缓存,最后缓存中的count还是为1.一开始我们任务count+1=1应该是一个不能再被拆开的原子操作。我们把一个或多个操作在CPU执行过程中的不被中断的特性称为 原子性。CPU能够保证的原子性,是CPU指令级别的。所以高级语言需要语言层面 保证操作的原子性。编译优化带来的有序性问题有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:a=6;b=7;编译器优化后可能变成b=7;a=6;,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。Java中的经典案例,双重检查创建单例对象;public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }}看似完美的代码,其实有问题。问题就在new上。想象中 new操作步骤:1,分配一块内存 M2,在内存M上 初始化对象3,把内存M地址赋值给 变量实际上就行编译后的顺序是:1,分开一块内存 M2,把内存M地址赋值给 变量3,在 内存M上 初始化对象优化导致的问题:如图所示,当线程A执行到第二步的时候,被线程切换了,这时候,instance未初始化实例的对象,而线程B这时候执行到instance == null ?的判断中,发现instance已经有“值”了,导致了返回了一个空对象的异常。总结1,缓存引发的可见性2,切换线程带来的原子性3,编译带来的有序性深刻理解这些前因后果,可以诊断大部分并发的问题! ...

March 18, 2019 · 1 min · jiezi

当我们在说“并发、多线程”,说的是什么?

这篇文章的目的并不是想教你如何造火箭(面试造火箭,工作拧螺丝),而是想通过对原理和应用案例的有限度剖析来协助你构建起并发的思维,并将操作系统的理论知识与工程实践结合起来,贯穿从学到会的全过程。当然,虽然我们是从实用角度出发,但具有实践意义的深层次知识点永远会是面试中的杀手锏,这可比只能口头造火箭的理论知识更吸引面试官。本文适合谁:希望能了解并发概念的初学者需要理清并发概念与技术的工程师对并发在工作中的应用与其底层实现原理感兴趣的读者在这篇文章中,你将了解到并发与多线程相关的一系列概念,通过一些例子我们可以在不纠结于具体的技术细节的情况下形成对并发与多线程相关的各种概念的抽象理解。有了这些概念以后,我们再去学习具体的理论和技术细节就是手到擒来的事了。什么是并发?最近几年淘宝发展得如火如荼,涌现出了一大批白手起家的卖家。想象一下你是一个刚刚起步的小卖家,自己运营一个服装网店,每天都要自己打包发货。刚开始时生意一般,每天自己一个人一个小时就能干完。随着生意的蓬勃发展,发货时间慢慢地从一个小时涨到了两个小时、四个小时,一次因为延迟发货导致被投诉之后,你终于觉得该招更多的人了。很快,两个小伙伴加入了你的事业,打包速度开始有了质的提高。这就是最基本的并发了,每个人都可以看成是一个线程,同样的工作量,干得人多了自然就快了。所以并发就是通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。数据竞争但是好景不长,周末一盘货,你发现少了不少。这办公室里也没遭贼,怎么就会少货呢?细细一查快递单,你发现竟然有几单发重了。之后的几天你都细细留意了一下发货的过程,最后发现是因为每个人都会拿着一张发货清单去备货,如果有一些订单不小心打印重复了,就有可能会被不同的人重复发货。虽然数量不多,但是也很心痛啊。这个问题产生的原因就是因为每个人在备货之前拿到的订单状态(未发货)在实际备货时发生了变化(已由其他人发货)。这种对一个共享数据(订单的发货状态)本应独占的读取、检查、修改过程,如果发生了并发,这种情况就被称为数据竞争。而这个读取、检查、修改的过程就被称为临界区,临界区指的就是一个存在数据竞争的代码片段。数据竞争出现的根本原因是一个数据本来应该只能由一个执行器完整地执行读取、检查、修改过程,但是如果出现了并发,那么就没办法保证到了“修改”这一步时的数据还保持了“读取”时的值了。确定原因后,有人想到了一个好办法,可以打印一张总的发货清单,这样所有人都必须以这个清单上的订单是否发货来确定是否要对订单进行备货并发货了。因为清单只有一份,所以每次只能由一个人来修改订单的发货状态。这种只能由一个执行器进行数据修改操作来避免发生数据竞争问题的做法就被称为互斥,也就是我们常说的锁了。分布式并发概念分布式因为你管理得当,生意发展得很快,现在的办公室里已经堆不下所有衣服了。所以你又租了一个仓库来同样进行发货。两个地方都会进行发货,那么就可以把每一个仓库理解为一台独立的计算机,这样通过多台计算机完成同一任务的方式就可以被称为分布式,这样的一组计算机的集合就被称为集群。这时候之前用一张纸质的总发货清单的数据竞争解决方式就行不通了,所以我们需要把这张总发货清单放到云端,让大家可以通过网络进行编辑,但是每次只能一个人编辑。在这种情况下,我们可以把两个仓库各自看成一台计算机/进程,而每个仓库里的人就是这个进程中的线程。这样的话这张总发货清单就成为了一个分布式锁,因为它每次只能有一个人编辑,所以是一个互斥锁,或者简称为锁;而因为它可以被两个进程/计算机(仓库)共同使用,所以被称为是分布式锁。什么是进程/线程?可以简单地将进程理解为我们电脑/手机上的一个应用,同一台手机上的每个App都是一个进程,同一个App在每个手机上也是一个进程。进程和进程之间可以理解为是两个仓库,互相之间物理隔离;而线程就是仓库里的每一个人,他们共享同一个办公空间。这里的办公空间就可以理解为操作系统中的虚拟内存空间,但是本文主要讨论并发相关的概念,就不继续展开了。分布式数据不一致因为生意比较好,所以所有人都很忙。有时候就会因为有一些人虽然在云端表格上已经勾上了一个订单,但是一忙就给忙忘了。其他人怕重复发货又不会再去处理已经勾上的订单了,因为这样导致的未发货订单让店铺被投诉了好多次,影响非常大。这种在并发过程中修改了数据状态但是没有完成后续执行的情况就会出现数据不一致,即订单已经被勾上,但实际并没有发货。但是作为聪明的老板,你又想到了解决的方法。每隔一小时两个仓库就会各派一个人检查一下已经勾上的订单是否已经都打包完贴上快递单了。这种每隔一段时间就检查并处理遗漏的数据不一致订单的任务就被称为兜底任务。而通过兜底任务实现的在最后所有订单都会达到数据一致状态的情况就被称为最终一致性。优化方式大家可能早就觉得前面介绍的总发货清单的方法太傻了,只要每个订单都只打印一张发货清单,由单独一个人去负责分发清单就可以了,其他人只要处理好自己被分配到的订单就可以了。最后再加上一个兜底任务对订单的发货情况进行二次校验基本上就不会发生漏发或者重发的情况了。这种由一个执行器进行任务拆分,再由一组执行器进行处理,最后再由一个或一组执行器进行结果汇总的处理方式就是现在非常流行的map-reduce方法了。这种方法在大数据或者程序语言标准库里都有大量的应用,比如大数据领域赫赫有名的Hadoop和Java语言中的ForkJoinPool都使用了这种思想。回顾在这篇文章中,我们涉及到了以下的技术名词:并发,通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。数据竞争,对一个共享数据本应独占的读取、检查、修改过程发生了并发的情况。临界区,存在数据竞争的代码片段。互斥锁(也可以简称为“锁”),同一时间只能由一个执行器获取的实体,用于实现对临界区的互斥(只有一个)访问。分布式,通过多台计算机完成同一任务的方式。集群,一组完成同一任务的机器。分布式锁,在不同机器/进程上提供互斥能力的锁。数据不一致,一系列操作不具有原子性,一部分执行成功而另一部分没有,导致不同数据之间存在矛盾,例如订单已经是发货状态,但是实际没有发货。兜底任务,处理数据不一致状态的任务。最终一致性,通过兜底任务或其他方式保证数据不一致的情况最终会消失。map-reduce,一种任务拆分-执行-再合并的任务执行方式,可以有效地利用多台机器、多核CPU的性能。后记因为并发的知识范围很大,而且对于一些抽象概念的传递必然会需要花费一些篇幅,所以这个主题将会包含一系列文章,主要覆盖以下主题:什么是并发?抛开冗长繁杂的技术点,直接理解并发相关的各种概念。什么是多线程?多线程是并发的一种重要形式。通过具体的多线程问题引出多线程编程中的关键点和对应的工具与知识点,轻松学会多线程编程。常用工具中的并发实现通过解析知名开源工具中的并发方案实现来深入理解并发编程实践。有兴趣的读者可以继续关注后续的文章,在之后的文章中会有对并发编程、操作系统原语、硬件原语等等理论与实践知识的详细介绍与案例。对数据库索引感兴趣的读者可以了解一下我之前的文章:数据库索引是什么?新华字典来帮你 —— 理解数据库索引融会贯通 —— 深入20分钟数据库索引设计实战 —— 实战数据库索引为什么用B+树实现? —— 扩展

March 5, 2019 · 1 min · jiezi