未完,代码还未上传,待整理...
第21章 并发
*****************************************************************************
并发通常是提高运行在“单处理器”上的程序的性能。
与阻塞不同,如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,
因此这个程序可以保持继续向前执行。
21.2基本线程机制:
implements Runnable
extends Thread
implements Callable
call Thread
call Executor
Sleep
Join
Priorities
Daemon
Exception
21.3共享受限资源:
基本上所有的并发模式在解决线程冲突问题的时候,都是采用【序列化访问共享资源】的方案。
这意味着在给定时刻只允许一个任务访问共享资源。
通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。
因为锁语句产生了一种互相排斥的效果,所以这种机制常常被称为【互斥量(mutex)】。
关键字synchronized
关键字volatile
lock() ReentrantLock()
ThreadLocal
21.3.3原子性与易变性:
不正确的知识:“原子操作不需要进行同步控制”
【原子操作】是不能被“线程调度机制”中断的操作。一旦操作开始,那么它一定可以在“切换到其他线程执行”之前,执行完毕。
依赖于原子性是很棘手而且很危险的,如果你是一个并发专家,或者你得到了来自这样的专家的帮助,你才应该使用原子性来替代同步。
原子性可以应用于【除long和double之外】的所有基本类型之上的“简单操作”。
但是,当你定义long或double变量时,如果使用volatile关键字,就会获得原子性。
volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,
那么只要对这个域产生了【写】操作,那么所有的【读】操作就都可以看到这个修改。
即便使用了本地缓存,情况也确实如此,volatile域会立即被写如到主存中,而读取操作就发生在主存中。
在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。
如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。
同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来保护,那就不必将其设置为volatile。
第一选择应该是使用synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。
什么才属于原子操作呢?
对域中的值做赋值和返回操作,通常都是原子性的。
21.3.4原子类:
Java SE5引入诸如AtomicInteger, AtomicLong, AtomicReference等特殊的原子性变量类,
它们提供下面形式的原子性条件更新操作:
boolean compareAndSet(expectedValue, updateValue);
对于常规编程来说,它们很少会派上用场,但是在涉及性能调优时,它们就会大有用武之地。
21.3.5临界区:
有时,你只是希望防止多个线程同时访问方法内部的【部分代码】,而不是防止访问整个方法。
通过这种方式分离出来的代码段被称为临界区(Critical Section),它也是用synchronized关键字建立。
synchronized(syncObject){ } ----> 同步控制块
还可以使用显式的Lock对象来创建临界区。
21.3.6在其他对象上同步:
21.3.7线程本地存储:
防止任务在共享资源上产生冲突的第二种方式就是根除对变量的个共享。
线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
21.4终结任务:
21.4.1装饰性花园
21.4.2在阻塞时终结
线程状态:
------------------------------------------
|New Runnable Blocked Dead |
|新建 就绪 阻塞 死亡 |
------------------------------------------
进入【阻塞Blocked】状态的情况:
1)调用sleep(),在指定的时间内不会运行。
2)调用wait()使线程挂起。直到线程得到了notify()/notifyAll(),或者java.util.concurrent中的signal()/signalAll()。
3)任务在等待某个输入/输出完成。
4)任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取这个锁。
suspend()和resume(),stop()方法都已经倍废止了。
suspend()和resume()可能导致死锁
stop()不释放线程获得的锁。 如果线程处于不一致的状态,其他任务可用在这种状态下浏览并修改它们。
21.4.3中止:
有时你希望能后终止处于阻塞状态的任务。
Thread类包含interrupt()方法,因此你可用终止倍阻塞的任务,这个方法将设置线程的中断状态。
新的concurrent类库似乎在避免对Thread对象的直接操作,转而尽量通过Executor来执行所有操作。
基本的interrupt()用法
sleep()可以被中断
synchronized和I/O不能被中断
被互斥所阻塞
在ReentrantLock上阻塞的任务具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同。
21.4.4检查中断:
21.5线程之间的协作:
21.5.1wait()与notifyAll()
21.5.2nofity()与nofityAll()
21.5.3生产者与消费者
21.5.4生产者-消费者与列队
wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作问题,即每次交互时都握手。
在许多情况下,你可以瞄向更高的抽象级别,使用【同步列队】来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。
在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现。
你通常可以使用LinkedBlockingQueue,它是一个无届队列,还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在它被阻塞之前,
向其中放置有限数量的元素。
21.5.5任务间使用管道进行输入/输出
21.6死锁
1)当一下四个条件【同时】满足时,就会发生死锁:
1.护持条件。任务使用的资源中至少有一个是不能共享的。
一根Chopstick一次只能被一个Philosopher使用。
2.至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。
philosopher必须拿着一根Chopstick并且等待另一根。
3.资源不能被任务抢占,任务必须把资源释放当成普通事件。
Philosopher不会从其他philosopher那里抢Chopstick
4.必须有循环等待,这时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,
直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。
2)要防止死锁的话,只需要破坏其中一个即可。
在程序中,防止死锁最容易的方法是破坏第4个条件。
Java对死锁并没有提供语言层面上的支持,能否通过仔细地设计程序来避免死锁,这取决与你自己。
21.7新类库的构件
21.7.1 CountDownLatch
21.7.2 CyclicBarrier
21.7.3 DelayQueue
21.7.4 PriorityBlockingQueue
21.7.5 ScheduledExecutor P765
21.7.6 Semaphore
21.7.7 Exchanger
Exchanger是在两个任务之间交换对象的栅栏。
当这些任务进入栅栏时,它们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。
Exchanger的典型应用场景是:
一个任务在创建对象,这些对象的生产代价很高昂,而且另一个任务在消费这些对象。
通过这种方式,可以有更多的对象在被创建的同时被消费。
21.8仿真
21.9性能调优
21.9.1比较各类互斥技术
21.9.2免锁容器
21.9.3乐观加锁
21.9.4ReadWriteLock
21.10活动对象
活动对象之所以称为“活动的”,是因为每个对象都维护着它自己的工作器线程和消息列队,
并且所有对这种对象的请求都将进入列队排队,任何时刻都只能运行其中的一个。
因此有了活动对象,我们就可以串行化消息,而不是方法,这意味着不再需要防备一个任务在其循环的中间被中断这种问题了。
当你向一个活动对象发送消息时,这条消息会转变为一个任务,该任务会被插入到这个对象的列队中,等待在以后的某个时刻运行。
>>>有了活动对象:
1.每个对象都可以拥有自己的工作器线程。
2.每个对象都将维护对它自己的域的全部控制权。(这比普通的类要更严苛一些,普通的类只是拥有防护它们的域的选择权)
3.所有在活动对象之间的通信都将以在这些对象之间的消息形式发生。
4.活动对象之间的所有消息都要排队。