多线程的基础
现代操作系统都可以执行多任务,即可以同时运行多个任务,比如说,当你打开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是将其当成原子操作实现。