导图社区 实战Java高并发程序设计模式
干货!最详尽的实战JAVA高并发程序设计模式思维导图,分十大板块构建框架,清晰明了。其中详细介绍并行程序基础、JAVA内存模型和线程安全、无锁和JDK并发包部分,所有零散知识点全包揽!
编辑于2019-04-19 02:49:20实战Java高并发 程序设计模式
一、前言
为什么需要并行
业务需求
性能
(1) 并不是为了提高系统性能,而是确实在业务 上需要多个执行单元; (2) 比如HTTP服务器,为每一个Socket连接新建一个处理线程; (3) 以后不同线程承担不同的业务工作; (4) 简化任务调度;
几个重要的概念
同步(synchronous)和异步(asynchronous)
并发(Concurrency)和并行(Parallelism)
并发:-->-->-->..-->; 并行:------------->; ------------->;
临界区
类似独立桥; 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking)
形容多线程间的相互影响;
非阻塞允许多个线程同时进入临界区
死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
死锁产生的4个必要条件
1、互斥: 某种资源一次只允许一个进程访问, 即该资源一旦分配给某个进程, 其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种), 同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源, 你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下 一个进程所需的至少一种资源。
并行的级别
阻塞
临界区中有线程,其他必须等待
无障碍
最弱的非阻塞调试
自由出入临界区
无竞争时,有限步内完成操作
有竞争时,回滚数据
非阻塞
无锁
是无障碍的
atomicVar.comparAndSet()...
保证有一个线程可以胜出
无等待
无锁的
要求所有的线程必须在有限步内完成
无饥饿的
2个重要的定理
Amdahl定律(阿姆达尔定律)
定义了串行系统并行化后的加速比的计算公式和理论上限
加速比定义:加速比 = 优化前系统耗时 / 优化后系统耗时
增加CPU处理器的数量并不一定能起到有效的作用 提高系统内可并行化的模块比重,合理增加并行处 理器数量,才能以最小的投入,得到最大的加速比
Gustafson定律(古斯塔夫森)
与明处理器个数,串行比例和加速比之间的关系
只要有足够的并行化,那么加速比和CPU个数成正比
四、无锁
无锁类的原理详解
CAS
CAS(更新的变量V,预期值E,新值N ) if(V==E) set(N) CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程 不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
通过CPU指令(cmpxchg)层面实现,是一个原子操作
CPU指令
cmpxchg /* accumulator = AL, AX, or EAX, depending on whether a byte, word, or doubleword comparison is being performed */ if(accumulator == Destination) { ZF = 1; Destination = Source; } else { ZF = 0; accumulator = Destination; }
单条指令实现
无锁类的使用
AtomicInteger
概述
继承Number
主要接口
public final int get() //取得当前值 public final void set(int newValue) //设置当前值 public final int getAndSet(int newValue) //设置新值,并返回旧值 public final boolean compareAndSet(int expect, int u) //如果当前值为expect,则设置为u public final int getAndIncrement() //当前值加1,返回旧值 public final int getAndDecrement() //当前值减1,返回旧值 public final int getAndAdd(int delta) //当前值增加delta,返回旧值 public final int incrementAndGet() //当前值加1,返回新值 public final int decrementAndGet() //当前值减1,返回新值 public final int addAndGet(int delta) //当前值增加delta,返回新值
主要接口的实现
compareAndSet
Unsafe
概述
使JAVA拥有像C一样的指针操作内存空间能力,过度使用会让其出错概率增大
提供了硬件级别的原子操作
park(挂起)、unpark(恢复)等方法。
非安全操作,根据偏移量设置值
非公开API,底层CAS操作
主要接口
//获得给定对象偏移量上的int值 public native int getInt(Object o, long offset); //设置给定对象偏移量上的int值 public native void putInt(Object o, long offset, int x); //获得字段在对象中的偏移量 public native long objectFieldOffset(Field f); //设置给定对象的int值,使用volatile语义 public native void putIntVolatile(Object o, long offset, int x); //获得给定对象对象的int值,使用volatile语义 public native int getIntVolatile(Object o, long offset); //和putIntVolatile()一样,但是它要求被操作字段就是volatile类型的 public native void putOrderedInt(Object o, long offset, int x);
AtomicReference
概述
对引用进行修改 是一个模板类,抽象化了数据类型 volatile Value v;
主要接口
get() set(V) compareAndSet() getAndSet(V)
AtomicStampedReference
概述
用标记(时间戳)解决ABA问题:第一个A和最后一个A不是同一个A,如:在第一个A计算完准备赋值 时另一个线程改成B,另一个线程又改回A; 用户存100,取一100,前后账户余额不变,这时候再用CAS会出现此问题;
主要接口
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳 public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) //获得当前对象引用 public V getReference() //获得当前时间戳 public int getStamp() //设置当前对象引用和时间戳 public void set(V newReference, int newStamp)
AtomicIntegerArray
概述
支持无锁的数组
主要接口
//获得数组第i个下标的元素 public final int get(int i) //获得数组的长度 public final int length() //将数组第i个下标设置为newValue,并返回旧的值 public final int getAndSet(int i, int newValue) //进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true public final boolean compareAndSet(int i, int expect, int update) //将第i个下标的元素加1 public final int getAndIncrement(int i) //将第i个下标的元素减1 public final int getAndDecrement(int i) //将第i个下标的元素增加delta(delta可以是负数) public final int getAndAdd(int i, int delta)
AtomicIntegerFieldUpdater
概述
让普通变量也享受原子操作
主要接口
AtomicIntegerFieldUpdater.newUpdater() incrementAndGet()
小说明
1. Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。 比如如果score申明为private,就是不可行的。 2. 为了确保变量被正确的读取,它必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单得 申明一下就行,这不会引起什么问题。 3. 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe. objectFieldOffset()不支持静态变量)。
无锁算法详解
无锁的Vector实现
五、JDK并发包
各种同步控制工具的使用
ReentrantLock
可重入
可中断
可限时
公平锁
lock.newCondition()绑定结合使用
Condition
概述
主要接口
condition.await(); condition.signal();
lock.newCondition()绑定结合使用
API详解
Semaphore(信号量)
概述
可以多个线程进入临界区
一 个线程可以根据业务需要拿多个授权
访问情况
semp.acquire(申请授权数量); semp.release(释放授权数量);
一批一批操作
ReadWriteLock
概述
JDK5开始提供
读写分离,对读加锁影响性能
访问情况
主要接口
ReentrantReadWriteLock rrw = new ReentrantReadWriteLock(); Lock rLock = rrw.readLock(); Lock wLock = rrw.writeLock();
CountDownLatch
概述
倒数计数器。
场景:火箭发射前进行各项检查;
主要接口
end.countDown();//检查线程线程完成执行汇报 end.await();//主线程等待
示意图
检查任务 完成 --> | -->| --> | -->| --> | -->| --->主线程
CyclicBarrier
概述
循环栅栏,一批执行过多再执行另一批
场景:士兵集合再做任务,集合有异常中断后停止
主要接口
示意图
集合完成 任务完成 --> | -->| --> | -->| --> | -->| --> | -->| --> | -->| --> | -->|
LockSupport
概述
提供线程阻塞原语 线程挂起
主要接口
LockSupport.park();//挂起 LockSupport.unpark(t1);//恢复
与suspend()比较
不容易引起线程冻结
unpark()发生在park()前不会永久挂起;
中断响应
能够响应中断,但不抛出异常 中断响应的结果是,park()函数的返回,可以从Thread.interrupted()得到中断标志
ReentrantLock的实现
CAS状态
0/1,修改不成功有人在使用
等待队列
CAS不成功进入等待队列
park()
并发容器及典型源码分析
集合包装
HashMap
Map m = Collections.synchronizedMap(new HashMap());
底层是数组,存储Entry对象 Entry包含key,value,,next(哈希冲突存同一条链)
List
synchronizedList();
Set
synchronizedSet();
线程安全,适用并发量小 每个操作使用synchronized包装,get和set的串行
ConcurrentHashMap
高性能并发Map,自旋等待
源码查看
BlockingQueue
阻塞队列
非高性能,使用ReentrantLock
多线程共享数据
非常适用生产者-->消费者模式,可直接使用
ConcurrentLinkedQueue
高性能
使用无锁
线程池的基使用
为什么使用线程池
JDK为我们提供了哪些支持
线程池的使用
种类
共同性
线程池使用的小例子
简单线程池
ScheduledThreadPool
扩展和增强线程池
回调接口
拒绝策略
自定义ThreadFactory
线程池及其核心代码分析
ForkJoin
思想
使用接口
RecursiveAction
RecursiveTask
简单例子
实现要素
六、并发设计模式
什么是设计模式
单例模式
不变模式
Future模式
生产者消费者
七、NIO和AIO
什么是NIO
Buffer
Channel
网络编程
AIO
为什么需要了解NIO和AIO
三、Java内存模型和线程安全
原子性
一个操作不可中断的。即使是在多个线程一起执行的时候,一wh 操作一旦开始,就不会被其他线程干扰。 i++读取--修改--写入; 先检查后执行;
有序性
指令执行分多步
取指 IF
译码和取寄存器操作数 ID
执行或者有效地址计算 EX
存储器访问 MEM
写回 WB
单硬件时间周期内只能做一件事情
a = b + c 指令处理 从上往下扫行 LW R1 , b IF ID EX MEM WB LW R2, c IF ID EX MEM WB ADD R3,R1,R2 IF ID X EX MEM WB (MEM被占用) SW A1,R3 IF X ID EX MEM WB 因为存在时间周期浪费,所以编译器或CPU会重排指令,所以多线程会出现有序性问题;
可见性
可见性是指一当一个线程修改了某一个共享变量的值 , 其他线程是否能够立即知道这个修改。 @编译器优化; @硬件优化(如写吸收,批操作写到内存) @每个CPU有自己的寄存器;
Happen-Befor(先后)规则
happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。 1 2 i = 1; //线程A执行 j = i ; //线程B执行 j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。这就是happens-before原则的威力。 happens-before原则定义如下: 1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
程序顺序原则:一个线程内保证语义串行性;
volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:unlock必然发生在随后人lock前
传递性:A先于B,B先于C,那么A必然先于C;
线程的start()方法先于它的每个动作;
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
对象的构造函数执行结束先于finalize(内存回收前调用)方法
线程安全的概念
指某个函数、函数库在多线程环境中被调用时,能 够正确地处理各个线程的局部变量,使程序功能正确完成;
九、并发调试和JDK8新特性
多线程调试的方法
线程dump及分析
JDK8对并发的新支持
LongAdder
CompletableFuture
StampedLock
十、Jetty/Tocmat分析
基本概念
充分条件:A-->B (如果A能推出B),那么A是B的充分条件。
串行
sychronized+wait/notifyAll
二、并行程序基础
什么是线程
进程内的执行单元
线程的基本操作
开始(START)-->运行(RUNNABLE)-->结束(TERMINATED) 运行<-->阻塞(BLOCKED) 运行<-->等待(WAITING),有限(自唤醒)或无限等待
新建、终止、中断、挂起(suspend)、继续执行(resume)、等待线程结束、谦让
新建: Thread t = new Thread(); t.start();
终止
Thread.stop();释放所有的monitor,不推荐使用(暴力终止,防止数据不一致,如对象字段只写入一半);
中断
Thread.interrupt(); //中断线程 Thread.isInterrupted();//判断是否被中断 Thread.interrupted();//判断是否被中断,并清除当前中断状态 sleep(); yield();
public void run(){ while(true){ Thread.yield(); } } t.interrupt();
挂起(suspend)与继续执行(resume)
suspend()不会释放锁
suspend:【方法已经遭到反对,因为它具有固有的死锁倾向。如果目标线程挂起时在保护关键系统资源的监视器上保持有锁,则在目标线程重新开始以前任何线程都不能访问该资源。如果重新开始目标线程的线程想在调用resume 之前锁定该监视器,则会发生死锁。】 package net.jcip.test; import java.io.ObjectInputStream; /** * Created by Administrator on 2019/4/13/013. */ public class TestSuspend { private static Object monitor = new Object(); static RunObject t1 = new RunObject("lxg"); static RunObject t2 = new RunObject("rym"); public static class RunObject extends Thread{ public RunObject(String name){ super.setName(name); } @Override public void run() { synchronized (monitor){ System.out.println("The thread "+this.getName()+" coming."); Thread.currentThread().suspend(); } } } public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(100); t2.start(); t1.resume(); t2.resume(); t1.join(); t2.join(); } }
如果加锁发生在resume()之前,则死锁发生
JDK不推荐使用
等待线程结束(join)和谦让(yield)
问:join方法的作用? 答: Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。 问:join方法传参和不传参的区别? 答:join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。 /** * 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行 * Created by Administrator on 2019/4/16/016. */ public class ThreadOrderTest { public static void main(String[] args) throws InterruptedException { ThreadTest t1 = new ThreadTest(); t1.setName("t1"); ThreadTest t2 = new ThreadTest(); t2.setName("t2"); ThreadTest t3 = new ThreadTest(); t3.setName("t3"); t1.start(); t1.join(); t2.start(); t2.join(); t3.start(); t3.join(); } private static class ThreadTest extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } } } }
join的本质: while(isAlive()){ wati(0); } @ 结束后会notifyAll(系统唤醒)所有等待当前线程的线程; @不要使用wait、notify、notifyAll在Thread(线程对象)实例上
不要在Thread实例上使用wait()和notify()方法
守护线程
在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT(Just in time编译,也叫做运行时编译)线程就可以理解为守护线程
当一个JAVA应用内,只有守护线程时,JAVA虚拟机就会自然退出
t.setDaemon(true);
线程优先级
(1) t.setPriority(Thread.MAX_PRIORITY); (2) 范围:1~10; (3) 不是绝对,大概率上先得资源
基本的线程同步操作
synchronized
指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁;
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁;
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁;
虚拟机内部实现
Object.wait();
把当前的锁释放,然后让出CPU,进入等待状态
必须获得当前对象monitor才能使用当前对象的wait,然后等待其他线程notify
Object.notify();
ReentrantLock+lock/unlock+Condition(await/signal)
八、锁的优化和注意事项
锁优化的思路和方法
虚拟机的锁优化
一个错误使用锁的案例
ThreadLocal及其源码分析