Java 多线程基础(一)

简介

在接触多线程之前,在我们程序中在任意时刻都只能执行一个步骤,称之为单线程。在单线程开发的程序中所有的程序路径都是顺序执行的,前面的必须先执行,后面的才会执行。单线程的优点也很明显,相对于多线程来说更加稳定、扩展性更强、程序开发相对比较容易。但是由于每次都要等上一个任务执行完成后才能开始新的任务,导致其效率比多线程低,甚至有时候应用程序会出现假死的现象。使用多线程有利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。多线程是 Java 学习的非常重要的方面,是每个 Java 程序员必须掌握的基本技能。本文是有关 Java 多线程的一些基础知识总结。

进程与线程的区别

进程

进程是操作系统资源分配的基本单位,它是操作系统的基础,是一个程序及其数据在处理机上顺序执行时所发生的活动。一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。进程的实质就是程序在操作系统中的一次执行过程,它是动态产生的、动态销毁的,拥有自己的生命周期和各种不同的运行状态。同时,进程还具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进(PS:并发性和并行性是不同的概念,并行指的是同一时刻,两个及两个以上的指令在多个处理器上同时执行。而并发指的是同一时刻只有一条指令执行,但是多个进程可以被 CPU 快速交换执行,给我们感觉好像是多个执行在同时执行一样)。 

线程

线程是任务调度和执行的基本单位,也被称为轻量级进程,线程由线程 ID,当前指令指针(PC),寄存器集合和堆栈组成。线程不拥有系统资源,它只会拥有一点儿在运行时必不可少的资源,但是它可以与同属于同一进程的线程共享该进程所拥有的所有资源。一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

二者的区别
  1. 调度 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  2. 并发性 不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
  3. 拥有资源 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
  4. 系统开销 在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销

创建线程的方式

在 Java 中使用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或者其子类的实例,Java 中创建线程主要有以下三种方式:

方式一 继承 Thread 类

step 1 定义一个类继承自 Thread 类,然后重写该类的 run 方法,这个方法的内容表示线程要完成的任务
step 2 创建线程对象,即创建 Thread 类子类的实例
step 3 调用步骤二中创建出来的对象的 start 方法来启动线程

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
/**
* @author mghio
* @date: 2019-12-07
* @version: 1.0
* @description: 通过继承 Thread 类的方式创建线程
* @since JDK 1.8
*/
public class CreateThreadByExtendsThread extends Thread {

@Override
public void run() {
IntStream.rangeClosed(1, 10).forEach(i -> System.out.println(Thread.currentThread().getName() + " " + i));
}

public static void main(String[] args) {
CreateThreadByExtendsThread threadOne = new CreateThreadByExtendsThread();
CreateThreadByExtendsThread threadTwo = new CreateThreadByExtendsThread();
CreateThreadByExtendsThread threadThree = new CreateThreadByExtendsThread();

threadOne.start();
threadTwo.start();
threadThree.start();
}

}
方式二 实现 Runnable 接口

step 1 定义一个类实现 Runnable 接口,然后实现该接口的 run 方法,这个方法的内容同样也表示线程要完成的任务
step 2 创建 Runnable 接口实现类的实例,并使用该实例作为 Thraed 构造方法的参数创建 Thread 类的对象,该对象才是真正的线程对象
step 3 调用线程对象的 start 方法来启动该线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mghio
* @date: 2019-12-07
* @version: 1.0
* @description: 通过实现 Runnable 接口的方式创建线程
* @since JDK 1.8
*/
public class CreateThreadByImplementsRunnable implements Runnable {

@Override
public void run() {
IntStream.rangeClosed(1, 10).forEach(i -> System.out.println(Thread.currentThread().getName() + " " + i));
}

public static void main(String[] args) {
CreateThreadByImplementsRunnable target = new CreateThreadByImplementsRunnable();
new Thread(target, "thread-one").start();
new Thread(target, "thread-two").start();
new Thread(target, "thread-three").start();
}

}
方式三 实现 Callable 接口

step 1 定义一个类实现 Callable 接口,然后实现该接口的 call 方法,这个方法的内容同样也表示线程要完成的任务,并且有返回值
step 2 创建 Callable 接口实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了 Callable 对象的 call 方法的返回值
step 3 并使用 FutureTask 对象作为 Thraed 构造方法的参数创建 Thread 对象,并调用该对象的 start 方法启动线程
step 4 调用 FutureTask 对象的 get 方法获取线程执行结束后的返回值

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
/**
* @author mghio
* @date: 2019-12-07
* @version: 1.0
* @description: 通过实现 Callable 接口的方式创建线程
* @since JDK 1.8
*/
public class CreateThreadByImplementsCallable implements Callable<Integer> {

@Override
public Integer call() {
AtomicInteger count = new AtomicInteger();
IntStream.rangeClosed(0, 10).forEach(i -> {
System.out.println(Thread.currentThread().getName() + " " + i);
count.getAndIncrement();
});

return count.get();
}

public static void main(String[] args) {
CreateThreadByImplementsCallable target = new CreateThreadByImplementsCallable();
FutureTask<Integer> futureTask = new FutureTask<>(target);

IntStream.rangeClosed(0, 10).forEach(i -> {
System.out.println(Thread.currentThread().getName() + " 的循环变量 i 的值" + i);
if (i == 8) {
new Thread(futureTask, "有返回值的线程").start();
}
});

try {
System.out.println("有返回值线程的返回值:" + futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}

}

通过以上可以看出,其实通过实现 Runnable 接口和实现 Callable 接口这两种方式创建线程基本相同,采用实现 RunnableCallable 接口的方式创建线程时,线程类只是实现接口,还可以继承其它类(PS:Java 单继承决定)。在这种方式下,多个线程可以共享同一个 target对象,所以非常适合多个相同线程来处理同一份资源的情况。还有一点就是,使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。,在实际项目中如果使用这三种方式创建线程,如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,所以在我们项目开发中主要还是使用线程池,有关线程池的可以看看这两篇 Java 线程池(一)Java 线程池(二)

线程的几种状态

线程是一个动态执行的过程,它也有一个从产生到死亡的过程,在 Java 中一个线程完整的生命周期一共包含以下五种状态:
新建状态(New)
当使用 new 关键字和 Thread 类或其子类创建一个线程对象后,那么线程就进入了新建状态,此时它和其它的 Java 对象一样,仅仅由 JVM 分配了内存,并初始化其成员变量值,它会一直保持这个状态直到调用该对象的 start 方法。

就绪状态(Runnable)
当线程对象调用了 start 方法之后,该线程就进入了就绪状态。就绪状态的线程会放在一个就绪队列中,等待 JVM 里的调度器进行调度。处于就绪状态的线程,随时可能被 CPU 调度执行。

运行状态(Running)
如果就绪状态的执行被 CPU 调度执行,就可以执行 run 方法,此时线程就处于线程状态。处于运行状态的线程最复杂,它可以变为阻塞状态就绪状态死亡状态。需要注意一点,线程变为运行状态之前的状态只能是就绪状态

阻塞状态(Blocked)
线程变为阻塞状态是因为某种原因放弃 CPU 的使用权,暂时停止运行,如果执行了 sleepsuspend 等方法,释放了所占用的资源之后,线程就从运行状态进入阻塞状态。等待睡眠时间结束或者获得设备资源之可以重新进入就绪状态。阻塞可以分为以下三种:

  1. 等待阻塞 处于运行状态的线程调用wait方法,会使线程进入等待阻塞状态
  2. 同步阻塞 当线程获取 synchronized 同步锁因为同步锁被其他线程占用而失败后,会使线程进入同步阻塞
  3. 其它阻塞 通过调用线程的sleepjoin发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep状态超时,join等待线程终止或超时,或者 I/O 处理完毕,线程重新回到就绪状态

死亡状态(Dead)
一个处于运行状态的线程执行完了 run 方法或者因为其它终止条件发生时,线程就会进入到死亡状态,该线程结束生命周期。
以上线程各种状态的流转用一张图表示如下:

thread-state-transfer.png

线程常用方法

线程中常用的方法按照来源可以分为两类,一类是继承自 Object 类的方法,如下所示:

方法 描述
public final native void notify() 唤醒在此对象监视器上等待的单个线程,使其进入就绪状态
public final native void notifyAll() 唤醒在此对象监视器上等待的所有线程,使其进入就绪状态
public final void wait() 让当前线程处于·等待阻塞状态,直到其他线程调用此对象的notify方法或notifyAll方法,当前线程被唤醒,会释放它所持有的锁
public final native void wait(long timeout) 让当前线程处于·等待阻塞状态,直到其他线程调用此对象的notify方法或notifyAll方法,当前线程被唤醒
public final void wait(long timeout, int nanos) 让当前线程处于·等待阻塞状态,直到其他线程调用此对象的notify方法或notifyAll方法或者其他某个线程中断当前线程,或者已超过某个实际时间量,当前线程被唤醒

另一类是 Thread 类定义的方法,如下所示:

方法 描述
public static native void yield() 暂停当前正在执行的线程对象,并执行其他线程,yield 方法不会释放锁
public static native void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),sleep 方法不会释放锁
public final void join() 当某个程序执行流中调用其他线程的 join 方法时,调用线程将被阻塞,直到被 join 的线程执行完毕
public void interrupt() 用于中断本线程,这个方法被调用时,会立即将线程的中断标志设置为 true
public static boolean interrupted() Thread 类的一个静态方法,它返回一个布尔类型指明当前线程是否已经被中断,interrupted 方法除了返回中断标记之外,它还会清除中断标记(即将中断标记设为 false)
public boolean isInterrupted() Thread 类的一个实例方法,它返回一个布尔类型指明当前线程是否已经被中断,isInterrupted 方法仅仅返回中断标记,不会清楚终端标记

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY )~ 10(Thread.MAX_PRIORITY )。默认情况下,每一个线程都会分配一个优先级NORM_PRIORITY(5)。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源,Thread 类提供了 setPrioritygetPriority 方法来更改和获取线程优先级(需要注意的是: 线程优先级不能保证线程执行的顺序,而且非常依赖于平台)。


参考文章

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
赏作者☕️