Java多线程必知概念

不管前台还是后台开发,总避免不了多线程的运用。是时候,好好梳理一下多线程的基本概念了。

线程的概念

进程一般指一个执行单元。

进程在PC和移动端通常指一个程序和一个应用。

在Android中,我们也可以通过在AndroidMenifest中指定四大组件的android:process属性,为应用开启多个进程。

线程是操作系统调度的最小单元,是进程的执行单元。

进程和线程的关系如下。

  1. 进程与线程是一对多的关系。一个进程可以拥有一个或多个线程,一个线程只有一个父进程。

  2. 线程之间共享父进程的共享资源,相互之间协同完成父进程所要完成的任务。

  3. 一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行。

线程的创建

继承Thread类创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {
// 新建一个类继承Thread类,并重写run()方法
@Override
public void run() {
System.out.println("run in my thread");
}

public static void main(String[] args) {
// 创建新建Thread类的实例
MyThread myThread = new MyThread();
// 调用实例的start()方法启动线程
myThread.start();
}
}

实际开发中,可以用匿名内部类快速开启一个线程。

1
2
3
4
5
new Thread(){
public void run(){
System.out.println("run in my thread");
}
}.start();

实现Runnable接口创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyRunnable implements Runnable {
// 新建一个类实现Runnable接口,并重写run()方法
@Override
public void run() {
System.out.println("run in my thread");
}

public static void main(String[] args) {
// 创建新建类的实例
MyRunnable myRunnable = new MyRunnable();
// 通过runnable实例,新建Thread类实例
Thread thread = new Thread(myRunnable);
// 调用新建Thread类实例的start()方法启动线程
thread.start();
}
}

同样,我们可以用匿名内部类快速开启一个线程。

1
2
3
4
5
new Thread(new Runnable() {
public void run(){
System.out.println("run in my thread");
}
}).start();

线程的生命周期

五大状态

当一个线程开启后,它会经过新建,就绪,运行,阻塞和死亡这五种状态。

  1. 新建状态:线程刚被创建出来,此时线程并无任何动态特征。

  2. 就绪状态:线程可以运行。

  3. 运行状态:线程正在运行。

  4. 阻塞状态:线程处被暂停。

  5. 死亡状态: 线程被停止。

线程优先级

设置和获取线程优先级的方法。

1
2
public final void setPriority(int newPriority)
public final int getPriority()

线程优先级的取值范围为1到10,也可以用静态变量设置。

1
2
3
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;

线程默认的优先级是父线程的优先级。Java主线程的默认优先级是NORM_PRIORITY = 5。

后台线程

设置后台线程以及判断线程是否为后台线程的方法。

1
2
public final void setDaemon(boolean on)
public final boolean isDaemon()

start() 和 run()

线程新建完之后,调用start()方法,使线程处于就绪状态。

当就绪状态的线程获得CPU资源后,会自动执行run()方法,使线程处于运行状态。

start

sleep()

线程在运行状态时,调用sleep()方法,使线程处于阻塞状态。

1
public static native void sleep(long millis) throws InterruptedException

这里Java中的native关键字,说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

接着我们在Activity中加入一个按钮,定义其点击方法sleep()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadActivity extends Activity {
public void sleep(View view) {
SleepThread sleepThread = new SleepThread();
sleepThread.start();
}

class SleepThread extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 10; i++) {
Log.d("Chen", "Thread Sleep...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

每隔一秒会输出”Thread Sleep…”,线程生命周期变化如下。

sleep

yield()

1
public static native void yield()

这个方法通常被翻译为线程让步。顾名思义,当一个线程正在运行的时候,做出了让步,回到就绪状态,重新回到与其他线程再重新分配CPU资源。

yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadActivity extends Activity {
public void yield(View view) {
YieldThread yieldThread1 = new YieldThread("线程A");
YieldThread yieldThread2 = new YieldThread("线程B");
yieldThread1.start();
yieldThread2.start();
}

class YieldThread extends Thread {
public YieldThread(String name){
super(name);
}
@Override
public void run() {
super.run();
for (int i = 0; i < 20; i++) {
Log.d("Chen", getName() + ": --> " + i);
if (i == 5) {
Thread.yield();
}
}
}
}
}

需要注意的是,这里的打印结果每次都不一定相同。因为线程调用过yield()后,线程调度器可能会启动其他线程,也可能继续启动该线程。

join()

1
2
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

这个方法需要有两个线程。比如在线程A执行体中,线程B调用join()方法。这时候线程A就会被阻塞,必须等线程B执行完或者join()的时间到了,线程A才会继续执行。

join

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 ThreadActivity extends Activity {
public void join(View view) {
new Thread("线程A") {
@Override
public void run() {
super.run();
try {
JoinThread joinThread = new JoinThread("线程B");
for (int i = 0; i < 10; i++) {
if (i == 4) {
joinThread.start();
joinThread.join();
}
Log.d("Chen", getName() + ": --> " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}

class JoinThread extends Thread {
public JoinThread(String name) {
super(name);
}

@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
Log.d("Chen", getName() + ": --> " + i);
}
}
}
}

运行结果如下。可以看出,如果没有设定join的时长,线程A会先让线程B一直执行完毕后,才会继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
03-31 06:29:45.976 999-1130/com.chen.study E/Chen: 线程A: --> 0
03-31 06:29:45.979 999-1130/com.chen.study E/Chen: 线程A: --> 1
03-31 06:29:45.979 999-1130/com.chen.study E/Chen: 线程A: --> 2
03-31 06:29:45.979 999-1130/com.chen.study E/Chen: 线程A: --> 3
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 0
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 1
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 2
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 3
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 4
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 5
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 6
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 7
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 8
03-31 06:29:45.985 999-1131/com.chen.study E/Chen: 线程B: --> 9
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 4
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 5
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 6
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 7
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 8
03-31 06:29:45.986 999-1130/com.chen.study E/Chen: 线程A: --> 9

多线程常见知识

了解完线程的基本概念,接下来我们需要知道多线程里常见的一些知识。

同步和异步

同步:调用者必须要等到调用的方法返回后才会继续后续的行为。
异步:调用者调用后,不需等调用方法返回就可以继续后续的行为。

通常单线程环境下都是同步行为,我们可以通过开启多条线程实现异步操作。

并发和并行

并发:多个任务交替运行
并行:多个任务同时运行

如果只有一个CPU,系统是不可能并行执行任务,只能并发。因为CPU每次只能执行一条指令。

原子性

原子性:指不可分割的操作。

1
2
int a = 1;
a++;

可以看出第一个语句是原子操作,而第二个语句包含读取、加、赋值三个操作,故不是原子操作。

Java内存模型中定义了八种原子操作。

操作 作用处 说明
lock(锁定) 主内存变量 把一个变量标识成一条线程独占的状态
unlock(解锁) 主内存变量 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取) 主内存变量 把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
load(载入) 工作内存变量 把 read 操作得到的变量放入到工作内存的变量副本中
use(使用) 工作内存变量 将工作内存中的一个变量的值传递给执行引擎
assign(赋值) 工作内存变量 将执行引擎接收到的值赋给工作内存的变量
store(存储) 工作内存变量 把工作内存中一个变量的值传给主内存中,以便给随后的 write 操作使用
write(写入) 主内存变量 把 store 操作从工作内存中得到的变量的值放入主内存变量中

在这里补充介绍一下内存模型。

Java内存模型

把一个变量从主内存中复制到工作内存中就需要执行read和load操作,将工作内存同步到主内存中就需要执行store和write操作。注意的是,java内存模型只是要求上述两个操作是顺序执行的,而并不是连续执行的。也就是说read和load之间、store和write之间可以插入其他指令。比如对主内存中的a,b进行访问可以出现这样的操作顺序:read a, read b, load b, load a。

可见性

可见性:指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

在开发过程中,一定要多关注可见性。如果一个线程在修改共享变量,而其他线程并不知道这个修改,就会出现一些意料之外的逻辑错误。

有序性

有序性: 指程序的运行顺序和编写代码的顺序一致。

编译器和处理器会对不具有数据依赖性的操作进行重排序,来提升运行速度。

而数据依赖性只会在单线程中出现。所以,仅观察单一线程,所有操作都是有序的,但多个线程之间的操作,很可能被重排序了。

这个特性也需要我们在处理多线程问题时多加关注。

线程安全

掌握上面的多线程知识,再来理解线程安全就非常容易了。这里我们用经典的多窗口案例来讲。

产生案例

假设一共有10张票,现在一共有两个窗口在卖。这里票就是共享变量,而窗口就是线程。我们可以很容易地写出下面的代码。

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
public class MainActivity extends AppCompatActivity {

private int ticketNum = 30;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void sell(View view) {
SellThread sellThread1 = new SellThread("窗口A");
SellThread sellThread2 = new SellThread("窗口B");
sellThread1.start();
sellThread2.start();
}

class SellThread extends Thread {
public SellThread(String name) {
super(name);
}

@Override
public void run() {
super.run();
while (true) {
if (ticketNum <= 0) {
break;
}
Log.d("Chen", getName() + " 卖出第 " + ticketNum + " 张票,剩余的票数:" + --ticketNum);
}
}
}
}

我们启动两个线程不停循环卖票,每次卖出一张,总票数就减少一张。如果总票数为0,就停止循环。运行结果如下(实际每次运行结果会不一样)。

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
03-31 09:51:42.669 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 30 张票,剩余的票数:29
03-31 09:51:42.669 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 29 张票,剩余的票数:28
03-31 09:51:42.669 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 28 张票,剩余的票数:27
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 27 张票,剩余的票数:26
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 26 张票,剩余的票数:25
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 25 张票,剩余的票数:24
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 24 张票,剩余的票数:23
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 24 张票,剩余的票数:22
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 22 张票,剩余的票数:21
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 21 张票,剩余的票数:20
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 20 张票,剩余的票数:19
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 20 张票,剩余的票数:18
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 18 张票,剩余的票数:17
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 17 张票,剩余的票数:16
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 16 张票,剩余的票数:15
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 15 张票,剩余的票数:14
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 14 张票,剩余的票数:13
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 13 张票,剩余的票数:12
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 12 张票,剩余的票数:11
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 11 张票,剩余的票数:10
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 10 张票,剩余的票数:9
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 9 张票,剩余的票数:8
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 8 张票,剩余的票数:7
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 7 张票,剩余的票数:6
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 6 张票,剩余的票数:5
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 5 张票,剩余的票数:4
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 4 张票,剩余的票数:3
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 3 张票,剩余的票数:2
03-31 09:51:42.670 19836-20275/com.chen.study D/Chen: 窗口B 卖出第 2 张票,剩余的票数:1
03-31 09:51:42.670 19836-20274/com.chen.study D/Chen: 窗口A 卖出第 1 张票,剩余的票数:0

可是我们看到,窗口A和窗口B同时都卖出了第24张和第20张票,这并不符合实际情况。而这个产生的原因就是线程不安全。

解决方案

我们可以用synchronized代码块来解决这个线程不安全的问题。

修改上面代码的run()方法,添加synchronized包裹代码块。

1
2
3
4
5
6
7
8
9
10
11
public void run() {
super.run();
while (true) {
synchronized (this) {
if (ticketNum <= 0) {
break;
}
Log.d("Chen", getName() + " 卖出第 " + ticketNum + " 张票,剩余的票数:" + --ticketNum);
}
}
}

运行结果如下,这次就跟我们预期一样了。

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
03-31 10:05:27.282 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 30 张票,剩余的票数:29
03-31 10:05:27.283 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 29 张票,剩余的票数:28
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 28 张票,剩余的票数:27
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 27 张票,剩余的票数:26
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 26 张票,剩余的票数:25
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 25 张票,剩余的票数:24
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 24 张票,剩余的票数:23
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 23 张票,剩余的票数:22
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 22 张票,剩余的票数:21
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 21 张票,剩余的票数:20
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 20 张票,剩余的票数:19
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 19 张票,剩余的票数:18
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 18 张票,剩余的票数:17
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 17 张票,剩余的票数:16
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 16 张票,剩余的票数:15
03-31 10:05:27.284 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 15 张票,剩余的票数:14
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 14 张票,剩余的票数:13
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 13 张票,剩余的票数:12
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 12 张票,剩余的票数:11
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 11 张票,剩余的票数:10
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 10 张票,剩余的票数:9
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 9 张票,剩余的票数:8
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 8 张票,剩余的票数:7
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 7 张票,剩余的票数:6
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 6 张票,剩余的票数:5
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 5 张票,剩余的票数:4
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 4 张票,剩余的票数:3
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 3 张票,剩余的票数:2
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 2 张票,剩余的票数:1
03-31 10:05:27.285 29787-32670/com.chen.study D/Chen: 窗口A 卖出第 1 张票,剩余的票数:0

总结

好了,关于多线程的一些相关概念知识就梳理到这了。相信会对我们在多线程编程时遇到的一些诡异问题的解决有所帮助。

Chen wechat
欢迎扫描二维码,订阅我的博客公众号MiracleChen