java并发编程
线程有哪些基本状态?
Java 线程状态变迁如下图所示
由上图可以看出:
线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。
异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。
阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。
非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。
----------------------------------------------------------------------------参考《Java高并发编程详解 多线程与架构设计》----------------------------------------------------------------------------
使用Jconsole观察线程
守护线程
守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程。
用户线程:我们平常创建的普通线程。
守护线程(即 Daemon thread):是个服务线程,用来服务于用户线程;不需要上层逻辑介入,当然我们也可以手动创建一个守护线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
TimeUnit代替Thread.sleep
Thread. sleep(12257088L);
TimeUnit.HOURS. sleep(3) ;
TimeUnit.MINUTES.sleep(24) ;
TimeUnit.SECONDS. sleep(17) ;
TimeUnit.MILLISECONDS.sleep(88) ;
synchronized
同步方法
public synchronized void sync(){
}
同步代码块
private final Object MUTEX = new object() ;
public void sync( )
{
synchronized (MUTEX)
{
}
}
synchronized关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源,很多资料、书籍将synchronized ( mutex)称为锁,其实这种说法是不严谨的,准确地讲应该是某线程获取了与mutex关联的monitor锁(当然写程序的时候知道它想要表达的语义即可)
( 1 ) Monitorenter
每个对象都与一个monitor相关联,一个monitor的lock的锁只能被-个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情。
如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了。.
如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
( 2 ) Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该monitorblock的线程将再次尝试获得对该monitor的所有权。
使用synchronized需要注意的问题
1.与monitor关联的对象不能为空
private final Object mutex = null;
public void syncMethod(){
synchronized(mutex){
}
}
Mutex为null,很多人还是会犯这么简单的错误,每一个对象和一个monitor关联,对象都为null了,monitor 肯定无从谈起。
2. synchronized作用域太大
3.不同的monitor企图锁相同的方法
public class Task implements Runnable { private final Object MUTEX = new Object(); @Override public void run() { synchronized (MUTEX) { System.out.println(); } } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(Task::new).start(); } } }
//正确的做法 private static final Object MUTEX = new Object();
上面的代码构造了五个线程,同时也构造了五个Runnable实例,Runnable 作为线程逻辑执行单元传递给Thread,然后你将发现,synchronized根本互斥不了与之对应的作用域,线程之间进行monitorlock的争抢只能发生在与monitor关联的同一个引用上,上面的代码每一个线程争抢的monitor关联引用都是彼此独立的,因此不可能起到互斥的作用。
4.多个锁的交叉导致死锁
private final object MUTEX_ READ = new object() ;
private final object MUTEX_ WRITE = new 0bject( ) ;
public void read( )
{
synchronized (MUTEX_ READ ) .
{
synchronized ( MUTEX_ WRITE)
{
//...
}
}
}
public void write( )
{
synchronized (MUTEX_ WRITE )
{
synchronized (MUTEX_ READ )
{
//...
}
}
}
This Monitor和Class Monitor
所有的非静态同步方法用的都是同一把锁——实例对象本身
所有的静态同步方法用的也是同一把锁——类对象本身
synchronized(this)和synchronized(object)区别
synchronized(this),锁的对象是this,是类的实例。
class STest{ public void print(){ synchronized (this){ System.out.println("xxxx"); } } } public class SynchronizeMain { public static void main(String[] args) throws InterruptedException { STest sTest = new STest(); // Thread 1 Thread t1 = new Thread(() -> { sTest.print(); }); // Thread 2 Thread t2 = new Thread(() -> { try { synchronized (sTest){ while (true); } } catch (Exception e) { System.out.println("Exception="+e.getMessage()); } }); t2.start(); t1.start(); } }
synchronized(this)的锁对象是this,如果使用这种方式,一旦锁对象(实例)被别人获取,别人只要开个线程像Thread 2 一样,那你的Thread 1,这个正常的工作线程,就永远得不到执行,造成死锁。
synchronized(object),锁的对象是object。
class STest{ private final Object object = new Object(); public void print(){ synchronized (object){ System.out.println("xxxx"); } } } public class SynchronizeMain { public static void main(String[] args) throws InterruptedException { STest sTest = new STest(); // Thread 1 Thread t1 = new Thread(() -> { sTest.print(); }); // Thread 2 Thread t2 = new Thread(() -> { try { synchronized (sTest){ while (true); } } catch (Exception e) { System.out.println("Exception="+e.getMessage()); } }); t2.start(); Thread.sleep(1000); t1.start(); } }
synchronized(object),锁的对象是object。所以,你拿到了类的实例,也不会影响到Thread1的正常执行。
volatile和synchronized的区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
java内存模型
假设主内存的共享变量为0,线程1和线程﹖分别拥有共享变量X的副本,假设线程1此时将工作内存中的x修改为1,同时刷新到主内存中,当线程⒉想要去使用副本x的时候,就会发现该变量已经失效了,必须到主内存中再次获取然后存入自己的工作内容中,这一点和CPU 与CPU Cache 之间的关系非常类似。
Java内存模型与CPU硬件架构交互图
volatile关键字的语义
被volatile修饰的实例变量或者类变量具备如下两层语义。
1 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volatile修饰的变量,另外一个线程会立即看到最新的值。
2 禁止对指令进行重排序操作。
(1)理解volatile 保证可见性
关于共享变量在多线程间的可见性,在VolatileFoo例子中已经体现得非常透彻了,Updater线程对init_value变量的每一次更改都会使得Reader线程能够看到(在happens-before规则中,第三条volatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作),其步骤具体如下。
1 ) Reader线程从主内存中获取init_value的值为0,并且将其缓存到本地工作内存中。2 )Updater线程将init_value的值在本地工作内存中修改为1,然后立即刷新至主内存中。
3 ) Reader线程在本地工作内存中的 init_value失效(反映到硬件上就是CPU的L1或者L2的Cache Line失效)。
4 )由于Reader线程工作内存中的init_value失效,因此需要到主内存中重新读取init_value的值。
经过上面几个步骤的分析,相信读者对volatile关键字保证可见性有了一个更加清晰的认识了。
(2)理解volatile 保证顺序性
volatile关键字对顺序性的保证就比较霸道一点,直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但是对于volatile前后无依赖关系的指令则可以随便怎么排序
(3)理解volatile不保证原子性
volatile的使用场景
(1) 开关控制利用可见性的特点
public class Threadcloseale extends Thread { private volatile boolean started = true; @Override public void run() { while (started) { //do work System.out.println(); } } public void shutdown() { this.started = false; } }
当外部线程执行ThreadCloseable的 shutdown方法时,ThreadCloseable会立刻看到started 发生了变化(原因是因为ThreadCloseable工作内存中的started失效了,不得不到主内存中重新获取)。
如果started没有被volatile关键字修饰,那么很有可能外部线程在其工作内存中修改了started之后不及时刷新到主内存中,或者ThreadCloseable一直到自己的工作内存中读取started变量,都有可能导致started=true不生效,线程就会无法关闭。
(2) 状态标记利用顺序性特点
关于使用状态标记说明顺序性的例子,我们之前用context进行过了举例,这里就不再赘述了。
(3) Singleton设计模式的double-check 也是利用了顺序性特点
死锁诊断
(1 )交叉锁弓|起的死锁
打开jstack工具或jconsole工具一般交叉锁引起的死锁线程都会进人BLOCKED状态,CPU资源占用不高,很容易借助工具来发现。
(2)死循环引起的死锁(假死)
工具不会给出明显的提示,严格意义.上来说死循环会导致程序假死,算不上真正的死锁,但是某个线程对CPU消耗过多,导致其他线程等待CPU,内存等资源也会陷人死锁等待。
线程间通信
与网络通信等进程间通信方式不一样,线程间通信又称为进程内通信,多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。
单线程间的通信
wait
notify
wait和sleep
从表面上看,wait和sleep方法都可以使当前线程进人阻塞状态,但是两者之间存在着本质的区别,下面我们将总结两者的区别和相似之处。
1 wait和sleep方法都可以使线程进人阻塞状态。
2 wait和sleep方法均是可中断方法,被中断后都会收到中断异常。
3 wait是Object的方法,而sleep是Thread特有的方法。
4 wait方法的执行必须在同步方法中进行,而sleep则不需要。
5 线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释
放monitor的锁。
6 sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要
被其他线程中断后才能退出阻塞。
多线程间通信
notifyAll
线程休息室wait set
在虚拟机规范中存在-一个wait set (wait set又被称为线程休息室)的概念,至于该wait set是怎样的数据结构,JDK官方并没有给出明确的定义,不同厂家的JDK有着不同的实现方式,甚至相同的JDK厂家不同的版本也存在着差异,但是不管怎样,线程调用了某个对象的wait方法之后都会被加入与该对象monitor关联的wait set中,并且释放monitor的所有权。
synchronized关键字的缺陷
synchronized关键字提供了一种排他式的数据同步机制,某个线程在获取monitorlock的时候可能会被阻塞,而这种阻塞有两个很明显的缺陷:第一,无法控制阻塞时长。第二,阻塞不可被中断。
ThreadGroup与Thread
守护ThreadGroup
Hook线程以及捕获线程执行异常
获取线程运行时异常
在Thread类中,关于处理运行时异常的API总共有四个,如下所示:
1 public void setUncaughtExceptionHandler (UncaughtExceptionHandler eh):为某个特定线程指定UncaughtExceptionHandler。
2 public static void setDefaultUncaughtExceptionHandler ( UncaughtExceptionHandlereh):设置全局的UncaughtExceptionHandler。
3 public UncaughtExceptionHandler getUncaughtExceptionHandler() :获取特定线程的UncaughtExceptionHandler。
4 public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler():获取全局的UncaughtExceptionHandler。
Hook线程介绍
JVM进程的退出是由于JVM进程中没有活跃的非守护线程,或者收到了系统中断信号,向JVM程序注入一个Hook线程,在JVM进程退出的时候,Hook线程会启动执行,通过Runtime可以为JVM注人多个Hook线程。
import java.util.concurrent.TimeUnit; public class ThreadHook { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.out.println("The hook thread 1 is running."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("The hook thread 1 will exit."); } }); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.out.println("The hook thread 2 is running."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("The hook thread 2 will exit."); } }); } }
给Java程序注人了两个Hook线程,在main线程中结束,也就是JVM中没有了活动的非守护线程,JVM进程即将退出时,两个Hook线程会被启动并且运行。
Hook线程应用场景以及注意事项
Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理。Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄、socket链接、数据库connection等。
尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出。
线程池原理
所谓线程池,通俗的理解就是有一个池子,里面存放着已经创建好的线程,当有任务提交给线程池执行时,池子中的某个线程会主动执行该任务。如果池子中的线程数量不够应付数量众多的任务时,则需要自动扩充新的线程到池子中,但是该数量是有限的,就好比池塘的水界线一样。当任务比较少的时候,池子中的线程能够自动回收,释放资源。为了能够异步地提交任务和缓存未被处理的任务,需要有一个任务队列,如图8-1所示。
通过上面的描述可知,一个完整的线程池应该具备如下要素。
任务队列:用于缓存提交的任务。
线程数量管理功能:一个线程池必须能够很好地管理和控制线程数量,可通过如下三个参数来实现,比如创建线程池时初始的线程数量init ;线程池自动扩充时最大的线程数量max ;在线程池空闲时需要释放线程但是也要维护一定数量的活跃数量或者核心数量core。有了这三个参数,就能够很好地控制线程池中的线程数量,将其维护在一个合理的范围之内,三者之间的关系是init<=core<=max。
任务拒绝策略:如果线程数量已达到上限且任务队列已满,则需要有相应的拒绝策略来通知任务提交者。
线程工厂:主要用于个性化定制线程,比如将线程设置为守护线程以及设置线程名称等。QueueSize :任务队列主要存放提交的Runnable,但是为了防止内存溢出,需要有limit数量对其进行控制。
Keepedalive时间:该时间主要决定线程各个重要参数自动维护的时间间隔。
1 ObservableThread 监控任务的生命周期
观察者模式遇到thread
2 Single Thread Execution 设计模式
Single Thread Execution模式是指在同一时刻只能有一个线程去访问共享资源,就像独木桥一样每次只允许一人通行,简单来说,Single Thread Execution就是采用排他式的操作保证在同一时刻只能有一个线程访问共享资源。
3 读写锁分离设计模式
Synchronized StampedLock ReadWriteLock
4 不可变对象设计模式
无论是synchronized关键字还是显式锁Lock,都会牺牲系统的性能,不可变对象的设计理念在这几年变得越来越受宠,其中Actor模型(不可变对象在Akka、jActor、Kilim等Actor模型框架中得到了广泛的使用)等都是依赖于不可变对象的设计达到lock free(无锁)的。
Java核心类库中提供了大量的不可变对象范例,其中java.lang.String的每一个方法都没有同步修饰,可是其在多线程访问的情况下是安全的,Java8中通过Stream修饰的ArrayList在函数式方法并行访问的情况下也是线程安全的,所谓不可变对象是没有机会去修改它,每一次的修改都会导致一个新的对象产生,比如 String s1 =“Hello”;s1=s1+”world”两者相加会产生新的字符串。
5 future设计模式
假设有个任务需要执行比较长的的时间,通常需要等待任务执行结束或者出错才能返回结果,在此期间调用者只能陷入阻塞苦苦等待,对此,Future设计模式提供了一种凭据式的解决方案。
JDK1.8时引入了CompletableFuture,其结合函数式接口可实现更强大的功能。
6 Guarded Suspension 确保挂起设计模式
当线程在访问某个对象时,发现条件不满足,就暂时挂起等待条件满足时再次访问,这一点和 Balking 设计模式刚好相反(Balking 在遇到条件不满足时会放弃)。
7 线程上下文设计模式
虽然有些时候后一个步骤未必会需要前一个步骤的输出结果,但是都需要将context从头到尾进行传递,假如方法参数比较少还可以容忍,如果方法参数比较多,在七八次的调用甚至十几次的调用,都需要从头到尾地传递context,很显然这是一种比较烦琐的设计,那么我们就可以尝试采用线程的上下文设计来解决这样的问题。
8 Balking (犹豫)设计模式
多个线程监控某个共享变量,A线程监控到共享变量发生变化后即将触发某个动作,但是此时发现有另外一个线程B已经针对该变量的变化开始了行动,因此A便放弃了准备开始的工作,我们把这样的线程间交互称为Balking(犹豫)设计模式。
9 Latch (门阀)设计模式
若干线程并发执行某个特定的任务,然后等到所有的子任务都执行结束之后再统一汇总,比如用户想要查询自己三年以来银行账号的流水,为了保证运行数据库的数据量在一个恒定的范围之内,通常数据只会保存一年的记录,其他的历史记录或被备份到磁盘,或者被存储于hive数据仓库,或者被转存至备份数据库之中,总之想要三年的流水记录,需要若干个渠道的查询才可以汇齐。如果一个线程负责执行这样的任务,则需要经历若干次的查询最后汇总返回给用户,很明显这样的操作性能低下,用户体验差,如果我们将每一个渠道的查询交给一个线程或者若干个线程去查询,然后统一汇总,那么性能会提高很多,响应时间也会缩短不少。
10 Thread-Per-Message 模式
Thread-Per-Message的意思是为每一个消息的处理开辟一个线程使得消息能够以并发的方式进行处理,从而提高系统整体的吞吐能力。这就好比电话接线员一样,收到的每一个电话投诉或者业务处理请求,都会提交对应的工单,然后交由对应的工作人员来处理。
11 Two Phase Termination (两阶段终止)设计模式
当一个线程正常结束,或者因被打断而结束,或者因出现异常而结束时,我们需要考虑如何同时释放线程中资源,比如文件句柄、socket套接字句柄、数据库连接等比较稀缺的资源。Two Phase Termination设计模式
12 Worker-Thread (流水线)设计模式
Worker-Thread模式有时也称为流水线设计模式,这种设计模式类似于工厂流水线,上游工作人员完成了某个电子产品的组装之后,将半成品放到流水线传送带上,接下来的加工工作则会交给下游的工人。
13 Active Objects (主动对象)设计模式
Active是“主动”的意思,Active Object是“主动对象”的意思,所谓主动对象就是指其拥有自己的独立线程,比如java.lang.Thread实例就是一个主动对象,不过Active Object Pattern不仅仅是拥有独立的线程,它还可以接受异步消息,并且能够返回处理的结果。System.gc()方法就是一个“接受异步消息的主动对象”。
标准的Active Objects要将每一个方法都封装成Message,然后提交至Message 队列中,这样的做法有点类似于远程方法调用(RPC : Remote Process Call)。如果某个接口的方法很多,那么需要封装很多的Message类;同样如果有很多接口需要成为Active Object,则需要封装成非常多的Message类,这样显然不是很友好。更加通用的ActiveObject框架,可以将任意的接口转换成Active Object。使用JDK动态代理的方式实现一个更为通用的Active Objects,可以将任意接口方法转换为Active Objects,当然如果接口方法有返回值,则必须要求返回Future类型才可以,否则将会抛出IllegalActiveMethod异常。
14 Event Bus (事件总线)设计模式
异步EventBus
15 Event Driven (事件驱动)设计模式
EDA (Event-Driven Architecture))是一种实现组件之间松耦合、易扩展的架构方式,在本节中,我们先介绍EDA的基础组件,让读者对EDA设计架构方式有一个基本的认识,一个最简单的EDA设计需要包含如下几个组件。
Events:需要被处理的数据。
Event Handlers:处理Events的方式方法。
Event Loop:维护Events和 Event Handlers之间的交互流程。
异步EDA框架设计
----------------------------------------------------------------------------参考《Java高并发编程详解 深入理解并发核心库》----------------------------------------------------------------------------
JMH微基准测试
JMH是Java Micro Benchmark Harness的简写,是专门用于代码微基准测试的工具集( toolkit)。JMH是由实现Java 虚拟机的团队开发的,因此他们非常清楚开发者所编写的代码在虚拟机中将会如何执行。
AtomicInteger
CAS包含3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做。
compareAndSwapInt方法是一个native方法,提供了CAS (Compare And Swap)算法的实现,AtomicInteger类中的原子性方法几乎都借助于该方法实现。
AtomicBoolean
AtomicBoolean的实现方式比较类似于AtomicInteger类,实际上AtomicBoolean内部的value本身就是一个volatile关键字修饰的int类型的成员属性。
AtomicLong
与AtomicInteger类似
AtomicReference
AtomicReference类提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法。在某些场合下该类可以完美地替代synchronized关键字和显式锁,实现在多线程下的非阻塞操作。
AtomicStampedReference
如何避免CAS算法带来的ABA问题呢?针对乐观锁在并发情况下的操作,我们通常会增加版本号,比如数据库中关于乐观锁的实现方式,以此来解决并发操作带来的ABA问题。在Java原子包中也提供了这样的实现AtomicStampedReference<E>。
AtomicArray
AtomicIntegerArray:提供了原子性操作int数据类型数组元素的操作。
AtomicLongArray:提供了原子性操作long 数据类型数组元素的操作。
AtomicReferenceArray:提供了原子性操作对象引用数组元素的操作。
AtomicFieldUpdater
在Java的原子包中提供了三种原子性更新对象属性的类,分别如下所示。
AtomicIntegerFieldUpdater:原子性地更新对象的int类型属性,该属性无须被声明成AtomicInteger。
AtomicLongFieldUpdater :原子性地更新对象的 long类型属性,该属性无须被声明成AtomicLong。
AtomicReferenceFieldUpdater:原子性地更新对象的引用类型属性,该属性无须被声明成AtomicReference<T>。
sun.misc.Unsafe
如对内存的手动管理,但是本章所学习的原子类型,甚至在接下来的章节中将要学习到的并发工具、并发容器等在其底层都依赖于一个特殊的类sun.misc.Unsafe,该类是可以直接对内存进行相关操作的,甚至还可以通过汇编指令直接进行CPU的操作。
sun.misc.Unsafe提供了非常多的底层操作方法,这些方法更加接近机器硬件(CPU/内存),因此效率会更高。不仅Java本身提供的很多API都对其有严重依赖,而且很多优秀的第三方库/框架都对它有着严重的依赖,比如LMAX Disruptor,不熟悉系统底层,不熟悉C/C++汇编等的开发者没有必要对它进行深究,但是这并不妨碍我们直接使用它。在使用的过程中,如果使用不得当,那么代价将是非常高昂的,因此该类被命名为Unsafe也就在情理之中了,总之一句话,你可以用,但请慎用!
CountDownLatch(倒计数门阀)
JDK官方:“CountDownLatch是一个同步助手,允许一个或者多个线程等待一系列的其他线程执行结束”。
CyclicBarrier(循环屏障)
CyclicBarrier与CountDownLatch非常类似,但是它们之间的运行方式以及原理还是存在着比较大的差异的,并且CyclicBarrier所能支持的功能CountDownLatch是不具备的。比如,CyclicBarrier可以被重复使用,而CountDownLatch当计数器为0的时候就无法再次利用。
Exchanger(交换器)
Exchanger简化了两个线程之间的数据交互,并且提供了两个线程之间的数据交换点,Exchanger等待两个线程调用其exchange()方法。调用此方法时,交换机会交换两个线程提供给对方的数据。
Semaphore(信号量)
Semaphore(信号量)是一个线程同步工具,主要用于在一个时刻允许多个线程对共享资源进行并行操作的场景。
phaser(阶段器)
Phaser 是在JDK 1.7版本中才加入的。Phaser同样也是一个多线程的同步助手工具,它是一个可被重复使用的同步屏障,它的功能非常类似于本章已经学习过的CyclicBarrier 和CountDownLatch 的合集,但是它提供了更加灵活丰富的用法和方法,同时它的使用难度也要略微大于前两者。
Lock&ReentrantLock
Lock接口是对锁操作方法的一个基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助于Lock创建不同的Condition对象进行多线程间的通信操作,与关键字synchronized进行方法同步代码块同步的方式不同,Lock提供了编程式的锁获取(lock())以及释放操作(unlock())等其他操作。
ReadWriteLock&ReentrantReadWriteLock
与ReentrantLock一样,ReentrantReadWriteLock的使用方法也是非常简单的,只不过在使用的过程中需要分别派生出“读锁”和“写锁”,在进行共享资源读取操作时,需要使用读锁进行数据同步,在对共享资源进行写操作时,需要使用写锁进行数据一致性的保护。
Condition详解
如果说显式锁Lock可以用来替代synchronized关键字,那么Condition接口将会很好地替代传统的、通过对象监视器调用wait()、notify()、notifyAll()线程间的通信方式。Condition对象是由某个显式锁Lock创建的,一个显式锁Lock可以创建多个Condition对象与之关联,Condition 的作用在于控制锁并且判断某个条件(临界值)是否满足,如果不满足,那么使用该锁的线程将会被挂起等待另外的线程将其唤醒,与此同时被挂起的线程将会进入阻塞队列中并且释放对显式锁Lock的持有,这一点与对象监视器的wait()方法非常类似。
建议使用Condition的方式完全替代对象(Object Monitor)监视器的使用。由于Condition的卓越表现,除了广泛应用于开发中之外,JDK本身的很多类的底层都是采用Condition来实现的。
StampedLock
如果对读写锁使用不得当,则还会引起饥饿写的情况发生,所谓的饥饿写是指在使用读写锁的时候,读线程的数量远远大于写线程的数量,导致锁长期被读线程霸占,写线程无法获得对数据进行写操作的权限从而进入饥饿的状态(当然可以在构造读写锁时指定其为公平锁,读写线程获得执行权限得到的机会相对公平,但是当读线程大于写线程时,性能效率会比较低下)。因此在使用读写锁进行数据一致性保护时请务必做好线程数量的评估(包括线程操作的任务类型)。
针对这样的问题,JDK1.8版本引入了StampedLock,该锁由一个long型的数据截(stamp)和三种模型构成,当获取锁(比如调用readLock(),writeLock())的时候会返回一个 long型的数据戳( stamp),该数据戳将被用于进行稍后的锁释放参数。如果返回的数据戳为0(比如调用tryWriteLock()),则表示获取锁失败,同时StampedLock还提供了一种乐观读的操作方式。
Guava之Monitor
无论使用对象监视器的wait notify/notifyAll还是Condition的 await signall signalAll方法调用,我们首先都会对共享数据的临界值进行判断,当条件满足或者不满足的时候才会调用相关方法使得当前线程挂起,或者唤醒wait队列/set中的线程,因此对共享数据临界值的判断非常关键,Guava的 Monitor工具提供了一种将临界值判断抽取成Guard 的处理方式,可以很方便地定义若干个 Guard也就是临界值的判断,以及对临界值判断的重复使用,除此之外Monitor还具备synchronized关键字和显式锁Lock的完整语义。
当对共享数据进行操作之前,首先需要获得对该共享数据的操作权限(也就是获取锁的动作),然后需要判断临界值是否满足,如果不满足,则为了确保数据的一致性需要将当前线程挂起(对象监视器的wait set或者Condition 的阻塞队列),这样的动作,前文中已经练习过很多次了,Monitor 以及 Monitor Guard则很好地将类似的一系列动作进行了抽象,隐藏了锁的获取、临界值判断、线程挂起、阻塞线程唤醒、锁的释放等操作。
Guava之RateLimiter
RateLimiter,顾名思义就是速率(Rate)限流器(Limiter),事实上它的作用正如名字描述的那样,经常用于进行流量、访问等的限制,这一点与Semaphore非常类似,但是它们的关注点却完全不同,RateLimiter关注的是在单位时间里对资源的操作速率(在RateLimiter内部也存在许可证(permits)的概念,因此可以理解为在单位时间内允许颁发的许可证数量),而Semaphore则关注的是在同一时间内最多允许多少个许可证可被使用,它不关心速率而只关心个数。
RateLimiter-----漏桶算法
因此在一个提供高并发服务的系统中,若系统无法承受更多的请求,则对其进行降权处理(直接拒绝请求,或者将请求暂存起来等稍后处理),这是一种比较常见的做法,漏桶算法作为一种常见的限流算法应用非常广泛
1 无论漏桶进水速率如何,漏桶的出水速率永远都是固定的。
2 如果漏桶中没有水流,则在出水口不会有水流出。
3 漏桶有一定的水容量。
4 如果流入水量超过漏桶容量,则水将会溢出(降权处理)。
令牌环桶算法
令牌环桶与漏桶比较类似,漏桶对水流进入的速度不做任何限制,它只对水流出去的速率是有严格控制的,令牌环桶则与之相反,在对某个资源或者方法进行调用之前首先要获取到令牌也就是获取到许可证才能进行相关的操作,否则将不被允许。比如,常见的互联网秒杀抢购等活动,商品的数量是有限的,为了防止大量的并发流量进入系统后台导致普通商品消费出现影响,我们需要对类似这样的操作增加令牌授权、许可放行等操作,这就是所谓的令牌环桶。
1 根据固定的速率向桶里提交数据。
2 口新加数据时如果超过了桶的容量,则请求将会被直接拒绝。
3 如果令牌不足,则请求也会被拒绝(请求可以再次尝试)。
java并发包之并发容器
基本链表
优先级链表
跳表(SkipList)
平衡树的另一个选择,也是redis的主要数据结构之一。
增加多个层级进行数据存储和查找,这种以空间换取时间的思路能够加快元素的查找速度,跳表( skiplist)正是受这种多层链表的想法启发而设计出来的。实际上,上面每一层链表的节点个数,都会是下面一层节点个数的一半左右,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。另外,跳表中的元素在插入时就已经是根据排序规则进行排序的,在进行查找时无须再进行排序。
BlockingQueue(阻塞队列)
以下七种队列都可称为BlockingQueue,所谓Blocking Queue是指其中的元素数量存在界限,当队列已满时(队列元素数量达到了最大容量的临界值),对队列进行写入操作的线程将被阻塞挂起,当队列为空时(队列元素数量达到了为0的临界值),对队列进行读取的操作线程将被阻塞挂起。
ArrayBlockingQueue
基于数组实现的FIFO“有边界”队列
PriorityBlockingQueue
优先级阻塞队列是一个“无边界”阻塞队列,与优先级链表类似的是,该队列会根据某种规则(Comparator)对插入队列尾部的元素进行排序,因此该队列将不会遵循FIFO ( first-in-first-out)的约束。
LinkedBlockingQueue
LinkedBlockingQueue是“可选边界”基于链表实现的FIFO队列。
DelayQueue
DelayQueue也是一个实现了BlockingQueue接口的“无边界”阻塞队列。
SynchronousQueue
尽管SynchronousQueue是一个队列,但是它的主要作用在于在两个线程之间进行数据交换,区别于Exchanger的主要地方在于(站在使用的角度)SynchronousQueue所涉及的一对线程一个更加专注于数据的生产,另一个更加专注于数据的消费(各司其职),而Exchanger则更加强调―对线程数据的交换。
LinkedBlockingDeque
LinkedBlockingDeque是一个基于链表实现的双向(Double Ended Queue,Deque)阻塞队列,双向队列支持在队尾写人数据,读取移除数据;在队头写入数据,读取移除数据。LinkedBlockingDeque实现自BlockingDeque ( BlockingDeque 又是BlockingQueue的子接口),并且支持可选“边界”,与LinkedBlockingQueue一样,对边界的指定在构造LinkedBlockingDeque时就已经确定了。
LinkedTransferQueue
TransferQueue是一个继承了BlockingQueue的接口,并且增加了若干新的方法。LinkedTransferQueue是 TransferQueue接口的实现类,其定义为一个无界的队列,具有FIFO的特性。
以上7种 BlockingQueue之间的继承关系
ConcurrentHashMap
在 JDK 1.8版本中几乎重构了ConcurrentHashMap 的内部实现,摒弃了segment 的实现方式,直接用table数组存储键值对,在JDK1.6中,每个bucket中键值对的组织方式都是单向链表,查找复杂度是O(n),JDK1.8中当链表长度超过8时,链表转换为红黑树,查询复杂度可以降低到O(log n),改进了性能。利用CAS+Synchronized可以保证并发更新的安全性,底层则采用数组+链表+红黑树(提高检索效率)的存储结构。
ConcurrentSkipListMap
ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上,其能够在O(log(n))时间内完成查找、插入、删除操作。调用ConcurrentSkipListMap的 size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素的个数,这个操作是个O(log(n))的操作。
在读取性能上,虽然ConcurrentSkipListMap不能与ConcurrentHashMap相提并论,但是ConcurrentSkipListMap存在着如下两大天生的优越性是ConcurrentSkipListMap所不具备的。
第一,由于基于跳表的数据结构,因此ConcurrentSkipListMap的key是有序的。
第二,ConcurrentSkipListMap支持更高的并发,ConcurrentSkipListMap的存取时间复杂度是o (log(n)),与线程数几乎无关,也就是说,在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出它的优势。
写时考贝算法(Copy On Write)
并发容器—CopyOnWrite容器,简称COw,该容器的基本实现思路是在程序运行的初期,所有的线程都共享一个数据集合的引用。所有线程对该容器的读取操作将不会对数据集合产生加锁的动作,从而使得高并发高吞吐量的读取操作变得高效,但是当有线程对该容器中的数据集合进行删除或增加等写操作时才会对整个数据集合进行加锁操作,然后将容器中的数据集合复制一份,并且基于最新的复制进行删除或增加等写操作,当写操作执行结束以后,将最新复制的数据集合引用指向原有的数据集合,进而达到读写分离最终一致性的目的。
CopyOnWrite常常被应用于读操作远远高于写操作的应用场景中。
线程池(Executor& ExecutorService)
public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy() ); executor.execute(new Runnable() { @Override public void run() { System.out.println("execute the runnable task"); } }); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() { return "execute the callable task and this is the result"; } }); System.out.println(future.get()); AtomicDouble result = new AtomicDouble(); Future<AtomicDouble> f = executor.submit(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(20); result.set(35.34D); } catch (InterruptedException e) { e.printStackTrace(); } } }, result); System.out.println("The task result: " + future.get()); System.out.println("The task is done? " + future.isDone()); }
Guava的Future
Future一般是被用于ExecutorService提交任务之后返回的“凭据”,本节对Executor-Service中所有涉及Future相关的执行方法都做了比较详细的讲解,Java中的Future不支持回调的方式,这显然不是一种完美的做法,调用者需要通过get方法进行阻塞方式的结果获取,因此在Google Guava工具集中提供了可注册回调函数的方式,用于被动地接受异步任务的执行结果,这样一来,提交异步任务的线程便不用关心如何得到最终的运算结果。
虽然Google Guava的 ListenableFuture是一种优雅的解决方案,但是CompletablFuture则更为强大和灵活,目前业已成为使用最广的Future实现之一。
Fork/Join Framework
Fork/Join框架是在 JDK1.7版本中被Doug Lea引入的,Fork/Join计算模型旨在充分利用多核CPU的并行运算能力,将一个复杂的任务拆分(fork)成若干个并行计算,然后将结果合并(join)。
CompletionService
Future除了“调用者线程需要持续对其进行关注才能获得结果”这个缺陷之外,还有一个更为棘手的问题在于,当通过ExecutorService的批量任务执行方法 invokeAll来执行一批任务时,无法第一时间获取最先完成异步任务的返回结果。
CompletionService则采用了异步任务提交和计算结果Future解耦的一种设计方式,在 CompletionService中,我们进行任务的提交,然后通过操作队列的方式(比如take或者poll)来获取消费 Future。
CompletableFuture
CompletableFuture是自JDK1.8版本中引入的新的Future,常用于异步编程之中,所谓异步编程,简单来说就是:“程序运算与应用程序的主线程在不同的线程上完成,并且程序运算的线程能够向主线程通知其进度,以及成功失败与否的非阻塞式编码方式”,无论是ExecutorService还是CompletionService,都需要主线程主动地获取异步任务执行的最终计算结果,如此看来,Google Guava所提供的ListenableFuture更符合这段话的描述,但是ListenableFuture无法将计算的结果进行异步任务的级联并行运算,甚至构成一个异步任务并行运算的 pipeline,但是这一切在CompletableFuture中都得到了很好的支持。
CompletableFuture实现自CompletionStage接口,可以简单地认为,该接口是同步或者异步任务完成的某个阶段,它可以是整个任务管道中的最后一个阶段,甚至可以是管道中的某一个阶段,这就意味着可以将多个CompletionStage链接在一起形成一个异步任务链,前置任务执行结束之后会自动触发下一个阶段任务的执行。另外,CompletableFuture还实现了Future接口,所以你可以像使用Future一样使用它。
Stream
1.8版本中,Stream为容器的使用提供了新的方式,它允许我们通过陈述式的编码风格对容器中的数据进行分组、过滤、计算、排序、聚合、循环等操作。
public static void main(String[] args) throws IOException { List<String> list = Files.lines(Paths.get("123.txt"), StandardCharsets.UTF_8).collect(Collectors.toList()); list.forEach(System.out::println); }
stream主要分为两类操作 Intermediate 和 Terminal
Collector
Parallel Stream(并行流)
对集合中、IO中、数组中等其他的元素以并行的方式计算处理元素数据。在并行流的处理过程中,元素会被拆分为多个元素块( chunks),每一个元素块都包含了若干元素,该元素块将被一个独立的线程运算,当所有的元素块被不同的线程运算结束之后,结果汇总将会作为最后的结果,这一切的一切都是由并行流( Parallel Stream)替我们完成
Spliterator
Spliterator也是Java8引入的一个新的接口,其主要应用于Stream中,尤其是在并行流进行元素块拆分时主要依赖于Spliterator的方法定义,这与我们在ForkJoinPool中进行子任务拆分是一样的,只不过对Spliterator的引入将任务拆分进行了抽象和提取
Metrics
Metrics最早是在Java的另外一个开源项目dropwizard中使用,主要是为了提供对应用程序各种关键指标的度量手段以及报告方式,由于其内部的度量手段科学合理,源码本身可扩展性极强,现在已经被广泛使用在各大框架平台中,比如我们常见的Kafka,Apache Storm,Spring Cloud等。
import java.util.concurrent.TimeUnit; public class ThreadHook { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.out.println("The hook thread 1 is running."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("The hook thread 1 will exit."); } }); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { System.out.println("The hook thread 2 is running."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("The hook thread 2 will exit."); } }); } }
AQs(AbstractQueuedSynchronizer)
AQs(AbstractQueuedSynchronizer),所谓的AQs即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的ReentrantLock 、 ReentrantReadlMriteLock ,CountDownLatch .Semaphore等都是基于AQ5来实现的。