更新时间: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. Microsoft Power BI Desktop概念学习系列之Microsoft Power BI Desktop的官网自带示例数据(图文详解)

    不多说,直接上干货! https://docs.microsoft.com/zh-cn/power-bi/sample-datasets 后续的博文系列,将进行深入的剖析和分享. 欢迎大家,加入我的微 ...

  2. jmeter笔记

    Jmeter性能测试 入门 Jmeter 录制脚本:使用一个叫badbody的工具录制脚步供jmeter使用,http://www.badboy.com.au/:也可以用jmeter来录制 Jmete ...

  3. [PY3]——Python的函数

    Python函数总结图 1.调用 1.1 如何调用 1.1.1  函数名(传参) # 函数使用函数名来调用,函数名后紧跟一对小括号,小括号里传入函数定义时要求的参数 add(9,9) #9+9=18 ...

  4. 配置内核源码make menuconfig时出现 #include CURSES_LOC错误

    配置内核时出现如下错误: liuxin@sunshine-virtual-machine:~/work/nfs_root/system/linux-2.6.22.6$ make menuconfig ...

  5. php 函数的嵌套

    /*一定要小心变量作用域*/ function insert_dynamic() { function bar() { echo "I don't exist until insert_dy ...

  6. HDU 2276 Kiki & Little Kiki 2 矩阵构造

    Kiki & Little Kiki 2 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java ...

  7. 谈缓存和Redis

    自从上次分享<Redis到底该如何利用?>已经有1年多了,这1年经历了不少.从码了我们网站的第一行开始到现在,我们的缓存模块也不断在升级,这之中确实略有心得,最近也有朋友探讨缓存,觉得可以 ...

  8. High Performance MySQL笔记:count

    在SQL中使用count()好像是非常自然的事情: SELECT COUNT(*) FROM TABLE_NAME; 有时候确实会想过,count(*)和单独的count(column_name)有什 ...

  9. Python之装饰器复习

    一.什么是装饰器? 装饰器他人的器具,本身可以是任意可调用对象,被装饰者也可以是任意可调用对象. 二.强调装饰器的原则: 1 不修改被装饰对象的源代码 2 不修改被装饰对象的调用方式 3:在遵循1和2 ...

  10. Canvas学习:globalCompositeOperation详解

    在默认情况之下,如果在Canvas之中将某个物体(源)绘制在另一个物体(目标)之上,那么浏览器就会简单地把源特体的图像叠放在目标物体图像上面. 简单点讲,在Canvas中,把图像源和目标图像,通过Ca ...