更新时间:2017-06-03

《Java并发编程实战》文摘,有兴趣的朋友可以买本纸质书仔细研究下。

一 线程安全性

1.1 什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。

示例:一个无状态的Servlet

/**
* @author Brian Goetz and Tim Peierls
*/
@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);
} void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
} BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
} BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}

与大多数Servlet相同,这个类是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存于线程栈上的局部变量中,并且只能由正在执行的进程访问。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

1.2 原子性

一个线程不安全的示例

@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);
} void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
} BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
} BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}

count域(实例变量)可以在多个线程中共享,++count又不是一个原子操作,它包含了读取-修改-写入等操作。当两个线程在没有同步的情况下同时对count进行操作将会导致不正确的结果,这两个线程很有可能读到相同的值并且同时都执行了递增操作。

这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。每个线程执行的都是原子操作,要么这个线程完全执行完,要么完全不执行。

下面我们修改之前的UnsafeCountingFactorizer类,使它成为一个线程安全的类:

@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);
} void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {}
BigInteger extractFromRequest(ServletRequest req) {return null; }
BigInteger[] factor(BigInteger i) { return null; }
}

java.util.concurrent.atomic包中有一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过使用AtomicLong来替代long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。

在实际情况中,应尽可能地使用现有的线程安全对象来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

1.2 加锁机制

当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理。如果想添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?

答案是否定的,要想保持状态一致性,就需要在单个原子操作中更新所有相关的状态变量。

Java提供了一种内置的锁机制来支持原子性:同步代码块。它包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

synchronized (lock) {
// 访问或修改由锁保护的共享状态
}

每个Java对象都可以作为一个用来实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是正常退出还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

Java的内置锁相当于一种互斥锁,这意味着最多只能有一个线程持有这种锁。如果某个线程持有这种锁由于某种原因没有释放锁,那么别的线程将永远等待下去。虽然这个锁保护的同步代码块会以原子方式执行,保证了线程安全,然而这种方法过于极端,因为多个客户端无法同时使用这个Servlet,服务的响应性非常低,无法令人接受。

幸运的是,通过缩小同步代码块的范围,我们很容易做到既确保Servlet的并发行,同时又维护线程的安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

当执行时间较长的计算或者可能无法快速完成的操作时(例如:网络I/O)一定不要持有锁。

二 对象的共享

2.1 非原子的64位操作

当线程在没有同步的情况下读取变量时,可能会得到一个实效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全保证被称为最低安全性。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(doublelong)。JVM允许将64位的读写操作分解为两个32位的操作。如果对某个非volatile类型的long变量的读和写操作在不同的线程中执行,那么很可能读到某个值的高32位和另一个值的低32位。所以在多线程程序中使用共享且可变的64位数值变量是不安全的,最低安全性也无法保证。除非我们使用关键字volatile来声明它们,或者使用锁保护起来。

2.2 加锁与可见性

加锁的含义不仅仅局限与互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读写操作的线程都必须在同一个锁上同步。

2.3 Volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总是会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:

  • 确保它们自身的可见性。
  • 确保它们所引用对象的状态的可见性
  • 标识一些重要的程序生命周期事件的发生(例如,初始化或者关闭)

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

未完,待续...

《Java并发编程实战》文摘的更多相关文章

  1. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  2. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  3. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  4. 【Java并发编程实战】-----“J.U.C”:CountDownlatch

    上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...

  5. 【Java并发编程实战】-----“J.U.C”:CyclicBarrier

    在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...

  6. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  7. 【Java并发编程实战】-----“J.U.C”:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...

  8. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析

    前篇博客LZ已经分析了ReentrantLock的lock()实现过程,我们了解到lock实现机制有公平锁和非公平锁,两者的主要区别在于公平锁要按照CLH队列等待获取锁,而非公平锁无视CLH队列直接获 ...

  9. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介

    注:由于要介绍ReentrantLock的东西太多了,免得各位客官看累,所以分三篇博客来阐述.本篇博客介绍ReentrantLock基本内容,后两篇博客从源码级别分别阐述ReentrantLock的l ...

随机推荐

  1. JDBC处理可滚动的处理集

    Statement createStatement(int resultSetType,                           int resultSetConcurrency,     ...

  2. 深入分析java web技术内幕目录一览

    Web请求过程 如何发起请求:browser,httpclient http解析:chrome ,cache Dns域名解析:域名缓存 cdn:负载,动态加速,回源 Java I/O I/0类库的基本 ...

  3. java多线程---------java.util.concurrent并发包----------ThreadPoolExecutor

    ThreadPoolExecutor线程池 一.三个构造方法 ThreadPoolExecutor(int corePoolSize,int MaxmumPoolSize,long KeepAlive ...

  4. DFT 工程师三大法宝的使用

    众所周知,芯片主要由三大部分构成. 芯片示例-可见下图 1.与电路板和其他芯片的接口-IO pad 2.存放程序的空间-RAM和ROM 3.搭建逻辑电路的基本组件 –标准逻辑单元 DFT工程师所有的工 ...

  5. Python基础(10) - 异常

    Python 异常:程序出现了错误而在正常控制流以外采取的行为 Python中常见的异常: 1. NameError:尝试访问一个未声明的变量 >>> something Trace ...

  6. GB2312编码(为什么要加2020H、8080H,外码→内码→交换码→字形码)

    为什么要加上2020H和8080H? 区位码.内码.国标码怎么转换非常简单,但是令人迷惑的是为什么要那么转换?这种转换不可能平白无故地那样转换! 我搜索很多资料,找到最好的解释,总结如下: 首先,注意 ...

  7. Spring中使用JMS

    JMS为了Java开发人员与消息代理(message broker)交互和收发消息提供了一套标准API.而且,由于每个message broker都支持JMS,所以我们就不需要学习额外的消息API了. ...

  8. tr td 移动变色

    jsp <table  id="tableList" class="table table-hover"></table> css .t ...

  9. DataGridView 单元格自动填充

    在DataGridView单元格中,当输入指定字符时,自动完成填充. 通过 TextBox实现 AutoCompleteMode AutoCompleteMode.Suggest: AutoCompl ...

  10. C#RabbitMQ基础学习笔记

    RabbitMQ基础学习笔记(C#代码示例) 一.定义: MQ是MessageQueue,消息队列的简称(是流行的开源消息队列系统,利用erlang语言开发).MQ是一种应用程序对应用程序的通信方法. ...