简介
在接触多线程
之前,在我们程序中在任意时刻都只能执行一个步骤,称之为单线程
。在单线程开发的程序中所有的程序路径都是顺序执行的,前面的必须先执行,后面的才会执行。单线程的优点也很明显,相对于多线程来说更加稳定、扩展性更强、程序开发相对比较容易。但是由于每次都要等上一个任务执行完成后才能开始新的任务,导致其效率比多线程低,甚至有时候应用程序会出现假死的现象。使用多线程有利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。多线程是 Java 学习的非常重要的方面,是每个 Java 程序员必须掌握的基本技能。本文是有关 Java 多线程的一些基础知识总结。
进程与线程的区别
进程
进程是操作系统资源分配
的基本单位,它是操作系统的基础,是一个程序及其数据在处理机上顺序执行时所发生的活动。一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。进程的实质就是程序在操作系统中的一次执行过程,它是动态产生的、动态销毁的,拥有自己的生命周期和各种不同的运行状态。同时,进程还具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进(PS:并发性和并行性是不同的概念,并行指的是同一时刻,两个及两个以上的指令在多个处理器上同时执行。而并发指的是同一时刻只有一条指令执行,但是多个进程可以被 CPU 快速交换执行,给我们感觉好像是多个执行在同时执行一样
)。
线程
线程是任务调度和执行
的基本单位,也被称为轻量级进程
,线程由线程 ID,当前指令指针(PC),寄存器集合和堆栈组成。线程不拥有系统资源,它只会拥有一点儿在运行时必不可少的资源,但是它可以与同属于同一进程的线程共享该进程所拥有的所有资源。一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
二者的区别
- 调度 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
- 并发性 不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
- 拥有资源 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
- 系统开销 在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销
创建线程的方式
在 Java 中使用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或者其子类的实例,Java 中创建线程主要有以下三种方式:
方式一 继承 Thread 类
step 1 定义一个类继承自 Thread
类,然后重写该类的 run
方法,这个方法的内容表示线程要完成的任务
step 2 创建线程对象,即创建 Thread
类子类的实例
step 3 调用步骤二中创建出来的对象的 start
方法来启动线程
/**
* @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
方法来启动该线程
/**
* @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
方法获取线程执行结束后的返回值
/**
* @author mghio
* @date: 2019-12-07
* @version: 1.0
* @description: 通过实现 Callable 接口的方式创建线程
* @since JDK 1.8
*/
public class CreateThreadByImplementsCallable implements Callable {
@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 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
接口这两种方式创建线程基本相同,采用实现 Runnable
和 Callable
接口的方式创建线程时,线程类只是实现接口,还可以继承其它类(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 的使用权,暂时停止运行,如果执行了 sleep
、suspend
等方法,释放了所占用的资源之后,线程就从运行状态
进入阻塞状态
。等待睡眠时间结束或者获得设备资源之可以重新进入就绪状态
。阻塞可以分为以下三种:
- 等待阻塞 处于
运行状态
的线程调用wait
方法,会使线程进入等待阻塞状态
- 同步阻塞 当线程获取
synchronized
同步锁因为同步锁被其他线程占用而失败后,会使线程进入同步阻塞
- 其它阻塞 通过调用线程的
sleep
或join
发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep
状态超时,join
等待线程终止或超时,或者 I/O 处理完毕,线程重新回到就绪状态
。
死亡状态(Dead)
一个处于运行状态
的线程执行完了 run
方法或者因为其它终止条件发生时,线程就会进入到死亡状态
,该线程结束生命周期。
以上线程各种状态的流转用一张图表示如下:
线程常用方法
线程中常用的方法按照来源可以分为两类,一类是继承自 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
类提供了 setPriority
和 getPriority
方法来更改和获取线程优先级(需要注意的是: 线程优先级不能保证线程执行的顺序,而且非常依赖于平台)。
参考文章
- 进程和线程的区别
- Java多线程系列--“基础篇”05之 线程等待与唤醒