1. 多线程概念
并发与并行
- 并发:指两个或多个事件在同一时间段内发生
- 并行:指两个或多个事件在同一时刻发生(同时发生)
多进程和多线程对比
- 多进程:
- 创建开销大,进程间通信慢
- 指一个内存中运行的应用程序,有独立内存空间,一个应用程序可以运行多个进程,是系统运行程序的基本单位
- 多线程:
- 开销小,速度快,读写同一变量(需要做到同步才能安全),属于抢占式的
- 是进程中的一个执行单元,一个进程至少有一个线程
注意:在
Java
中每次程序运行至少会启动 2 多线程,一个为main
线程,一个为垃圾收集线程;因为当使用Java
命令执行一个类的时候,都会启动一个JVM
,每个JVM
就是在操作系统中启动了一个进程!
2. 创建多线程
两种方式:
- 继承
Thread
类,java.lang.Thread
- 实现
Runnable
接口
方法一
构造方法:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行;Java
虚拟机调用此线程的run
方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
package a1;
public class CreateThread {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("多线程!");
}
}
方法二
传入一个 Runnable
实例:
package a1;
public class CreateThread {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动多线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("多线程!");
}
}
方法三
方法二的简写,Java8
后的新特性:
package a1;
public class CreateThread {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("多线程!");
});
t.start();
}
}
Thread 类与 Runnable 接口实现多线程的区别
如果一个类继承 Thread
,则不适合资源共享。但是如果实现了Runable
接口的话,则很容易的实现资源共享。
实现 Runnable
接口比继承 Thread
类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免
java
中的单继承的局限性。 - 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现
Runable
或Callable
类线程,不能直接放入继承Thread的类。
3. 线程状态
New
:新创建的线程,尚未执行Runnable
:运行中线程,正在执行run()
方法中逻辑Blocked
:运行中的线程,当前被阻塞而挂起Waiting
:运行中的线程,因为某些操作在等待Timed Waiting
:运行中的线程,sleep()
计时等待Terminated
:线程终止
线程一旦创建后就会在 Runnable、Blocked、Waiting、Time Waiting
四个状态间来回切换,直至终止 Terminated
。
线程终止的原因
- 线程运行正常完毕
- 因为异常导致程序中断
- 调用
Thread
实例的stop()
方法强行终止(不推荐)
线程等待
一个线程可以等待另一个线程直至其结束:
package a1;
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("多线程!");
});
t.start();
// 等待子线程结束后,才会继续往下执行
// t.join();
System.out.println("主线程运行结束!");
}
}
运行结果:
// 未调用 t.join()
主线程运行结束!
多线程!
// 调用 t.join()
多线程!
主线程运行结束!
注意:调用
t.join()
时需要捕获其异常InterruptedException
!
4. 线程中断
两种方式:
- 任务中一般有循环结构,通过标记来控制循环
- 若线程出于冻结状态,无法读取标记,调用
interrupt()
将线程从冻结状态强制恢复到运行状态,让线程具备CPU
的执行资格
4.1 循环标记
package a1;
public class TerminatedThread {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(2000);
t.running = false;
}
}
class MyThread extends Thread {
public volatile boolean running = true;
@Override
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n);
}
}
}
标志位 boolean running
是一个线程间共享的变量。线程间共享变量需要使用 volatile
关键字标记,确保每个线程都能读取到更新后的变量值。
变量的值存在主内存中,当线程访问变量时,会先获取一个副本保存到自己工作内存中;若线程修改了变量,虚拟机会将值回写到主内存中,但时间不确定,这样多线程共享变量时就有可能导致该变量值不是最新的。
volatile
关键字作用:
- 每次访问变量时,总是获取主内存的最新值
- 每次修改变量后,立刻回写到主内存
4.2 interrupt
interrupt
只是改变中断状态,不会中断一个正在运行的线程,需要自己去监视线程的状态并处理;另外线程中断后会抛出 interruptedException
的异常,需要捕获。该方式本质是给线程发出一个中断信号,受阻塞的线程检查到中断标识,就会退出阻塞状态。
- 如果线程被
Object.wait, Thread.join和Thread.sleep
三种方法之一阻塞,此时调用该线程的interrupt()
方法,那么该线程将抛出一个InterruptedException
中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态 - 如果线程没有被阻塞,这时调用
interrupt()
将不起作用,直到执行到wait(),sleep(),join()
时,才马上会抛出InterruptedException
package a1;
public class TerminatedThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThreadClass();
t.start();
Thread.sleep(20);
t.interrupt(); // 中断线程
t.join();
System.out.println("End!");
}
}
class MyThreadClass extends Thread {
@Override
public void run() {
int n = 0;
// 需要自己不断检查 isInterrupted() 是否为 true,调用 interrupt() 后,它会变为 false
while (!isInterrupted()) {
n++;
System.out.println(n);
}
}
}
实例二:
package a1;
public class TerminatedThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThreadClass();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断线程
t.join();
System.out.println("End!");
}
}
class MyThreadClass extends Thread {
@Override
public void run() {
Thread hello = new HelloThreadInterrupt();
hello.start();
try {
hello.join(); // 等待 hello 线程结束
} catch (InterruptedException e) {
System.out.println("中断 hello 线程:" + e);
}
hello.interrupt();
}
}
class HelloThreadInterrupt extends Thread {
@Override
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " Hello!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
main
线程通知 t
线程中断,此时 t
线程正等待 hello
线程,此方法会立即结束等待并抛出InterruptedException
异常,结束前通知 hello
线程中断。
5. 守护线程
守护线程类似于后台任务,所有线程结束、JVM
退出,进程结束不会管守护线程是否结束,适用于能够无线后台循环的线程,设置方式:
t.setDaemon(true);
t.start();
6. 线程同步
多线程编程最大的问题是保证共享变量的线程安全,不会出现共享变量污染,一般都会采用同步机制来解决。
首先来看一个非线程安全例子:
package a1;
public class ThreadSync {
public static void main(String[] args) throws InterruptedException {
Thread a = new Add();
Thread d = new Desc();
a.start();
d.start();
a.join();
d.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
class Add extends Thread {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
Counter.count += 1;
}
}
}
class Desc extends Thread {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
Counter.count -= 1;
}
}
}
两个类对 Counter.count
分别进行 1000 此加减,理论上来说结果应该为 0,但是实际每次运行结果都不一致,这就是非线程安全导致的。
通过加锁和解锁同步机制保证同一时间只能有一个线程操作共享变量可以避免此类问题发生,这种加锁和解锁之间的代码块称之为临界区(Critical Section
):
synchronized (同步锁) {
// 需要同步操作的代码
}
上述例子修改为线程安全:
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
class Add extends Thread {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
synchronized (Counter.lock) {
Counter.count += 1;
}
}
}
}
class Desc extends Thread {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
synchronized (Counter.lock) {
Counter.count -= 1;
}
}
}
}
注意:需要保证
synchronized
锁住的是同一实例对象,若是不同对象,并不能保证线程安全!同步机制会影响程序性能,但是在某些时候必须保证线程安全,比如:银行系统、票务系统等!