多线程基础

Smile_slime_47

创建线程


在一个Java程序中创建线程有如下几种方式:

继承Thread类

1
2
3
4
5
6
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}

继承Runnable类

1
2
3
4
5
6
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}

可以通过Thread.setPriority(int n) // 1~10, 默认值5设置线程优先级,但不能保证优先级高的线程一定会先被执行

线程状态


在JVM中一个线程有如上六种状态:

  • NEW:线程被创建但未执行start方法
  • RUNNABLE:线程被执行了start方法并开始运行
    • JVM中不区分RUNNING和READY状态,因为线程切换速度迅速,READY占用时间极小
  • BLOCKED:阻塞,等待资源锁被释放
  • WAITING:等待其他线程动作,如通知和中断
  • TIME_WAITING:超时等待
  • TERMINATED:终止
    • 正常终止:线程执行到return语句
    • 意外终止:中途抛出异常
    • 外界进程对Thread实例调用的stop()方法

守护线程


守护线程是一类专门为其他线程服务的线程,如GC等,这类线程往往有着无限循环检测的特点,以便在需要的时候随时调用:

1
2
3
4
5
6
7
8
class DaemonThread extends Thread {
@Override
public void run() {
while (true) {
...
}
}
}

但是对于常规线程,一定要保证有一个出口,否则JVM进程无法正常结束,因此这类线程需要被特别指定为守护线程,当所有常规线程运行完毕时,JVM会无视守护线程而退出

1
2
3
Thread t = new MyThread();
t.setDaemon(true);
t.start();

中断


通知线程中断有两种实现方式:interrupt方法标志位变量

interrupt方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
}
}

class MyThread extends Thread {
public void run() {
while (! isInterrupted()) {
...
}
}
}

设置标志位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() {
while (running) {
...
}
}
}

不论是哪种方法,都要求线程循环检测才能实现

线程共享变量

虽然父线程对子线程变量的直接访问是可行的,但这并不代表父线程可以任意访问子线程的变量,对于供线程间通信的变量,需要加上volatile关键字(上面的标志位方法中已有用到)

这是因为当A线程修改了B线程的值时,JVM并不会将其立刻写回内存中,这会导致其他线程(包括B自身)访问同一变量出现不一致,而volatile关键字会通知虚拟机:每次修改变量立刻写回内存

线程同步


对于n=n+1这行操作也分为了三步:取值、加法、存储,而当两个线程同时执行时可能会导致结果错误,如:

1
2
3
4
5
6
7
8
9
10
11
┌───────┐    ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼

这时我们需要对这样的一组操作声明为原子操作(类似SQL里的事务),在一组原子操作执行完毕后才会执行下一组原子操作,同一个原子操作不允许同时有两个线程执行

1
2
3
synchronized(lock) {
n = n + 1;
}

在通过synchronized关键字修饰后,执行顺序就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌───────┐     ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│-- lock -- │
│ILOAD (100) │
│IADD │
│ISTORE (101) │
│-- unlock -- │
│ │-- lock --
│ │ILOAD (101)
│ │IADD
│ │ISTORE (102)
│ │-- unlock --
▼ ▼

对于锁对象,它可以是任意一个Object,我们只是需要获取一个抢占资源来实现线程的同步,在synchronized过程块中即便抛出异常也会在结束时正确释放锁

要注意的是,虽然锁对象可以是任意一个Object,但是对于需要同步的操作代码块,必须传入相同的资源对象,这样才能保证通信的正确性,而对于可以并发执行的操作代码块,则需要传入不同的对象来保证并发性

此外也有不需要同步的情况

  • 单行语句的赋值操作
  • 对于final对象的读写操作

synchronized方法

对于整个方法体均为synchronized的,我们可以将整段代码都用synchronized包含起来,并传入this对象

  • 这样其他synchronized方法就无法并发操作当前对象实例的内容
1
2
3
4
5
public void add(int n) {
synchronized(this) {
count += n;
}
}

Java允许synchronized关键字修饰方法,这与上面的代码是等价的:

1
2
3
public synchronized void add(int n) {
count += n;
}

特别地,当对static方法用synchronized修饰时,由于static方法是Class对象字段,因此锁定的也是当前类的Class对象

可重入锁

要注意的是,Java中的锁是可重入锁,即对于同一个线程同一个锁可以被获取多次

对于都被synchronized修饰的两个方法A和B,你可以在A中再去嵌套调用B,这会导致锁计数器+2,并在每个方法退出时-1,在计数器为0时被释放

死锁和避免死锁

当线程A占用a锁,请求b锁,线程B占用b锁,请求a锁时,就陷入了一种死锁状态

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}

public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}

为了避免死锁,每个synchronized方法必须按照相同的顺序获取和释放锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}

public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}

常用的多线程容器

  • ConcurrentHashMap : 线程安全的 HashMap
  • CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
  • ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列
  • BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找
Comments