Java 并发系列之一:java 并发体系

时间:2022-01-21 03:14:44

1.  java 并发机制的底层原理实现

Java 并发系列之一:java 并发体系

1.1 volatile

Java 并发系列之一:java 并发体系

1.2 synchronized

Java 并发系列之一:java 并发体系

1.3 原子操作

Java 并发系列之一:java 并发体系

2. java 内存模型(JMM)

Java 并发系列之一:java 并发体系

3. java并发基础线程

Java 并发系列之一:java 并发体系

4. java 锁

Java 并发系列之一:java 并发体系

5. java 并发容器

Java 并发系列之一:java 并发体系

6. java阻塞队列(7个)

Java 并发系列之一:java 并发体系

7. java 并发工具(4个)

Java 并发系列之一:java 并发体系

8. java 原子操作类Atomic(13个)

Java 并发系列之一:java 并发体系

9. java并发框架(2个)

Java 并发系列之一:java 并发体系 Java 并发系列之一:java 并发体系

10. txt

 java 并发机制的底层原理实现
volatile
特性
volatile可见性:对一个volatile的读,总可以看到任意线程对这个变量最终的写
volatile原子性:volatile对单个读/写具有原子性(64位Long、Double),但是复合操作除外,例如 i++
volatile有序性:编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主存中读取共享变量
volatile的写-读与 锁的释放-获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义。这也是增强volatile的原因,提供了一种比锁更轻量级的线程之间的通信机制。
重排序规则
1. 当第二个操作是volatile写操作时,不管第一个操作是什么,都不能重排序
2. 当第一个操作是volatile读操作时,不管第二个操作是什么,都不能重排序
3. 当第一个操作是volatile写操作,第二个操作是volatile读操作,不能重排序
内存语义的实现
内存屏障
内存屏障是一组处理器指令,用于实现对内存操作的顺序限制
基于保守策略的JMM内存屏障插入策略
在每个volatile写操作的前面插入一个StoreStore屏障
保证之前的都能刷新到主存
在每个volatile写操作的后面插入一个StoreLoad屏障
保证先写后读,能提高效率
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障
x86处理器只会对写读作重排序,故只有一个屏障StoreLoad即可正确实现volatile写-读的内存语义
操作系统语义
主存、高速缓存(线程私有)缓存一致?
解决方案
通过在总线加 Lock 锁的方式,有些是缓存锁定
通过缓存一致性协议 MESI协议(修改、独占、共享、无效)
实现原则
Lock前缀指令会引起处理器缓存回写到内存
一个处理器的缓存回写到内存会导致其他处理器的缓存失效
内存模型
内存屏障 限制重排序
happens-before中的volatile规则
volatile VS synchronized
volatile是轻量级的synchronized
使用恰当,它比synchronized使用和执行成本更低,因为不会引起线程上下文的切换和调度
synchronized
同步、重量级锁
只有使用Synchronized线程处于阻塞,其他Lock, AQS, LockSupport等线程都是处于等待状态
原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证变量的内存可见性。
锁对象
1. 普通同步方法锁,是当前实例对象
2. 静态同步方法,锁是当前类的class对象
3. 同步方法块,锁是括号里面的对象
java中的每一个对象都可以作为锁
实现机制
Java对象头
synchronized用的锁是保存在Java对象头里的
包括两部分数据
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
Klass Pointer(类型指针)
monitor
java 中 Object和Class中都关联了一个Monitor,一个monitor只能被一个线程拥有
Owner 活动线程
初始时为NULL表示当前没有任何线程拥有该monitor record, 当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
实现
同步代码块采用 monitorenter、monitorexit指令显示的同步
同步方法使用ACC_SYNCHRONIZED标记符隐式的实现
锁优化
自旋锁
该线程等待一段时间,不会被立即挂起,循环看持有锁的线程是否会很快释放锁
自旋数字难以控制(XX: preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失。
缺点
自旋次数无法确定
适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据
变量逃逸
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获得锁
轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
通过CAS(CompareandSwap),即比较并替换,来获取和释放锁
缺点
在多线程环境下,其运行效率比重量级锁还会慢
性能依据
对于绝大部分的锁,在整个生命周期内部都是不会存在竞争的
偏向锁
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必要的CAS操作,如果竞争锁失败,则升级为轻量级锁
原子操作
处理器
使用总线锁保证原子性
使用缓存锁保证原子性
JMM

JVM中除了偏向锁,其他锁(轻量级锁、互斥锁)的实现方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
JAVA1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态
CAS
Compare And Swap, 整个JUC体系最核心、最基础理论,Java中通过锁和CAS实现原子操作
内存地址V,旧的预期值A,要更新的值B,当且仅当内存地址V中的值等于旧的预期值A时才会将内存值V得值修改为B,否则什么也不干
native中存在四个参数
JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现的。
缺陷
ABA问题
检查不到值的变化,实际上却变化了
解决方案
变量前追加版本号版本号
AtomicStampedReference
这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。
循环时间太长
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
解决方法
JVM如果能支持处理器提供的pause指令,那么效率一定会提升
pause作用
1. 可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源
2. 避免在退出循环的时候因内存顺序冲突而引起的CPU流水线被清空,从而提高CPU的执行效率
只能保证一个共享变量原则操作
对多个共享变量操作时,CAS无法保证操作的原子性,需要用锁
解决方案
AtomicReference类保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作
原子操作类Atomic
java.util.concurrent.atomic里的原子操作类提供了线程安全地更新一个变量的方式
4大类型13个原子操作类
基本类型类
AtomicBoolean
AtomicInteger
AtomicLong
数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
引用
AtomicReference
AtomicReferenceFieldUpdater
AtomicMarkableReference
属性
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicStampedReference
核心底层
CAS
Unsafe只提供了3中CAS方法
final native boolean compareAndSwapObject()
final native boolean compareAndSwapInt()
final native boolean compareAndSwapLong()
CAS V.S. 锁
JVM中除了偏向锁,其他锁(轻量级锁、互斥锁)的实现方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
java 内存模型(JMM)
并发编程的挑战
线程上下文切换
死锁
资源限制的挑战
并发编程需要解决的两大问题
线程之间如何通信
线程通信机制
内存共享
消息传递
线程之间如何同步
同步是指程序中用于控制不同线间操作发生相对顺序的机制
内存模型
处理器内存模型
硬件级的内存模型
处理器的内存屏障
内存屏障是一组处理器指令,用于实现对内存操作的顺序限制
为了保证内存可见性,Java编译器再生产指令序列的适当位置会插入内存屏障制定来禁止特定类型的处理器重排序
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
4大类内存屏障指令
LoadLoad Barriers
LoadStore Barriers
StoreStore Barriers
StoreLoad Barriers
全能型的屏障,同时具有其他三个屏障的效果,但执行该屏障开销会很大,因为Buffer Fully Flush
顺序一致性内存模型
多线程环境下的理想化的理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照
为程序提供了极强的内存可见性保证
数据竞争
定义
在一个线程中写一个变量,在另一个线程中读同一个变量,而且写和读没有通过同步来排序
当程序未正确同步时,就可能会存在数据竞争
如果程序是正确同步的,程序的 执行将具有顺序一致性
这里的同步是广义上的同步,包括常用同步原语的正确使用(Synchronized、volatile 、lock和 final)
特性
一个线程中的所有操作必须按照程序的顺序来执行
不管程序是否同步,所有程序都只能看到一个单一的操作执行顺序,每个操作都必须原子执行且立即对所有的线程可见
JMM
语言级的内存模型
JMM的内存可见性保证3方面
基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门(重排序)
1. 单线程程序。
2. 正确同步的多线程程序。
3. 未同步或未正确同步的多线程程序。
happens-before
JMM中最核心的理论,保证跨线程的内存的可见性(可以在一个线程之内,也可以再不用线程之间)通过内存屏障实现
背景
JMM设计的两大核心目标
为程序员提供足够强的内存可见性保证
对编译器和处理器的约束尽可能放松(只要不改变程序的执行结果,编译器处理器怎么优化都行)首先保证正确性,然后再追求执行效率
简单定义
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系
定义
JMM对程序员保证
如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
JMM对编译器和处理器重排序的约束原则
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则判定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
原生java满足8大规则
1. 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2. 监视器锁规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
happens-before与JMM的关系
happens-before规则是JMM呈现给程序员的视图
一个happens-before规则对应于一个或者多个编译器和处理器重排序规则
happens-before VS as-if-serial
as-if-serial 语义保证单线程内程序的执行结果不被改变;程序顺序执行
happens-before 关系保证正确同步的多线程程序的执行结果不被改变;happens-before制定的顺序执行
java并发基础线程
线程的状态
6大状态
NEW
初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE
运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行状态”
BLOCK
阻塞状态,表示线程阻塞于锁
WAITING
等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或者等待)
TIME_WAITING
超时等待状态,可以在等待的时间自行返回的
TERMINATED
终止状态,表示当前线程已经执行完毕
Java状态转移
WAITING-->RUNNABLE
Object.notify()
Object.notifyAll()
LockSupport.unpark(Thread)
TIME_WAITING-->RUNNABLE
Object.notify()
Object.notifyAll()
LockSupport.unpark(Thread)
BLOCK-->RUNNABLE
获取到锁
1. 实例化后New
2. New-->RUNNABLE
系统调度
Thread.start()
running-->ready
Thread.yield
ready-->running
3. RUNNABLE-->WAITING
Object.wait()
Thread.join()
LockSupport.park()
4. RUNNABLE-->TIME_WAITING
Object.wait(long)
Thread.sleep(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()
5. RUNNABLE-->BLOCKED
等待进入synchronized方法
等待进入synchronized块
6. RUNNABLE-->TERMINATED
Java 线程状态变迁
yield
暂停当前正在执行的线程对象。
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
yield()只能使同优先级或更高优先级的线程有执行的机会。
注意:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
注意
等待进入synchronized方法/块 为阻塞状态
java.concurrent.Lock为 等待状态,因为Lock接口对于阻塞的实现使用了LockSupport类中的相关方法
程序 VS 进程 VS 线程
程序
一组指令的有序结合,是静态的指令,是永久存在的
进程
具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源(打开的文件,创建的socket)分配和调度的一个独立单元。进程的存在是暂时的,是一个动态的概念。
线程
线程是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单元。本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈)。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
进程 VS 线程
定义
进程是资源分配和调度的基本单位;线程是CPU/任务调度和执行的基本单位
包含关系
1个进程包含有多(大于等于1)个线程; 线程是进程的一部分(轻量级进程)
地址空间
进程之间地址空间独立; 同一进程内的线程共享本进程的地址空间
切换开销
进程之间切换开销大; 线程之间切换开销小(创建和销毁)
创建
进程fork/vfork; 线程pthread_create
销毁
进程结束,它拥有的所有线程都将销毁; 线程结束,不会影响同个进程中的其他线程
私有属性
进程:PCB(进程控制块); 线程:TCB(线程控制块),线程Id,寄存器,上下文
一个程序至少只有一个进程,一个进程至少有一个线程
启动和终止线程
线程间通信
volatile & synchronized
共享内存和本地内存拷贝的同步更新问题,使得变量不一定能是最新的
volatile: 保证所有线程对变量的可见性
synchronized:保证线程对变量访问的可见性和排他性,获取monitor; 主要确保多个线程在同一时刻,智能有一个线程处于方法或者同步块中。
等待/通知机制
任意java对象所具备的
依托于同步机制,目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量作出的修改
等待通知机制:线程A调用了对象O的wait()方法进入了等待状态,而线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象的wait()与notify()或notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待超时模式
等待超时模式就是在等待/通知范式基础上增加了超时控制,避免执行时间过长,也不会“永久”阻塞调用者,而是按照调用者的要求返回。
超时等待:调用一个方法时,等待一段时间(一般给定一个时间段),如果该方法能够在给定的时间段内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
管道输入/输出流
管道输入输出流与普通的文件输入输出流或者网络输入输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介是内存
4种具体的实现
PipedOutputStream
PipedInputStream
PipedReader
PipedWriter
面向字节
面向字符
Thread.join()的使用
涉及了等待/通知机制,等待前驱线程结束,接收前驱线程结束通知,源码本质也是wait()和notifyAll()
ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
方法
initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。如果set()方法没有调用,第一次get()方法调用时会进行初始化 initialValue(),每个线程会调用一次。
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
set(T value)用来设置当前线程中变量的副本
remove()用来移除当前线程中变量的副本
工作原理
Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value,值时则类似。
ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的,
存储结构的好处
1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
弱引用GC
1、使用完线程共享变量后,显示调用remove方法清除线程共享变量可以及时清除
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
3、对于ThreadLocal变量,我们可以手动的将其置为Null,比如tl =null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。
解决Hash冲突方法
线性探测
应用场景: 用来解决 数据库连接、Session管理、AOP耗时统计等。
注意
不过有点遗憾的是只能放一个值,再次调用set设置值,会覆盖前一次set的值。如果要多个变量,新建多个ThreadLocal对象
是单个线程内函数和组件的共享变量,不是多线程的共享变量,线程隔离

Lock接口
锁是用来控制多个线程访问共享资源的方式。一般来说一个锁可以防止多个线程同时访问共享资源(但有些锁可以允许多个线程访问共享资源,如读写锁)。
在Lock接口出现前,java使用synchronized关键字实现锁的功能,但是在javaSE5之后,并发包中提供了Lock接口(以及其实现类)用来实现锁的功能。
Lock V.S. synchronized
Lock接口有而synchronized不具备的主要特性
尝试非阻塞地获取锁
当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
超时获取锁
在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回
能被中断地获取锁
与synchronized不同,获取到所得线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
Lock提供了与synchronized相似的功能,但必须显示的获取锁与释放锁,虽然不及隐式操作方便,但是拥有了锁获取与释放的可操作性、可中断的锁获取与超时获取锁等多重功能。
队列同步器AQS
队列同步器AbstractQueuedSynchronizer(以下简称同步器), 是用来构建锁或者其他同步组件(继承lock)的基础框架,
它使用了一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步框架,提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列
基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask(任务)
使用方式
继承
同步器的主要使用方法是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
静态内部类
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放方法来供自定义同步组件使用
独占式共享式获取同步状态
同步器即可以支持独占式获取同步状态,也可以支持共享式地获取同步状态,这样方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock、CountDownLatch等)。
锁 VS 同步器
联系
同步器是实现锁(也可以是任何同步组件)的关键:在锁中聚合同步器,利用同步器实现锁的语义。lock方法内部调用实现了AQS的内部类的require方法
区别
锁是面向使用者的,他定义了使用者与锁交互的接口(比如允许两个线程并行访问),隐藏了实现细节;
同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步管理状态、线程的排队、等待与唤醒等底层操作。
锁让使用者仅仅是调用其方法既可以实现同步效果、同步器让实现者通过重写抽象方法进行了队列的底层操作。他们两个是使用者和实现者关注不同的领域实现了相同的效果。
API
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件
同步器基于模板设计模式实现的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法会调用使用者重写的方法。
自定义同步组件(extends Lock)将使用同步器 (静态内部类) 提供的模板方法来实现自己的同步语义
重入锁
ReentrantLock
可重入锁,是一种递归无阻塞的同步机制
比synchronized更强大,灵活的锁机制,可以减少死锁发生的概率
分为公平锁和非公平锁
底层采用AQS实现,通过内部Sync继承AQS
读写锁
ReentrantReadWriteLock
读写锁,两把锁:共享锁-读锁、排它锁-写锁
支持公平性、非公平性,可重入和锁降级
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁可以降级成为读锁。
LockSupport工具
阻塞和唤醒一个线程
Condition接口
Lock提供条件Condition,对线程的等待、唤醒操作更加详细和灵活,依赖于lock
Lock提供条件Condition,对线程的等待、唤醒操作更加详细和灵活
内部维护一个Condition队列,当前线程调用await()方法,将会以当前线程构造一个节点(Node),并将节点加入到该队列的尾部。
java 并发容器
ConcurrentHashMap
并发编程中需要用到线程安全的HashMap
1.8与1.7的区别
数据结构
JDK 1.7 数组+链表:Segment(ReentrantLock)+HashEntry
          JDK 1.8 数组+链表/红黑树 Node(HashEntry) + Synchronized+CAS+红黑树
线程安全
JDK 1.7 Segment(ReentrantLock)
          JDK 1.8 Synchronized+CAS
锁粒度
JDK1.7 锁的粒度是基于Segment的,包含多个HashEntry
          JDK1.8 锁的粒度是基于Node的(HashEntry首节点),实现降低锁的粒度
查询时间复杂度
JDK1.7 遍历链表 O(N)
          JDK1.8 遍历红黑树 O(log N)
链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
辅助类
JDK 1.8 例如TreeBin,Traverser等对象内部类。
ConcurrentLinkedQueue
并发编程中需要用到线程安全的队列
1. 使用阻塞算法
1个锁(入队和出队用同一把锁)和2个锁(入队和出队用不同的锁)
阻塞队列
2. 使用非阻塞算法
CAS
ConcurrentLinkedQueue
基于链接节点的无边界的线程安全队列,采用FIFO原则对元素进行排序,内部采用CAS算法实现
精妙之处:利用CAS来完成数据操作,同时允许队列的不一致性,弱一致性表现淋漓尽致。
ConcurrentSkipListMap
ConcurrentSkipListMap其内部采用SkipLis数据结构实现。
ConcurrentSkipListSet
内部采用ConcurrentSkipListMap实现
Java阻塞队列(7个)
阻塞队列是一个支持两个附加操作的队列
两个附加操作
支持阻塞的插入方法
当队列满时,队列会阻塞插入元素的线程,直到队列不满
支持阻塞的移除方法
当队列空时,队列会阻塞获取元素的线程,直到队列非空
实现原理
使用通知模式
当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用
一个抽象类
AbstractQueue,改类在Queue接口中扮演着非常重要的作用,该类提供了对queue操作的骨干实现
一个接口
BlockingQueue继承java.util.Queue为阻塞队列的核心接口,提供了在多线程环境下的出列、入列操作,作为使用者,则不需要关心队列在什么时候阻塞线程,什么时候唤醒线程,所有一切均由BlockingQueue来完成。
七个阻塞队列
ArrayBlockingQueue
一个由数组组成的有界阻塞队列
LinkedBlockingQueue
一个由链表组成的有界阻塞队列
LinkedBlockingDeque
一个由链表组成的双向阻塞队列
PriorityBlockingQueue
一个支持优先级排序的*阻塞队列
DelayQueue
一个使用优先级队列实现的*阻塞队列
SynchronousQueue
一个不存储元素的阻塞队列
LinkedTransferQueue
一个由链表组成的*阻塞队列
即可以像其他的BlockingQueue一样有容量又可以像SynchronousQueue一样不会锁住整个队列
有界
对读或者写都是锁上整个队列,在并发量大的时候,各种锁是比较耗资源和耗时间的
阻塞队列的4中处理方式
抛出异常
返回特殊值
一直阻塞
超时退出
注意: 如果是*阻塞队列, 队列不可能出现满的情况,使用put或offer方法永远不会被阻塞,而且使用offer方法时,永远返回true
java 并发工具(4个)
CountDownLatch
等ABCD 4个人都结束了,自己才能开始,结束一个减一个
CountDownLatch 它允许一个或多个线程等待其他N个指定数量线程完成操作
CountDownLatch也可以实现join的功能,并且比join的功能更多
AQS(队列同步器) 共享锁
await()
CyclicBarrier
我和A,B,C,D 5个人互相等待,会合了再一起进电影院,到一个减一个
定义
可循环使用的同步屏障:它允许一组线程互相等待,直到达到某个公共屏障点(common barrier point)
通俗讲:让一组线程到达一个屏障是被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
底层采用ReentrantLock + Condition 实现
应用场景
多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景
Semaphore
计数器,鸡蛋篮子里只能放5个鸡蛋,缺几个,才能放几个
信号量
计数器,用来控制同时访问特定资源的线程数量,他通过协调各个线程,以保证合理的使用公共资源
内部采用共享锁实现
从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对许可的号码进行计数,并采取相应的行动。
应用场景
用于流量控制,特别是公用资源有限的应用场景,比如数据库连接。
Exchanger
A和B交换数据
定义
交换者,是一个用于线程间协作的工具类,用于进行线程间的数据交换,两个线程
它提供一个同步点,用于进行线程间成对配对及交换数据,
具体
第一个线程先执行exchange()方法,它会一只等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,两个线程就可以交换数据,将本线程生产出来的数据传递给对方
允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中
应用场景
遗传算法
交叉
校对工作
AB岗录入电子银行流水
Exchanger 可能被视为 SynchronousQueue 的双向形式。Exchanger 可能在应用程序(比如遗传算法和管道设计)中很有用。
java 原子操作类Atomic(13个)
java.util.concurrent.atomic里的原子操作类提供了线程安全地更新一个变量的方式
4大类型13个原子操作类
基本类型类
AtomicBoolean
原子更新布尔类型
AtomicInteger
原子更新整型
AtomicLong
原子更新长整型
用于通过原子的方式更新基本类型
数组
AtomicIntegerArray
原子更新整形数组里的元素
AtomicLongArray
原子更新长整型数组里的元素
AtomicReferenceArray
原子更新引用类型数组里的元素
通过原子的方式更新数组里的某个元素
引用
AtomicReference
原子更新引用类型
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
AtomicMarkableReference
原子更新带有标记位的引用类型
如果要原子地更新多个变量,就需要使用这个原子更新引用类型提供的类
属性
AtomicIntegerFieldUpdater
原子更新整型的字段的更新器
AtomicLongFieldUpdater
原子更新长整型的字段的更新器
AtomicStampedReference
原子更新带有版本号的引用类型
如果需要某各类的某个字段,使用原子更新字段类
核心底层
CAS
Unsafe只提供了3中CAS方法
final native boolean compareAndSwapObject()
final native boolean compareAndSwapInt()
final native boolean compareAndSwapLong()
java并发框架(2个)
Fork/Join框架
定义
一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
核心思想
分治
fork分解任务,join收集任务
工作窃取算法
定义
工作窃取算法work-stealing: 某个线程从其他队列里窃取任务来执行
背景
将一个不较大的任务分割为若干个互不依赖的子任务,为了减少线程之间的竞争,把这些子任务分别放到不同的队列里,并未每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。
执行快的线程帮助执行慢的线程执行任务,提升整个任务效率
为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列
窃取任务线程永远从双端队列的尾部拿任务执行
被窃取任务线程永远从双端队列的头部拿任务执行
优点
充分利用线程进行并行计算,减少了线程间的竞争
缺点
在某些情况下还是存在竞争,比如双端队列里只有一个任务时,并且该算法会消耗了更多的资源,比如创建多个线程和多个双端队列
核心类
ForkJoinTask
子类,用于继承
继承子类 RecursiveAction
用于没有返回结果的任务
继承子类 RecursiveTask
用于有返回结果的任务
方法
fork
分解任务
join
合并任务结果
isCompletedAbnormally()
检查任务是否已经抛出异常或已经被取消了
getException()
获取异常
ForkJoinWorkerThread
执行任务的工作线程
ForkJoinPool
执行任务ForkJoinTask的线程池,ForkJoinTask需要通过ForkJoinPool来执行
submit(task);
内部结构
ForkJoinTask数组
存放任务
ForkJoinWorkerThread数组
执行任务
Executor框架
Executor框架的结构
任务
包括被执行任务需要实现的接口
Runnable接口
Callable接口
任务的执行
任务执行机制的核心接口
Executor接口
继承自Executor的接口
ExecutorService接口
execute(Runnable command)
submit(Runnable task)
submit(Callable<T>task)
返回值是FutureTask对象
实现了ExecutorService接口的实现类
ThreadPoolExecutor
ScheduledThreadPoolExecutor
异步计算的结果
Future接口
get
等待任务执行完成
cancel
取消任务完成
实现了Future接口的实现类
FutureTask
ThreadPoolExecutor(线程池)
是线程池的核心实现类,用来执行被提交的任务
ThreadPoolExecutor
corePoolSize
线程池中核心线程的数量。当提交一个任务时,线程池会新建一个线程来执行任务,直到当前线程数等于corePoolSize。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize
线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,如果当前的线程数小于maximumPoolSize,则会新建线程来执行任务。注意,如果使用的是*队列,该参数也就没有什么效果了。
keepAliveTime
线程空闲的时间。线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime。默认情况下,该参数只有在线程数大于corePoolSize时才会生效。
unit
keepAliveTime的单位。TimeUnit
workQueue
用来保存等待执行的任务的阻塞队列,等待的任务必须实现Runnable接口。我们可以选择如下几种:
分类
ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作,反之亦然。
PriorityBlockingQueue:具有优先界别的*阻塞队列。
threadFactory
用于设置创建线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字,该对象可以通过Executors.defaultThreadFactory()
handler
RejectedExecutionHandler,线程池的拒绝策略。所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务。
四种拒绝策略
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
当然我们也可以实现自己的拒绝策略,例如记录日志、持久化存储不能处理的任务等等,实现RejectedExecutionHandler接口自定义即可。
工厂类Executors创建3种类型的ThreadPoolExecutor
SingleThreadExecutor
使用单个worker线程的Executor
应用场景
适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景
特点
corePool和maximumPoolSize均被设置为1
使用的是相当于*的有界阻塞队列LinkedBlockingQueue,所以带来的影响和FixedThreadPool一样。
FixedThreadPool
固定线程数的线程池
应用场景
为了满足资源管理的需求,需要限制当前线程数量的应用场景
适用于负载比较重的服务器
特点
corePoolSize 和 maximumPoolSize都设置为创建FixedThreadPool时指定的参数nThreads,意味着当线程池满时且阻塞队列也已经满时,如果继续提交任务,则会直接走拒绝策略
默认的拒绝策略,即AbortPolicy,则直接抛出异常。
keepAliveTime设置为0L,表示空闲的线程会立刻终止。
workQueue则是使用LinkedBlockingQueue,但是没有设置范围,那么则是最大值(Integer.MAX_VALUE),这基本就相当于一个*队列了。
*队列对线程池的影响
1. 当线程池中的线程数量等于corePoolSize 时,如果继续提交任务,该任务会被添加到*阻塞队列workQueue中,因此线程中的线程数不会超过corePoolSize
2. 由于1,使用*队列时的 maximumPoolSize是一个无效参数
3. 由于1和2,使用*队列时的 keepAliveTime 是一个无效参数
4. 不会拒绝任务
CachedThreadPool
根据需要创建新线程,是大小*的线程池
应用场景
适用于执行很多的短期异步任务的小程序
适用于负载较轻的服务器
特点
corePool为0,maximumPoolSize为Integer.MAX_VALUE,这就意味着所有的任务一提交就会加入到阻塞队列中。
keepAliveTime这是为60L,unit设置为TimeUnit.SECONDS,意味着空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
阻塞队列采用的SynchronousQueue,每个插入操作都必须等待另一个线程对应的移除操作,此处把主线程提交的任务传递给空闲线程去执行。
SynchronousQueue是一个没有元素的阻塞队列,加上corePool = 0 ,maximumPoolSize = Integer.MAX_VALUE,这样就会存在一个问题,如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地创建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。
重要操作
offer
主线程执行offer操作与空闲线程执行的poll操作配对成功后,主线程把任务交给空闲线程执行
execute
执行任务
poll
让空闲线程在SynchronousQueue中等待60s,如果等待到新任务则执行,否则,空闲线程将终止
ScheduledThreadPoolExecutor
可以在给定的延迟后运行命令,或者定期执行命令,与Timer类似,但比其功能更强大,更灵活
FutureTask
实现了Future接口和Runnable接口,代表异步计算的结果,
应用场景
当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行

11. 参考网址:

  1. 参考来源:http://cmsblogs.com/wp-content/resources/img/sike-juc.png
  2. 《Java并发编程的艺术》_方腾飞PDF 提取码:o9vr
  3. http://ifeve.com/the-art-of-java-concurrency-program-1/
  4. Java并发学习系列-绪论
  5. Java并发编程实战
  6. 死磕 Java 并发精品合集

Java 并发系列之一:java 并发体系的更多相关文章

  1. 【Java并发系列】--Java内存模型

    Java内存模型 1 基本概念 程序:代码,完成某一个任务的代码序列(静态概念) 进程:程序在某些数据上的一次运行(动态) 线程:一个进程有一个或多个线程组成(占有资源的独立单元) 2 JVM与线程 ...

  2. java并发系列&lpar;八&rpar;-----java异步编程

    同步计算与异步计算 从多个任务的角度来看,任务是可以串行执行的,也可以是并发执行的.从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的. Runnable.Callable.Future ...

  3. java并发系列&lpar;六&rpar;-----Java并发:volatile关键字解析

    在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...

  4. ☕【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南

    前言介绍 在Java编程语言中,操作文件IO的时候,通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于Mapp ...

  5. 【java虚拟机系列】java虚拟机系列之JVM总述

    我们知道java之所以能够快速崛起一个重要的原因就是其跨平台性,而跨平台就是通过java虚拟机来完成的,java虚拟机属于java底层的知识范畴,即使你不了解也不会影响绝大部分人从事的java应用层的 ...

  6. 【java多线程系列】java内存模型与指令重排序

    在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...

  7. Java Web系列:Java Web 项目基础

    1.Java Web 模块结构 JSP文件和AXPX文件类似,路径和URL一一对应,都会被动态编译为单独class.Java Web和ASP.NET的核心是分别是Servlet和IHttpHandle ...

  8. 【java开发系列】—— java输入输出流

    前言 任何语言输入输出流都是很重要的部分,比如从一个文件读入内容,进行分析,或者输出到另一个文件等等,都需要文件流的操作.这里简单介绍下reader,wirter,inputstream,output ...

  9. Java多线程系列一——Java实现线程方法

    Java实现线程的两种方法 继承Thread类 实现Runnable接口 它们之间的区别如下: 1)Java的类为单继承,但可以实现多个接口,因此Runnable可能在某些场景比Thread更适用2) ...

  10. java多线程系列五、并发容器

    一.ConcurrentHashMap 1.为什么要使用ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,HashMap在 ...

随机推荐

  1. html框架—多对话框&lpar;相同id&rpar;处理

    一个网站的数据大多数都是异步刷新的,这没什么好说的,然后现在很多前后端框架,大家都知道框架很好用,不用自己写样式,只要利用框架上的语法就能做出漂亮的动态的效果来,而用框架的话大多数的动态效果都是动态生 ...

  2. http&colon;&sol;&sol;blog&period;sina&period;com&period;cn&sol;s&sol;blog&lowbar;705cc5dd01012ehb&period;html

    http://blog.sina.com.cn/s/blog_705cc5dd01012ehb.html

  3. &lbrack;ActionScript 3&period;0&rsqb; AS3 用于拖动对象时跟随鼠标的缓动效果

    package com.fylibs.components.effects { import flash.display.DisplayObject; import flash.events.Even ...

  4. android 根据SD卡中图片路径读取并显示SD中的图片——源代码

    package com.guo; import java.io.File; import android.app.Activity; import android.graphics.Bitmap; i ...

  5. Android开发周报:反编译对抗研究、动手制作智能镜子

    新闻 <Android Wear落地中国 谷歌增强安卓生态控制力> :9月8日,由摩托罗拉推出的智能手表Moto 360二代作为国内发售的第一款搭载官方Android Wear的设备,正式 ...

  6. mysql 登录中用户管理

    管理员账号 root 新增普通用户 修改root密码

  7. 序列化Serializable和Parcelable

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 简单记录下序列化Serializable和Parcelable的使用方法. Android中Intent如果要传递类对象,可以通过两 ...

  8. 如何在 Xcode 中修改应用的名字

    找到 TARGETS 中的 Build Setting ,下拉找到 Packaging ,修改 Product Name 即可.

  9. 悟空模式-java-单例模式

    [那座山,正当顶上,有一块仙石.其石有三丈六尺五寸高,有二丈四尺围圆.三丈六尺五寸高,按周天三百六十五度:二丈四尺围圆,按政历二十四气.上有九窍八孔,按九宫八卦.四面更无树木遮阴,左右倒有芝兰相衬.盖 ...

  10. A system tap script to detect UDP beacons

    https://gist.github.com/jbradley89/178bbf3944786c494bd78f3df16a5472