Java并发编程-知识前瞻(第一章)
前言:
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…Z26Martin Fowler在一篇LMAX文章中介绍,这一个高性能异步处理框架,其单线程一秒的吞吐量可达六百万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速度有几个数量级的差距,因此现代计算机都不得不加入一层尽可能接近处理器运算速度的高速缓存来做缓冲:将内存中运算需要使用的数据先复制到缓存中,当运算结束后再同步回内存。如下图:
在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,类似于一个加锁过程
以下是一个多线程打印时间的逐步优化案例
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,线程池结合ThreadLocalpublic 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
线程
列举了如此多的案例都是围绕线程展开的,所以我们需要更深地掌握线程,它的概念,它的原则,它是如何实现交互通信的。以下的一张图可以更通俗地解释进程、线程的区别单线程:单线程就是一个叫做“进程”的房子里面,只住了你一个人,你可以在这个房子里面任何时间去做任何的事情。你是看电视、还是玩电脑,全都有你自己说的算。想干什么干什么,想什么时间做什么就什么时间做什么。多线程:但是如果你处在一个“多人”的房子里面,每个房子里面都有叫做“线程”的住户:线程1、线程2、线程3、线程4,情况就不得不发生变化了。在多线程编程中有”锁”的概念,在你的房子里面也有锁。如果你的老婆在上厕所并锁上门,她就是在独享这个“房子(进程)”里面的公共资源“卫生间”,如果你的家里只有这一个卫生间,你作为另外一个线程就只能先等待。
为了阐述线程间的通信,简单模拟一个生产者消费者模型:
生产者
CarStock carStock;
public CarProducter(CarStock carStock){
this.carStock = carStock;
}
@Override
public 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;
}
@Override
public 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)有序性问题双检索单例懒汉模式
06
思考题
有时为了尽快释放资源,避免无意义的耗费,会令部分功能提前结束,例如许多抢名额问题,这里出一个思考题供大家参考实现:题:8人百米赛跑,要求前三名跑到终点后停止运行,设计该问题的实现。参考资料:
1.亿级流量Java高并发与网络编程实战
2.LMAX文章(http://ifeve.com/lmax/)
下章预告:Volatile和Syncronize关键字
Volatile关键字
Synchronized关键字Volatile关键字
Synchronized关键字
我就知道你“在看”