多线程的基础
现代操作系统都可以执行多任务,即可以同时运行多个任务,比如说,当你打开 QQ 的同时,你还可以播放音乐,cpu 执行代码虽然都是一条一条执行的,但是即便是单核 cpu,也可以同时运行多个任务,实际上只是 cpu 轮流执行多个任务而已。
进程与线程是存在区别的,一个进程可以包括多个线程或一个线程,但是至少一个。
java 语言内置多线程支持:一个 java 程序实际上是一个 JVM 进程,JVM 进程用一个主进程来执行 main () 方法,在 main () 方法内我们还可以执行多个线程。此外 JVM 还有负责垃圾回收的其他工作线程等。
一般情况下,多任务大多都是利用多线程实现的,多线程的难点在于:经常需要读写共享数据,还需要同步。
新线程的创建
要创建一个新线程很简单,只需要实例化一个 Thread 实例,然后调用它的 start () 方法:
1 2 3 4 5 6
| public class Main { public static void main(String[] args) { Thread t = new Thread(); t.start(); } }
|
但是上面创建的新线程不会做什么就结束了,如果想要让该线程做点什么,只需要覆写 run () 方法即可,或者直接利用 lambda 语法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); } }
class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } }
|
main () 方法执行和线程执行还是存在一定区别的,比如下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Main1 { public static void main(String[] args) { System.out.println("thread start....."); Thread t = new Thread() { public void run() { System.out.println("hello thread"); } }; t.start(); System.out.println("thread end....."); } }
|
运行结果如下:
1 2 3
| thread start..... thread end..... hello thread
|
从这里可以看出,当线程 t 启动后,它是和主进程同时执行,也就是说最后一个打印语句的执行时间点是不确定的,可能在 hello thread 前,可能在其后,如果想要确定下来,可以利用 Thread.sleep () 方法使线程暂停一段时间。
线程里还有一个方法叫 join (),表示等待该线程结束后,再往下继续执行自身线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Main1 { public static void main(String[] args) { System.out.println("thread start....."); Thread t = new Thread() { public void run() { System.out.println("hello thread"); } }; t.start(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("thread end....."); } }
|
这里就是指:当 t 线程结束后,才能执行其下面的 main 线程,所以打印结果一定是 thread end 在 hello thread 的后面。
java 线程对象的状态包括:New,Runnable,Blocked,Waiting,Timed Waiting 和 Terminated。
线程的中断
如果线程需要执行长时间任务,就有可能需要中断线程,中断线程其实就是其他线程给线程本身发个信号,线程本身收到信号后,结束执行 run () 方法,立刻结束运行,举个例子:如果需要下载一个 100g 的文件,如果下的太慢了,不想下了,就需要取消下载,这就是给线程一个信号,停止下载这个方法的执行。
中断线程很简单,直接在其他线程中对目标线程调用 interrupt () 方法,目标线程反复检测自身状态是否是 interrupted 状态,如果是,立刻停止执行。
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
| public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); t.join(); System.out.println("end"); } }
class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); try { hello.join(); } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); } }
class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } }
|
main 线程通过调用 t.interrupt () 通知 t 线程中断,但是 t 线程处于等待 hello 线程的结束中,该方法会立刻结束并抛出 Interruptedexception,由于 t 线程中存在捕获异常,所以可以直接结束,t 结束前,对 hello 线程也执行了 interrupt (),所以 hello 线程也会结束。
还有一种中断方法,就是设置标志位 running,将其设置为 false 即可中断线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; } }
class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); } }
|
running 是线程间共享的变量,因此需要使用 volatile 关键字确保每个线程都能读取到更新后的变量值。
实际上,该关键字的作用就是:
- 每次访问变量时,总是获取驻内存的最新值;
- 每次修改变量后,立刻写回内存(时效性)。
守护线程
java 程序中有时候需要根据需求设置一些无限循环的线程,比如定时触发的线程:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class TimerThread extends Thread { @Override public void run() { while (true) { System.out.println(LocalTime.now()); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } } }
|
如果这个线程不结束,JVM 进程就无法结束,问题是,谁来负责结束这个线程?
但是这类线程一般没有负责人来结束它,那就只能使用守护线程,守护线程是指为其他线程服务的线程,在 JVM 里,所有非守护线程执行完毕后,无论有没有守护线程,JVM 都会退出。
如何将线程标记为守护线程?在调用 start () 方法前,调用 setDaemon (true) 将该线程标记为守护线程。
注意:守护线程没办法持有任何需要关闭的资源,如打开文件。
线程的同步
举例说明:
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
| public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } }
class Counter { public static int count = 0; }
class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count += 1; } } }
class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count -= 1; } } }
|
按理说,对 count 加 10000,减 10000,最后结果应该为 0,但是实际上结果不为 0,这是因为操作顺序的原因。
保证一段代码的原子性,可以利用 synchronized:
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
| public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); 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 count = 0; }
class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count += 1; } } } }
class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } } }
|
这样最后的结果就是 0,相当于每一次只能有一个线程对共享变量 count 进行操作。
在使用锁的时候,需要弄清楚到底谁与谁之间是不能同步执行的,不然可能会造成执行效率的下降。
不需要 synchronized 的操作包括:
- 基本类型(long 和 double 除外)赋值,例如:int n = m;
- 引用类型赋值,例如:List list = anotherList。
关于 long 和 double,JVM 没有明确定义为原子操作,但是在 x64 平台的 JVM 是将其当成原子操作实现。
预览: