前言:
Java并发编程学习分享的指标:

Java并发编程中罕用的工具用处与用法;

Java并发编程工具实现原理与设计思路;

并发编程中遇到的常见问题与解决方案;

依据理论情景抉择更适合的工具实现高效的设计方案

学习分享团队:
学而思培优-经营研发团队
Java并发编程分享小组:
@沈健 @曹伟伟 @张俊勇 @田新文 @张晨
本章分享人:@张晨

学习分享纲要:

01初识并发

什么是并发,什么是并行?

用个JVM的例子来解说,在垃圾回收器做并发标记的时候,这个时候JVM不仅能够做垃圾标记,还能够处理程序的一些需要,这个叫并发。在做垃圾回收时,JVM多个线程同时做回收,这叫并行。

02为什么要学习并发编程

直观起因
1)JD的强制性要求
随着互联网行业的飞速发展,并发编程曾经成为十分热门的畛域,也是各大企业服务端岗位招聘的必备技能。

2)从小牛通往大牛的必经之路
架构师是软件开发团队中十分重要的角色,成为一名架构师是许多搞技术人奋斗的指标,掂量一个架构师的能力指标就是设计出一套解决高并发的零碎,由此可见高并发技术的重要性,而并发编程是底层的根底。无论游戏还是互联网行业,无论软件开发还是大型网站,都对高并发技术人才存在微小需要,因而,为了工作为了晋升本人,学习高并发技术迫不及待。

3)面试过程中极容易踩坑
面试的时候为了考查对并发编程的掌握情况,常常会考查并发平安相干的常识和线程交互的常识。例如在并发状况下如何实现一个线程平安的单例模式,如何实现两个线程中的性能交互执行。

以下是应用双检索实现一个线程平安的单例懒汉模式,当然也能够应用枚举或者单例饿汉模式。

private static volatile  Singleton singleton;private Singleton(){};public Singleton getSingleton(){    if(null == singleton){        synchronized(Singleton.class){            if(null == singleton){                singleton = new Singleton();            }        }    }    return singleton;}

在这里第一层空判断是为了缩小锁管制的粒度,应用volatile润饰是因为在jvm中new Singleton()会呈现指令重排,volatile防止happens before,防止空指针的问题。从一个线程平安的单例模式能够引申出很多,volatile和synchronized的实现原理,JMM模型,MESI协定,指令重排,对于JMM模型后序会给出更具体的图解。

除了线程平安问题,还会考查线程间的交互。 例如应用两个线程交替打印出A1B2C3…Z26

考查的重点并不是要简略的实现这个性能,通过此面试题,能够考查常识的整体掌握情况,多种计划实现,能够应用Atomicinteger、ReentrantLock、CountDownLat ch。下图是应用LockSupport管制两个线程交替打印的示例,LockSupport外部实现的原理是应用UNSAFE管制一个信号量在0和1之间变动,从而能够管制两个线程的交替打印。

4)并发在咱们工作应用的框架中处处可见,tom cat,netty,jvm,Disruptor

相熟JAVA并发编程根底是把握这些框架底层常识的基石,这里简略介绍下高并发框架Disruptor的底层实现原理,做一个勾画的作用:
Martin Fowler在一篇LMAX文章中介绍,这一个高性能异步解决框架,其单线程一秒的吞吐量可达六百万

Disruptor外围概念

Disruptor特色

  • 基于事件驱动
  • 基于"观察者"模式、"生产者-消费者"模型
  • 能够在无锁的状况下实现网络的队列操作

RingBuffer执行流程

Disruptor底层组件,RingBuffer密切相关的对象:Sequ enceBarrier和Sequencer;

SequenceBarrier是消费者和RingBuffer之间的桥梁。在Disruptor中,消费者间接拜访的是SequenceBarrier,由SequenceBarrier缩小RingBuffer的队列抵触。

SequenceBarrier 通过waitFor办法当消费者速度大于生产者的生产速度时,消费者可通过waitFor办法给予生产者肯定的缓冲工夫,协调生产者和消费者的速度问题,waitFor执行机会:

Sequencer是生产者和缓冲区RingBuffer之间的桥梁,生产者通过Sequencer向RingBuffer申请数据寄存空间,通过WaitStrategy应用publish办法告诉消费者,WaitStrategy是消费者没有数据能够生产时的期待策略。每个生产者或者消费者线程,会先申请能够操作的元素在数组中的地位,申请到之后,间接在该地位写入或者读取数据,整个过程通过原子变量CAS,保障操作的线程平安,这就是Disruptor的无锁设计。

以下是五大罕用期待策略:

  • BlockingWaitStrategy:Disruptor的默认策略是BlockingWaitStrategy。在BlockingWaitStrategy外部是应用锁和condition来控制线程的唤醒。BlockingWaitStrategy是最低效的策略,但其对CPU的耗费最小并且在各种不同部署环境中能提供更加统一的性能体现。
  • SleepingWaitStrategy:SleepingWaitStrategy 的性能体现跟 BlockingWaitStrategy 差不多,对 CPU 的耗费也相似,但其对生产者线程的影响最小,通过应用LockSupport.parkNanos(1)来实现循环期待。
  • YieldingWaitStrategy:YieldingWaitStrategy是能够应用在低提早零碎的策略之一。YieldingWaitStrategy将自旋以期待序列减少到适当的值。在循环体内,将调用Thread.yield()以容许其余排队的线程运行。在要求极高性能且事件处理线数小于 CPU 逻辑外围数的场景中,举荐应用此策略;例如,CPU开启超线程的个性。
  • BusySpinWaitStrategy:性能最好,适宜用于低提早的零碎。在要求极高性能且事件处理线程数小于CPU逻辑外围数的场景中,举荐应用此策略;例如,CPU开启超线程的个性。

目前,包含Apache Storm、Camel、Log4j2在内的很多出名我的项目都利用了Disruptor以获取高性能。

5)JUC是并发大神Doug Lea灵魂力作,堪称榜样(第一个支流尝试,它将线程,锁和事件之外的抽象层次晋升到更平易近人的形式:并发汇合, fork/join 等等)

通过并发编程设计思维的学习,施展应用多线程的劣势

  • 施展多处理器的弱小能力
  • 建模的简略性
  • 异步事件的简化解决
  • 响应更灵活的用户界面

那么学不好并发编程根底会带来什么问题呢

1)多线程在日常开发中使用中处处都是,jvm、tomcat、netty,学好java并发编程是更深层次了解和把握此类工具和框架的前提因为计算机的cpu运算速度和内存io速度有几个数量级的差距,因而古代计算机都不得不退出一层尽可能靠近处理器运算速度的高速缓存来做缓冲:将内存中运算须要应用的数据先复制到缓存中,当运算完结后再同步回内存。如下图:

因为jvm要实现跨硬件平台,因而jvm定义了本人的内存模型,然而因为jvm的内存模型最终还是要映射到硬件上,因而jvm内存模型简直与硬件的模型一样:

操作系统底层数据结构,每个CPU对应的高速缓存中的数据结构是一个个bucket存储的链表,其中tag代表的是主存中的地址,cache line是偏移量,flag对应的MESI缓存一致性协定中的各个状态。

MESI缓存一致性状态别离为:

M:Modify,代表批改

E:Exclusive,代表独占

S:Share,代表共享

I:Invalidate,代表生效

以下是一次cpu0数据写入的流程:

在CPU0执行一次load,read和write时,在做write之前flag的状态会是S,而后收回invalidate音讯到总线;

其余cpu会监听总线音讯,将各cpu对应的cache entry中的flag状态由S批改为I,并且发送invalidate ack给总线

cpu0收到所有cpu返回的invalidate ack后,cpu0将flag变为E,执行数据写入,状态批改为M,相似于一个加锁过程

思考到性能问题,这样写入批改数据的效率太过漫长,因而引入了写缓冲器和有效队列,所有的批改操作会先写入写缓冲器,其余cpu接管到音讯后会先写入有效队列,并返回ack音讯,之后再从有效队列生产音讯,采纳异步的模式。当然,这样就会产生有序性问题,例如某些entry中的flag还是S,但实际上应该标识为I,这样拜访到的数据就会有问题。使用volitale是为了解决指令重排带来的无序性问题,volitale是jvm层面的关键字,MESI是cpu层面的,两者是差了几个档次的。

2)性能不达标,找不到解决思路。

3)工作中可能会写出线程不平安的办法
以下是一个多线程打印工夫的逐渐优化案例

new Thread(new Runnable() {    @Override    public void run() {        System.out.println(new ThreadLocalDemo01().date(10));    }}).start();new Thread(new Runnable() {    @Override    public void run() {        System.out.println(new ThreadLocalDemo01().date(1007));    }}).start();

优化1,多个线程使用线程池复用

for(int i = 0; i < 1000; i++){    int finalI = i;    executorService.submit(new Runnable() {        @Override        public void run() {            System.out.println(new ThreadLocalDemo01().date2(finalI));        }    });}executorService.shutdown();public String date2(int seconds){        Date date = new Date(1000 * seconds);        String s = null;//        synchronized (ThreadLocalDemo01.class){//            s = simpleDateFormat.format(date);//        }        s = simpleDateFormat.format(date);        return s;}

优化2,线程池联合ThreadLocal

public String date2(int seconds){    Date date = new Date(1000 * seconds);    SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();    return simpleDateFormat.format(date);}

在多线程服用一个SimpleDateFormat时会呈现线程平安问题,执行后果会打印出雷同的工夫,在优化2中应用线程池联合ThreadLocal实现资源隔离,线程平安。

4)许多问题无奈正确定位
踩坑:crm仿真定时工作阻塞,无奈继续执行
问题:crm仿真使用schedule配置的定时工作在某个工夫节点后的所有定时工作均未执行
起因:定时工作配置导致的问题,@Schedule配置的定时工作如果未配置线程池,在启动类应用@EnableScheduling启用定时工作时会默认应用单线程,后端配置了多定时工作,会呈现问题.配置了两定时工作A和B,在A先占用资源后如果始终未开释,B会始终处于期待状态,直到A工作开释资源后,B开始执行,若要防止多任务执行带来的问题,须要应用以下办法配置:

@Bean public ThreadPoolTaskScheduler taskScheduler(){   ThreadPoolTaskScheduler scheduler = new       ThreadPoolTaskScheduler();   scheduler.setPoolSize(10);   return scheduler; }

crm服务因为定时工作配置的不多,并且在资源足够的状况下,工作执行速度绝对较快,并未设置定时工作的线程池

定时工作里程序办法如何造成线程始终未开释,导致阻塞。

在问题定位时,产生的问题来自CountDownLatch无奈归零,导致整个主线程hang在那里,无奈开释。

在api中当调用await时候,调用线程处于期待挂起状态,直至count变成0再持续,大抵原理如下:

因而将眼光焦点转移至await办法,使以后线程在锁存器倒计数至零之前始终期待,除非线程被中断或超出了指定的等待时间。如果以后计数为零,则此办法立即返回true 值。如果以后计数大于零,则出于线程调度目标,将禁用以后线程,且在产生以下三种状况之一前,该线程将始终处于休眠状态:因为调用 countDown() 办法,计数达到零;或者其余某个线程中断以后线程;或者已超出指定的等待时间。

Executors.newFixedThreadPool这是个有固定流动线程数。当提交到池中的工作数大于固定流动线程数时,工作就会放到阻塞队列中期待。CRM该定时工作里为了放慢工作解决,使用多线程解决,设置的CountDownLatch的count大于ThreadPoolExecutor的固定流动线程数导致工作始终处于期待状态,计数无奈归零,导致主线程始终无奈开释,从而导致crm一台仿真服务的定时工作处于瘫痪状态。

03如何学习java并发编程

为了学习好并发编程根底,咱们须要有一个上帝视角,一个宏观的概念,而后由点及深,把握必备的知识点。咱们能够从以下两张思维导图列举进去的逐渐进行学习。

必备知识点

04线程

列举了如此多的案例都是围绕线程开展的,所以咱们须要更深地把握线程,它的概念,它的准则,它是如何实现交互通信的。

以下的一张图能够更艰深地解释过程、线程的区别

过程: 一个过程好比是一个程序,它是 资源分配的最小单位 。同一时刻执行的过程数不会超过外围数。不过如果问单核CPU是否运行多过程?答案又是必定的。单核CPU也能够运行多过程,只不过不是同时的,而是极快地在过程间来回切换实现的多过程。电脑中有许多过程须要处于「同时」开启的状态,而利用CPU在过程间的疾速切换,能够实现「同时」运行多个程序。而过程切换则意味着须要保留过程切换前的状态,以备切换回去的时候可能持续接着工作。所以过程领有本人的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行过程的记录、回复、切换等等。

线程:线程是独立运行和独立调度的根本单位(CPU上真正运行的是线程),线程相当于一个过程中不同的执行门路。

单线程:单线程就是一个叫做“过程”的房子外面,只住了你一个人,你能够在这个房子外面任何工夫去做任何的事件。你是看电视、还是玩电脑,全都有你本人说的算。想干什么干什么,想什么工夫做什么就什么工夫做什么。

多线程:然而如果你处在一个“多人”的房子外面,每个房子外面都有叫做“线程”的住户:线程1、线程2、线程3、线程4,状况就不得不发生变化了。

在多线程编程中有”锁”的概念,在你的房子外面也有锁。如果你的老婆在上厕所并锁上门,她就是在独享这个“房子(过程)”外面的公共资源“卫生间”,如果你的家里只有这一个卫生间,你作为另外一个线程就只能先期待。

线程最为重要也是最为麻烦的就是线程间的交互通信过程,下图是线程状态的变动过程:

为了论述线程间的通信,简略模仿一个生产者消费者模型:

生产者

CarStock carStock;public CarProducter(CarStock carStock){    this.carStock = carStock;}@Overridepublic void run() {    while (true){        carStock.produceCar();    }}public synchronized void produceCar(){  try {    if(cars < 20){      System.out.println("生产者..." + cars);      Thread.sleep(100);      cars++;      notifyAll();    }else {      wait();    }  } catch (InterruptedException e) {    e.printStackTrace();  }}

消费者

CarStock carStock;public CarConsumer(CarStock carStock){    this.carStock = carStock;}@Overridepublic void run() {    while (true){        carStock.consumeCar();    }}public synchronized void consumeCar(){    try {        if(cars > 0){            System.out.println("销售车..." + cars);            Thread.sleep(100);            cars--;            notifyAll();        }else {            wait();        }    } catch (InterruptedException e) {        e.printStackTrace();    }}

生产过程

通信过程

对于此简略的生产者消费者模式能够使用队列、线程池等技术对程序进行改良,使用BolckingQueue队列共享数据,改良后的生产过程

05并发编程三大个性

并发编程实现机制大多都是围绕以下三点:原子性、可见性、有序性

1)原子性问题

for(int i = 0; i < 20; i++){    Thread thread = new Thread(() -> {        for (int j = 0; j < 10000; j++) {            res++;            normal++;            atomicInteger.incrementAndGet();        }    });    thread.start();}

运行后果:

volatile: 170797
atomicInteger:200000
normal:182406

这就是原子性问题,原子性是指在一个操作中就是cpu不能够在中途暂停而后再调度,既不被中断操作,要不执行实现,要不就不执行。
如果一个操作是原子性的,那么多线程并发的状况下,就不会呈现变量被批改的状况。

2)可见性问题

class MyThread extends Thread{    public int index = 0;    @Override    public void run() {        System.out.println("MyThread Start");        while (true) {            if (index == -1) {                break;            }        }        System.out.println("MyThread End");    }}

main线程将index批改为-1,myThread线程并不可见,这就是可见性问题导致的线程平安,可见性就是指当一个线程批改了线程共享变量的值,其它线程可能立刻得悉这个批改。Java内存模型是通过在变量批改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的办法来实现可见性的,无论是一般变量还是volatile变量都是如此,一般变量与volatile变量的区别是volatile的非凡规定保障了新值能立刻同步到主内存,以及每应用前立刻从内存刷新。因为咱们能够说volatile保障了线程操作时变量的可见性,而一般变量则不能保障这一点。

3)有序性问题

双检索单例懒汉模式

有序性: Java内存模型中的程序人造有序性能够总结为一句话:如果在本线程内察看,所有操作都是有序的;如果在一个线程中察看另一个线程,所有操作都是无序的。前半句是指“线程内体现为串行语义”,后半句是指“指令重排序”景象和“工作内存中主内存同步提早”景象。

06思考题

有时为了尽快开释资源,防止无意义的消耗,会令局部性能提前结束,例如许多抢名额问题,这里出一个思考题供大家参考实现:
题:8人百米赛跑,要求前三名跑到起点后进行运行,设计该问题的实现。

参考资料:
1.亿级流量Java高并发与网络编程实战
2.LMAX文章(http://ifeve.com/lmax/)

下章预报:

  • Volatile和Syncronize关键字
  • Volatile关键字
  • Synchronized关键字Volatile关键字
  • Synchronized关键字

对于好将来技术更多内容请:微信扫码关注「好将来技术」微信公众号