Java - 并发
-- # 以下内容总结来自《Java编程思想》第四版 #--
一、线程
1、多线程编程与顺序编程各自缺点?
多线程编程:上下文切换; 顺序编程:阻塞; 如果没有任务会阻塞,在单处理器上使用并发就没有意义;
2、Runnable vs. Thread
Runnable:描述任务的一种方式;
Thread:将Runnable对象转变为工作任务(创建任务,将线程附着到任务上);
Thread t = new Thread (Runnable 实例);
简单情况下,可以直接从 Thread类 继承来代替 Runnable 方式;
3、使用Executor(线程池)?
Executor 在Java SE5/6中是启动任务的优选方法,Executor 将为你管理 Thread 对象,从而简化并发编程;
Executor 子接口:ExecutorService、ScheduledExecutorService
一个ExecutorService使用可能的几个池线程之一执行每个提交的任务,通常使用Executors工厂方法配置线程池:
Executors.newCachedThreadPool() - 无界线程池,可以自动线程回收
Executors.newFixedThreadPool() - 固定大小线程池
Executors.newSigleThreadExecutor - 单个后台线程
ExecutorService exec=Executors.newCachedThreadPool();
exec.execute(new WaitPerson()); //WaitPerson implement Runnable
exec.execute(new Chef()); //Chef implement Runnable
exec.shutdownNow();
4、Runnable vs. Callable
Runnable 是执行工作的独立任务,不返回任何值;如果希望任务在完成时能够返回一个值,可以实现Callable接口而非Runnable接口;
Callable 配合 ExecutorService.submit()方法使用;
5、yield(让步) vs. sleep(休眠)
yield:主动让出时间片,可以让其他线程使用CPU了;(【注意】yield只是给线程调度器一个建议,没有任何机制保证它将会被采纳)
sleep:调用sleep后,线程处于阻塞状态,线程调度器将忽略该线程,不会分配CPU时间片,直到线程重新进入就绪状态;
join:线程A调用线程B.join,线程A将被挂起,直到线程B结束才恢复执行; 【http://uule.iteye.com/blog/1101994】

6、后台进程:当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程;
一个后台线程创建的任何线程将被自动设置为后台线程;
Thread daemon = new Thread(Runnable实例);
daemon.setDaemon(true); //Must call before start()
daemon.start();
7、volatile 可见性
第一层语义:可见性,指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),可以马上反应在其它线程的读取操作中,即线程可以正确读取其他线程的写入。工作内存和主内存可以近似理解为电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的;
第二层语义:禁止指令重排序优化,由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题;
二、共享受限资源
1、并发编程中需要防止两个任务访问相同的资源,解决方法就是当资源被一个任务使用时,在其上加锁(锁语句产生了一种互相排斥的效果,这种机制常常被称为互斥量 mutex );
2、Java 提供关键字 synchronized 为防止资源冲突提供了内置支持;当任务要执行被synchronized保护的代码片段时,将检查锁是否可用,然后获取锁,执行代码,释放锁;
synchronized(syncObject){ // 临界区 }
// synchronized方法中隐性的syncObject为对象本身(this)
-- 所有对象都自动含有单一的锁(也称为监视器),当在对象上调用其任意synchronized方法时,此对象都被加锁;
-- 一个任务(线程)可以多次获得同一个对象的锁
[注] Java提供的内部锁是可重进入的,因为线程在试图获取它自己占有的锁时,请求会成功。重进入意味着锁的请求是基于"每线程",而不是基于"每调用";
3、使用显示的Lock对象:显示的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁机制,赋予用户更细粒度的控制力(无条件的、可轮询的、定时的、可中断的锁获取操作);
- 使用Lock接口的规范形式:锁必须在finally块中释放;
Lock lock = new ReentranLock();
lock.lock();
try{
//更新对象状态
//...
}finally{
lock.unlock();
}
- 可中断的锁获取,lock.lockInterruptibly() - 可中断的锁获取操作,中断将抛出InterruptedException;
public boolean sendOnShareLine(String message) throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message)
throws InterruptedException{ ... }
4、线程进入阻塞状态:
-- 通过调用 sleep() 使线程进入休眠状态;
-- 通过调用 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息,线程才会进入就绪状态;
-- 任务等待某个 输入/输出 完成;
-- 任务试图在某个对象上调用其同步方法,但是对象锁不可用(另一个任务已经获取了这个锁);
5、终结任务:
1、Thread.interrupt() 这个方法将设置线程的中断状态; (http://gityuan.com/2016/01/03/java-thread-wait-sleep/)
-- 如果一个线程已经被阻塞,或者试图执行一个阻塞操作(如,sleep()),那么设置这个线程的中断状态将抛出 InterruptedException;
-- 如果没有产生任何阻塞调用,可以通过Thread.interrupted() 检查中断状态,该函数不仅可以判断interrupt()是否被调用过,还可以清除中断状态;
try{ while(!Thread.interrupted()){ // do ... } }catch(InterruptedException e){}
2、通过Excecutor方法代替Thread.interrupt():
终止所有进程:Executor.shutdownNow() 将发送一个interrupt()调用给它启动的所有线程;
终止单个进程:通过executor.submit()而非executor()启动任务,就可以持有任务上下文 Future<?>, 其cancel(true)将调用interrupt();
3、可以中断对sleep()的调用(或任务要求抛出InterruptedException的调用),但是不能中断正在试图获取synchronized锁或者等待I/O执行的线程;
中断I/O类型阻塞的一种解决方案:关闭任务在其上发生阻塞的底层资源;
4、Java SE5并发库中的ReentranLock上阻塞的任务具备可以被中断的能力,与synchronized方法或临界区上阻塞的任务完全不同;
三、线程之间的协作
1、实现任务协作使用的基础特性:互斥;在互斥之上,我们为任务添加一种途径,可以将自身挂起,直至某些外部条件发生变化;
2、Object的方法 wait() 和 notify()/notifyAll() ;
3、wait() vs. sleep()/yield()
调用 sleep()/yield() 的时候锁并没有被释放(sleep使线程进入阻塞状态,yield使线程让出时间片);
调用 wait() 的时候线程被挂起,对象上的锁被释放,意味着另一个任务可以获得这个锁;
4、实际上,只能在同步控制方法或同步控制块(synchronized)中调用 wait()、notify()和notifyAll();如果在非同步控制块中调用这些方法,运行时将得到IllegalMonitorStateException;
5、必须用一个检查感兴趣的条件的while循环包围wait(),因为:
-- 可能有多个任务出于相同原因在等待同一个锁,而第一个唤醒任务可能会改变这种状况,如果属于这种情况,那么任务应该被再次挂起,直至感兴趣的条件发生变化;
-- 可能有多个任务出于不同原因在等待对象上的锁,在这种情况下,需要检查是否已经由正确的原因唤醒,如果不是,就再次调用wait()来将任务挂起;
6、缺失的信号:在缺陷实现中,point 1时刻,线程调度器可能切换到T1,T1将执行其设置,然后调用notify(),当T2继续执行时,T2将盲目进入wait(),此时notify()将错失,T2将无限等待已经发送过的信号,从而产生死锁;
T1: synchronized(sharedMonitor){ <setup condition for T2> sharedMonitor.notify(); } T2: 有缺陷的实现,错失信号 while(someCondition){ //point 1 synchronized(sharedMonitor){ sharedMonitor.wait(); } } T2:正确的实现 (解决方案:防止在someCondition变量上产生竞争条件) synchronized(sharedMonitor){ while(someCondition){ sharedMonitor.wait(); } }
四、学习
线程互斥 synchronize
线程同步 wait/notify/notifyAll
Java Memory Mode
JMM描述了Java线程如何通过内存进行交互
Locks & Condition
Java 锁机制和等待条件高层实现
线程安全性
原子性与可见性
java.util.concurrent.atomic
synchronized & volatile
死锁
多线程编程常用交互模型
生产者消费者
读者与写者
Java5中并发编程工具
java.util.concurrent
线程池ExecutorService
Callable & Future
BlockingQueue
浙公网安备 33010602011771号