多线程基础
# 1. Java 线程基础
# 1.1. 概念术语
- 进程: 是操作系统的结构基础,是一次程序的执行,它是系统进行资源分配和调度的一个单位。
- 线程: 是指进程中执行的一个子任务,一个进程中可能会有多个线程。
- 锁: 是指控制多个线程获取资源的一种方式,如保证多个线程顺序获取某个资源。
- 上下文切换(线程通信): 泛指多个线程之间的切换,如从A线程切换到B线程,再从B线程切换到A线程的过程。
- 程序: 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
# 1.2. 线程的理解
- NEW: 初始状态,线程创建,没有调用start();
- RUNNABLE: 可运行状态,线程处于一切准备就绪,等待获取CPU资源执行;
- TERMINATED: 终止状态,线程顺利执行完成;
- WAIING: 等待状态,线程通过外力wait()/join()/park()进入等待状态,也就是让出资源,给其他线程;只有通过唤醒才能回到可运行准备就绪状态;
- TIMED_WAITING: 超时等待状态,当线程进入等待状态时间结束会自动返回到执行就绪状态;
- BLOCKED: 阻塞状态,多个线程竞争获取锁资源时,没有获取到锁的线程会进入阻塞状态,会一直等到获取所资源后才进入准备就绪状态。
# 1.2.1. 线程的实现方式
- 继承java.lang.Thread类
public class MyThread extend Thread{
@Override
public void run() {
// 需要执行的程序代码逻辑
}
}
1
2
3
4
5
6
2
3
4
5
6
- 实现java.lang.Runnable接口
public class MyThread implements Runnable{
@Override
public void run() {
// 需要执行的程序代码逻辑
}
}
1
2
3
4
5
6
2
3
4
5
6
- 实现java.util.concurrent.Callable接口,可以实现异步、同步;
public class MyThread implements Callable<String> {
public static void main(String []args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyThread myThread = new MyThread();
Future<String> submit = executorService.submit(myThread);
System.out.println(submit.get());
executorService.shutdown();
}
@Override
public String call() throws Exception {
return "测试实现多线程";
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 1.2.2. 线程之间的通信
- 基于volatile和synchronized控制资源的访问,实现多个线程之间通信;
- 基于Object类下wait()、wait(long)、wait(long,int)、notify()、notifyAll()实现线程的等待与通知的方式;
- 还可以使用java.util.concurrent.locks.LockSupport类中的方法实现通信,其中功能类似Object的中的几个方法,详细请查看API (opens new window)
注意: wait和notify需要放到synchronized中,因为wait是去获取一个监视锁,拿到锁后进入一个等待队列中;同理,notify去唤醒对应等待中的线程,也需要找到一个监视锁,通过释放锁唤醒线程。
# 1.3. JMM
Java Memory Model ,是一种抽象的内存模型;用于实现多线程并发情况下,屏蔽操作系统、以及硬件之间的差异性,实现各平台统一内存访问可见性、原子性、有序性。
如图,线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
线程A和线程B之间通信,需要经过主内存实现;线程A需要把改变的变量副本刷新到主内存,而线程B需要设置本地变量副本失效,然后获取主内存数据,从而实现线程之间的通信;通过主内存来实现多线程之间数据可见性。
在并发编程情况下,为了防止指令重排序、多线程间数据可见性、操作原子性、程序执行的有序性提供了很多基于抽象的JMM的规范,如常用关键字volatile、synchronized、final。
# 1.3.1. volatile
使用volatile 修饰变量,保证多线程之间变量的可见性、原子性以及防止指令重排序。
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入;
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性;
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量;
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
对于volatile的原子性必须满足以下条件
# 1.3.2. synchronized
同步锁,依赖于虚拟机实现的,synchronized用的锁是存在Java对象头里的
- synchronized的使用
- 修饰普通方法(普通同步锁),锁时当前对象;
- 修饰静态方法(静态同步方法),锁是当前类的Class对象;
- 修饰代码块(同步方法块),锁是Synchonized括号里配置的对象。
- 释放锁:当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
- 获取锁:当线程获取锁时,JMM会把该线程对应的本地内存中的共享变量置为无效。从而使得被监视器保护的 临界区代码必须从主内存中读取共享变量
- synchronized分为偏向锁、轻量级锁、重量级锁
这里需要了解synchronized实现机制,在虚拟机中对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础
- 偏向锁: 当一个线程访问同步块获取锁时,在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word (标志位-用于存储对象自身的运行时数据) 里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 轻量级锁: 当多个线程竞争偏向锁,由于竞争激烈,这时候会将锁升级,获取一个轻量级的锁。
- 重量级锁: 重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统实现。
注意: 从偏向锁到重量级锁,随着竞争情况,锁会升级,但是不会降级。
# 1.3.3. final
JVM 通过final修饰符来保证被修饰变量对多个线程可见,但是在构造函数中不能有对象逸出;
- final修饰的变量必须要初始化,或者在构造函数中初始化
- 最好不要使用构造函数来初始化final,避免造成对象逸出
- 只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
FinalReferenceEscapeExample.writer();
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
FinalReferenceEscapeExample.reader();
}
},"B").start();
}
public FinalReferenceEscapeExample() {
// 这里可能会重排序,当多线访问的时候可能得不到期望的数据
i = 1;
// 对象逸出
obj = this;
}
public static void writer() {
new FinalReferenceEscapeExample();
}
public static void reader() {
if (obj != null) {
int temp = obj.i;
System.out.println(Thread.currentThread().getName() + "---->" + temp);
}
System.out.println(Thread.currentThread().getName() + "对象没有初始化");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
上次更新: 2024/03/02, 14:21:03