一、线程基础
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体
- 线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程
二、线程的基本操作
1. 新建线程(new)
- 匿名类
- 继承 Thread
- 实现 Runnable
1 | Thread t1 = new Thread() { |
- 底层代码:
Thread.java
1 | public class Thread implements Runnable { |
2. 终止线程(stop)
- 一般来说,线程执行完毕就会结束,无须手工关闭。但有些后台线程可能会常驻系统,用以提供某些服务
stop()
停止线程的缺点:已废弃,过于暴力,强行把执行到一半的线程终止,并立即释放这个线程持有的锁,可能引起数据不一致的问题stop()
案例:写线程写到一半时强行终止,锁被释放,另一个线程读到的数据就可能有问题
1 | public class StopThreadUnsafe { |
- 建议方法:自行决定线程何时退出
1 | static class WriteUserThread extends Thread { |
3. 线程中断(interrupt)
- 线程中断并不会使线程立即退出,而是给线程发送一个通知,至于目标线程接到通知后如何处理,则完全由目标线程自行决定
- 线程中断相关的方法:
1 | public void Thread.interrupt(); // 通知目标线程中断,并设置中断标志位 |
- 中断案例:
1 | public static void main(String[] args) throws InterruptedException { |
Thread.sleep()
会让当前线程休眠若干时间,它会抛出InterruptedException
中断异常,它不是运行时异常,因此程序必须捕获并处理- 中断 sleep 案例:
1 | public static void main(String[] args) throws InterruptedException { |
4. 等待和通知(wait/notity)
- 任何对象都可以使用这两个方法:
1 | public final void wait() throws InterruptedException {...} |
- 如果线程 A 调用了
obj.wait()
方法,线程 A 就会停止继续执行,转为等待状态。线程 A 会进入obj
对象的等待队列,等待队列中可能有多个线程 - 当调用了
obj.notify()
方法,就会从等待队列中随机选择一个线程,并将其唤醒 - 当调用了
obj.notifyAll()
方法,则会唤醒这个等待队列中的所有等待线程 obj
相当于多个线程之间的通信手段
obj.wait()
和obj.notify()
方法并不能随便调用,必须包含在对应的synchronized(obj)
语句中,即方法执行前都需要获得目标对象的锁
1 | public class WaitAndNotify { |
- wait 和 sleep 区别:
- wait 可被唤醒
- wait 可释放目标对象的锁
5. 挂起和继续执行(suspend/resume)
- 已废弃:
suspend()
方法在导致线程暂停的同时,并不会释放任何锁资源。直到对应的线程上执行了resume()
操作,被挂起的线程才能继续 - 案例:如果
resume()
方法意外的在suspend()
方法前执行了,被挂起的线程就很难有机会被继续执行,且其占用的锁不会被释放
1 | public class BadSuspend { |
- 使用
jstack
打印系统线程信息,可以看到线程 t2 是被挂起的,但他的线程状态却是RUNNABLE
1 | $ jstack -l 3552 |
- 使用
wait()
和notify()
方法实现suspend()
和resume()
方法
1 | public class GoodSuspend { |
6. 等待线程结束和谦让(join/yeild)
- 如果一个线程的输入依赖于另一个或多个线程的输出,该线程就需要等待依赖线程执行完毕后,才能继续执行
1 | // 无线等待,直到目标线程执行完毕 |
- join 案例:
1 | public class JoinThread { |
join()
方法的本质是让调用线程wait()
在其对象实例上。线程在退出前会调用notifyAll()
方法通知所有等待线程继续执行- 不要在 Thread 对象实例上使用
wait()
或notify()
方法,可能影响系统 API 正常工作
1 | while (isAlive()) { |
yield()
是一个静态方法,执行后会让当前线程让出 CPU。当前线程让出 CPU 后,仍会进行 CPU 资源的争夺
1 | public static native void yield(); |
三、volatile 与 Java 内存模型(JMM)
- 用 volatile 声明一个变量,如果变量被修改,则应用程序范围内的所有线程都能看到这个改动
- volatile 不能替代锁,也无法保证一些复合元素操作的原子性
1 | public class Visibility { |
- 上述代码在虚拟机的 Client 模式下(64 位没有该模式),由于 JIT 没有做优化,线程能够发现改动,并退出程序。但在 Server 模式下将无法退出
四、线程组
- 可以将相同功能的线程放在同一个线程组中
- 线程组也有
stop()
方法,它会停止组中的所有线程,但其和Thread.stop()
方法有相同的问题
1 | public class ThreadGroupTest implements Runnable { |
五、守护线程
- 守护线程会在后台默默完成一些系统性服务,比如垃圾回收线程、JIT 线程等。用户线程是系统的工作线程,它会完成程序应该完成的业务操作。如果用户线程结束,意味着程序执行完毕,守护线程也会随之结束
- 设置守护线程需要在线程
start()
之前设置,否则会抛出异常,但程序和线程依然能正常执行
1 | public class DaemonTest { |
六、线程优先级
- 优先级高的线程在竞争资源时更有优势。线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,无法精准控制,因此仍需在应用层解决线程调度问题
- Java 中使用 1-10 表示线程优先级,一般使用内置的三个静态变量表示,数字越大则优先级越高
1 | public final static int MIN_PRIORITY = 1; |
- 优先级测试案例:
1 | public class PriorityTest { |
七、线程安全与 synchronized
volatile
无法保证线程安全,它只能确保一个线程修改数据后,其他线程能够看到这个改动。如果两个线程同时修改一个数据,就可能导致数据错误
1 | public class IncreaseVol implements Runnable { |
synchronized
的作用是对同步的代码加锁,使得每次只有一个线程进入同步块,从而保证线程间的安全性- 使用方式:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
- 直接作用于实例方法:对当前实例加锁,进入同步代码前要获得当前实例的锁
- 直接作用于静态方法:对当前类加锁,进入同步代码前要获得当前类的锁
1 | public class IncreaseSync implements Runnable { |
八、隐蔽的错误
1. 无提示的错误案例
1 | int v1 = 1073741827; |
2. 并发下的 ArrayList
1 | public class ArrayListMultiThread { |
- 由于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问。有三种结果:
- 程序正常结束,list 大小为 20000
- 程序抛出 ArrayIndexOutOfBoundsException 异常
- 程序正常结束,list大小不为 20000
3. 并发下的 HashMap
1 | public class HashMapMultiThread { |
- (JDK 7)有三种结果:
- 程序正常结束,map 大小为 10000
- 程序正常结束,map 大小不为 10000
- 程序无法结束(由于多线程冲突,链表结构遭到破环,链表成环。卡在 put 方法,遍历内部数据时死循环)
4. 错误的加锁
1 | public class LockOnInteger implements Runnable { |
- Java 中的 Integer 属于不变对象,即对象一旦被创建,就不可能被修改。因此
i++
本质上时创建一个新的对象,并将其引用赋值给i