1、线程安全性。
当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的。
具体说,就是当多个线程访问某个类时,不管运行时环境采用何种调度方式将这些线程交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
2、无状态的对象一定是线程安全的。
什么叫无状态的对象:无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有成员变量和其它类中成员对象的引用的对象。
下面的这个类的对象就是无状态对象。这个类主要用来对请求中的参数进行因式分解,计算过程中的临时状态仅存在线程栈上的局部变量中,并且只能由正在执行的线程访问。
@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
3、原子性
需要增加命中计数器来统计所处理的请求数量。可以在Servlet中增加一个long类型的成员变量,每处理一个请求这个值就加1,但这不是线程安全的。因为++count虽然只有1行,但包含了三个独立的操作:读取count的值,将值加1,再将值的结果写入count。这是一个“读取--修改--写入”的操作序列,并且其结果状态依赖于之前的状态。如果计数器的初始值是9,而碰巧在某些情况下,两个线程读到的值都是9,那么最后计数器的赋值为10。与实际值偏差1。这种由于执行时序的不恰当出现不正确的结果,称为:竞态条件。
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
先检查后执行(本例是延迟初始化)产生的竞态条件。解释如下:假定线程A和线程B同时执行getInstance方法,A看到instance为空,new一个ExpensiveObject。此时B看到的instnce可能为空,也可能不为空,这取决于新对象(ExpensiveObject)的加载速度。如果B检查时,instance为空,那么两次调用getInstance方法时返回的是不同的结果。如果B检查时,instance不为空,那么两次调用getInstance方法时返回的是相同的结果。
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
4、使用AtomicLong型的原子变量解决计数器类的线程安全问题
AtomicLong是java.util.concurrent.atomic包下的原子变量类(线程安全类,使“读取--修改--写入”这三步操作满足原子性),用来代替long型成员变量,解决线程安全问题。由于Servlet类的状态就是计数器count的状态,既然计数器是线程安全的,那么Servlet类也是线程安全的。
@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
5、使用Java内置锁支持原子性
什么是Java内置锁:Java提供了一种同步代码块的机制来支持原子性。同步代码块分为两部分:一个作为锁的对象引用,一个作为这个锁保护的代码块。以synchronized来修饰的方法就是横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态synchronized方法以Class对象作为锁。每一个Java对象都可以作为一个实现同步的锁,这些锁被称为内置锁。
用这种方法解决的代码使得多个客户端无法同时同时使用因数分解Servlet,服务的响应非常低。
synchronized(lock)
{
//同步的代码块
}
@ThreadSafe
public class SynchronizedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req,
ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
6、重入
问题:1个或2个线程调用doSomething方法,会死锁吗?
public class LoggingWidget extends Widget{
public synchronized void doOtherthing()
{
System.out.println("doOtherthing");
}
public synchronized void doSomething()
{
System.out.println("doSomething");
doOtherthing();
}
}
答案:不会。因为内置锁是可重入的。重入意味着获取锁的操作粒度是线程,而不是方法(调用)。即如果某个线程试图获得一个由自己持有的锁,那么这个请求就会成功。正常情况下,线程A占用锁,线程B想获取,那么线程B阻塞。锁有自己的计算器,当计数值为0时,这个锁就被认为是没有被任何线程拥有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将计数器置为1。如果同一个线程再次请求这个锁时,计数器递增,当线程退出同步代码块时,计数器会相应地递减。当计数器值为0时,这个锁被释放,可以被任何线程获取。
问题:下面代码的输出是什么?
public class Test implements Runnable{
public synchronized void get(){
System.out.println(Thread.currentThread().getId());
set();
}
public synchronized void set(){
System.out.println(Thread.currentThread().getId());
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
Test ss=new Test();
new Thread(ss).start();
new Thread(ss).start();
new Thread(ss).start();
}
}
Threadid: 8
Threadid: 8
Threadid: 10
Threadid: 10
Threadid: 9
Threadid: 9
说明重入锁可以避免死锁。
7、用锁保护状态和性能问题
前面主要将的是用锁机制来使得复合操作成为原子操作。而实际上,不但如此,对于可能被多个线程同时访问的可变状态变量(成员变量),在访问它时都需要持有同一个锁。后面章节会讲。
性能问题以Servlet类为类,希望尽量缩小synchronized块的范围,可以做到既保持Servlet的并发性,同时又维护线程的安全性。