多线程
强烈推荐廖雪峰老师的网站
线程与进程的区别
在计算机中,我们把一个任务称为一个进程。在进程内部可以同时执行多个子任务,我们把子任务称为线程。
即:一个进程至少包含一个线程。
Java多线程
一个Java程序就是一个JVM进程,该进程用一个主线程来执行main()
方法,在main()
方法内部又可以存在多个线程。
创建线程
线程的创建
public class Main{
public static void main(String[] args) {
Thread thread = new Thread();
t.start(); //启动线程t
}
}
使用线程执行任务
方法一
做一个Thread
类的子类,覆盖run()
方法。
public class Main{
public static void main(String[] args) {
Thread thread = new ThreadTest();
t.start();
}
}
class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("Start!");
}
}
执行start方法后会自动调用run方法输出Start
。
方法二
通过Runnable
接口实现。
public class Main{
public static void main(String[] args) {
Thread thread = new ThreadTest(new Runnable());
t.start();
}
}
class ThreadTest implements Runnable{
@Override
public void run() {
System.out.println("Start!");
}
}
性质
-
可以看到,调用
run()
方法时,并没有使用t.run()
的方式。如果使用了这种方式,实质上只是调用了一个普通的Java方法打印语句完成任务,并没有新建一个线程,所有的操作都是在
main
线程中执行的。必须调用
Thread
的start()
方法才能启动新线程。其内部调用了private native void start0()
方法。native
修饰符表示这个方法由JVM虚拟机内部的C语言代码实现。 -
线程之间是并发执行的。即线程之间的调度顺序不能由程序本身确定。
比如:
public class Main { public static void main(String[] args) { System.out.println("main run"); Thread t = new Thread() { public void run() { System.out.println("thread run"); System.out.println("thread end"); } }; t.start(); System.out.println("main end"); } }
除了可以确定首先输出
main run
外,不能确定语句的输出顺序。模拟并发执行的效果,可以调用Thread.sleep()
方法,强迫当前线程暂定一段时间。
线程的优先级
Thread.setPriority(int n); // 级别1-10, 默认5
注意,优先级高的线程只是会被操作系统调度更频繁,并不能通过设置优先级来保证 优先级高的线程一定会先执行。
如果想要使得某个线程在另一个线程结束后运行,可以使用join()
方法。
class Main {
public static void main(String[] args) {
System.out.println("Main Run");
Thread t = new Thread() {
public void run () {
System.out.println("Thread Run");
System.out.println("Thread End");
}
};
try {
t.join();
// 可以指定时间,当指定时间后线程未结束则不再等待。
// 如果在指定时间内完成,线程仍会继续等待,指定到达指定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main End");
}
}
线程的状态
Java线程有以下状态:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,随时被CPU调度;
- Blocked:运行中的线程,因某些操作被阻塞而挂起;
- Waiting:运行中的线程,因某些操作而在等待中;
- Timed Waiting:运行中的线程,因执行
sleep()
方法而计时等待; - Terminated:线程已终止,因
run()
方法执行完毕。
在Java程序中,一个线程对象只能调用一次start()
方法。
启动后,线程在Runnable
, Blocked
, Waiting
, Timed Waiting
状态间切换,直到变成Terminated
状态。
线程终止的原因
- 正常终止:
run()
方法执行完毕; - 意外终止:
run()
方法碰到未捕获的异常; - 强制终止:对线程实例调用
stop()
方法(不推荐)。
线程中断
方法一
在一个线程执行一个任务的过程中,我们可能需要打断这个线程并执行其它线程,这时我们就需要使用interrupt()
方法。
class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new ThreadTest();
// t线程启动
t.start();
// 当前线程暂停, 执行t线程
Thread.sleep(1000);
// 暂停结束,二者并行, 打断t线程
t.interrupt();
// 等待t线程结束
t.join();
System.out.println("Main End");
}
}
class ThreadTest extends Thread {
@Override
public void run() {
// 新建p线程
Thread p = new ThreadTest2();
// p线程启动
p.start();
try {
// 等待p线程结束
p.join();
} catch (InterruptedException e) {
System.out.println("T was Interrupted!");
}
p.interrupt();
}
}
class ThreadTest2 extends Thread {
@Override
public void run() {
int n = 0;
// 在未被中止前持续循环
while (!isInterrupted()) {
System.out.println(n++ + "asdf");
try {
// 当前线程暂停
Thread.sleep(100);
} catch (InterruptedException e) {
// System.out.println("P was Interrupted!");
// 被中止后break跳出循环
break;
}
}
}
}
main
线程调用t.interrupt()
方法中断t
线程,此时t
线程中正在等待p
线程结束,t.interrupt()
方法会立刻结束t
线程并抛出InterruptedException
异常,在t
线程结束前,对p
线程进行了p.interrupt()
方法调用使p
线程中断。
如果没有进行该操作,p
线程会持续运行,JVM也不会退出。有些时候,我们不得不使得一些线程无限循环,那么当我们要退出时,谁来关闭这些线程?
因此有了 守护线程
方法二
我们可以通过设置标志位来判断线程是否继续运行。
class Main {
public static void main(String[] args) throws InterruptedException {
// 注意这里是子类的引用
ThreadTest t = new ThreadTest();
// 启动线程t
t.start();
// 当前线程暂停
Thread.sleep(1000);
// 改变标志位的值
t.running = false;
}
}
class ThreadTest extends Thread {
// 标志位
public volatile boolean running = true;
@Override
public void run() {
int n = 0;
while (running) {
System.out.println(n++ + "asdf");
}
System.out.println("T End");
}
}
注意到这里使用了volatile
关键字对running
进行标记,该关键字用于标记线程间的共享变量,确保每个线程都能读取到更新后的变量值。
线程间共享变量
为什么要用该关键字呢?
这涉及到Java的内存模型。在JVM中,变量的值保存在主内存中。当线程访问变量时,会先获取一个副本,并保存在该线程的工作内存中。
如果线程修改了变量,JVM会在某个时刻把修改后的值写到主内存中,这个时刻是不确定的!因此,会出现变量更改后,某个线程访问到的变量依然是更改前的变量值的情况。
所以我们需要使用关键字volatile
告诉JVM:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻写到主内存中。
守护线程
当某个用户线程无限循环时,我们不能正常结束JVM, 但是我们很需要这个无限循环的线程,比如Java的垃圾回收线程,怎么做?守护线程。
守护线程用来服务于用户线程。在JVM中,所有用户线程结束后,无论是否有守护线程运行,虚拟机都会退出。
守护线程的创建
Thread t = new ThreadTest();
t.setDaemon(true);
t.start();
性质
-
不能将已经启动的线程设置为守护线程,即
t.start(); t.setDaemon(true); // 将会抛出异常
是错误的,会抛出
IllegalThreadStateException
异常。 -
在守护线程中产生的新线程也是
Daemon
的, 即也是守护线程; -
守护线程不能访问文件等固有资源。因为它可能会在任何时候中断;
-
Java自带的多线程框架,比如
ExecutorService
,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用Java的线程池。
线程同步
多个线程同时运行时,线程的调度有操作系统决定,因此任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。那么,如果多个线程同时读写共享变量,就会出现数据不一致的问题。
如:
class Main {
public static void main(String[] args) throws Exception {
ThreadAdd add = new ThreadAdd();
ThreadDec dec = new ThreadDec();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static int cnt = 0;
}
class ThreadAdd extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
Counter.cnt++;
}
}
}
class ThreadDec extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
Counter.cnt--;
}
}
}
这个程序的结果是多少?0?
实际上每次运行的结果都是不一样的。
原子操作
对变量进行读取和写入时,必须保证操作是原子操作,即不能被中断的操作。
JVM规定以下操作为原子操作:
- 基本类型赋值;
- 引用类型赋值。
加锁解锁
如果操作是非原子操作,那么在若干个线程同时执行的过程中,该操作可能会中断导致得到错误的结果。
为了避免这种情况的发生,我们需要使用加锁和解锁的操作。
加锁解锁操作可以保证若干条指令总是在一个线程执行,不会有其它线程进入此指令区间。即使在执行中,线程被中断,其它线程也会因为无法获得锁, 导致无法进入此指令区间。只有执行线程将锁释放,即解锁后,其它线程才有机会获得锁并执行。
这种加锁和解锁之间的代码块我们称之为临界区
,任何时候临界区中最多只有一个线程执行。
加锁和解锁可以看作将非原子操作实现成原子操作。易得,原子操作是不需要加锁的。
加锁解锁的实现
我们通过synchronized
关键字对一个对象进行加锁。
即:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) {...}
.
举个例子就是这样的:
synchronized(lock) {
//加锁
n += 1;
} //自动解锁
上面的代码经过加锁操作后,改写为:
class Main {
public static void main(String[] args) throws Exception {
ThreadAdd add = new ThreadAdd();
ThreadDec dec = new ThreadDec();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object();
public static int cnt = 0;
}
class ThreadAdd extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
synchronized(Counter.lock) {
Counter.cnt++;
}
}
}
}
class ThreadDec extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
synchronized(Counter.lock) {
Counter.cnt--;
}
}
}
}
其中的代码:
synchronized(Counter.lock) {
//....
}
其中用Counter.lock
实例作为锁,两个线程在临界区先获得锁,再执行操作。
执行结束且语句块结束后,会自动释放锁。
参考资料
- https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
- https://zhuanlan.zhihu.com/p/28049750