|
|
|
|
线城是进程的最小执行单元,Java对操作系统线程抽象的到了Thread类。
|
|
|
|
|
# 一、线程的创建
|
|
|
|
|
+ 继承Thread类重写run方法
|
|
|
|
|
```java
|
|
|
|
|
public class MyThread extends Thread{
|
|
|
|
|
@override
|
|
|
|
|
public void run() {
|
|
|
|
|
//do something
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//启动线程
|
|
|
|
|
MyThread th=new MyThread()
|
|
|
|
|
th.start()
|
|
|
|
|
```
|
|
|
|
|
+ 实现runnable接口
|
|
|
|
|
```java
|
|
|
|
|
public class MyRunnable implements Runnable{
|
|
|
|
|
@override
|
|
|
|
|
public void run() {
|
|
|
|
|
//do something
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//启动线程
|
|
|
|
|
MyThread th=new MyThread(new MyRunnable())
|
|
|
|
|
th.start()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
其余几种方法是基于上面两种演化而来。
|
|
|
|
|
# 二、线程的状态
|
|
|
|
|
| 状态 | 说明 |
|
|
|
|
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
| NEW | 已创建而未启动,可以理解为调用start()方法前的状态。一个线程只可能有一次处于次状态 |
|
|
|
|
|
| RUNNABLE | 包含了READY和RUNNING两种状态,两种状态可以互相转换。READY表示可以被线程调度器进行调度而进入RUNNING状态,RUNNING表示线程正在运行(可以理解为调用run方法)。当执行Thread.yield()时,状态会由RUNNING转为READY |
|
|
|
|
|
| BLOCKED | 线程发起阻塞式操作或者申请一个被其他线程持有的独占资源时,就会处于此状态。当操作完成或获得了申请的资源,又会转为RUNNABLE |
|
|
|
|
|
| WAITING | 线程执行了某些特定方法后处于等待状态,Object.wait()、Thread.join()、LockSupport.park(Object)。能够从WAITING变成RUNNING,Object.notify()和LockSupport.unpark(Object) |
|
|
|
|
|
| TIMED_WAITING | 与WAITING类似,区别在于并非处于无限制的等待。当其他线程没有在指定时间内执行该线程锁期望的操作时,线程状态转为RUNNABLE |
|
|
|
|
|
| TERMINATED | 已经执行结束的线程处于该状态,Thread.run()或者异常退出都会导致处于该状态 |
|
|
|
|
|
# 三、竞争条件
|
|
|
|
|
+ 读改写
|
|
|
|
|
例如 val++或者++val操作
|
|
|
|
|
+ 检测而后行动
|
|
|
|
|
例如 读取某个变量的值,根据改变量的值而决定下一步的动作,线程1执行完操作1准备执行操作2时,线程2对seq更新使得操作1的判断为false,导致不成立。
|
|
|
|
|
```java
|
|
|
|
|
if(seq>=999){ //操作1
|
|
|
|
|
seq=0; //操作2
|
|
|
|
|
}else {
|
|
|
|
|
seq++;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
# 四、活性故障
|
|
|
|
|
|
|
|
|
|
| 故障 | 说明 |
|
|
|
|
|
| ---- | ---- |
|
|
|
|
|
| 死锁 | 两个线程互相等待对方释放资源,永远处于非RUNNAING状态 |
|
|
|
|
|
| 锁死 | 一直处于WAITING状态,并且没有其他线程来激活 |
|
|
|
|
|
| 活锁 | 线程处于RUNNAING状态,但是执行的任务没有进展,做无用功 |
|
|
|
|
|
| 饥饿 | 线程因无法获得其所需资源而使任务无法执行,可以理解为一直没有被分配时间片 |
|
|
|
|
|
|
|
|
|
|
# 五、java线程同步机制
|
|
|
|
|
## 5.1 锁
|
|
|
|
|
锁的思路就是将多线程并发访问强制变成串行访问。锁具有排他性,同一时间一个锁只能由一个线程持有。锁分为拍他锁(互斥锁)和读写锁(对拍他锁的改进)。
|
|
|
|
|
|
|
|
|
|
**锁对原子性、可见性和有序性的保障并不是一定的,必须满足以下2个条件:
|
|
|
|
|
+ 线程在访问同一组共享数据时必须使用同一个锁
|
|
|
|
|
+ 任意一个线程,即使只是读取操作,也需要在读取时持有相应的锁
|
|
|
|
|
### 5.1.1 锁的重要概念
|
|
|
|
|
#### 1.可重入性
|
|
|
|
|
可重入性是指:一个线程在持有锁的时候,能够再次或多次申请该锁。
|
|
|
|
|
可重入性解决了锁嵌套的问题,例如:
|
|
|
|
|
```java
|
|
|
|
|
void methodA(){
|
|
|
|
|
acquireLock(lock);//申请锁
|
|
|
|
|
methodB();//执行方法B
|
|
|
|
|
releaseLock(lock);//释放锁
|
|
|
|
|
}
|
|
|
|
|
void methodB(){
|
|
|
|
|
acquireLock(lock);//申请锁
|
|
|
|
|
releaseLock(lock);//释放锁
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
假如锁不可重入,在执行方法B是就会造成死锁,因为方法A还未释放锁。
|
|
|
|
|
|
|
|
|
|
**可重入锁的实现:一个包含计数器的对象。计数器初始值为0,表示未被持有。当有线程申请成功时计数器+1,释放时-1。初次申请时的开销最大,因为需要和其他线程竞争。**
|
|
|
|
|
#### 2.锁的竞争与调度
|
|
|
|
|
锁的调度分为公平策略与非公平策略,相应的锁就分为公平锁和非公平锁。**内部锁就属于非公平锁**,显示锁可以是公平或者非公平。
|
|
|
|
|
#### 3.锁的粒度及开销
|
|
|
|
|
一个锁保护的共享数据大小就是锁的粒度。
|
|
|
|
|
锁的开销包括锁的申请和释放所产生的开销,以及锁可能导致的上下文切换的开销,开销主要体现在处理器时间上。
|
|
|
|
|
#### 4.内部锁:synchronized关键字
|
|
|
|
|
synchronized关键字可以用来修饰方法以及代码块
|
|
|
|
|
```java
|
|
|
|
|
void synchronized methodA(){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
synchronized(){//传入锁句柄,可以填写this关键字或变量,例如private final Object lock=new Object();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
**内部锁的使用不会导致锁泄漏,java编译器对synchronized关键字修饰的代码中未捕获的异常会做特殊处理,即使出现异常也会释放锁。
|
|
|
|
|
|
|
|
|
|
内部锁的调度机制:jvm会为每个内部锁分配一个入口集,用于记录等待获取锁的线程信息。当多个线程申请同一个锁时,只有一个线程能申请成功,其余线程则进入入口集。此时入口集中线程的状态为BLOCKED(暂停),入口集中的线程就被称为相应内部锁的等待线程。当锁释放时,入口集中的任意一个线程会被JVM唤醒再次申请锁。由于内部锁是非公平的,被唤醒的线程可能还会与其他活跃线程竞争锁,因此被唤醒线程不一定能获得锁。
|
|
|
|
|
另外,如何选择唤醒线程,可能是等待时间最长的,也可能是最短的,或者是完全随机的。
|
|
|
|
|
#### 5.显示锁:Lock接口
|
|
|
|
|
ReentrantLock是Lock接口的默认实现。
|
|
|
|
|
```java
|
|
|
|
|
private final Lock lock=
|
|
|
|
|
lock.lock();//获取锁
|
|
|
|
|
try{
|
|
|
|
|
|
|
|
|
|
}finally{
|
|
|
|
|
lock.unlock();//释放锁
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
显示锁的调度:ReentrantLock可以构造出公平与非公平锁,公平锁的调度公平性是增加了线程暂停和唤醒的可能性(增加了上下文切换)。**公平锁适合于锁被持有的时间相对长的或者线程申请锁的平均间隔时间相对长的场景**。使用公平锁的开销比非公平锁的开销大,因此ReentrantLock默认是非公平锁。
|
|
|
|
|
|
|
|
|
|
如何选择内部锁还是外部锁:相对保守的策略是默认情况使用内部锁,仅当多数线程持有一个锁的时间相对长或者申请锁的平均时间间隔长的时候使用显示锁。
|
|
|
|
|
|
|
|
|
|
#### 6.读写锁(显示锁的改进)
|
|
|
|
|
对共享变量仅进行读取的而没有更新的线程称为只读线程,对共享变量更新(包括先读区后更新)的线程被称为写线程。
|
|
|
|
|
**读写锁允许多个线程可以同时读区共享变量,但一次只允许一个线程对共享变量进行更新。任何线程读区共享变量时,其他线程无法更新变量;任意线程更新变量时,其他线程无法访问变量。
|
|
|
|
|
|
|
|
|
|
读写锁是通过2种角色锁实现,读锁(ReadLock)和写锁(WriteLock),线程在访问共享变量是必须持有相应的读锁,读锁可以同时被多个线程持有(共享的);写锁具有排他性,一个线程持有写锁的时候其他线程无法获取相应的写锁或读锁。任何一个线程持有读锁时,其他线程无法获取写锁来保障数据一致性。总的来说读锁起到了保护数据在共享期间不被修改的作用以及提高了并发性,而写锁保障了数据一致性。
|
|
|
|
|
|
|
|
|
|
| | 获得条件 | 排他性 | 作用 |
|
|
|
|
|
| ---- | -------------------------- | ------------------------ | ------------------------------------------------------------ |
|
|
|
|
|
| 读锁 | 相应的写锁未被任何线程持有 | 对读线程共享,写线程排他 | 允许多个读线程可以同时读取变量,并保障读区期间数据不发生改变 |
|
|
|
|
|
| 写锁 | 写锁未被其他任何线程持有并且相应的读锁未被其他任何线程持有| 对写线程和读线程都是排他的|以独占方式访问共享变量|
|
|
|
|
|
|
|
|
|
|
#### 7.锁的使用场景
|
|
|
|
|
+ check-then-act(检查后执行)
|
|
|
|
|
+ read-modify-write(度改写),例如val++
|
|
|
|
|
+ 多个线程对多个共享数据进行更新
|
|
|
|
|
#### 8.内存屏障(如何实现刷新、更新处理器缓存)
|
|
|
|
|
![[Pasted image 20231110094439.png]]
|
|
|
|
|
根据有序性保障来划分:获取屏障(Acquire Barrier)和释放屏障(Release Barrier)。获取屏障是在一个读操作后插入该屏障,**作用是禁止该读操作与其后续任何读写操作之间进行重排序**;释放屏障是在写操作前插入该屏障,**作用是禁止该写操作与其前面任何读写操作之间进行重排序**
|
|
|
|
|
如图所示,java虚拟机会在MonitorEnter(读)对应的机器码指令后插入获取屏障,在MonitorExit(写)之前插入释放屏障,使得**临界区**执行的操作具有原子性。
|
|
|
|
|
|
|
|
|
|
#### 9.锁与重排序
|
|
|
|
|
与锁有关的重排序可以理解为对临界区‘许进不许出’。
|
|
|
|
|
重排序规则:
|
|
|
|
|
1. 临界区内的操作不允许被重排序到临界区外
|
|
|
|
|
该规则保证了原子性和可见性的基础,通过在临界区前后插入内存屏障来实现。
|
|
|
|
|
2. 临界区内的操作之间允许被重排序
|
|
|
|
|
通过JIT优化提升性能
|
|
|
|
|
3. 临界区外的操作之间可以被重排序
|
|
|
|
|
与2相同
|
|
|
|
|
4. 锁申请与锁释放操作不能被重排序
|
|
|
|
|
为了确保锁申请与释放总是匹配成对的
|
|
|
|
|
5. 两个锁申请操作不能被重排序
|
|
|
|
|
6. 两个锁释放操作不能被重排序
|
|
|
|
|
4、5、6确保了嵌套锁的使用,并且避免了可能导致的死锁
|
|
|
|
|
7. 临界区外的操作可以被重排到临界区内
|
|
|
|
|
## 5.2 volatile
|
|
|
|
|
```java
|
|
|
|
|
private volatile int val;
|
|
|
|
|
```
|
|
|
|
|
**volatile**关键字表示被修饰的变量值容易变化。与锁的区别在于,此关键字只能保证可见性和有序性。在原子性方面它仅能保障写volatile变量操作的原子性(long和double变量)。另外使用关键字不会引起上下文切换。
|
|
|
|
|
在java中long和double是64位存储,更新时是拆分为2个32位进行,volatile关键字能保障这一过程的原子性。
|
|
|
|
|
|
|
|
|
|
*光使用volatile无法保障例如val++的原子性
|
|
|
|
|
|
|
|
|
|
#### 1.volatile的使用场景
|
|
|
|
|
+ 作为状态标志
|
|
|
|
|
+ 保障可见性
|
|
|
|
|
+ 替代锁
|
|
|
|
|
+ 实现简易版读写锁
|
|
|
|
|
![[Pasted image 20231110170446.png]]
|
|
|
|
|
同步代码块保障了count++的原子性。
|
|
|
|
|
|
|
|
|
|
*volatile关键字和锁各有其适应的场景,前者更适合多个线程共享一个状态变量(对象),而后者更适合与多个线程共享一组状态变量。某些情况下,可以将一组状态变量合并成一个对象,可以避免锁的使用。
|
|
|
|
|
|
|
|
|
|
# 总结
|
|
|
|
|
|
|
|
|
|
### 线程同步机制的功能与开销
|
|
|
|
|
|
|
|
|
|
| | 锁 | volatile | CAS | final | static |
|
|
|
|
|
| ---------- | ---- | -------- | ------ | ------ | ------ |
|
|
|
|
|
| 原子性 | 具备 | 具备(2) | 具备 | 不涉及 | 不涉及 |
|
|
|
|
|
| 可见性 | 具备 | 具备 | 不具备 | 不具备 | 不具备(3) |
|
|
|
|
|
| 有序性 | 具备 | 具备 | 不涉及 | 具备 | 具备(4) |
|
|
|
|
|
| 上下文切换 | 可能(1) | 不会 | 不会 | 不会 | 可能(5) |
|
|
|
|
|
| 备注 |(1)被争用的锁可能导致上下文切换 |(2)仅能保障对volatile变量读/写操作本身的原子性| ||(3)(4)仅在一个线程初次读区一个类的静态变量时起作用 (5)静态变量所属类的初始化可能导致上下文切换|
|
|
|
|
|
|