乐趣区

关于java: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…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;}

@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)有序性问题

双检索单例懒汉模式

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

06 思考题

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

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

下章预报:

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

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

退出移动版