多线程的学习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是将其当成原子操作实现。