多线程的学习 1

多线程的基础

现代操作系统都可以执行多任务,即可以同时运行多个任务,比如说,当你打开 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) {
// TODO Auto-generated method stub
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) {
// TODO Auto-generated method stub
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) {
// TODO Auto-generated catch block
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线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} 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; // 标志位置为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 关键字确保每个线程都能读取到更新后的变量值。

实际上,该关键字的作用就是:

  1. 每次访问变量时,总是获取驻内存的最新值;
  2. 每次修改变量后,立刻写回内存(时效性)。

守护线程

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 的操作包括:

  1. 基本类型(long 和 double 除外)赋值,例如:int n = m;
  2. 引用类型赋值,例如:List list = anotherList。

关于 long 和 double,JVM 没有明确定义为原子操作,但是在 x64 平台的 JVM 是将其当成原子操作实现。