Java并发编程学习笔记(第2章:线程安全性)

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();
 }
}


答案:(8,9,10顺序不定,但88,99,1010必定相连)
Threadid: 8
Threadid: 8
Threadid: 10
Threadid: 10
Threadid: 9
Threadid: 9
说明重入锁可以避免死锁。

7、用锁保护状态和性能问题

前面主要将的是用锁机制来使得复合操作成为原子操作。而实际上,不但如此,对于可能被多个线程同时访问的可变状态变量(成员变量),在访问它时都需要持有同一个锁。后面章节会讲。

性能问题以Servlet类为类,希望尽量缩小synchronized块的范围,可以做到既保持Servlet的并发性,同时又维护线程的安全性。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值