读书笔记篇-Java并发编程实战
第1章 介绍
1.1 并发的(非常)简短历史
在发展的初期,计算机还没有操作系统;它们自始至终执行一个程序,这个程序直接访问机器的所有资源。这样-个程序运行 在无保护的金属器件上,不仅写起来困难,而且每次只运行一个程序,不能很好地利用昂贵且稀缺的计算机资源。
操作系统的发展使得多个程序能够同时运行,程序在各自的进程(processes)中运行:相互分离,各自独立执行,由操作系统来分配资源,比如内存、文件句柄、安全证书。
如果需要的话,进程会通过-些原始的机制相互通信: Socket. 信号处理(signal handlers)、共享内存(shared memory)、信号量(semaphores) 和文件。
1.2 线程的优点
恰当的使用线程,可以降低开发和维护的开销,线程通过异步的工作流程转化为普遍存在的顺序流程,使程序模拟人类和交互变得容易。
因为程序调度的基本单位是线程,一个单线程应用程序一次只能运行在一个处理器上。在双核处理器系统中,一个单线程程序,放弃了其中一半的空闲CPU资源。如果让每个CPU都得到有效的利用,其效率更加高。
1.3 线程的风险
- 安全危险
- 活跃度的危险
- 性能危险
1.4 线程无处不在
通过从框架线程中调用应用程序的组件,框架把并发引入了应用程序。组件总是需要访问程序的状态。因此要求在所有的代码路径访问状态时,必须是线程安全的。
第2章 线程安全
无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。
2.1 什么是线程安全性
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
有状态对象(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。
无状态对象永远是线程安全的。多数Servlet都可以实现为无状态的,这一事实 极大地降低了确保Servlet线程安
全的负担,只有当Servlet要为不同的请求记录一些信 息时,才会将线程安全的需求提到日程上来。
2.2 原子性
原子性,顾名思义就是不可再分的单位。在这里指的是原子操作,其指的是在对数据操作的过程中不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
竞争条件
当计算的正确性依赖于运行时中相关的时许或者多线程的交替时,会产生竞争条件。
最常见的竞争条件是“检查再运行”(check then act),使用一个潜在的过期值作为决定下一步操作的依据。
检查再运行的的常用用法就是惰性初始化,惰性初始化的目的是延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。
复合操作
将“检查再运行”和“读-写-改”操作的全部执行过程看作是复合操作
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面描述的状态。
2.3 锁
为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
内部锁
Java提供了强制原子性的内置锁机制:synchronized块。有两种作用:1.是锁饮用的对象 2.锁代码块。
每个Java对象都可以隐式地扮演一个用于同步的锁的角色:这些内置的锁被称作内部锁(intrinsic locks)或监视器锁(monitor locks)。执行线程进入synchronized块之前会自动获得锁;而无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。获得内部锁的唯一途径是: 进入这个内部锁保护的同步块或方法。
内部锁在Java中扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。
重进入(Reentrancy)
当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程在试图获得它自已占有的锁时,请求会成功。重进入意味着所的请求是基于“每线程(per-thread) ”,而不是基于“每调用(per invocation) ”的。重进入的实现是通过为每个锁关联-个请求计数(acquisition count)和-个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求-个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一线程再次请求这个锁,计数将递增:每次占用线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。
2.4 用锁来保护状态
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。
对象的内部锁与它的状态之间没有内在的关系。尽管大多数类普遍使用这样一种非常有效的锁机制:
用对象的内部锁来保护所有的域,然而这并不是必需的。即使获得了与对象关联的锁也不能阻止其他线程访问这个对象一-获得 对象的锁后,唯-可以做的事情是阻止其他线程再获得相同的锁。作为一种便利, 每个对象都有一个内部锁,所以你不需要显式地创建锁对象。你可以构造自己的锁协议或同步策略,使你可以安全地访问共享状态,并且贯穿程序都始终如一地使用它们。
每个共享的可变变量都需要由唯一一个确定 的锁保护。而维护者应该清楚这个锁。
锁保护的变量,意味着每次访问变量都需要获得改锁,确保在同一时刻只有一个线程可以访问这个变量。
2.5 活跃度与性能
上图表示多个请求到达同步的Factoring Servlet时所发生的事情:这些请求排队等候并依次被处理。我们把这种Web应用的运行方式描述为**弱并发(poor concurrency) **的。
一种表现:限制并发调用数量的,并非可用的处理器资源,而恰恰是应用程序自身的结构。幸运的是,通过缩小synchronized块的范围来维护线程安全性,我们很容易提升Servlet的并发性。你应该谨慎地控制synchronized块不要过小:你不可以将-个原子操作分解到多个synchronized块中。不过你应该尽量从synchroni zed块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。
有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。
第3章 共享对象
3.1 可见性
Java内存模型(JMM)
Java的内存模型如上图所示,在多线程进行操作数据的时候,会先从主内存中拷贝一份到自己的工作内存中,在处理数据完成后,再将其写入主内存中,完成操作。
过期数据
当线程读取数据时,可能读取到的是一个过期的数据,除非每一次访问数据都是同步的,否则很肯读取到过期数据。
非原子的64位操作
当一个线程在没有同步的情况下读取变量,它可能会得到一个过期值。但是至少它可以看到某个线程在那里设定的一个真实数值,而不是一个凭空而来的值。这样的安全保证被称为是最低限的安全性(out- of-thin- air safety)
最低限的安全性应用于所有的变量,除了一个例外:没有声明为volatile的64位数值变量(double 和long) 。Java存储模型要求获取和存储操作都为原子的,但是对于非volatile的long和double变量,JVM允许将64位的读或写划分为两个32位的操作。如果读和写发生在不同的线程,这种情况读取-一个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位3。因此,即使你并不关心过期数据,但仅仅在多线程程序中使用共享的、可变的long 和double变量也可能是不安全的,除非将它们声明为volatile类型,或者用锁保护起来。
锁和可见性
当访问一个共享的可变变量时,为什么要求所有线程由同一个锁进行同步,我们现在可以给出另一个理由一为 了保证一个线程对数值进行的写入,其他线程也都可见。另一方面,如果一个线程在没有恰当地使用锁的情况下读取了变量,那么这个变量很可能是一个过期的数据。
锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步.
volatile变量
在Java中volatile关键字确保对一个变量的更新以可预见的方式告知其他线程。
加锁可以保证可见性与原子性; volatile 变量只能保证可见性。
只有满足了下面所有的标准后,你才能使用volatile变量:
- 写入变量时 并不依赖变量的当前值:或者能够确保只有单一的线程修改变量的值:
- 变量不需 要与其他的状态变量共同参与不变约束;
- 而且,访问变量时,没有其他的原因需要加锁。
发布和逸出
发布(publishing)一个对象的意思是使它能够被当前范围之外的代码所使用。比如将一个引用存储到其他代码可以访问的地方,在-一个非私有的方法中返回这个引用,也可以把它传递到其他类的方法中。在很多情况下,我们需要确保对象及它们的内部状态不被暴露(publish)。在另外-些情况下,为了正当的使用目的,我们又的确希望发布一个对象,但是用线程安全的方法完成这些工作时,可能需要同步。如果变量发布了内部状态,就可能危及到封装性,并使程序难以维持稳定:如果发布对象时,它还没有完成构造,同样危及线程安全。一个对象在尚未准备好时就将它发布,这种情况称作逸出(escape) 。
让我们看看一个对象是如何逸出的。
最常见的发布对象方式就是将对象的引用存储到公共静态域中,任何类和线程都可以看到这个域。
1 |
|
不要让this引用在构造期间逸出。
3.3 线程封闭
访问共享的、可变的数据要求使用同步。-个可以避免同步的方式就是不共享数据。如果数据仅在单线程中被访问,就不需要任何同步。线程封闭(Thread confinement)技术是实现线程安全的最简单的方式之-。 当对象封闭在一个线程中时,这种做法会自动成为线程安全的,即使被封闭的对象本身并不是。
Swing发展了线程封闭技术。Swing的可视化组件和数据模型对象并不是线程安全的,它们是通过将它们限制到Swing 的事件分发线程中,实现线程安全的。为了正确地使用Swing,运行在不同于事件线程(event thread)的其他线程中的代码不应该访问这些对象(为了简化这些, Swing提供了invokelater 机制,用于在事件线程中安排执行Runnable实例)。
Ad-hoc线程限制
Ad-boc线程限制10 是指维护线程限制性的任务全部落在实现上的这种情况。因为没有可见性修饰符与本地变量等语言特性协助将对象限制在目标线程上,所以这种方式是非常容易出错的。事实上,对于像GUI应用中的可视化组件或者数据模型这些线程限制对象,对它们的引用通常是公用域。
如果决定将一个像GUI这样特定的子系统实现为“单线程化”的子系统,通常就要使用线程限制技术。单线程化子系统有时所带来的简便性的好处远远胜过ad-hoc线程限制的易损性”。
线程限制的一种特例是将它用于volatile 变量。只要你确保只通过单一线程写入共享的volatile变量,那么在这些volaile变量上执行“读-改-写”操作就是安全的。在这种情况下,你就将修改操作限制在单- -的线程中, 从而阻止了竞争条件。并且,可见性保证volatile变量能够确保其他线程能看到最新的值。
鉴于ad-hoc线程限制固有的易损性,因此应该有节制地使用它。如果可能的话,用一种线程限制的强形式(栈限制或者Thread Local)取代它。
栈限制
栈限制是线程限制的一种特例,在栈限制中,只能通过本地变量才可以触及对象。正如封装使不变约束更容易被保持,本地变量使对象更容易被限制在线程本地中。本地变量本身就被限制在执行线程中:它们存在于执行线程栈。其他线程无法访问这个栈。
栈限制(也称线程内部或者线程本地用法,但是不要与核心库类的ThreadLocal混淆)与ad-hoc线程限制相比,更易维护,更健壮。.
ThreadLocal
一种维护线程限制的更 加规范的方式是使用ThreadLocal,它允许你将每个线程与持有数值的对象关联在一起。ThreadLocal提供了get与set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。
线程本地(Thread Local)变量通常用于防止在基于可变的单体(Singleton) 或全局变量的设计中,出现(不正确的)共享。比如说,一个单线程化的应用程序可能会维护-个全局的数据库连接,这个Connection在启动时就已经被初始化了。这样就可以避免为每个方法都传递一个Connection. 因为JDBC规范并未要求Connection;自身一定是线程安全的,因此,如果没有额外的协调时,使用全局变量的多线程应用程序同样不是线程安全的。通过利用ThreadLocal存储JDBC连接,每个线程都会拥有属于自己的Connection.
ThreadLocal的ThreadLocalMap 中key为什么使用WeakReference ?
1 |
|
可以看到Entry是继承WeakReference(弱引用类型),其传入的ThreadLocal<?>(也就是key)在GC时会被收集掉,但value并不是,那为啥要这么设计呢 ?
WeakReference的定义是如果没有强引用指向它,在JVM进行GC的时候会被收集掉(无论内存是否满)
设计原理
先来看看ThreadLocal的组成,可以发现ThreadLocal类中的ThreadLocalMap 是由Thread类持有的,每个Thread有自己的ThreadLocalMap实例,ThreadLocalMap的键是ThreadLocal,值是 T,也就是说在当前线程中可以存在多个ThreadLoca存储与ThreadLocalMap中。其生命周期也就是说只有线程Thread被销毁了,其ThreadLocalMap才会被回收。
1 |
|
get方法,获取值时是通过先拿到当前线程,再获取当前线程中的ThreadLocalMap,再通过当前ThreadLocal类作为key去获取对应的value
1 |
|
set方法
1 |
|
remove方法
1 |
|
从上面的的三个方法中get、set、remove中都会去调用expungeStaleEntry 方法,其实这个方法就是清理无引用的value(或者是被GC收集掉key的value)
现在我们再回过头来看我们的问题 ,为什么ThreadLocalMap的key是weakReference类型的 ?
首先如果我们代码中对ThreadLocal已经无需使用了,那么在ThreadLocalMap中的key也就没有强引用了,所以在GC时就会被收集掉了,同时我们的get、set、remove 方法进行操作时也是根据key==null来清理value的,所以其作用体现在这里。
如果我的ThreadLocal没有被回收但是他的key却被GC收集掉了会不会无法获取到我需要的值?不会的大哥,只要你还有非弱引用指向ThreadLocal就不会被回收的
1 |
|
所以说key设置为weakReference是在一定程度上防止发生内存泄露的,方便对象的回收。
Entry为啥还要存储key ? 这个就比较简单了主要是为了解决在我们进行哈希的时候可能会有冲突,ThreadLocal会往后去查找空桶,在get的时候通过key进行匹配。
什么情况下使用ThreadLocal会产生内存泄露 ?这种情况当然是存在的,当你不正确使用时,如直接将ThreadLocal引用指向空,就会导致内存泄露后内存溢出
1 |
|
3.4 不可变性
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的:
- 它的状态不能在创建后再被修改;
- 所有城都是final类型3:并且,
- 它被正确创建(创建期间没有发生this引用的逸出)。
从技术上讲,不可变对象的城并未全部声明为final类型,这样的情况是可能存在的,string就是这种类。设计这种类依赖于对良性数据竞争的精准分析,还需要对Java存储模型有深入的理解。(满足一 下你的好奇心: string会惰性地 (Lazily)计算哈希值:当第一次调用hashcode时,string计算哈希值,并将它缓存在一个非final域中。之所以可以这样做,仅是因为这个城所表现的非默认的( nondefault)值,在每次计算后都得到相同的结果,因为该结果来自一个已经确定的不可变的状态。但请不要自己这样做! )
Final域
final关键字源于C++的const机制,不过受到了更多的限制。它对不可变性对象的创建提供了支持。final 域是不能修改的(尽管如果final 域指向的对象是可变的,这个对象仍然可被修改),然而它在Java存储模型中还有着特殊的语义。final域使得确保初始化安全性(initialization safety) 成为可能,初始化安全性让不可变性对象不需要同步就能自由地被访问和共享。
3.5 安全发布
不可变对象可以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。
如果一个对象不是不可变的,它就必须被安全地发布,通常发布线程与消费线程都必须同步化。此刻让我们关注一下, 如何确保消费线程能够看到处于发布当时的对象状态;我们要解决对象发布后对其修改的可见性问题。
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。
一个正确创建的对象可以通过下列条件安全地发布:
- 通过静态初始化器初始化对象的引用;
- 将它的引用存储到volatile域或AtomicReference;
- 将它的引用存储到正确创建的对象的final域中;
- 或者将它的引用存储到由锁正确保护的城中。
第4章组合对象
4.1设计线程安全的类
三要素
- 确定对象状态由哪些变量构成的。
- 确定限制状态变量的不变约束。
- 制定一个管理并发访问对象状态的策略。
如果对象的域都是基本类型(primitive)的,那么这些域就组成了对象的完整状态。如果一个对象的域引用了其他对象,那么他的状态也同时包含了被引用对象的域。LinkedList的状态包括了所有存储在链表中的节点对象的状态。
同步策略(synchronization policy)
定义了对象如何协调对其状态的访问,并且不会违反他的不变约束或后验条件。它规定了如何把不可变性、线程限制和锁结合起来,从而维护线程的安全性,还指明了哪些锁保护哪些变量。为了保证开发者于维护者可以分析并维护类,应该将类的同步策略写入文档。
4.2实例限制
即使一个对象不是线程安全的,仍有许多技术可以让它安全的用于多线程程序。比如你可以确保它只被单一的线程访问(线程限制),也可以确保所有的访问都正确的被锁保护。
将数据封装走对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁。
限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全时,就不必检查完整的程序。
Java监视器模式
线程限制原则的直接推论之一是Java监视器(Java monitor pattern)。遵循Java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。
私有锁保护状态
1 |
|
使用私有锁对象,而不是对象的内部锁(或任何其他可公共访问的锁),有很多好处。私有的锁对象可以封装锁,这样客户代码无法得到它。然而可公共访问的锁允许客户代码涉足它的同步策略一-正 确地或不正确地。客户不正确地得到另一一个对象的锁,会引起活跃度方面的问题。另外要验证程序是正确地使用着一个可公共访问的锁,需要检查完整的程序,而不是一个单独的类。
4.3委托线程安全
可以将线程的安全委托于一些安全的类或者容器、如JUC下的一些并发类。
非状态依赖变量
我们可以将线程安全委托到多个隐含的状态变量上,只要这些变量彼此独立的,这意味组合对象并未增加任何涉及多个状态变量的不变约束。
当委托无法胜任时
如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量。
4.4向已有的线程安全类添加功能
- 客户端加锁
对于一个由Collections.synchronizedList封装的ArrayList,向原始类中加入方法或者拓展类都不正确,因为客户代码不知道同步封装工厂方法返回List对象的类型。第三个策略是拓展功能,而不是拓展类本身,并将拓展代码置入一个“助手(helper)”类。
1 |
|
4.5同步策略的文档化
为类的用户编写类线程安全性担保的文档;为类的维护编写类的同步策略文档。
第5章构建块
5.1同步容器
- Vector
- Hashtable
- Collections.synchronizedXxx工厂方法创建的容器
5.2并发容器
用并发容器替换同步容器,这种做法以很小的风险带来了可拓展性显著的提高。