1 并发与并行
并发是指在某个时间段内,多任务交替处理的能力。并行是指同一时刻处理多任务的能力。在并发环境下,由于程序的封闭性被打破,出现了以下特点:
1)并发程序之间有相互制约的关系。 直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争共享资源,如处理器、缓冲区等。
2)并发程序的执行过程是断断续续的。 程序需要记忆现场指令及执行点。
3)当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率。
2 线程的五种状态
为方便理解,可以认为线程在其生命周期中有五种状态:新建状态(New),就绪状态(Runnable),运行状态(Running),阻塞状态(Blocked),终止状态(Terminated)。他们之间的关系可以表示为:
3 创建线程的三种方式
1)继承Thread类;2)实现Runnable接口;3)实现Callable接口
4 线程的内存分配
每个线程拥有自己的虚拟机栈(Java Stacks)、本地方法栈(Native Method Stacks)和程序计数器(Program Counter Register)。虚拟机栈包括局部变量表、操作栈、动态链接和方法返回地址等。
5 线程安全
在一个多线程环境的程序中,当程序结束时能够得到预期的结果,那么称这种状态为线程安全。单线程中,永远是线程安全的。
6 线程交互的可见性
6.1 指令重排(Instruction Reordering)
编译器在编译代码时,为了提升CPU的工作效率,会对指令进行重新排序。在并发执行的情况下,重排序可能导致意想不到的运行结果。
6.2 原子性
一个不能被分割的操作叫做原子操作。非原子操作在并发条件下会得到意想不到的结果,如下例:
public class Increment { private int value; private static final int TIMES = 10000; private static final int THREAD_NUMBERS = 20; public void increase() { value++; } public int getValue() { return value; } public static void test() { Increment increment = new Increment(); Thread[] threads = new Thread[THREAD_NUMBERS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < TIMES; i++) { increment.increase(); } } }); threads[i].start(); } for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { } } System.out.println("result:" + increment.getValue()); } public static void main(String[] args) { test(); /* 输出(一个小于200,000的值): result:71641 */ } }6.3 可见性
可见性是指,当一个线程修改了共享变量的值,其他线程也能够感知这个修改。因为每个线程拥有自己的工作内存,所以一个线程的操作不是对其他线程都可见的(每个线程在运行时会有自己的工作内存,工作内存的值会不停写回到主内存)。
6.4 volatile关键字与Happends-before原则
volatile关键字的作用有两点:1)保证volatile修饰的变量的可见性,即每次有线程更新共享变量时,其他线程也可以感知到此更新操作;2)禁止指令重排序优化。由此可见,volatile只有在多线程程序中,才有用处。
Happends-before原则是一种偏序关系:有两个操作A和B,如果A操作happends-before操作B,那么A操作就会在B操作之前发生,且A的所有改动对B都是可见的。记做hb(A, B)。
Happends-before原则包含以下几条:
1)程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作happends-before于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
2)管程锁定规则(Monitor Lock Rule):一个unlock操作happends-before于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
3)volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作happends-before于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
4)线程启动规则(Thread Start Rule):Thread对象的start()方法happends-before于此线程的每一个动作。
5)线程终止规则(Thread Termination Rule):线程中的所有操作都happends-before于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
6)线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用happends-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
7)对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)happends-before于它的finalize()方法的开始。
8)传递性(Transitivity):对于操作A,B和C,如果有hb(A, B)且hb(B, C)那就可以得出hb(A, C)的结论。理解Happends-before原则有什么用处呢?先来看下面的例子:
T1 x = 5; // 语句1 y = 6; // 语句2 T2 if (y == 6) System.out.println(x); // 语句3x, y初始值都为0,那么T2的输出可能是5,也可能是0。因为在T1线程中可能会对两条语句进行重排。从上面的happends-before原则1)可能会推测,x = 5这条指令一定会在y = 6之前运行,实际上是错误的。对于T1来说,语句1和语句2谁先执行谁后执行不影响最终的一致性,所以可能因为指令重排导致先执行语句2,在执行语句1,那么语句3的输出就是0了。
参考:
- 《码出高效——Java开发手册》
- 《深入理解Java虚拟机——JVM高级特性与最佳实践》
- 17.4.5. Happens-before Order