1.线程的几种方式
1.1 继承Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
1.2 使用 Runnable 配合 Thread
继承Runnable接口
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
1.3 通过Callable 和 FutureTask创建线程
1 创建Callable接口的实现类 ,实现它的Call方法
2 使用FutureTask类来包装Callable对象,这个FutureTask对象需要封装Callable对象的Call方法的返回值
3 使用FutureTask对象作为Thread对象的target创建并调用start方法启动线程
1.4 通过线程池创建
2.线程方法
2.1 run,start,
2.2 sleep
阻塞线程
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
2.3 yield
让出线程-重新进入就绪状态
停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。 如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行。
2.4 wait
Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待 此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象 锁定池准备获得对象锁进入运行状态。
2.5 notify
只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程 等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
2.7 notifyAll
唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系 统的实现。
2.8 join
用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执 行结束后,再继续执行当前线程。如:t.join();//主要用于等待 t 线程运行结束,若无此句, main 则会执行完毕,导致结果不可预测
区别 | sleep | wait | join |
---|---|---|---|
概念 | 线程休眠 | 线程等待 | 线程等待 |
方法 | Thread类的静态方法 | Object类的成员方法 | Thread类的成员方法 |
参数 | 必须有时间参数((long millis) | 非必须,可以无参也可有时间参数 | 非必须,可以无参也可有时间参数 |
状态 | 调用后进入timed_waiting | 无参则waiting有参则timed_waiting | 无参则waiting有参则timed_waiting |
竞争锁 | 不涉及锁 | 必须有锁 | 不涉及锁 |
🎯使用 | 只关注休眠时间 | 📌调用前需要先获得锁,调用后会释放锁进入等待状态;📌如果是带时间参数,则等待时间结束重新获得锁,如果超时等待过程中notify或者notifyAll也会被唤醒;📌如果是无参,需要notify或者notifyAll来唤醒,然后重新获得锁 | 📌重点在于谁是“主线程”,有参则主线程等待子线程的参数的时间,无参则知道子线程执行结束再开始执行。 |
2.9 interrupt**
打断阻塞
Java的线程中断是一种线程间的协作模式,通过设置线程的中断标志,并不能直接终止该线程的执行,而是被中断线程会根据自己的中断状态自行处理。
打断 sleep,wait,join 的线程
打断这些sleep 会报异常
打断正在睡眠的线程后,线程的打断标记并不会置为true,而是false。
被打断的睡眠的线程并不全是直接使用cpu时间片,仍然是处于就绪状态,还是要排队滴!
正常的线程在被打断时是不会停止继续占用cpu时间片的,而是改变此线程的打断标记为true。 所以打断正常线程就相当于通知该线程有人要让你停下来,而不是强制让你停下来
2.10 两阶段终止模式
通过打断,在线程1优雅的通过打断标记,优雅的终止线程2
2.11 park和unpark
park,unpark这两个方法都是LockSupport类名下的方法,park用来暂停线程,unpark用来将暂停的线程恢复。
先park再unpark的方式是容易理解的。但还有一个场景,先unpark后再次执行park方法,也不会阻塞调用了park方法的线程。理解为park方法就是校验获取一个通行令牌,而unpark方法是获取到一个通行令牌的过程。先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,消耗掉这个令牌然后就可以继续往下走。
wait/notify与park/unpark区别
首先是行为上的不同,wait,notify组合使用代表了阻塞,唤醒操作,如果先调用notify,当前线程原本就是醒着,唤醒这个操作无效的,再次调用wait,那线程就阻塞了。park,unpark可以按照通行令牌走,先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,然后消耗掉这个令牌就可以继续往下走。
其次,wait/notify依赖于锁资源,所以只能在synchronized中来进行使用。LockSupport这俩中没有这个限制。
最后,wait/notify的唤醒是随机的,不确定具体唤醒了哪个等待的线程,而park,unpark可以在线程层面上来对特定线程进行唤醒。
原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻
线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
调用 park 就是要看需不需要停下来歇息
如果备用干粮耗尽,那么钻进帐篷歇息
如果备用干粮充足,那么不需停留,继续前进
调用 unpark,就好比令干粮充足
如果这时线程还在帐篷,就唤醒让他继续前进
如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
3.线程状态
新建,就绪,等待,超时等待,运行,终止
线程的状态在java中有明确的定义,在java.lang.Thread.State中有6种。
① New
线程被创建,未执行和运行的时候
② Runnable
不代表线程在跑,两种:被cpu执行的线程,随时可以被cpu执行的状态。
③ Blocked
线程阻塞,处于synchronized同步代码块或方法中被阻塞。
④ Waiting
等待先生的线程状态。线程当前不执行,如果被其他唤醒后会继续执行的状态。依赖另一个线程的通知的。这个等待是一直等,没人叫你,你起不来。
⑤ Time Waiting
指定等待时间的等待线程的线程状态。带超时的方式:Thread.sleep,Object.wait, Thread .join,LockSupport.parkNanos,LockSupport.parkUntil
⑥ Terminated
正常执行完毕或者出现异常终止的线程状态。
4.synchronized
这个链接不错
https://www.cnblogs.com/gaokeji/p/16058072.html
1.概述
如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字,可以说在并发控制中是必不可少的部分。
2.原理之Monitor概述
Java 对象头
以 32 位虚拟机为例
Mark Word 主要用来存储对象自身的运行时数据
Klass Word 指向Class对象
数组对象
相对于普通对象多了记录数组长度
Mark Word 结构
其中 Mark Word 结构为
不同对象状态下结构和含义也不同
64 位虚拟机 Mark Word
3.原理之 Monitor(锁) **
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。
Monitor 结构
ObjectMonitor 中有几个值得关注的成员变量:
_owner:指向获得ObjectMonitor对象的线程。(即获得锁的线程)
_EntryList:处于等待锁block状态的线程,会被加入到这里。
_WaitSet:处理wait状态的线程,会被加入到这里。(调用同步对象wait方法)
● 刚开始 Monitor 中 Owner 为 null
● 当 Thread-2 执行 synchronized(obj) 就会将
Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
● 在 Thread-2上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行synchronized(obj),就会进入EntryList BLOCKED
● Thread-2 执行完同步代码块的内容,然后唤醒
EntryList 中等待的线程来竞争锁,竞争的时是非公平的
● 图中 WaitSet 中的 Thread-0,Thread-1
是之前获得过锁,但条件不满足进入 WAITING 状态的线程
当多个线程同时访问一同步代码时,首先会进入 _EntryList 集合,进行阻塞等待,当线程获取到对象的 Monitor(锁)后,会把_owner变量指向该线程,同时 Monitor 的计数器_count自加1。
若线程调用同步对象的方法 wait()方法,将释放当前持有的Monitor(锁),_owner变量重置为null,且 _count会自减1,同时线程进入 _WaitSet中等待唤醒。当线程执行完同步代码后,也将_owner和_count重置。
4. synchronized锁的状态
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
底层CAS操作
锁的级别由低到高分别是:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态。这几个状态会随着锁的竞争情况逐渐升级(只能升级,不能降级),也叫锁的膨胀。
4.1 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
轻量级锁用的锁记录,偏向锁用的ThreaId
4.2 轻量级锁
- 使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
1.创建锁记录(LockRecor)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的markWord
2.让锁记录中Object Referenece指向锁对象,并尝试用cas替换Object 的Mark Word ,将Mark Word 的值存入锁记录
3. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。
4.如果 cas 失败,有两种情况
○ 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程(锁升级)
○ 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数(锁重入)
5.当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
6.当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
○ 成功,则解锁成功
○ 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
4.3 重量级锁
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
1.当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
2.这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
○ 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
○ 然后自己进入 Monitor 的 EntryList BLOCKED
4.当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
4.4 重量级锁自旋
重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)
自旋重试成功的情况
4.5 偏向锁、轻量级锁、重量级锁的升级过程
4.4 可重入锁
synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁
5. ReentrantLock
默认是不公平锁
可以设置为公平锁
相对于 synchronized 它具备如下特点
● 可中断
● 可以设置超时时间
● 可以设置为公平锁
● 支持多个条件变量
与 synchronized 一样,都支持可重入
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
6.volatile关键字
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
synchronized:保证原子性。
1.可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2.有序性:禁止进行指令重排序。
3. 不保证原子性
读写volatile变量时,加入内存屏障
可见性:
写屏障:保证在改屏障之前的t1对共享变量的改动,都同步到主存当中
读屏障:保证在该屏障之后t2对共享变量的读取,加载的是主存中最新数据
有序性:
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
cpu层面的内存屏障
写屏障:告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对写屏障之后的读或者写是可见的
读屏障:处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
全屏障:确保屏障钱的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。
7. CAS(比较并交换)**
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
CAS的特点:
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
● CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
●synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
● CAS体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思, 因为没有使用
synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 , 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
CAS:比较并交换,就是把当前线程的最新值和共享内存的最新值来比较,不一致,则继续自旋,一致则设置值
CAS比较并交换:借助valatile,才能读取共享变量的最新值
8. 原子类
1.AtomicBoolean
2.AtomicInteger
3.AtomicLong
原子引用
4.AtomicReference
5.AtomicMarkableReference
6.AtomicStampedReference
原子数组
7.AtomicIntegerArray
8.AtomicLongArray
9.AtomicReferenceArray
原子更新器
10.AtomicReferenceFieldUpdater
11.AtomicIntegerFieldUpdater
12.AtomicLongFieldUpdater
原子累加器
13.AtomicLong
14.LongAdder
9.ABA问题
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号
AtomicStampedReference(维护版本号)
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C
,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference
10. 线程池
1.构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
● corePoolSize 核心线程数目 (最多保留的线程数)
● maximumPoolSize 最大线程数目
● keepAliveTime 生存时间 - 针对救急线程
● unit 时间单位 - 针对救急线程
● workQueue 阻塞队列
● threadFactory 线程工厂 - 可以为线程创建时起个好名字
● handler 拒绝策略
任务调度线程池 ScheduledExecutorService
可以用来定时执行任务。