线程基础
一、定义
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
< 1 > Java从根本上支持线程的概念。
< 2 > 不仅Java语言本身能够很方便地支持多线程应用程序开发,Java虚拟机也是一个多线程进程,它为Java应用程序提供调度机制和内存管理。
< 3 > 能够直接从多核系统资源中获益的Java程序包括Sun公司的Java应用服务器、BEA公司的WebLogic、IBM公司的WebSphere和开源的Tomcat应用服务器。基于J2EE开发的所有应用程序也可以直接从多核技术中获益。
[ 疑惑 ] 如何理解Java从根本上支持线程?
< 1 > Java线程在JDK1.2之前,基于称为“绿色线程”(Green Threads)的用户线程实现。而到操作系统层面就是用户空间内的线程实现。
< 2 > 在JDK1.2及以后,JVM选择了更加稳健且方便使用的操作系统原生的线程模型,通过系统调用,将程序的线程交给了操作系统内核进行调度。线程模型替换为基于操作系统原生线程模型来实现。
- 现在的Java中线程的本质,就是操作系统中的线程,线程切换需进行
内核态/用户态
切换。 - Linux下是基于
pthread
库实现的轻量级进程,Windows下是原生的系统Win32 API
提供系统调用从而实现多线程。
二、状态 & 生命周期
- Java 的 线程拥有 6 种线程状态,它的描述位于 Thread 类下的一个 内部枚举类 Status。
对于操作系统来说,理论上线程仅需要拥有三种状态,即
Ready
、Running
、Blocked
,其余多余的状态是没有必要的,因为进程包含了新建态、就绪态、终止态及其变体,线程6态只是对于JVM来说,但操作系统实际上只有3种状态。
JVM、操作系统 线程 对应关系:
Ready
:Runnable (ready)
Running
:Runnable (running)
Blocked
:Timed_Waiting、Waiting、Blocked
Java 线程 6态 示意图:
1、NEW – 新建态
- 已经创建了一个进程对象,但是还没有调用start() 方法。
2、RUNNABLE – 可执行态
- Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
① RUNNABLE(ready) - 就绪
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。
- 就绪状态的线程在获得CPU时间片后变为运行中状态(running),即有资格运行,调度程序没有挑选到该进程,则其保持就绪状态。
② RUNNABLE(running) - 运行
- 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。
- 即线程正在运行的状态。
3、BLOCKED – 阻塞态
- 表示线程阻塞于锁。
- 阻塞状态是线程阻塞在进入 有锁 的方法或代码块时的状态。
4、WAITING – 等待态
- 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
5、TIMED_WAITING – 超时等待态
- 该状态不同于WAITING,它可以在指定的时间后自行返回。
- 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
6、TERMINATED – 终止态
- 表示该线程已经执行完毕。
- 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。
- 调用已终止线程的 start() 方法,会抛出java.lang.IllegalThreadStateException异常。# 三、线程操作
- 我们可以通过代码对线程进行操作,线程操作的示意图如下:
1、线程创建
(1)继承 Thread 类
- 通过让类继承 Thread类 创建线程,由于Java的类是单继承,所以一般在定义专用的线程类时使用。
- 若需要执行线程,则需重写 run()方法。
public class MyThreadDemo01{
//启动线程
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
//定义静态内部类 MyThread,继承 Thread 类:
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("我是继承了Thread类创建出的线程:" + currentThread().getName());
}
}
}
运行结果如下:
(2)实现 Runnable 接口
- 通过让类实现 Runnable 接口 创建线程,这种方法方便我们的灵活使用。
- 使用 该接口 须重写 run()方法。
public class MyThreadDemo02 {
//启动线程
public static void main(String[] args) {
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
}
//定义静态内部类 MyThread,实现 Runnable 接口:
static class MyThread implements Runnable{
@Override
public void run() {
System.out.println("我是实现了Runnable接口创建出的线程:" + Thread.currentThread().getName());
}
}
}
运行结果如下:
(3)实现 Callable 接口
- 通过让类实现 Callable 接口 创建线程,这种方法需要 FutureTask 类的辅助。
- 而 FutureTask 类实现了Runnable 接口,方便我们启动线程。
- 使用 该接口 须重写 call()方法。
public class MyThreadDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//返回值:String
FutureTask task1 = new FutureTask<String>(new MyThread01());//FutureTask的泛型可以省略,但其类型必须与线程返回值保持一致
new Thread(task1).start();
System.out.println(task1.get()); //通过FutureTask 的成员方法 get() 获取其返回值
//返回值:Integer
FutureTask task2 = new FutureTask<Integer>(new MyThread02());//FutureTask的泛型可以省略,但其类型必须与线程返回值保持一致
new Thread(task2).start();
System.out.println(task2.get()); //通过FutureTask 的成员方法 get() 获取其返回值
}
static class MyThread01 implements Callable<String> {
@Override
public String call() throws Exception {
return "我是实现了Callable接口创建出的线程";
}
}
static class MyThread02 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 1; i <= 100; i++){
result += i;
}
return result;
}
}
}
运行结果如下:
[ 思考 ] Callable 与其他两种创建方式的区别
实现 Callable 接口创建的线程的三大特点:
- 可以拥有返回值。
- 可以抛出异常。
- 重写 call() 方法。
2、线程启动 start()【Thread类】
- 启动 start(),源于Thread类的非静态方法,使得当前线程进入运行态 RUNNABLE(Running)。
3、线程暂停 Thread.sleep(long mills)
- 暂停 sleep(long mills) / sleep(long mills, int nanos) ,为线程指定暂停的毫秒数。
- 当前线程进入TIMED_WAITING状态,但
不释放对象锁
,经过指定的 millis 后,线程自动苏醒进入就绪状态。 - 作用:给其它线程执行机会的最佳方式。
4、线程礼让 yield()【Thread类】
- 礼让 yield(),源于Thread类的静态方法,该线程礼貌性地释放CPU资源,给其他线程抢占CPU 资源的机会,随后它立即进入就绪态,重新参与抢占过程。
- 即当前线程放弃获取的CPU时间片,但
不释放锁
资源,由运行状态变为就绪状态,让OS再次选择线程。 - 作用:给其它线程参与抢占CPU资源的机会。
5、线程插队 Thread.join() / Thread.join(long mills)
- 插队 join() / join(long mills),使得该线程独自享有CPU资源,它执行终止后,其他线程才可以继续执行(即使得
部分操作串行执行
)。 - 作用:使得部分操作串行执行。
6、线程等待 Object.wait() / Object.wait(long mills)
- wait()属于Object 类,它在放弃 CPU资源 的同时,也放弃锁。
7、线程唤醒 Object.notify() / Object.notifyAll()
- notify()、notifyAll() 属于Object 类,执行
notify()
可唤醒需要线程,执行notifyAll()
操作可唤醒全部线程。
三、三大特性
① 原子性
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在JAVA中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
② 可见性
先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。因此需要使用volatile关键字来满足线程的可见性,将线程的工作内存的资源刷新到主内存中。
③ 有序性
程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。因此需要volatile保证有序性,防止指令重排。
四、三大特性保障策略
1、CAS 操作
- CAS(Compare & Swap),比较与交换,是用于实现多线程同步的原子指令,即这种操作可以保证三大特性中的 原子性。
- 它有三个相关值:分别是内存数值、旧预期值、要替换的新值。
- 它将 内存数值 与 旧预期值 进行比较,如果相等则将其修改为 新值。
① CAS存在的问题
< 1 > ABA问题
< 2 > 自旋时间长消耗cpu资源
< 3 > 只能对一个共享变量操作
我们就ABA问题进行探讨:
ABA问题
- 如果另一个线程修改的值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前值是否发生过变化。
解决思路: - ABA 问题的解决思路就是使用版本号。
- 从 Java 1.5 开始,JDK 的 atomic 包里提供了一个类
AtomicStampedReference类
来解决 ABA 问题。 - 则正确执行CAS操作为:同时满足CAS的期望值匹配、版本号匹配。
② AtomicInteger(底层基于CAS操作)与int的区别
- int是线程不安全的,在做自增运算时不满足线程的原子性。
- 而AtomicInteger在高并发的情况下是线程安全的。
- AtomicInteger类只能保证在自增或者自减的情况下保证线程安全。
2、加锁
① synchronized 关键字(同步锁)
- 当两个线程并发访问同一对象中的synchronized代码块时,一个时间内只能有一个线程得到执行。其他线程须等待此线程执行完这个代码块后,才能执行该代码。
- 当一个线程访问对象的synchronized代码块时,另外一个线程仍然能访问非synchronized 修饰的代码块,其它线程对对象中所有的其它synchronized同步代码块访问被阻塞。
- 同一时间只有一个线程能够取得对象的锁。
锁定方法:
public synchronized void example{
//方法体
}
锁定代码块:
public void example{
synchronized(this){
//代码块
}
}
② ReentrantLock 类(可重入锁)
- Lock 是一个提供无条件、可轮训的、定时的、可中断的锁获取操作的接口,所有加锁和解锁的方法都是显示的。
- ReentrantLock是Lock接口的实现类。
以下是官方给出的使用方法:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
synchronized锁与ReentrantLock的对比:
(1)synchronized 关键字 不需要手动释放和开启,对象之间是互斥关系。
(2)ReentrantLock 对象 使用灵活,但必须有手动开启锁和释放锁的操作,只适应于代码块。
3、volatile 关键字
- 解决多个线程共享同一资源时,线程的可见性、有序性问题。
- Java中的volatile关键字,将被其修饰的变量在被修改后,立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。
五、死锁问题
生活中的死锁问题:
两线程间的死锁问题:
※ 四个必要条件
这里以OS进程
① 互斥
某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
② 占有且等待
一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
③ 不可抢占
其他线程已经占有了某项资源,该线程不能因为也需要该资源,抢占其他线程。
④ 循环等待
存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
- 构建一个死锁实例:
public class MyThreadDemo04 {
public static void main(String[] args) {
DeadLock a = new DeadLock("A");
DeadLock b = new DeadLock("B");
Thread t1 = new DeadLockThread(a, b);
Thread t2 = new DeadLockThread(b, a);
t1.start();
t2.start();
}
public static class DeadLock {
private String name;//锁的名字
public DeadLock(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static class DeadLockThread extends Thread {
private DeadLock a;
private DeadLock b;
public DeadLockThread(DeadLock a, DeadLock b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (a) {
System.out.println(a.getName() + "资源已被锁住");
System.out.println("准备锁" + b.getName() + "资源");
synchronized (b) {
System.out.println(b.getName() + "资源已被锁住");
}
System.out.println(b.getName() + "资源已被释放");
}
System.out.println(a.getName() + "资源已被释放");
synchronized (b) {
System.out.println(b.getName() + "资源已被锁住");
System.out.println("准备锁" + a.getName() + "资源");
synchronized (a) {
System.out.println(a.getName() + "资源已被锁住");
}
System.out.println(a.getName() + "资源已被释放");
}
System.out.println(b.getName() + "资源已被释放");
}
}
}
运行结果如下(死锁发生):
若不手动手动停止,程序会一直等待下去:
六、活锁问题
生活中的活锁问题:
两人面对面走来,即将相撞,秉持避让原则,两人都朝着自己一方的同一方向避让,结果没过几次就会相撞。
两线程间的活锁问题:
两线程都秉持着 “谦让” 原则,主动将资源释放给他人使用,那么资源就不断地在两个线程间反复横跳,而没有一个线程可以同时拿到所有资源正常执行。
七、饥饿问题
定义:
饥饿,某一个 / 多个线程因种种原因无法获得所需要的资源,导致一致无法执行。
常见情况:
- 该线程优先级太低,高优先级线程不断抢占它需要的资源,导致低优先级线程无法工作。
- 某线程长时间占着关键资源不释放,导致其他需要这个资源的线程无法正常执行。