1.线程的风险
1.1安全风险
线程不安全:
Public class UnsafeSequence{
Private int value;
Public int getNext(){
Return value++;
}
}
线程安全:
Public class UnsafeSequence{
Private int value;
Public synchronized int getNext(){
Return value++;
}
}
1.2活跃度(liveness)风险
当一个活动进入某种永远它无法再继续执行的状态时,liveness failure就发生了。例如死锁(deadlock)、饥饿(starvation)和活锁(livelock)。
1.3性能风险
性能问题涉及很多方面,包括服务时间、响应性、吞吐量、资源消费或者可伸缩性的不良表现。设计良好的应用程序中使用线程,能够获得纯粹的性能收益,但是线程仍然会给运行带来一定的开销:
上下文切换:当调度程序临时挂起当前运行的线程时,另一个线程开始运行,这再多个线程组成的应用程序中是很频繁的,并且带来巨大的系统开销:
l 保存和恢复线程执行的上下文,离开执行现场,并且CPU时间会花费在对线程的调度而不是在运行上。
l 当线程共享数据的时候,他们必须使用同步机制,这个机制会限制编译器的优化
l 同步也能够清空或者锁定内存和高速缓存,并在共享内存的总线上创建同步通信
1.4线程无处不在
线程实际上是无处不在的,即使你的程序没有使用多线程,然而你依赖的第三方或者你的程序所处的框架会为你创建线程。在你使用第三方包的时候,尤其要注意线程安全问题。
2.线程安全
2.1概览
编写线程安全的代码,本质上就是管理对状态的访问,而且通常都是共享的、可变的状态。无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用协调线程对变量的访问。
并发编程是保证正确性的前提下对性能的追求。
在没有正确使用同步的情况下,如果多个线程访问了同一个变量。你的程序就存在隐患。有3个方法可以修复它:
l 不要跨线程共享变量
l 使变量为不可变的
l 在任何访问状态变量的时候都使用同步
设计线程安全的类时,优秀的面向对象技术---封装、不可变性以及明确的不变约束—会给你提供诸多的帮助。
2.2什么是线程安全
非线程安全的实例:
Public class ImageServlet implements Servlet{
Public String imageURL;
Public void service(ServletRequest req, ServletResponse resp) {
imageURL = req.getParameter(“imageURL”);
…
out.println(imageURL);
}
}
分析:由上面图可以看出, ThreadA,写入新值后,在还没有写入Client之前,Thread B又写入了新值,从而客户A上传图片,显示在页面上时,变成了客户B上传的图片。
改进:
Public class ImageServlet implements Servlet{
Public void service(ServletRequest req, ServletResponse resp) {
String imageURL = req.getParameter(“imageURL”);
…
out.println(imageURL);
}
}
这样改进以后,就线程安全了,记住:
无状态的Servlet(对象)永远是线程安全的。
2.3原子性
由于不是原子性操作而造成的线程安全问题:
Public class UnsafeCount implements Servlet{
Public int count;
Public void service(ServletRequest req, ServletResponse resp) {
count++;
…
out.println(imageURL);
}
}
2.3.1竞争条件 – check-then-act
线程安全问题的形成,通常有一定的竞争条件。检查再运行是典型的竞争条件:
Public class LazyInitRace {
Private ExpensiveObject instance = null;
Public ExpensiveObject getInstance(){
If (instance == null)
Instance = new ExpensiveObject();
Return instance;
}
}
2.3.2复合操作
刚才计数器的问题和check-then-act的问题的根本原因是这些操作并不是一步操作,专业术语称为原子操作:
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部全部执行完成,要么一点都没有执行,这样A和B互为原子操作。
对于上面说的计数器,我们可以如下改进:
Public class CountFactorizer implements Servlet{
Public final AtomicLong count = new AtomicLong(0);
Public void service(ServletRequest req, ServletResponse resp) {
Count.incrementAndGet();
…
out.println(imageURL);
}
}
2.4锁
2.4.1内部锁
Java提供了原子性的内置锁机制:sychronized块。它包含两个部分:锁对象的引用和这个锁保护的代码块:
synchronized(lock) {
//访问或修改被锁保护的共享状态
}
内部锁扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,一个线程拥有锁的时候,别的线程阻塞等待。
2.4.2重进入(Reentrancy)
重入性:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。
Public class Widget {
Public synchronized void doSomething(){
…
}
}
Public class LoggingWidget extends Widget {
Public synchronized void doSomething(){
System.out.println(toString()+”:calling doSomething”);
Super.doSomething();
}
}
2.6活跃度和性能
弱并发性:
Public class CountFactorizer implements Servlet{
Public int count = 0;
Public synchronized void service(ServletRequest req, ServletResponse resp) {
Count++;
…
}
}
改进:
Public class CountFactorizer implements Servlet{
Public int count = 0;
Public void service(ServletRequest req, ServletResponse resp) {
Synchronized(this){
Count++;
}
…
}
}
并不是同步块越小越好,决定synchronized块大小的需要权衡各种设计要求,包括安全性、简单性和性能。有些耗时的计算或者操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁。