本章主要是多线程。
记得第7版的这一章貌似是卷II的。。。
1、线程:一个进程同时执行多个任务。
2、进程与线程的主要区别:每个线程之间共享数据,每个进程则拥有独立的数据空间。
3、睡眠当前线程:
java.lang.Thread.sleep(long millis);
4、启动一个新线程的步骤:
(1)实现Runnable接口,以及其中的run方法(包含线程要执行的业务逻辑)。
(2)创建上述Runnable对象,并放入Thread中:Thread t = new Thread(new XXRunable())
(3) 启动线程:t.start()
需要睡眠的时候可以sleep,如3中所述,sleep时可能产生InterruptedException异常。
5、也可以通过继承Thread的方式实现线程,但显然更推荐Runnable的方法。因为Java中是没有多继承的(而实现多个接口是可以的)。
6、不要直接调用Runnable的run方法,一定要放到Thread中。否则就变成单线程了。。。下面是多线程一个基本的例子,开启新线程用于打印,主线程等待System.in,当输入q时,结束子线程并退出。
import java.util.*; public class MT { public static void main(String [] args) { Thread t = new Thread(new Runnable() { public void run() { int i=0; while(true) { System.out.println(i++); try { Thread.sleep(100); } catch(InterruptedException e) { break; } } } }); t.start(); Scanner scan = new Scanner(System.in); while(scan.hasNext()) { String tok = scan.next(); if(tok.equals("q")) { break; } } // end thread t.interrupt(); } }
7、Java已经不建议使用stop等强制性方法来停止线程的执行,目前主要有两种方式:
(1)内部维护:同步一个内部变量决定是否退出:
public void run() { while(bRun) { .... } }
(2)外部可以通过调用Thread的interrupt()来“打扰”线程,但是线程什么时候被“打扰醒”是不一定的,有两种情况:
(a)线程一直在用CPU,没有任何中断产生,那么最好在run里的while循环自己检查一下:
while(!Thread.currentThread().isInterrupted() && ...) { //normal work } //after main thread call Thread.inpterrupt(), would go here
(b)线程正在阻塞(产生了中断,如I/O、sleep,调度等),此时会抛出InterruptedException,因此要捕获:
public void run() { try { //...... } catch(Exception e) { // thread was interrupted, usually exit current thread } }
8、Thread有两个类似方法:
(1)interupted():类方法,会清楚打扰位。
(2)isInterrupted():实例(对象) 方法,不会清楚打扰位。
9、线程的6种状态:
(1)New(新生):new Thread(xxx)
(2)Runnable(可运行):t.start()
(3) Blocked(被阻塞):不运行任何代码,等待再次被激活。条件:锁阻塞(获得锁、I/O等待)等
(4)Waiting(等待) :join
(5)Timed wating(计时等待) :wait、await等
(6)Terminated(被终止) :run自然运行完毕或者interupted等导致内部判断后结束。
10、Java的线程也有优先级,可以设置为1~10,越大越优先。然而,这个优先级如何调度,取决于操作系统,因此不要让程序的多线程依赖于优先级。
public class Thread { // .... public final void setPriority(int newPriority); // ... }
11、守护线程,可以直接用Thread实现:
t.setDaemon(true);
必须在.start()之前调用。
如果还有一个或以上的非守护线程,则Java进程就不会退出。因此不可以把业务逻辑放在守护线程中。当一个线程无需JVM等待它结束时(它的操作可有可无),就把它设置为守护线程吧,如定时的清空缓存等。
12、线程中一旦发生未捕获的异常,就会退出,因此要注意。除了直接catch Exception拦截所有异常外。JDK5开始,可以为每个线程设立自己的异常处理器:UncaughtExceptionHandler:
public class Thread { // ... public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler(); }
被这个Handler处理后(前),线程还是会停止的
13、竞争条件(race condiction)的例子:两个线程同时访问并修改一个对象。
14、还是经典的银行转帐例子:初始每个账户都是50块钱,每个线程都随机从一个账户转帐随机金额到随机另外一个账户,但是经过一段时间后,所有账户的总钱数却少了:
import java.util.*; public class Bank { public Bank(int n) { accounts = new int[n]; for(int i=0; i< accounts.length; i++) { accounts[i] = 50; } } public void transfer(int from ,int to, int amount) { if(accounts[from]<amount) { return; } accounts[from] -= amount; System.out.println("Transfer $"+amount+" from "+from+" to "+to); accounts[to] += amount; System.out.println("Total amount "+getTotalBalance()); } public int getAccountsCount() { return accounts.length; } public int getTotalBalance() { int sum = 0; for(int account: accounts) { sum+=account; } return sum; } private int [] accounts; public static void main(String [] args) { Bank bank = new Bank(50); int thds = 10; Thread [] threads = new Thread[thds]; for(int i=0; i< 10; i++) { threads[i] = new Thread(new TransferRun(bank)); threads[i].start(); } } } class TransferRun implements Runnable { TransferRun(Bank bank) { this.bank = bank; } public void run() { while(true) { int from = rand.nextInt(bank.getAccountsCount()); int to = rand.nextInt(bank.getAccountsCount()); int amount = rand.nextInt(60); bank.transfer(from, to, amount); } } private Bank bank; private Random rand = new Random(); }
15、上述现象理论上是随机出现的,主要原因是+=、-=不是原子操作。书上636的那个图很好的说明了这个问题。
16、JDK5开始,提供了锁对象,与sychronized关键字类似,但更加OO:
//May be field of class ReetrantLock mylock = new ReetrantLock(); mylock.lock(); try { // do sth may create race condiction } catch() { lock.unlock(); }
一定要确保释放锁,因此放在finally里最省心。
加了这个锁之后,就可以保证同一时间,只能有一个线程进入try内的关键区。
下面是加了锁之后的代码,不管再怎么运行,总额都是2500了。
import java.util.*; import java.util.concurrent.locks.*; public class Bank { public Bank(int n) { accounts = new int[n]; for(int i=0; i< accounts.length; i++) { accounts[i] = 50; } } public void transfer(int from ,int to, int amount) { mylock.lock(); try { if(accounts[from]<amount) { return; } accounts[from] -= amount; System.out.println("Transfer $"+amount+" from "+from+" to "+to); accounts[to] += amount; System.out.println("Total amount "+getTotalBalance()); } finally { mylock.unlock(); } } public int getAccountsCount() { return accounts.length; } public int getTotalBalance() { int sum = 0; for(int account: accounts) { sum+=account; } return sum; } private int [] accounts; private ReentrantLock mylock = new ReentrantLock(); public static void main(String [] args) { Bank bank = new Bank(50); int thds = 10; Thread [] threads = new Thread[thds]; for(int i=0; i< 10; i++) { threads[i] = new Thread(new TransferRun(bank)); threads[i].start(); } } } class TransferRun implements Runnable { TransferRun(Bank bank) { this.bank = bank; } public void run() { while(true) { int from = rand.nextInt(bank.getAccountsCount()); int to = rand.nextInt(bank.getAccountsCount()); int amount = rand.nextInt(60); bank.transfer(from, to, amount); } } private Bank bank; private Random rand = new Random(); }
17、ReentrantLock是可重入的(可嵌套的)。
18、有的时候,虽然进入了临界区,却发现还是必须满足某些条件才能继续,这种叫做条件对象或者条件变量(condictional variable)
19、当使用条件变量时,主要的流程如下:
(1)在ReentrantLock上创建新的条件,这相当于锁下更细粒度上的“子条件锁”,需要注意的是,虽然叫“条件”锁,但它不包含任何的条件判断逻辑。
Condition cond= ReentrantLock.newCondition();
(2)在条件判断时,构造如下的结构:
while(!condiction_is_achieve) { cond.await(); } //here is condiction already achieveed
在上述结构下,一旦cond.await(),当前线程就被阻塞,它将只能由其他线程唤醒。当其他线程调用cond.signalAll()时,所有await在这个cond上的线程被唤醒,然后进行条件判断:cond_is_achieve,如果不满足,将继续进入await,否则,跳出这个循环,执行其他逻辑。
除了signalAll之外,还有signal方法,它随机选择一个await的线程解锁,但是这样的话,非常容易导致死锁,因此不建议使用。
21、synchronized关键字的作用和ReentrantLock基本是一致的:
public synchronized void method() { // all be locked, one thread at any moment !!! }
22、如上所示,当使用synchronized时,实际上内部有一个锁对象,可以直接调用wait()和notifyAll()实现条件等待。(实际是Object实现了这个,注意Lock和Condiction是await()和signalAll()有所区别)
23、如果将静态方法声明为synchronized,则将锁住整个.class,其他线程此时将不能再调用该类的其他静态方法。
24、使用synchronized处理同步问题更为简单,因此应该优先使用,不能满足时再用Lock/Condiction。
25、还有另外一种不对整个函数synchronized,而只同步部分的方法:自己建一个Object对象,如下:
class ClassXX { public void func() { synchronized (lock) { // partly sync } } Object lock = new Object(); }
26、锁和条件是有用的条件,但它们不是OO的,一种较新的技术是“监视器”,它具有如下特性:
(1)监视器是只包含私有域的类
(2) 每个监视器只有一个相关的锁,可以有若干条件
(3)使用该锁对所有
27、如果仅仅是为了同步一两个变量,可以用volatile。一个域加上volatile后,编译器就知道它可能被其他线程共享,从而不会进行可能导致危险的编译优化。
28、但是volatile本身不提供原子操作,因此,像+=这种还是危险的,而简单的直接get、直接set是可以的。因此,建议只让volatile修饰boolean。。。
29、还有AtomicBoolean、AtomicInteger等,提供了同步、原子操作如getAndAdd+=,可以使用。
30、怎么判断一个域的存取是安全的?只要满足下述三个条件之一:
(1)域是final的,并且在构造调用完成之后被访问
(2)对域的访问有公有的锁保护
(3)域是volatile,且没有其他危险原子操作
31、银行转帐问题是有可能会发生死锁的,此时按Ctrl + \可以看到死锁线程条件。Java没有提供条件打破死锁,因此必须好好设计,以避免死锁。
32、直接的lock是锁定,可能会导致死锁,可以用trylock,如果申请失败,就去做其他的事情。
33、trylock、await也有带时间参数的版本,当时间到了还没成功,就取消阻塞返回。
34、Java还提供了读写锁:java.util.concurrent.locks.ReentrantReadWriteLock。写操作与读操作互斥。简言之,多读一写。
读锁:可以被多个读操作共享的读锁,但会排斥所有的写操作。
写锁:得到一个写锁,排斥所有其他读操作和写操作。
(1)创建读写锁:
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
(2)抽出读锁和写锁:
ReentrantReadWriteLock.ReadLock rlock = rwlock.readLock(); ReentrantReadWriteLock.WriteLock wlock = rwlock.writeLock()
(3)对所有读加读锁,对所有写加写锁:
rlock.lock(); //...... rlock.unlock(); wlock.lock(); //...... wlock.unlock()l
35、JDK从1.2开始就已经不再推荐使用suspend和stop,应该避免使用它们。
36、上面的多线程同步都是Java提供的最基础方法,更多的时候,我们可以选择已经设计好的模块。
37、很多多线程设计模型都可以转化成队列:生产者线程把任务插入队列,消费者线程取出它们后再处理。Java提供了阻塞队列,当队列满插入时,或者队列为空取出时,都将阻塞。
BlockingQueue(接口):
异常/只返回false或者null
add/offer 添加
element/peek 返回头,不删除
remove/poll 删除头返回
以下两个方法是阻塞的:
put/take 放、取
此外,offer和poll都有带超时的版本。
38、上面的BlockingQueue只是个接口,Java提供了几个变种实现:
(1)LinkedBlockingQueue:可无限长度(也可限定) ,双端
(2)ArrayBlockingQueue:需要指定容量 ,可有公平策略(等待最长的获得get)
(3)PriorityBlockingQueue: 无线长度,带优先级,最优先的先被移出,如果队列空,阻塞。
(4)DelayQueue:可以延迟取回。
39、一个阻塞队列的例子:一个线程产生随机数,其他N个线程把它打印出来,有点小无聊,主要是使用阻塞队列实现这种同步模型任务。
import java.util.concurrent.*; import java.util.*; class GenRunnable implements Runnable { public GenRunnable(BlockingQueue<Integer> bq) { this.bq = bq; } public void run() { Random rand = new Random(); while(true) { try { bq.put(rand.nextInt()); Thread.sleep(10); } catch(InterruptedException exp) { // Just break the loop break; } } } private BlockingQueue<Integer> bq; } class PrintRunnable implements Runnable { public PrintRunnable(BlockingQueue<Integer> bq) { this.bq = bq; } public void run() { while(true) { try { System.out.println(bq.take()); Thread.sleep(200); } catch(InterruptedException exp) { //Break the loop break; } } } private BlockingQueue<Integer> bq; } public class BQTest { public static void main(String [] args) { BlockingQueue<Integer> bq = new LinkedBlockingDeque<Integer>(); Thread tw = new Thread(new GenRunnable(bq)); tw.start(); for(int i=0; i< 100; i++) { Thread tr = new Thread(new PrintRunnable(bq)); tr.start(); } } }
40、除了上面的阻塞队列外,Java也提供了Map、Set的线程安全版本,常用的有:
(1)ConcurrentHashMap
(2)ConcurrentLinkedDeque
(3)ConcurrentLinkedQueue
(4)ConcurrentSkipListMap
(5)ConcurrentSkipListSet
它们的size()操作耗时较长(需要遍历所有的)、支持大量读和少量的写者,默认可以有16个同时写(通过concurrencyLevel参数控制)。超过这个值的写线程将被阻塞。
其他操作,如replace替换、putIfAbsent如果没有则插入等都在内部进行了线程同步处理,是原子操作。
41、CopyOnWriteArrayList和CopyOnWriteArraySet也是线程安全的,所有修改都对底层数组进行了复制。好处是当迭代的时候,返回前一次的修改,迭代是完全安全的(但可能不新)。
42、JDK1的Vector和Hashtable都是线程安全的,JDK1.2之后就弃用了,而ArrayList等都不是线程安全的。可以用同步包装器来搞定:Collections.synchronizedListList/Map等。但是,使用迭带器的时候(或者遍历的时候),仍然需要自己进行线程同步(锁来保护部分代码或者synchHashMap)
43、一般情况下,ConcurrencyXXX的性能好于包装的synchronizedXXX,或者CopyOnWriteXXX,除了包装的ArrayList,它比CopyOnWrite好。
44、除了Runnable之外,还有一个Callable,它可以看作特殊的Runnable:有返回值。
Interface Callable<V> { V call(); }
只有这一个函数,返回值类型是泛型V,例如Callable<Integer>,则表示Callable的返回类型是Integer。
45、上面也看到了,Callable只有一个函数,我们只能直接调用(call)它,怎么和多线程结合起来呢?答案是Future接口(一般是用FutureTask这个子类):
Callable<Integer> callint = ....; FutureTask<Integer> task = new FutureTask<Integer>(callint); Thread t = new Thread(task); t.start(); ... Integer result = task.get();
最后这个get有阻塞版本和带时间的版本,这个默认的是阻塞版本,只有Callable运行完毕后,才会返回。否则将阻塞。
可以用cancel取消计算。下面的例子用单独的线程异步计算,并在主线程中get它:
import java.util.concurrent.*; public class FutureTest { public static void main(String [] args) { FutureTask<Integer> task = new FutureTask(new Callable<Integer>() { public Integer call() { int sum = 0; for(int i=1; i<=100; i++) { sum+=i; } return sum; } }); Thread t = new Thread(task); t.start(); try { System.out.println(task.get()); } catch(InterruptedException ie) { } catch(ExecutionException ee) { } } }
46、如果程序中线程的生命周期很短,应该考虑现线程池。另一个好处是可以限制并发线程数量(放置突发流量挂掉进程)。
47、Java使用执行器(Executor) java.util.concurrent.Executors用来创建线程池:
(1)newCachedThreadPool:必要时才创建新线程,空闲线程保留60秒。
(2)newFixedThreadPool:包含固定数量的线程,空闲线程一直保留。
(3)newSingleThreadExecutor:只有一个线程的“线程池”,实际是顺序提交。
(4)newScheduledThreadPool:用于定时执行任务,替代java.util.Timer
(5)newSingleThreadScheduledExecutor:用于预定执行的线程池。
48、Java的线程池用ExecutorService表示,提供的下列方法可用于将Runnable、Callable等逻辑直接转化为线程的“任务”:
<T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task);
在调用上述submit时,会得到一个Future对象,用于将来查询结果。最后一个版本由于Runnable无返回,因此这个Callable只能isDone,等,get返回的是个null。其他的可以获取返回值。
49、当用完线程池的时候,应该shutdown。此时线程池不再接受新任务,但需要到所有之中的任务都执行完,才能完全释放。
50、使用线程池实现多线程的流程:
(1)Executors.newFixedThreadPool,或者其他线程池
(2)submit一个Runnable或者Callable,保存好Future
(3)如果想取消, 直接在上一步的Future上cancel。
(4)当不再需要提交任何任务时,提交shutdown。
51、ScheduledExecutorService用于预定执行重复的任务。schedule()方法或者schedulerWithFiexedRate()或者scheduleWithFixedDelay,比较实用。
52、如果想立即结束线程池中所有任务,可以实用shutdownNow。
53、invokeAny提交所有对象到一个Callable对象集合中,只返回最先完成的任务,对于RSA密码分解这种,你提交多种分解拆解,最后能成功的那个会返回,就可以模拟并行计算了。实际是获得解空间的一个可行解。
54、invokeAll提交任务到所有Callable集合中,实际是获得解空间的所有解。
55、Java还提供了一些让各个线程“相互合作”的对象。
(1)CyclicBarrier:创建一个“栅栏 ”,llows a set of threads to all wait for each other to reach a common barrier point。相当于“预备。。。条件发生时再一起跑”,比较贴切了吧。
(2)CountDownLatch:A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. 等待,直到指定数目个线程事件完成后。(可以用于等待,直到所有线程都执行完毕)
(3)Exchanger:
(4)Semaphore:信号量,限制线程访问资源的数量,很传统不解释了。
(5)SynchronousQueue:允许一个线程把对象交给另一个线程?
56、信号量: Semaphore.acquire()/Semaphore.release(),也可以try
57、构造一个n个线程参与的CyclicBarrier:
CyclicBarrier barier = new CyclicBarrier(nThds);
每个线程完成一些工作后(不一定是执行完run),调用:
barrier.await();
提供给栅栏一个可选的动作,当栅栏够了后,去执行这个action。
new CyclicBarrier(thds, barrierAction);
这个栅栏是循环的,不同于CountDown,它有reset,可以重置。
58、交换器Exchanger,典型的是:一个线程向缓冲区填充数据,另外一个线程消耗这些数据,但他们完成后,交换缓冲区。
59、同步队列:一个线程调用SynchronousQueue的put方法时,它会阻塞直到另外一个线程take为止反之毅然,这个很有意思。。。
swing的不研究了,本章结束,卷I结束:-)