Java并发编程学习笔记
Java编程思想,并发编程学习笔记.
一.基本的线程机制
1.定义任务:Runnable接口
线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现Runnable接口并编写run方法,使得该任务可以执行你的命令.
class MyTask implements Runnable {
private String mName;
public MyTask(String name) {
mName = name;
}
@Override
public void run() {
System.out.println(mName + " run in thread pid is " + Thread.currentThread().getId());
}
}
2.使用Thread
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器.
Thread thread = new Thread(new MyTask("Thread"));
thread.start();
// Thread run in thread pid is 9
3.使用Executor
java.util.concurrent包中的执行器将为你管理Thread对象,从而简化并发编程.Executor允许管理异步任务的执行,而无须管理线程的生命周期.是启动任务的优选方法.
ExecutorService cachedExecutor = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++)
cachedExecutor.execute(new MyTask("Cached executor " + i));
cachedExecutor.shutdown();
ExecutorService exec = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++)
exec.execute(new MyTask());
ExecutorService singleExcutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++)
singleExcutor.execute(new MyTask("Single executor " + i));
singleExcutor.shutdown();
输出结果:
Cached executor 0 run in thread pid is 10
Cached executor 1 run in thread pid is 11
Cached executor 2 run in thread pid is 12
Fixed executor 0 run in thread pid is 13
Fixed executor 1 run in thread pid is 14
Fixed executor 2 run in thread pid is 13
Single executor 0 run in thread pid is 15
Single executor 1 run in thread pid is 15
Single executor 2 run in thread pid is 15
newCachedThreadPool:将为每一个任务分配一个线程.通常在程序执行过程中会创建与所需任务相同的线程,然后在它回收旧线程时停止创建新线程,因此它是Executor的首选.
newFixedThreadPool:将一次性预先执行线程分配,预先分配需要高昂的代价,但可以限制线程的数量,不用为每个任务都付出创建线程的开销.
newSingleThreadExecutor:如同数量为1的newFixedThreadPool.每一个任务都按照提交给它的顺序执行,因此它会序列化提交给他的任务,并维护自己的悬挂任务队列.
shutdown方法将防止新任务被提交给这个Executor,当前任务将继续运行shutdown被调用之前提交的所有任务.程序将在Executor中所有的任务完成之后退出.
4.从任务中产生返回值
使用Callable接口代替Runable接口,可取得返回值.Callable是一种具有参数类型的泛型,它的参数类型表示的是从call方法中返回的值,并且需要使用ExecutorService的submit方法调用它.
class TaskWithResult implements Callable<String> {
private String mName;
public TaskWithResult(String name) {
mName = name;
}
@Override
public String call() throws Exception {
return "Result -> " + mName + " " + Thread.currentThread().getId();
}
}
ExecutorService exec = Executors.newCachedThreadPool();
Future<String> future = exec.submit(new TaskWithResult("t"));
System.out.println(future.get());
exec.shutdown();
5.休眠
Thread类的sleep方法,将暂停给定的时间(ms).
TimeUnit类允许延迟指定时间单位的时间.如
TimeUnit.SECONDS.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
使用休眠方法,使得线程调度器从一个线程切换到另一个线程,进而驱动另一个任务.
但是这种顺序依赖于底层的线程机制,因此顺序执行的任务不能依赖于此类方法的调用,应该手动编写自己的协作例程,这些例程按照指定的顺序在相互之间传递控制权.
6.优先级
线程的优先级该线程的重要性传递给了调度器,调度器优先让优先级最高的线程优先执行,但这并不是意味着优先级较低的线程得不到执行,即优先级不会导致死锁.优先级低的线程仅仅只是执行的频率较低.
可以使用getPriority和setPriority方法来获取和修改优先级.如,Thread.currentThread().getPriority()
7.让步
调用yield方法让具有相同优先级的线程可以运行.这个操作只是建议上的.
但是对于重要的控制或在调整应用时都不能依赖于yield方法.原理上同使用sleep方法一样.
8.后台线程
后台(Daemon)线程,是指程序运行的时候在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的一部分.
当所有非后台线程结束时,程序也就终止了,此时会杀掉所有的后台线程.
必须在线程启动之前(调用start方法前)调用setDaemon方法,才能将一个线程设置为后台线程.
一个后台线程创建的任何线程都将被自动设置为后台线程.可调用isDaemon方法来确定线程是否是一个后台线程.
9.编码的变体
直接使用Thread类创建任务.
Thread类实现了Runable接口,可直接继承自Thread类,重写run方法,实现任务.
class SimpleThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("haha pid:" + getId());
}
}
另一种惯用法是自管理的Runable.
class SelfManage implements Runnable {
private Thread thread = new Thread(this);
public SelfManage() { thread.start(); }
@Override
public void run() {
System.out.println("self manage thread pid is " + Thread.currentThread().getId());
}
}
或者使用内部类或匿名内部类将线程隐藏在代码中.
public void startThread() {
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("inner thread tid is " + Thread.currentThread().getId());
}
};
t.start();
}
10.加入一个任务
一个线程可以调用其它线程的join方法,其效果是该线程将等待被调用join方法的线程结束,才继续运行.
class Sleeper extends Thread {
@Override
public void run() {
System.out.println("Sleeper run begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Sleeper run out");
}
}
class Joiner extends Thread {
private final Thread joinerThread;
public Joiner(Thread joiner) { joinerThread = joiner; }
@Override
public void run() {
System.out.println("joiner run begin");
try {
joinerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("joiner run out");
}
}
Thread t = new Sleeper();
t.start();
new Joiner(t).start();
join方法有带参数的版本,参数表示超时的时间,如果被调用的线程超时未返回,将停止等待.
public final synchronized void join(long millis) throws java.lang.InterruptedException;
public final synchronized void join(long millis, int nanoseconds) throws java.lang.InterruptedException;
public final void join() throws java.lang.InterruptedException;
其中millis为毫秒,nanoseconds为纳秒.
11.线程异常捕获
Thread类的setUncaughtExceptionHandler方法,允许设置一个异常处理器,在发生未捕获的异常时,调用异常处理器的uncaughtException方法.
方法声明 public void setUncaughtExceptionHandler(java.lang.Thread$UncaughtExceptionHandler);
class ExceptionThread extends Thread {
@Override
public void run() {
System.out.println("Thread " + getId() + " will throw excetion.");
throw new RuntimeException("This is a exception!");
}
}
ExceptionThread eThread = new ExceptionThread();
eThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable arg1) {
System.out.println("catch exception! thead id is " + thread.getId());
}
});
eThread.start();
二.共享受限资源(同步)
基本上并发模式在解决线程冲突问题时,都是采用序列化访问共享资源的方案.通常是通过在代码前面加上一条锁语句来实现,这就使得在一段时间内只有一个任务可以运行这段代码.因为锁语句产生了一种互斥效果,所以这种机制常常称作互斥量(Mutex).
共享资源一般是以对象存在的内存片段,或者是文件,输入输出端口,打印机等.要控制对共享资源的访问,首先需要把它包装进一个对象.
1.同步规则
Brian的同规则:如果正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么此时必须使用同步,并且读写的代码都必须使用相同的监视器锁进行同步.
2.synchronized
java以synchronized提供内置支持以防止资源冲突.当任务要执行被synchronized保护的资源时,它将检查锁是否可用,然后获取锁,执行代码,释放锁.
所有对象都含有单一的锁,当在对象上调用sychronized方法时,此对象都会被加锁,该对象上的其它synchronized方法只有等到前一个方法调用完毕并释放了锁以后才能被调用.
一个任务可以多次获得对象的锁.最开始对象的计数为0;每当这个任务在这个对象上获得锁时,计数增加1;离开synchronized方法时减1.计数为0时,锁被释放,其它任务就可以使用此资源.
针对每个类也有一个锁,作为类的Class对象的一部分,所有synchronized static方法也可以在类的范围内防止对static数据的并发访问.
3.使用显示的Lock对象
private java.util.concurrent.locks.Lock lock = new java.util.concurrent.locks.ReentrantLock();
public void lockFunction() {
lock.lock();
try {
System.out.println("locked");
} finally {
lock.unlock();
}
}
如果使用synchronized同步一个方法,那么在方法在某些情况下失败了,可能抛出了一个异常,此时将没有任何机会去做清理工作,以恢复状态.如果使用Lock对象,就可以在finally中将系统的状态正确恢复了.
如果要实现尝试获取锁,或者尝试在一段时间内获取锁,则必须使用Lock对象,synchronized不能实现此类功能.
public void test() {
untimed();
timed();
new Thread() {
@Override
public void run() {
lock.lock();
System.out.println("lock but not unlock it.");
}
}.start();
Thread.yield();
untimed();
timed();
}
private java.util.concurrent.locks.Lock lock = new java.util.concurrent.locks.ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if (captured)
lock.unlock();
}
}
public void timed() {
boolean captured = false;
try {
captured = lock.tryLock(1, TimeUnit.SECONDS);
System.out.println("tryLock(): " + captured);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (captured)
lock.unlock();
}
}
4.原子性和易变性
原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的上下文切换(切换到其它线程)之前执行完毕.
原子性保证,对于除long和double以外的所有基本类型变量,读取和写入这样的操作,它们会被当作不可分(原子)的操作来操作内存.Java SE5开始,当你定义long或double时,使用关键字volatile,就会获得原子性(简单的赋值和返回操作).
volatile确保了应用中的可视性.一个域如果被声明为volatile,那么只要产生了写操作,那么所有的读操作都可以看到这个修改.
volatile使用规则:如一个域可能被多个任务同时访问,就应当是volatile的;如果一个域由同步块来保护,那么就不需要声明为volatile.如果一个域只需要在某个任务内可见,那么就不需要声明为volatile.
volatile无法工作的情况:一个域的值依赖于它之前的值(如递增计数器);某个域的值受到其它域的值限制(如Range类的).
使用volatile而不是synchronized的唯一安全选择是,类中只有一个可变的域.因此应尽量使用synchronized进行保护.
原子操作:对域中的值作赋值或返回操作.java中自增运算或+=都不是原子操作.
5.临界区
使用多个线程同时访问方法内的一部分代码而不是整个方法,通过这种方法分离出来的代码段就是临界区.它也是使用synchronized建立.也可以显示的使用Lock对象建立.
synchronized(syncObject) {
// ...
}
三.终止任务
1.线程的状态
新建(new):当一个线程被创建时短暂的处于这个状态,此时已经为它分配了必须的系统资源并进行了初始化,有资格获得时间片,此后调度器将把它转为可运行状态或阻塞状态.
就绪(Runable):只要调度器分配时间片就可以运行的状态,即此时可运行或不运行.
阻塞(Blocked):能够运行但有某个条件阻止它运行的状态.调度器不会给他分配时间片直到进入了就绪状态.
死亡(Dead):死亡或终止状态不再可调度,不可能再获得时间片.任务已结束,或不再可运行,通常是由于run返回,或任务的线程被中断.
2.进入阻塞状态
进入阻塞状态通常有以下原因
调用sleep方法,这种情况下任务在指定的时间内不能运行.
调用wait方法,还没有收到notify或notifyAll的通知(或者SE5中的signal或signalAll)而进入就绪状态.
进入同步代码,但未获得锁.
等待输入输出完成.
注意:使用suspend和resume也会阻塞和唤醒线程,但会导致死锁,因此被废弃了.stop方法也被废弃了,因为他不能释放锁.
3.中断
(1)对Thread对象,调用interrupt方法,可以终止被阻塞的任务,并设置中断状态.如果一个线程已经被阻塞,或试图执行一个阻塞操作,那么设置这个中断状态将抛出InterruptException异常.当抛出该异常或调用Thread.interrupted方法时,将清除该状态.
(2)对Executor对象,调用shotdownNow方法,那么将给Executor启动的所有线程调用interrupt方法.如果使用的是submit方法启动的线程,使用submit返回的Future对象来调用cancel方法,传入true,可以中断该任务.cancel是中断Executor启动的单个线程的方式.
四.线程之间的协作
线程之间的协作,指多个任务一起工作去解决某个问题.这样,问题不是相互干涉,而是彼此之间的协调,因为这类问题中,某些部分必须在其它部分解决之后解决.
1.wait()和notifyAll()方法
wait提供了任务之间对活动同步的方式.wait等待某个条件发生变化,这种条件由别的任务来提供.wait只有当收到notify或notifyAll时,才被唤醒.
调用wait时,线程的执行被挂起,对象上的锁被释放.
public final void wait() throws InterruptedException;
public final void wait(long timeout) throws InterruptedException;
在wait期间,对象上的锁被释放; notify或notifyAll时,从wait中恢复执行.执行带参数版本的wait时,超时后从wait中恢复执行.
注意:wait(),notify(),notifyAll()方法只能在同步代码块中调用.否则,运行时将抛出IllegalMonitorStateException异常,这表示,调用这些方法必须拥有对象的锁.
2.await()和signalAll()方法
通过Lock对象的newCondition()方法,可创建一个Condition对象,Condition类使用互斥并允许任务挂起.
Condition对象的awati和signalAll方法,可用来挂起和唤醒这个Condition对象上的任务.与notifyAll相比,signalAll是更安全的方式.
一个线程协作的例子,演示一直开关灯的事件:
interface Light {
void open();
void close();
void waitForOpen() throws InterruptedException;
void waitForClose() throws InterruptedException;
}
class SyncLight implements Light {
private boolean mIsOpen;
public synchronized void open() {
mIsOpen = true;
notifyAll();
}
public synchronized void close() {
mIsOpen = false;
notifyAll();
}
public synchronized void waitForOpen() throws InterruptedException {
while (mIsOpen == false)
wait();
}
public synchronized void waitForClose() throws InterruptedException {
while (mIsOpen == true)
wait();
}
}
class LockLight implements Light {
private boolean mIsOpen;
private Lock mLock = new ReentrantLock();
private Condition mCondition = mLock.newCondition();
public void open() {
mLock.lock();
try {
mIsOpen = true;
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
public void close() {
mLock.lock();
try {
mIsOpen = false;
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
public void waitForOpen() throws InterruptedException {
mLock.lock();
try {
while (mIsOpen == false)
mCondition.await();
} finally {
mLock.unlock();
}
}
public void waitForClose() throws InterruptedException {
mLock.lock();
try {
while (mIsOpen == true)
mCondition.await();
} finally {
mLock.unlock();
}
}
}
class OpenLight implements Runnable {
Light mLight;
public OpenLight(Light light) {
mLight = light;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(10);
System.out.print("Open light! ");
mLight.open();
mLight.waitForClose();
}
} catch (InterruptedException e) {
System.out.println("exit...open");
}
}
}
class CloseLight implements Runnable {
Light mLight;
public CloseLight(Light light) {
mLight = light;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
mLight.waitForOpen();
TimeUnit.MILLISECONDS.sleep(10);
System.out.print("Close light! ");
mLight.close();
}
} catch (InterruptedException e) {
System.out.println("exit...close");
}
}
}
public class ThreadCooperation {
public static void main(String[] args) throws InterruptedException {
// Light light = new LockLight();
Light light = new SyncLight();
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new CloseLight(light));
service.execute(new OpenLight(light));
TimeUnit.SECONDS.sleep(1);
service.shutdownNow();
}
}
3.解决带循环的语句,不同步的问题:将循环写到同步块中.
synchronized(object) {
while(condition) {
// do something;
}
}
五.其它类及其方法
Thread类
Thread类自身不执行任何操作,它只是驱动赋予它的任务.创建线程可能是一个高昂的代价,因此必须保存并管理它们.
1.start方法: 用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源.
public synchronized void start();
2.yield方法: 切换线程函数,调用此方法建议线程调度器可以将CPU从一个线程转移给另一个线程.
public static native void yield();
3.interrupt方法: 中断一个线程.调用此方法时,将给线程设置一个标志,表明该线程已被中断.然而异常捕获时将清理这个标志.因此在异常被捕获后,再调用isInterrupted将返回false,而异常代码处理中则为true.
isInterrupted方法: 判断一个线程是否被中断.
interrupted方法: 检查中断状态,并清除中断状态.
public void interrupt();
public boolean isInterrupted();
注:interrupt方法,可以终止被阻塞的任务,并设置中断状态.如果一个线程已经被阻塞,或试图执行一个阻塞操作,那么设置这个中断状态将抛出InterruptException异常.
4.isAlive方法:判断线程是否已运行.调用start方法后,返回true; run结束后返回false.如果被强制中断,仍返回true
public final native boolean isAlive();
ExecutorService类
awaitTermination方法:等待每一个任务结束,如果所有任务在超时时间内结束,则返回true,否则返回false,表示不是所有任务都结束了.
public abstract boolean awaitTermination(long, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException;
shutdownNow方法,那么将给Executor启动的所有线程调用interrupt方法
public abstract java.util.List<java.lang.Runnable> shutdownNow();