Java多线程
1、进程与线程
程序和进程:
程序是一组有序指令周期,是静态概念;进程是程序在数据集合上的一次执行过程,是动态概念。
同一程序同时运行于不同数据集合上时,就构成了不同的进程,进程和程序不是一一对应的。
线程的行为很像进程,是比进程更小的执行单位,一个进程可以产生多个线程,每个线程都有自己的生命周期。和进程共享操作系统的资源类似,线程间也可以共享某些内存单元;与进程不同的是,线程的中断和恢复更加节省系统的开销。
2、Java中的线程
Java的多线程机制
Java语言内置对多线程的支持,多线程指一个应用中同时存在多个执行体,按不同方法共同工作的情况。
计算机在给定的时刻只能执行一个线程,JVM(Java Virtual Machine)会快速地从一个线程切换到另一个线程,每个线程轮流执行,给人一种几个事件同时发生的感觉。
主线程(main线程)
每个Java程序都有一个缺省的(默认的)主线程。Java程序总是从main方法开始启动一个线程,这个线程就被称为“主线程”;而在main方法执行过程中创建的线程就称为其他线程。
如果main方法结束时没有其他线程在执行,JVM就会结束Java程序。同理,如果有其他线程在执行,JVM就会等到所有的线程都结束之后再结束Java程序。
线程的状态和生命周期
Java中使用Thread类或它的子类表示线程,新建的线程在它的一个完整生命周期中通常会经历以下四种状态。
- 新建,当一个Thread类或它的子类对象被声明并创建时,新生的线程对象处于新建状态,此时它已经有了相应的内存空间和其他资源。
- 运行,线程创建后必须通过调用
start()
方法通知JVM,JVM给这个新创建的线程分配CPU资源后才算真正成为一个运行的线程。 - 中断,有四种原因可能导致线程中断:
- JVM将CPU资源切换给其它线程,使本线程失去CPU资源并处于中断状态。
- 线程执行了
sleep(long millis)
方法,使当前线程进入休眠状态。当经过参数指定的毫秒数后,该线程就重新进到线程队列中排队等待CPU资源,继续运行。 - 线程执行了
wait()
方法,使得线程进入等待状态。等待状态的线程必须等待其他线程调用notify()
方法通知才能进入排队队列,继续运行。 - 线程执行某个操作进入阻塞状态(如文件读取),进入阻塞状态的线程不能进入排队队列,只有等待阻塞原因消除后才重新进入线程队列中等待CPU资源,继续运行。
- 死亡,处于死亡状态的线程不具有继续运行的能力,线程有两种死亡原因:
- 线程正常完成了所有工作,自动释放了内存。
- 线程被提前强制终止,提前释放了内存。
线程调度与优先级
由于同时处于排队状态的线程可以有多个,Java调度器把线程的优先级分为10个级别(1-10),如果没有明确的设置优先级,每个线程的优先级都为常数5。可以通过setPriority(int newPriority)
方法调整,高优先级的线程能优先运行。
3、Thread类与线程的创建
Java中使用Thread类或其子类创建线程对象,在编写Thread类的子类时,要重写 run()
方法规定线程的操作,否则线程什么都不会执行。
/* 子类 */
class A extends Thread {
@Override
public void run() {
System.out.println("Thread1");
}
}
public static void main(String[] args) {
A a = new A(); // 利用子类创建线程
}
使用Thread类的子类创建线程的好处是可以在子类中添加新的变量和方法,使线程具有某种功能。
使用Thread类
也可以直接使用Thread类创建线程,其常用的构造方法为public Thread(Runnable target)
,这个方法会创建一个新的Thread对象。其中参数是一个Runnable类型的接口。因此,在创建线程时必须向构造方法的参数传递一个实现了该接口类的实例。当线程使用CPU时,就会调用接口中的run()方法(接口回调)。使用Runnable接口比使用Thread类的子类更具有灵活性。
/* 匿名内部类实现Runnable接口 */
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread");
}
};
Thread thread = new Thread(runnable);
也可以使用
public Thread(Runnable target, String name);
构造方法设置线程名
4、线程的常用基本方法
1. start()
线程调用此方法将启动线程,使之从新建状态进入就绪队列排队,一旦它获取到CPU资源,就会脱离创建它的线程独立开始自己的生命周期。
需要注意,如果一个线程调用了start()方法后且线程还未结束,再调用start()方法就会导致IllegalThreadState-Exception异常。
2. run()
run()方法用于定义线程对象被调度后执行的操作,Thread类中run()方法没有具体的内容,所以用户程序需要重写run()方法来覆盖原有的方法。
当run()方法执行完毕,线程就会进入死亡状态,即释放了分配给线程的内存。
3. sleep()
sleep()方法可以使线程进入休眠状态,让出CPU资源。如果线程在休眠时被打断,JVM就会抛出InterruptedException异常。因此,必须在try-catch语句块中调用sleep方法。
4. isAlive()
这个方法可以判断线程是否处于存活状态,当一个线程已经start()并且还没有死亡就认为是存活。
需要注意,如果一个线程处于存活状态,再给该线程的对象分配新的实体,就会导致原本正在运行的实体成为“垃圾”,但不会被垃圾收集器回收。
5. currentThread()
类方法,获得当前正在使用CPU资源的线程。
6. interrupt()
用于强制唤醒正在休眠的线程,这样会导致休眠线程发生Interrupted异常,从而结束休眠,重新排队等待CPU资源。
5、线程同步
在处理多线程问题时,必须考虑一个问题:当多个线程同时访问同一个变量,并且对该变量进行修改,就可能导致每个线程获得的结果不一致。因此,当某个线程对变量进行修改时,必须保证其它线程无法修改该变量。
Java中给出了一个synchronized
(同步)修饰符用于修饰方法,当一个线程调用该方法时,其他线程需要调用该方法就必须等待。
public class Test {
public static void main(String[] args) {
A a = new A();
Thread increase = new Thread(a, "increase");
Thread decrease = new Thread(a, "decrease");
increase.start();
decrease.start();
while (increase.isAlive() || decrease.isAlive()) {} // 等待线程执行完毕
System.out.println(A.getNum());
}
}
/* A类实现了Runnable接口 */
class A implements Runnable {
private static int num = 0;
final static int INCREASE = 0;
final static int DECREASE = 1;
@Override
public void run() {
Thread thread = Thread.currentThread();
if (thread.getName().equals("increase")) {
for (int i = 0; i < 10000; i++) {
change(INCREASE, i);
}
} else if(thread.getName().equals("decrease")) {
for (int i = 0; i < 10000; i++) {
change(DECREASE, i);
}
}
}
/* 对修改变量且可能被多个线程调用的方法使用 synchronized 修饰 */
private synchronized void change(int sign, int value) {
if (sign == INCREASE) {
num += value;
} else if(sign == DECREASE) {
num -= value;
}
}
public static int getNum() {
return num;
}
}
上面这个程序的输出结果为0,但如果去掉change()方法中的synchronized修饰符,就会导致最终的输出结果无法确定。
6、协调同步的线程
当一个线程使用的同步方法中用到某个变量,但此变量又需要其他线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()
方法,wait方法可以中断线程的执行,使线程暂时让出CPU。若希望中断的线程重新进入队列,就需要其它线程在该同步方法中通过notify()方法通知使用这个同步方法的某一个中断线程或通过notifyAll()方法通知所有使用这个同步方法的中断线程。
比如在处理货币时,我们不希望货币的数量为负数:
public class Test {
public static void main(String[] args) {
A a = new A();
Thread consumption = new Thread(a, "consumption");
Thread revenue = new Thread(a, "revenue");
consumption.start();
revenue.start();
while (consumption.isAlive() || revenue.isAlive()) {} // 等待线程执行完毕
System.out.printf("最终剩余%d摩拉", A.getMora());
}
}
/* A类实现了Runnable接口 */
class A implements Runnable {
private static int mora = 500; // 设置初始有500摩拉
final static int CONSUMPTION = 0;
final static int REVENUE = 1;
@Override
public void run() {
Thread thread = Thread.currentThread();
if (thread.getName().equals("consumption")) {
for(int i = 500; i >= 100; i -= 100) {
change(CONSUMPTION, i);
}
} else if(thread.getName().equals("revenue")) {
for(int i = 100; i < 500; i += 100) {
change(REVENUE, i);
}
}
}
private synchronized void change(int sign, int value) {
if (sign == CONSUMPTION) {
try {
while (mora < value) { // 发现剩余货币数量不足以支付消费,等待
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
mora -= value;
System.out.printf("支出%d摩拉,剩余%d摩拉\n", value, mora);
} else if(sign == REVENUE) {
mora += value;
System.out.printf("收入%d摩拉,剩余%d摩拉\n", value, mora);
notify(); // 通知在该同步方法中等待的线程
}
}
public static int getMora() {
return mora;
}
}
不可以在非同步方法中使用wait()、notify()和notifyAll()。
7、线程联合
一个线程A在占有CPU期间,可以让其它线程调用join()和本线程联合。如B.join()
,这时线程A会立刻中断执行,直到线程B执行完毕,A线程才会重新进入排队等待CPU资源。如果B已经执行完毕,那么B.join()
不会有任何效果。
public static void main(String[] args) {
Thread makeFood = new Thread(new Runnable() { // 通过匿名内部类实现Runnable接口
@Override
public void run() {
try {
Thread.sleep(1000); // 休眠一秒钟,保证该方法执行更慢
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("做饭");
}
});
Thread takeFood = new Thread(() -> { // Runnable接口中仅一个抽象方法,可以使用Lambda表达式实现
try {
makeFood.join(); // 联合做饭线程(等待做饭线程执行完毕该线程才会继续执行)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("吃饭");
});
takeFood.start();
makeFood.start();
}
8、守护线程
线程默认为非守护线程,也可以称为用户(user)线程,一个线程调用void setDaemon(boolean on)
方法可以将自己设置为一个守护(Daemon)线程:
thread.setDaemon(true);
当程序中的所有用户线程结束,即使守护线程中还有语句未执行,程序也会立刻结束运行。守护线程可以做一些不太严格的工作,需要在运行前就进行设置。