Java并发编程-知识前瞻(第一章)

前言:java

Java并发编程学习分享的目标:面试

  • Java并发编程中经常使用的工具用途与用法;编程

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

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

  • 根据实际情景选择更合适的工具完成高效的设计方案数组

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

学习分享大纲:tomcat

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上真正运行的是线程),线程至关于一个进程中不一样的执行路径。

单线程:单线程就是一个叫作“进程”的房子里面,只住了你一我的,你能够在这个房子里面任什么时候间去作任何的事情。你是看电视、仍是玩电脑,全都有你本身说的算。想干什么干什么,想什么时间作什么就什么时间作什么。

多线程:可是若是你处在一个“多人”的房子里面,每一个房子里面都有叫作“线程”的住户:线程一、线程二、线程三、线程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关键字

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

4月23日世界读书日「好将来技术」微信公众号免费送书啦~

扫码关注「好将来技术」微信公众号回复「送书」便可参与本次活动

专属宠粉福利,数量有限先到先得,还在等什么快来关注吧!!

 

相关文章
相关标签/搜索