文章目录
(一)线程安全性
原子性
提供了互斥访问,同一时刻只能有一个线程来对他进行操作:Atomic包、CAS算法、synchronized、Lock
-
主要由
Atomic
包实现,使用unsafe
类(可以访问主内存而非工作内存),基于CAS(CompareAndSwap
)实现多线程原子操作:CAS:比较主内存中的值和当前工作内存中的值是否一致,若一致则由当前线程改为期望值,Atomic用一个do…while循环循环判断
CompareAndSwap
条件来实现。 -
AtomicLong
在只需要求得最后结果的时候可以用LongAddr
替代因为
Atomic
类在线程竞争很大的时候,循环判断会使得CPU额外开销非常大,LongAddr
通过将值分为多个节点计算,首先计算base值是否可以更改,如果可以则直接更改base值,否则更改其内部的Cell对象的值(实际上是对每个线程计算了一个哈希值,对对应位置线程的哈希桶填入值得过程,避免了CAS循环判断的过程),最后的结果由base值和Cell对象的值相加得到,在多线程计算结束之前的中途任意时刻获取LongAddr
对象的值,都是不准确的,因为获取的时候,值可能已经被其他线程更新了。
可见性
一个线程对主内存的修改可以及时被其他线程观察到
- 导致共享变量在线程之间不可见的原因:
- 线程交叉执行
- 重排序、线程交叉执行
- 共享变量更新后没有在工作内存与主内存之间及时更新
- 解决办法
- 使用
synchronized
关键字:- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,让需要使用共享变量的时候从主内存中重新读取最新的值
- 使用
volatile
关键字:- 基于内存屏障
- 对
volatile
变量写操作的时候,会在写操作后加一条store
指令,将本地内存中的共享变量值刷新到主内存 - 对
volatile
变量读操作的时候,会在写操作后加一条load
指令,将主内存中的共享变量值刷新到工作内存
- 对
- 禁止重排序
volatile
写:- 前面插入
StoreStore
指令,禁止前面的普通写和volatile
写重排序 - 后面插入
StoreLoad
指令,防止volatile
写与之后可能出现的volatile
读/写重排序
- 前面插入
volatile
读:- 后面先插入
LoadLoad
屏障,禁止下面所有的普通读和volatile
读重排序 - 再插入
LoadStore
屏障,禁止下面所有的写操作和volatile
读重排序
- 后面先插入
volatile
可以作为一种线程通信机制使用(通过主内存通信)
- 基于内存屏障
- 使用
有序性
一个线程观察其他线程中的指令执行顺序,由于指令重排序(JVM和CPU导致)的存在,该观察结果一般杂乱无序。
实现:volatile
、synchronized
、Lock
深入理解JVM里面的happens-before原则:
- 程序次序规则:一个线程内,线程内的代码是有序执行的
- 锁定规则:一个
unLock
操作先行发生于对同一个锁的Lock
操作 volatile
变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作(WAR)- 传递规则:操作A先于B,B先于C,那么A先于C
- 线程启动规则:
Thread
对象的start
方法线性发生于该线程的每个动作 - 线程中断规则:对线程的
interrupt
方法调用先行发生于被中断线程的代码检测到中断事件的发生 - 线程终结规则:线程中的所有操作都先行发生于线程的终止检测(
Thread.join()
、Thread.isAlive()
) - 对象终结规则:一个对象的初始化完成先行发生于他的
finalize()
方法的开始
(二)安全发布
原则
- 不可以直接把私有引用变量直接发布出去
- 对象的构造器执行之前,不可以发布对象
安全发布策略
-
在静态初始化函数中初始化一个对象引用
-
饿汉模式:类加载就实例化单例对象(静态域、静态块)
-
懒汉模式:
public class SingletonExample4 { // 私有构造函数 private SingletonExample4() {} // 1、memory = allocate() 分配对象的内存空间 // 2、ctorInstance() 初始化对象 // 3、instance = memory 设置instance指向刚分配的内存 // JVM和cpu优化,发生了指令重排 // 1、memory = allocate() 分配对象的内存空间 // 3、instance = memory 设置instance指向刚分配的内存 // 2、ctorInstance() 初始化对象 // 单例对象 private static volatile SingletonExample4 instance = null; // 静态的工厂方法 public static SingletonExample4 getInstance() { if (instance == null) { // 双重检测机制 // B synchronized (SingletonExample4.class) { // 同步锁 if (instance == null) instance = new SingletonExample4(); // A - 3 } } return instance; } }
可能发生上述情况中,线程A先开辟内存,还没按构造函数初始化,线程B就直接拿到了对象引用,导致短时间内的对象还未初始化就被拿去使用。使用
volatile
限制指令重排可以解决这个问题 -
枚举模式:JVM保证构造器绝对只调用一次
-
-
对象引用设为保存到
volatile
类型域或者AtomicReference
对象中Atomic
类是通过自旋CAS操作volatile
变量实现的,只要有volatile
修饰,就无法重排指令! -
将对象引用保存到某个正确构造对象的
final
类型域中 -
将对象引用保存到一个由锁保护的域中
(三)线程安全策略
不可变对象
-
条件如下:
- 对象创建后其状态不可被修改
- 对象的所有域都是
final
类型 - 对象是正确创建的(对象创建期间不暴露其
this
指针)
以上条件可以通过将类声明为
final
(不可继承),其所有成员变量均声明为final
来实现,对变量不提供set
方法,get
方法不直接返回对象本身,而是返回对象深拷贝 -
final
关键字- 修饰类:不能被继承
- 修饰方法:方法不会被子类重写
- 修饰变量:基本数据类型(值不变)、引用类型变量(引用地址不变)
-
不可变对象
Collections.unmodifiableXXX
:Collection
、List
、Set
、Map
都是不可变对象,仅仅是保留可变对象的视图,unmodifiableXXX
不支持修改,但是会随着可变对象的变化而更改他自己的视图
线程封闭
-
Ad-hoc
程序控制实现,最糟糕
-
堆栈封闭
多个线程访问方法中的局部变量,不会被共享,无并发问题。
-
ThreadLocal
线程封闭内部维护了一个
Map
,一个线程对应一个key
,ThreadLocal<T>
仅对当前线程可见。
线程不安全的类与写法
StringBuilder
,其线程安全类为StringBuffer
,StringBuffer
所有方法都用了synchronized
关键字来限制多线程访问SimpleDateFormat
,多线程访问同一个对象会报异常ArrayList
、HashSet
、HashMap
等Collection
- 先检查再执行:
if (condition(a)) { handle(a) };
,两个线程同时判断通过,同时执行了,主要是因为没有保证原子性
同步容器
主要是采用synchronized
关键字实现同步
-
ArrayList
->Vector
,Stack
Vector
和Stack
这两种都使用synchronized
修饰方法来同步,如果不需要同步应该使用ArrayList
,性能更高,但是这两个容器并不保证绝对的线程安全,因为在外部操作时的顺序可能引发越界访问。 -
HashMap
->HashTable
(key
、value
不能为null
) -
Collections.synchronizedXXX
(List
、Set
、Map
)
一个线程在遍历同步容器,同时另一线程在修改同步容器时,应该使用synchronized
关键字或者Lock
或者使用CopyOnWriteXXX
并发容器来替代同步容器
并发容器
-
ArrayList
->CopyOnWriteArrayList
:写(修改)操作的时候先复制数组,再写入,将原本的引用指向新的内存,通常用于读远大于写的场景,否则会触发频繁GC;读操作时,都原数组不需要加锁,写操作会加锁防止复制出多个不同的数组;基于ReentrantLock
实现原子操作(这个ArrayList
不能太大,否则复制的时候开销很大) -
HashSet
->CopyOnWriteArraySet
、TreeSet
->ConcurrentSkipListSet
前者也是基于
ReentrantLock
实现,后者基于Map
集合,单步操作保证线程安全,批量操作不保证,因为批量操作是多次调用单步操作实现的,无法保证每个单步操作之间的线程安全性(只允许一个线程调用批量操作),且也不允许key
和value
为null
-
HashMap
->ConcurrentHashMap
、TreeMap
->ConcurrentSkipListMap
ConcurrentHashMap
针对读操作做了大量优化,高并发场景下,性能很高;ConcurrentSkipListMap
基于跳表,存取时间和线程数无关(支持并发度更高)影响
HashMap
的性能主要有两个参数:初始容量、加载因子- 初始容量:16
- 加载因子:0.75
当
HashMap
中的数据量超过容量*加载因子时,就会调用resize
方法,把容量翻倍;如图,是单线程再哈希过程,使用
HashMap
在多线程情形下容易出现死循环ConcurrentHashMap
基于分段(segment)锁来处理(JDK7)基于红黑树(JDK8)
因为求hash的过程需要取模,而对于计算机而言,取模开销比位操作大得多,所以
HashMap
采用位操作实现取模,也就导致了HashMap
的容量为2^n
;即使使用带参数的构造器传入不是2的幂的容量大小,它也会根据给出的参数计算出一个2的幂作为初始容量。
安全共享对象策略
- 线程限制:线程独占对象
- 共享只读:任何线程都不能修改
- 线程安全对象:对象内部是同步的
- 被守护对象:获取特定的锁后才能访问这个对象
(四)JUC之AQS
AQS接口
AbstractQueuedSynchronizer
,简称AQS
底层使用双向链表Sync queue实现,当需要使用Condition的时候,会引入单向链表Condition queue
使用及性质
- 使用Node实现FIFO队列,可用于构建锁或者其他同步装置的基础框架
- 用
int
表示状态(state成员变量),state表示获取锁的线程锁 - 通过继承来使用,通过实现acquire和release来管理状态
- 可同时实现排他锁和共享锁模式(独占控制、共享控制)
实现思路:
AQS内部维护了一个队列来管理锁,线程首先尝试获取锁,若获取失败,则把当前线程以及等待状态等信息包装成Node节点加入sync queue,不断循环尝试获取锁(只有当前节点为head的直接后继才会进行尝试),若失败则阻塞自己,直到被唤醒;当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
除了Node节点的这个FIFO队列,还有一个重要的概念就是waitStatus一个volatile关键字修饰的节点等待状态。在AQS中waitstatus有五种值:
SIGNAL
值为-1
、后继节点的线程处于等待的状态、当前节点的线程如果释放了同步状态或者被取消、会通知后继节点、后继节点会获取锁并执行(当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点)CANCELLED
值为1
、因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收(一旦节点状态值为1说明被取消,那么这个节点会从同步队列中删除)CONDITION
值为-2
、节点在等待队列中、节点线程等待在Condition、当其它线程对Condition调用了signal()
方法该节点会从等待队列中移到同步队列中PROPAGATE
值为-3
、表示下一次共享式同步状态获取将会被无条件的被传播下去(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)initial
值为0
、表示当前没有线程获取锁(初始状态)
CountDownLatch
同时只能有一个线程去操作这个对象,其他线程一直处于阻塞状态(阻塞是没能拿到锁、等待是拿到了锁但是wait
了)
这个计数器不能被重置次数,拆分大任务为多个子任务时,可以采用这个类
Semaphore
可以控制并发量,提供acquire
和release
方法,常用于仅能提供有限访问的资源:如数据库连接池
通过tryAcquire
方法,使得超过semaphore
并发量的内容可以被丢弃
CyclicBarrier
允许一组线程相互等待,直到到达某个公共的屏障点(CommonBarrierPort),每有一个线程await
,计数器+1。
和CountDownLatch
的区别:
CyclicBarrier
可用reset
方法重置- 前者描述一个或n个线程等待其他线程的关系,后者描述了多个线程相互等待的关系
ReentrantLock 与 Condition
Java主要分两类锁:
synchronized
修饰的锁- JUC提供的锁——
ReentrantLock
- 区别:
- 后者可重入(拥有锁的计数器)
- 锁的实现:前者依赖于JVM,后者依赖于JDK
- 性能:引入偏向锁(自旋锁)后,前者效率接近后者
- 功能:前者更简便,由编译器保证加锁和释放;后者要手工加锁和释放。后者比前者更灵活
- 公平性:后者可以指定公平/非公平锁,前者只能是先获得先拥有
- 后者提供
Condition
类,可以分组唤醒需要唤醒的线程;前者要么唤醒全部线程,要么随机唤醒一个线程 - 后者提供能够中断等待锁的线程的机制(
lock.lockInterruptibly()
),如果当前线程没有被中断则锁定,否则抛出异常
ReentrantReadWriteLock
:在没有任何读写锁的情况下,才能获取写入锁。是悲观锁。
stampLock
:提供乐观锁方式
Condition.await()
:线程在sync队列中的状态会变成Condition,会加入condition队列
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
try {
reentrantLock.lock();
System.out.println("wait signal");// 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get signal");// 4
reentrantLock.unlock();
}).start();
new Thread(() -> {
reentrantLock.lock();
System.out.println("get lock");// 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
System.out.println("send signal ~ ");// 3
reentrantLock.unlock();
}).start();
}
上述代码的执行顺序如注释所表示:线程1首先获得reentrantlock,进入AQS队列,之后由于condition.await
,在AQS队列中被视为放弃锁(但还未移除),进入condition队列;之后线程2获得reentrantlock,并调用condition.signalAll()
将线程1从condition队列移除,当线程2释放了reentrantlock后,AQS队列将锁分配给线程1,线程1得以继续执行。
(五)JUC扩展
通常有两种方法使用多线程:
- 实现
Runnable
接口 - 继承
Thread
类
但是这两种方式都无法获得任务执行的结果
Callable接口
是泛型接口,其call
函数的返回值就是泛型参数类型,能够抛出异常
Future接口
是泛型接口
对Runnable
、Callable
任务,可以取消任务、查询任务状态(取消/完成)、获取结果等;
这个接口可以监视目标线程调用call
的情况,调用Future
接口的get
方法时,就可以获得线程的执行结果
FutureTask类
其父类RunnableFuture
实现了Runnable
和Future
两个接口 ,如果函数参数为Runnable
类型,他会转换为Callable
类型
这个类统一了Runnable
和Callable
,十分方便
Fork/Join框架
核心:工作窃取算法
窃取任务的线程永远从其他线程的队列尾部拿任务。
局限性:
- 只能通过Fork/Join同步
- 任务不能是IO
- 任务不能抛出/检查异常
核心:两个类
ForkJoinPool
:提供工作线程、管理任务状态
ForkJoinTask
:提供在任务中执行Fork、Join操作的机制
任务:必须是ForkJoinTask
的子类
BlockingQueue接口
阻塞队列
队列满入队、队列空出队,都会造成阻塞;主要用在生产者、消费者模型
操作阻塞:put(o)
、take()
实现类如下:
ArrayBlockingQueue
:大小有限,初始化就要指定,FIFODelayQueue
:其中的元素必须实现Delayed
接口(继承了Comparable
接口),内部实现是锁、排序,常用于延迟关闭资源LinkedBlockingQueue
:可以指定大小,也可以不指定(使用默认最大值)PriorityBlockingQueue
:没有大小限制,允许插入null
对象,迭代器并不保证按照优先级顺序迭代SynchronousQueue
:只允许插入一个元素,一个线程插入元素后就会被阻塞,直至被另一个线程消费,因此又称为同步队列。
(六)线程池
对比Thread类
new Thread
弊端
- 每次
new Thread
需要新建对象,开销高 - 线程缺乏统一管理,可能无限制新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM(耗尽内存)
- 缺少更多的功能:如定期执行、线程中断
线程池的好处:
- 重用存在的线程,减少对象创建、消亡
- 可有效控制最大并发线程数,提高系统资源利用率,避免过多资源竞争、避免阻塞
- 提供定时执行、定期执行、单线程、并发数控制等功能
ThreadPoolExecutor类
corePoolSize
:核心线程数量,线程数小于此,直接创建新线程。线程池内的线程数大于等于corePoolSize
时,将任务放入workQueue
等待。
maximumPoolSize
:大于等于corePoolSize
,小于此,只有当workQueue
满才创建新线程
workQueue
:阻塞队列,存储等待执行的任务,接收BlockingQueue
类型参数
keepAliveTime
:线程没任务执行时最多保持多久时间终止
threadFactory
:线程工厂,用来创建线程
rejectHandler
:当拒绝处理任务时的策略(直接抛异常、用调用者所在的线程来执行、丢弃队列中最旧的任务、直接丢弃任务)
线程池实例的状态
Running
:能接受新提交的任务、阻塞队列中的任务
ShutDown
:不能接受新提交的任务,可以处理阻塞队列任务
Stop
:不处理任何任务
TiDying
:如果所有任务都终止了,有效线程数为0。
Terminated
常用方法
execute
:提交任务,交给线程池执行
submit
:提交任务,能够返回结果,相当于execute+Future
shutdown
:关闭线程池,并等待任务执行完
shutdownNow
:关闭线程池,不等待任务执行完
Executor框架接口
Executors.newCachedThreadPool
:线程池长度超过了任务量,则回收
Executors.newCachedThreadPool
:控制并发数
Executors.newScheduledThreadPool
:定期执行
Executors.newSingleThreadExecutor
:保证任务以指定顺序执行
(七)死锁(这个在OS里讲的很详细)
条件
- 互斥
- 请求和保持条件
- 非剥夺
- 循环等待