读java并发编程的艺术,记录
各种关键字以及概念
CAS算法
campare and swap,比较并交换算法。比较旧值有没有更改,没有,才放入新值。
面临的问题/隐患
ABA问题,比较的旧值是A,写入值是B,当前值虽然是A,但是实际上是A->B->A,第三个A了,而不是第一个A。针对这种情况,在变量前追加版本号来解决。
针对多个变量的情况,就不能用CAS,要用锁了,或者取巧的方式,比如i=2,j=a,合并一下,写入ij=2a。这样就可以用CAS操作了。
线程上下文切换
当并发执行累加操作不超过百万次时,速度会比串行执行累加操作(i++)要慢,因为线程有创建和上下文切换的开销。
减少上下文切换的方法:减少锁开销,通过无锁并发编程,比如CAS算法。
volatile
在多处理器开发中保证了共享变量的可见性,是轻量级的Synchronized,
不会引起线程上下文的切换和调度,使用恰当的话,比Synchronized成本更低。
确保所有线程看到这个变量的值是一致的。
具体实现:
平时的线程写数据,都是把写入的数据缓存在缓存行上的(cache line),缓存行在cpu内部,这样的好处是提高了处理速度,节省了cpu到内存之间的通信操作。
而实现了volatile关键字的变量,jvm会在把数据写入到缓存行后,再发送一个lock前缀的指令。
1,cpu就会把缓存行的数据写回到系统内存中。
2,这个写回到内存中的操作,会使其他cpu里缓存了该内存地址的数据无效。(其他线程在cpu上的缓存行,也缓存了相同的数据,会变的无效,所以就保证了及时性)
synchronized
通过monitorenter和monitorexit两个指令来实现锁。
monitorenter在编译后插入到同步代码块的开始位置,而monitorexit插入到方法的结束处和异常处。
当线程执行到monitorenter指令的时候,就会去尝试获取对象所对应的monitor的所有权,也就是尝试获取锁。
锁是存放在java的对象头里的,mark word(header的一个)默认存储对象的hashcode,分代年龄和锁标记位,锁状态,是否是偏向锁。
锁类型
在JDK1.6以后,一共有4种锁状态无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
偏向锁状态
经常是同一个线程反复获得同一个锁,
在线程a进入方法的时候,把a记录到偏向锁(锁偏向的线程id),这样下次再进来的时候,如果偏向锁的id还是a,就不需要去检查锁有没有被占用或者被释放这些操作。
同样线程在进入和退出的时候也不需要再去做加锁和释放锁的动作。因为通过偏向锁id就表示占用了。
如果这个时候来的是线程b,发现偏向锁已经被a占用了,那就要让偏向锁去检查线程a的状态(过程略),如果线程a已经不活动了(不在锁范围),那就把对象头设置成无锁状态。
轻量级锁状态, 重量级锁状态
(jvm)在当前线程创建用于存储锁记录的空间,把对象头中的mark word复制到锁记录中,然后尝试使用CAS算法将头对象中的mark word替换为指向锁记录的指针。成功,变成轻量级锁(表示锁上了)
不成功,自旋转获取试试,
还不行,表示存在竞争,升级锁为重量级锁。等待别个线程把锁用完了再通知我。
重量级锁的好处是不用自旋转等待,不消耗cpu,缺点是响应慢,线程阻塞。
java内存模型
两个不同线程间的通信由java内存模型(JMM)控制,
在概念中,线程会有一个属于自己的本地内存,里面存放了共享变量的副本,线程对共享变量做修改的时候,都是修改的副本中的数据,然后在A线程需要和B线程通信的时候,把自己本地内存里的变量刷到真正的共享内存里去(主内存);
本地内存是一个概念,不是真实存在,实际上可能是包括了缓存,缓冲区,寄存器等等的软件硬件优化。
通信过程:
A把本地内存中更新过的数据刷新到主内存
B到主内存去读取A线程更新的数据。
重排序
在程序执行的时候,为了提高性能,编译器和处理器常常会对指令做重排序。
1,源代码->1,编译器优化重排序->2,指令级并行重排序->3,内存系统重排序->最终执行的指令序列
其中1属于编译器重排序,2,3属于处理器重排序,Java编译器处理重排序规则时,会要求在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。
JMM(内存模型)属于语言级内存模型,确定在不同的编译器和不同的处理器平台之上,禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
2,常见的处理器都不允许对存在数据依赖的操作做重排序。(两个操作访问同一个变量,并且其中有一个是写操作的情况下,两个数据之间就存在数据依赖),(不同线程之间的数据依赖,不被编译器和处理器考虑)
3,有四种屏障类型,读读(LoadLoad Barriers),
读写(StoreStore Barriers),
写读(StoreStore Barriers),
写写(StoreLoad Barriers),其中写读是开销最大的,并且具有其他三个屏障的效果,需要把缓冲区中的所有数据都刷到内存中。
P26页
构造函数:在构造函数的内部,不能让这个被构造对象引用为其他线程所见,如果构造函数里有对final引用的对象的成员域的写入
比如说
public class Example{
final int j;
static Example e;
public Example(){
j=0;
e=this;
}
}
在上面这个例子中的构造函数返回之前,被构造对象的引用(e)不能为其他线程所见。因为此时final域可能还没有被初始化。因为j=0和e=this可以重排序。
happends-before关系
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
可以是一个线程内,也可以是不同线程之间的。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!*happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见*,且前一个操作按顺序排在第二个操作之前。
顺序一致性:在顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,在模型中的每个操作,必须立即对任意线程可见。所以每个线程了解到的操作顺序是一致的。
而JMM(Java内存模型)中是没有这个保证的,java中所有线程看到的操作执行顺序可能是不一致的,比如在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本没有被当前线程执行。在这种情况下,当前线程和其他线程看到的操作执行顺序将不一致。
保证一致性的方式就是加锁
JMM保证线程读操作读取到的值不会无中生有,JVM在堆上分配对象时,首先会对内存空间进行清零,然后才会在上面分配对象。
volatile
把对volatile变量的单个读/写看出是使用同一个锁对执行单个读/写操作做了同步。
内存可见性;读的时候,保证能看见任意线程最后写入的值。
原子性单个读写具有原子操作,但是volatile++这种复合操作不具有原子性
volatile写-读与锁的释放-获取,有相同的内存效果。
volatile的写和锁的释放有相同的内存语义
volatile的读和锁的获取有相同的内存语义
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
锁:当锁释放的时候,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中,当锁获取的时候,JMM会把该线程对应的本地内存置为无效。从而使锁内的代码(被监视器保护的临界区代码)必须从主内存中读取共享变量
concurrent包的实现
volatile变量的读/写和CASkey实现线程间的通信,这些特性形成了整个concurrent得以实现的基石。
通用模式大概是:
1,声明共享变量为volatile
2,用CAS的院子条件更新来实现线程之间的同步
3,同时配合volatile的内存语义来实现线程之间的通信
基于volatile的单例模式双重检查锁
普通的双重检查锁,其实是有隐患的
private static XX instance;
public static XX getInstance(){
if(instance==null){
synchronize(XX.class){
if(instance == null){
instance = new XX();
}
}
}
return instance;
}
因为内存分配的时候
1,分配内存地址,2初始化对象,3,把对象地址指向分配的内存地址
这3个步骤中的2和3是可以重排序的,而在没有volatile的情况下,3执行了以后,外界就可以看见这个对象的真正地址了,但是这个时候可能还没有初始化完毕,就会调用出bug了
所以用 private volatile static XX instance;来代替,保证就算2和3重排序了,但是在123步骤结束之前,外界没有办法看见这个地址。
(因为会在正常的123或者132后面加一个屏障,保证这个屏障也执行了以后别人才能看见)
ThreadLocal
线程变量,以ThreadLocal对象为key,任意对象为值的存储结构
比如ThreadLocal
key就是线程,使用场景:接口耗时,在aop(切面)的时候,在begin和end两个不同的方法中分别设置set和get来设置/获取开始时间。默认的key就是当前线程(不用写),这个aop的begin是被很多线程同时调用的哟
Lock 锁
synchronized vs volatile vs Lock
volatile 锁变量的,保证每个线程看见的变量都是“相同的”,状态同步的
synchronized 锁代码块的,保证一次就一个线程在用这个代码块,便捷性,隐式的获取释放锁
Lock 锁代码块的,比synchronized功能多,获取&释放锁的可操作性增加,显示的获取释放锁。可以实现可重入锁(ReentrantLock)等等(CountdownLatch)。
重入锁:线程在获取到锁之后,再次获取该锁不会被锁阻塞,并且获取锁n次再释放n次后,可以被其他线程获取。
阻塞队列
JDK7提供了7种阻塞队列
ArraylockingQueue 数组实现,有界阻塞队列,先进先出FIFO,不保证公平访问(公平访问会降低吞吐量)
LinkedBlockingQueue链表实现,有界阻塞队列,默认and最大长度是Interger.MAX_VALUE
PriporityBlockingQueue支持优先级的,无界阻塞队列。默认升序排练,可以通过自定义compareTo来改变
DelayQueue支持延迟获取元素的无界队列,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
适合场景:缓存有效期,如果可以被取出,表示有效期到了;定时调度任务,定时取出开始执行任务。
SynchronousQueue不存储元素的阻塞队列,每一个put操作都必须等待一个take操作,否则不能继续添加元素。默认不公平访问,可以设置公平访问。适合做传递性动作,吞吐量高
LinkedTransferQueue链表结构,无界阻塞队列。多了tryTransfer和tranfer方法。
tranfer是方法:如果当前有消费者正在等待接收元素(消费者使用take()or poll()方法),transfer方法可以把生产者传入的元素立刻tranfer给消费者,如果没有,就放在队列的tail节点(尾),并且阻塞等待,被消费后才返回
tryTranfer是不阻塞等待,立刻返回。
LinkedBlockingDeque链表,双向阻塞队列,可以从队列的两端插入和移出元素。在多线程同时入队时,减少了一半的竞争
阻塞队列实现原理:
通知模式实现,put动作以后,put函数里会做lock.unLock操作,take如果这时候是处于notEmpty.await(),就会被通知unlock然后启动,表示有数据可以被消费,就可以继续去队列里取新的数据了;
同理take动作,会在函数里做lock.unlock操作,如果put里是处于notFull.await()中,就会被启动,然后就可以继续put新的东西进去了。
并发工具类
等待多线程完成的CountDownLatch: 允许一个或多个线程等待其他线程完成操作。
同步屏障CyclicBarrier:让一组线程到达一个屏障(同步点)时,被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。CyclicBarrier使用场景比CountDownLatch更复杂,可以通过reset重复使用,CountDownLatch只能用一次。
CyclicBarrier c = new CyclicBarrier();
c.await()//来表示达到屏障;
new CyclicBarrier(new Runnable(){..});//也可以通过这种写法,这里的线程会在到达屏障后执行。
控制并发线程数的Semaphore:(信号量)是用来控制同时访问特定资源的线程数量,通过协调各个线程,来保证合理的使用公共资源。可以用于做流量控制,特别是公共资源有限的场景,比如数据库连接。
Semaphore s = new Semaphore(10);
s.acquire();
//或者
s.tryAcquire();//尝试获取许可
....
s.release();
线程间交换数据的Exchanger:(交换者),用于线程间协作的工具类。用于进行线程间的数据交换。
提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都达到同步点时,就可以交换数据,将本线程生产出来的数据传递给对方。使用场景,比如校对工作。
private static final Exchanger<String> exgr = new Exchanger<String>();
//线程1
String A ="银行流水A";
exgr.exchange(A);
//线程2
String B = "银行流水B";
String A = exgr.exchange(B);
//这里得到的A就是“银行流水A”了
线程池,Executor框架
线程池创建,corePoolSize是线程池的基本大小,当有任务要提交到线程池时,线程池会创建一个新的线程,即使这个时候其他空闲的基本线程能够执行新任务,也会创建线程,直到符合基本线程数的时候,才会停止。
如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前启动所有基本线程。
Executor框架
直接用:ExecutorService.submit(Callable task或Runnable task);返回类型是实现了FutureTask,一个实现了Future接口的对象,同时FutureTask实现了Runnable,也可以交给ExecutorService.submit执行。
主线程可以执行FutureTask.get()来等待任务执行完成,或者.cancal来取消任务执行。
ThreadPoolExecutor 三种类型 ####
SingleThreadPool,适用于保证顺序执行的任务。
FixedThreadPool,适用于负载比较重的服务器,限制当前线程数量的应用场景
CachedThreadPool.负载较轻,或者执行很多短期异步任务的小程序。
ScheduledThreadPoolExecutor 两种类型
ScheduledThreadPoolExecutor 固定线程数的,适用于后台线程执行周期任务。
SingleThreadScheduledExecutor 只包含一个线程,适合顺序执行后台周期性任务。
Callable & Runnable
Callable会返回结果,而Runnable不会
都能被ScheduledThreadPoolExecutor或者ThreadPoolExecutor的实现类执行
也可以用工厂类Executors来把Runnable包装成一个Callable
ThreadPoolExecutor详解
包含
corePool:基础/核心线程数
maximumPool:最大线程数
keepAliveTimes:存活时间(大于核心线程数部分的线程)
BlockingQueue:用来暂时保存任务的工作队列的类型
RejectedExecutionHandler:如果线程池已经关闭或者饱和的情况下,错误处理的handler
以及三种实现类略
流程:
1,初始化核心线程数
2,把任务放入linkedBlockingQueue
3,线程反复的从linkedBlockingQueue里获取任务来执行。
FixedThreadPool使用linkedBlockingQueue,所以是无界队列(最大长度是Interger.MAX_VALUE,就是无限长,无边界的队列);影响:线程池数固定,所以keepAliveTimes和maximumPool都是无效的参数,因为多余的线程会放入无界队列中,并且RejectedExecutionHandler也不会被调用,因为无限长度的情况下,不会拒绝执行任务,除非shutdown被调用。
其他线程池类型略