多线程基础
创建线程
在一个Java程序中创建线程有如下几种方式:
继承Thread类
1 | class MyThread extends Thread { |
继承Runnable类
1 | class MyRunnable implements Runnable { |
可以通过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 | class DaemonThread extends Thread { |
但是对于常规线程,一定要保证有一个出口,否则JVM进程无法正常结束,因此这类线程需要被特别指定为守护线程,当所有常规线程运行完毕时,JVM会无视守护线程而退出
1 | Thread t = new MyThread(); |
中断
通知线程中断有两种实现方式:interrupt方法和标志位变量
interrupt方法:
1 | public class Main { |
设置标志位:
1 | public class Main { |
不论是哪种方法,都要求线程循环检测才能实现
线程共享变量
虽然父线程对子线程变量的直接访问是可行的,但这并不代表父线程可以任意访问子线程的变量,对于供线程间通信的变量,需要加上volatile关键字(上面的标志位方法中已有用到)
这是因为当A线程修改了B线程的值时,JVM并不会将其立刻写回内存中,这会导致其他线程(包括B自身)访问同一变量出现不一致,而volatile关键字会通知虚拟机:每次修改变量立刻写回内存
线程同步
对于n=n+1
这行操作也分为了三步:取值、加法、存储,而当两个线程同时执行时可能会导致结果错误,如:
1 | ┌───────┐ ┌───────┐ |
这时我们需要对这样的一组操作声明为原子操作(类似SQL里的事务),在一组原子操作执行完毕后才会执行下一组原子操作,同一个原子操作不允许同时有两个线程执行
1 | synchronized(lock) { |
在通过synchronized关键字修饰后,执行顺序就变成了这样:
1 | ┌───────┐ ┌───────┐ |
对于锁对象,它可以是任意一个Object,我们只是需要获取一个抢占资源来实现线程的同步,在synchronized过程块中即便抛出异常也会在结束时正确释放锁
要注意的是,虽然锁对象可以是任意一个Object,但是对于需要同步的操作代码块,必须传入相同的资源对象,这样才能保证通信的正确性,而对于可以并发执行的操作代码块,则需要传入不同的对象来保证并发性
此外也有不需要同步的情况
- 单行语句的赋值操作
- 对于final对象的读写操作
synchronized方法
对于整个方法体均为synchronized的,我们可以将整段代码都用synchronized包含起来,并传入this对象
- 这样其他synchronized方法就无法并发操作当前对象实例的内容
1 | public void add(int n) { |
Java允许synchronized关键字修饰方法,这与上面的代码是等价的:
1 | public synchronized void add(int n) { |
特别地,当对static方法用synchronized修饰时,由于static方法是Class对象字段,因此锁定的也是当前类的Class对象
可重入锁
要注意的是,Java中的锁是可重入锁,即对于同一个线程,同一个锁可以被获取多次
对于都被synchronized修饰的两个方法A和B,你可以在A中再去嵌套调用B,这会导致锁计数器+2,并在每个方法退出时-1,在计数器为0时被释放
死锁和避免死锁
当线程A占用a锁,请求b锁,线程B占用b锁,请求a锁时,就陷入了一种死锁状态
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
1 | public void add(int m) { |
为了避免死锁,每个synchronized方法必须按照相同的顺序获取和释放锁:
1 | public void add(int m) { |
常用的多线程容器
- ConcurrentHashMap : 线程安全的 HashMap
- CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
- ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列
- BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道
- ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找