Java并发

几个概念

同步(synchronous)和异步(asynchronous)

同步和异步通常用来形容异常方法的调用。同步方法一旦开始,调用者必须等到方法调用完成后,才能进行后续行为。而异步方法更像是一个消息的传递,一旦开始,方法调用会立即返回,调用者可以继续后面的行为,而异步方法通常会在另一个线程真实地执行,整个过程不会阻碍调用者的执行。

并发(concurrent)和并行(parallelism)

它们都表示多个任何可以一起执行,但偏重点不一样。并行是多个任务同时执行,而并发是多个任务交替执行,而多个任务间可能还是串行的。

实际上,如果系统内只有一个CPU,由于一个CPU只能执行一条指令,所以多进程或多线程任务是不可能并行执行的,只能是并发。真实的并行只存在在多个CPU中。

临界区

临界区用来表示一组共享数据,它可以被多个线程使用。每次只有一个线程来使用它,一旦临界资源被占用,其它线程若想要使用这个资源,只有等待。

阻塞(blocking)和非阻塞(non-blocking)

阻塞和非阻塞通常用来形容线程之间的相互影响。如一个线程占用了一个临界资源,而其他线程想要访问这个资源,只有等待,等待会导致线程挂起,这种情况就是阻塞。而非阻塞强调一个线程可以妨碍其他线程的执行。

死锁(deadlock)、饥饿(starvation)和活锁(livelock)

死锁、饥饿和活锁都属于多线程的活跃性问题。
1) 死锁
(1)死锁的产生:互斥;请求和保持;不剥夺;环路条件。
(2)处理死锁:
预防死锁:破坏产生死锁的条件2、3、4中任意一个或多个;
避免死锁:防止进入不安全状态,即下一次线程调度存在一个安全序列,满足每个进程对资源的最大需求,使每个进程都可顺利地完成。
检测和解除死锁。
2) 饥饿
饥饿是指一个或多个线程因为种种原因无法获得所需资源,如线程优先级太低,导致高优先级线程不断抢占它需要的资源。饥饿与死锁的不同在于,饥饿在未来一段时间内还是可以解决的。
3) 活锁
活锁就是两个线程主动将资源释放给他人使用,导致最终没有一个线程获得资源从而正常执行。

与并行有关的两个定理

Amdahl定理

Amdahl定义了串行系统并行化后的加速比的计算公式和理论上限。
加速比=优化前系统耗时/优化后系统耗时
加速比越高,表明优化效果越明显。
以下是Andahl公式的推导,T1表示只有一个处理器时的耗时,Tn表示有n个处理器时的耗时。F是程序中只能执行串行执行的比例。
加速比=1/[F+(1/n(1-F))]
极端情况下,当n趋近与无穷,得到加速比的理论极限:
加速比=1/F
因此只增加CPU的数量而不降低串行化比重,也是无法提高系统系统的。

Gustafson定理

Gustafson也试图说明加速比、处理器个数和串行比之间的关系。但与Amdahl定理的角度不同,它们之间是不矛盾的。Amdahl定理强调,当串行比例一定时,加速比是有上限的。而Gustafson定理关心的是,如果可被并行化的代码所占比重足够多,则加速比就能随着CPU的数量线性增长。
volatile:轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

线程的基本操作

新建线程

new创建一个线程,并且start()。
start()后会自动调用run()方法开始执行线程(注意不需要显式调用run()方法)。
如果没有特别的需要,可以通过继承Thread,重载run()方法定义线程。但Java是单继承的,所以继承也是一种很宝贵的资源,可以使用Runnable接口实现同样的操作。Runnable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable{
public abstract run();
}
默认的Thread.run()调用的是内部的Runnable接口,因此使用Runnable接口告诉线程应该做什么更合理。Thread类中的一个非常重要的构造方法:

1
2
3
4
5
public Thread(Runnable target){
if (target != null){
target.run();
}
}

可见,Thread类的这个构造方法传入一个Runnable接口的实例,在调用start()方法时,新的线程就会执行Ruannable.run()方法。

线程终止

不要用stop()方法,一般自己定义一个结束线程的方法。

线程中断

严格地讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!
public void Thread.interrupted() //中断线程,并设置中断标志位
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

Thread.sleep()函数
public static native void sleep(long millis) throws InterruptedException
Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException异常,这不是运行时异常,因此程序必须捕获并且处理它。当线程在sleep()休眠时被中断,就会抛出这个异常。
Thread.sleep()方法由于中断而抛出异常,它会清除中断标志位,因此为了下次能捕获这个中断,在异常处理中,需要再次设置中断标志位。

wait和notify

为了支持多线程之间的协作,JDK提供了wait()方法和notify()方法,它们是属于Object类,因此所有对象都可以调用这两个方法。
如果线程调用了Object.wait()方法,它就会进入object对象的等待队列,但并不是先等待的线程先执行,这种选择完全是随机的。
无论是wait()还是notify(),都需要先获得目标对象的监视器。

线程挂起suspend()和继续执行resume()

suspend()和resume()是一对相反的操作。不推荐使用suspend()挂起线程的原因是,suspend()在导致线程暂停的同时,并不会去释放任何的锁资源,会导致其他线程想要访问被它占用的锁时,都无法继续运行。

等待线程结束join()和谦让yield()

当一个线程的输入可能要依赖于