Java并行程序基础
基本概念
Linux Torvalds认为,并行计算只能在图像处理和服务端编程两个领域使用。
并行计算的相关概念
同步(Synchronous)和异步(Asynchronous)
同步和异步通常用来形容一次方法的调用,同步方法一旦调用,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
并发(Concurrent)和并行(Parallelism)
并发和并行都可以表示多个任务一起执行,但侧重点不同。并发侧重于多个任务交替执行,而多个任务之间有可能还是串行的。并行是真正意义上的“同时进行”,出现在拥有多个CPU的系统(例如多核CPU)。
临界区
用来表示一种公共资源或者说共享数据,可以被多个线程使用。但是每一次只能有一个线程使用他,一旦临界资源被占用。其他线程想使用这个资源就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞通常用来形容多线程间的相互影响。阻塞,一个线程占用了临界区资源,其他线程必须在临界区中等待。非阻塞,强调没有一个线程可以妨碍其他线程的执行。
死锁(Deadlock)、饥饿(Starvation)、活锁(Livelock)
死锁、饥饿和活锁都属于线程的活跃性问题。
死锁:两个或两个以上的线程在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,若无外力作用,他们都无法推进下去。
饥饿:指一个线程尽管能继续执行,但被调度器无限期地忽视,而不能被调用的情况。
活锁:任务执行没有被阻塞,由于某些条件没有满足,导致一直重复着尝试-失败-尝试-失败的过程。例如行人迎面来回“谦让”。
并发级别
阻塞的:阻塞、无饥饿。
非阻塞的:无障碍,无锁、无等待。
阻塞(Blocking)
如synchronized或重入锁,会产生阻塞的线程。
无饥饿(Starvation-Free)
如果线程之间是由优先级的,低优先级的线程可能会产生饥饿。也就是说对于同一个资源的调配是不公平的。下图表示了公平锁和非公平锁的两种情况。
无障碍(Obstruction-Free)
不加锁,都进入临界区,若数据不一致则进行回滚。
可使用“一致性标记”,操作之前与操作之后分别读取,若不一致则回滚。
无锁(Lock-Free)
在无障碍的基础上,保证有一个线程能在有限步内完成任务离开临界区。通常包含了一个无穷循环。
while(!atomicVar.compareAndSet(LocalVar, localVar + 1)){
localVar = atomicVar.get();
}
无等待(Wait-Free)
要求所有的线程必须在有限步之内完成。
例如:RCU(Read-Copy
Update,读-复制更新)。读,无限制。写,先复制成副本,在副本上做修改,最后选择时机进行替换。关键在于怎么判断所有读者已经完成访问。
关于并行的两个重要定律
Amdahl定律
定义了串行系统并行化之后的加速比的计算公式和理论上限。
加 速 比 = 优 化 前 的 系 统 耗 时 优 化 后 的 系 统 耗 时 \ 加速比 = \frac{优化前的系统耗时}{优化后的系统耗时} 加速比=优化后的系统耗时优化前的系统耗时
T n = T 1 ( F + 1 n ( 1 − F ) ) \ T_{n} = T_{1}(F + \frac{1}{n}(1 - F)) Tn=T1(F+n1(1−F))
加
速
比
=
T
1
T
n
=
T
1
T
1
(
F
+
1
n
(
1
−
F
)
)
=
1
F
+
1
n
(
1
−
F
)
\ 加速比=\frac{T_{1}}{T_{n}}=\frac{T_{1}}{T_{1}(F + \frac{1}{n}(1 - F))}=\frac{1}{F + \frac{1}{n}(1 - F)}
加速比=TnT1=T1(F+n1(1−F))T1=F+n1(1−F)1
其中,n表示处理器个数,F表示串行比例,1-F表示并行比例。优化后的效果取决于cpu的数量和串行化比例。
Gustafson定律
执行时间:a + b(a:串行时间,b:并行时间)
总执行时间:a + nb(n:处理器个数)
加
速
比
=
a
+
n
b
a
+
b
\ 加速比=\frac{a+nb}{a+b}
加速比=a+ba+nb
定义:
F
=
a
a
+
b
\ F=\frac{a}{a+b}
F=a+ba为串行比例
加
速
比
=
a
+
n
b
a
+
b
=
a
a
+
b
+
n
b
a
+
b
=
F
+
n
(
a
+
b
−
a
a
+
b
)
=
F
+
n
(
1
−
F
)
=
n
−
F
(
n
−
1
)
\ 加速比=\frac{a+nb}{a+b}=\frac{a}{a+b}+\frac{nb}{a+b}=F+n(\frac{a+b-a}{a+b})=F+n(1-F)=n-F(n-1)
加速比=a+ba+nb=a+ba+a+bnb=F+n(a+ba+b−a)=F+n(1−F)=n−F(n−1)
JMM(Java内存模型)
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。
原子性(Atomicity)
原子性指一个操作时不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
例如两个线程对int i进行赋值,线程A对它赋值为1,线程B对它赋值为-1。那么i的值只可能是1或者-1。但如果是32位虚拟机对long类型做赋值,则不是一个原子性操作。
可见性(Visibility)
可见性指的是当前一个线程修改了某一个共享变量的值时,其他线程能否立刻知道这个修改。缓存优化、硬件优化、指令重排、编译器优化都可能导致可见性问题。
有序性(Ordering)
指令重排使指令顺序与原指令顺序不一致,目的是减少流水线的停顿,提高性能。
例子
class OrderExample{
int a = 0;
boolean flag = false;
public void write(){
a = 1;
flag = true;
}
public void reader(){
if (flag){
int i = a + 1;
......
}
}
}
一条指令可以简单分为以下步骤:
- 取指IF。
- 译码和取寄存器操作数ID。
- 执行或者有效地址计算EX。
- 存储器访问MEM。
- 写回WB。
指令重排的例子
处理 a=b+c;d=e+f;
可以进行指令重排
指令重排之后
Happen-Before原则
哪些指令不能重排:
- 程序顺序原则:一个线程内保证语义的串行性。
- volatile规则:volatile变量的写先于读发生,保证了volatile的可见性。
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
- 传递性:A先于B,B先于C,则A必然先于C。
- 线程的start()方法先于每一个操作。
- 线程的所有操作先于线程的终结(Thread.join())。
- 线程的中断(interrupt())先于被中断线程的代码。
- 对象的构造函数的执行,先于finalize()方法
Java并行程序基础
线程的生命周期
线程是轻量级的进程,是程序执行的最小单位,进程是线程的容器。
线程的基本操作
新建
Thread t1 = new Thread();
t1.start(); // 调用Thread.run()
注意:不要使用 t1.run() 启动线程,只会在当前线程中串行执行。
- 重写run() 方法
Thread t1 = new Thread(){
@Override
public void run(){
...
}
};
t1.start();
- 可通过extends Thread,再重写run() 方法。但Java是单继承,继承本身也是一种宝贵的资源。
- 实现Runnable接口
public interface Runnable{
public abstract void run();
}
// Thread中的构造方法
public Thread(Runnable target)
public void run(){
if (target != null){
target.run();
}
}
public class CreateThread implements Runnable{
public static void main(String[] args){
Thread t1 = new Thread(new CreateThread());
t1.start();
}
@Override
public void run(){
逻辑代码...
}
}
终止
一般情况下线程执行完后会自行结束,但服务端线程的执行本体可能就是个大循环while(true)。
Thread.stop()方法会直接终止线程,立即释放线程所持有的锁,可能会导致数据不一致的问题。
例如:修改User中的ID和Name。
用户1 ID=1 Name=小明
用户2 ID=2 Name=小王
正确停止的做法:
public class ChangeObjectThread implements Runnable{
volatile boolean stopme = false;
public void stopMe(){
stopme = true;
}
@Override
public void run(){
while(true){
if(stopme){
break; // 自行选择合适的时机退出
}
synchronized(锁对象){
逻辑代码...
}
Thread.yield();
}
}
}
中断
在Java中,停止一个线程的主要方法就是中断。线程中断并不会导致线程立即退出,而是给线程发送一个通知,告知目标线程。接到通知后如何处理,完全由目标线程决定。
public void Thread.interrupt() // 中断,设置中断标志位
public boolean Thread.isInterrupted() // 判断是否中断
public static boolean Thread.interrupted() // 判断是否中断,并清除中断标志位
Thread t1 = new Thread(){
@Override
public void run(){
while(true){
if(Thread.currentThread.isInterrupted()){ // 读取标志位
break;
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt(); // 中断,修改标志位
上述代码中,与stopme() 标记的手法非常相似,但中断的功能更强。例如循环体中出现了wait() 方法或者sleep() 方法这样的操作。
Thread t1 = new Thread(){
@Override
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interrupted!");
break;
}
try{
/*
Thread.sleep() 方法会让当前线程休眠若干时间,他会抛出一个InterruptedException中断异常。
当线程在sleep时,若被中断,这个异常就会产生。
此时,他会清除中断标志位。
*/
Thread.sleep(2000);
}catch(InterruptedException e){
System.out.println("Interrupted when sleep");
// 设置中断标志位
Theread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
等待(wait)和通知(notify)
等待和通知是为了支持多线程之间的协作。
wait() 和notify() 方法不在Thread中,而是在Object中,这意味着任何对象都可以调用这个方法。
public final void wait() throws InterruptedException
public final native void notify()
工作过程:当一个object调用wait() 方法后,当前线程会进入这个object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object调用notify() 方法后,会从这个等待队列上随机挑选一个线程唤醒。
注意:wait() 方法不能随便调用,无论是wait() 还是notify() 都需要首先获得目标对象的一个监视器。
public class SimpleWN{
final static Object object = new Object();
public static class T1 extends Thread{
public void run(){
synchronized(object){
T1 Start!
try{
T1 Wait For Object...
object.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
T1 End!
}
}
}
public static class T2 extends Thread{
public void run(){
T2 Start! Notify One Thread
object.notify();
T2 End!
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args){
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
注意:Object.wait() 方法和Thread.sleep() 方法都会让线程等待若干时间。除wait() 方法可以被唤醒之外,另一个主要的区别就是wait() 方法会释放对象的锁,而Thread.sleep() 方法不会释放任何资源。
挂起(suspend)和继续执行(resume)
这两个方法已经废弃,原因是suspend() 方法在暂停线程的同时,不会释放任何资源。
坏例子:
public class BadSuspend{
public static Object u = new Object();
static ChangeObjectThread tl = new ChangeobjectThread("T1");
static ChangeObjectThread t2 = new ChangeObjectThread("T2");
public static class ChangeObjectThread extends Thread{
public ChangeObjectThread(String natne){
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println( ” in ” +getName());
Thread.currentThread().suspend();
}
}
}
public static void main(String[] args) throws InterruptedException {
tl.start();
Thread.sleep(lOO);
t2.start();
tl.resume();
t2.resume();
tl.join();
t2.join();
}
}
利用wait() 和notify() 方法改造
public class GoodSuspend{
public static Object u = new Object();
public static class ChangeObjectThread extends Thread {
volatile boolean susperidme = false;
public void suspendMe() {
suspendme = true;
}
public void resumeMe() {
suspendme=false;
synchronized (this} {
notify(};
}
}
@Override
public void run() {
while (true) {
synchronized (this) {
while (suspendme){
try{
wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
synchronized(u){
System.out.println("in ChangeObjectThread");
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread{
@Override
public void run(){
while(true){
synchronized(u){
System.out.println("in ReadObjectThread");
}
Thread.yield();
}
}
}
public static void main(String[] args) throws InterruptedException{
ChangeObjectThread t1 = new ChangeObjectThread();
ReadObjectThread t2 = new ReadObjectThread();
t1.start();
t2.start();
Thread.sleep(1000);
t1.suspemdMe();
System.out.println("suspend t1 2 sec");
Thread.sleep(2000);
System.out.println("resume t1");
t1.resumeMe();
}
}
}
等待线程结束(join)和谦让(yield)
join
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
public class JoinMain{
public volatile static int i = 0;
public static class AddThread implements Runnable{
@Override
public void run(){
for(int i = 0;i < 10000000;i++);
}
}
public static void main(String[] args)throws InterruptedException{
AddThread at = new AddThread();
at.start();
at.join();
System.out,println(i);
}
}
join() 方法的本质是让调用线程wait() 方法在当前线程对象实力上
while(isAlive()){
wait(0);
}
不要在应用程序中,在Thread对象实例上使用类似wait() 方法或者notify() 方法,可能会影响系统API的工作。
yield
public static native void yield()
当前线程让出CPU
volatile
释义:易变的、不稳定的
提醒虚拟机这个变量可能被某些线程改动。确保本条指令不会因为编译器的优化而省略。
特性:
- 保证可见性。
- 禁止指令重排。
- 只能保证单次读/写的原子性。不能保证i++这种操作的原子性。
线程组
public class ThreadGroupName implements Runnable {
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("PrintGroup");
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-" + Thread.currentThread().getName();
while (true){
System.out.println("I am "+groupAndName);
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
守护线程
public class DaemonDemo {
public static class DaemonT extends Thread{
@Override
public void run() {
while (true){
System.out.println("I am alive");
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t = new DaemonT();
t.setDaemon(true);
t.start();
Thread.sleep(2000);
}
}
线程优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
Thread high = new HighPriority();
Thread low = new LowPriority();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
线程安全与synchronized
synchronized可完全替换volatile,开销大于volatile,用法如下:
- 指定加锁对象:对给定对象加锁,进入同步代码前需要获得给定对象的锁。
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized (instance) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前需要获得当前实例的锁。
public class AccountingSync2 implements Runnable {
static AccountingSync2 instance = new AccountingSync2();
static int i = 0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
错误的写法:
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AccountingSync());
Thread t2 = new Thread(new AccountingSync());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
t1,t2 所包含的Runnable实例不是同一个。
- 对类加锁
把上例中的increase() 方法修改为静态方法。
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
public static synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AccountingSync());
Thread t2 = new Thread(new AccountingSync());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
Reference
参考书籍:
《实战Java高并发程序设计》
参考博客:
https://www.cnblogs.com/kukri/p/9595263.html