- 浏览: 2444414 次
- 性别:
- 来自: 杭州
文章分类
- 全部博客 (574)
- Book (62)
- Architecture (6)
- Java (39)
- Taobao (41)
- Distributed (4)
- Life (72)
- Database (7)
- Spring (16)
- Photography (15)
- Bicycle (41)
- Test (20)
- jBPM (8)
- Business (12)
- Movie (3)
- Ajax (15)
- Code (7)
- Eclipse (96)
- VIM (2)
- Music (6)
- Groovy (10)
- AutoHotKey (3)
- Dorado (10)
- Maven (7)
- Scrum (5)
- English (20)
- Financial (12)
- OSGi (3)
- Other (4)
- Tool (6)
- Browser (1)
- PPT (1)
- Project Management (4)
- Agile (6)
- Nosql (1)
- Search engine (6)
- Shell (2)
- Open Source (4)
- Storm (10)
- Guava (3)
- Baby (1)
- netty (1)
- Algorithm (1)
- Linux (1)
- Python (2)
最新评论
-
roy2011a:
https://github.com/ebottabi/sto ...
storm的序列化问题及与spring的结合方式 -
roy2011a:
能抗能打 写道哥们儿,你好!能共享下那个storm与sprin ...
storm的序列化问题及与spring的结合方式 -
Alick1:
兄弟,你之前是不是在深圳的正阳公司呆过啊?
storm的ack和fail -
liuleixwd:
先点个赞,写的非常好!有个问题请教下,如果我再bolt里不用e ...
storm的ack和fail -
yao-dd:
solr的facet查询
并发编程在编写高性能, 可伸缩应用的时候经常用到的一项技术, 也是相对来说比较高级的一项技术, 是每一个做后端开发的必备技能.这本书差不多是对Java并发包的一个非常详细的用法介绍.
在看的过程中做了一些笔记, 方便以后备忘.
并发基础
编写线程安全的代码, 本质上就是管理对状态的访问,而且通常都是共享的, 可变的状态.
通俗的说, 一个对象的状态就是他的数据.
所谓共享就是指一个变量可以被多个线程访问, 所谓可变是指变量的值在其生命周期内可以改变, 而真正要做到线程安全是在不可控的并发访问中保护数据
一个对象是否应该是线程安全取决于它是否被多个线程访问.
无论如何, 只要有多于一个的线程访问给定的状态变量, 而其中某个线程会写入该变量, 此时必须使用同步来协调线程对该变量的访问.
最简单的保证数据的线程安全:
- 不要跨线程共享变量
- 使状态变量为不可变
- 在任何访问状态变量的时候使用同步.
竞争条件
最常见的一种竞争条件是"检查再运行(check-then-act)", 使用一个潜在的过期值作为下一步操作的依据.
检查再运行: 你观察到一些事情为真, 然后基于你的观察去执行一些动作, 但事实上, 从观察到执行操作的这段时间内, 观察结果可能已经无效了, 从而引发错误.
为了避免竞争条件, 必须阻止其他线程同时访问我们正在修改的变量, 让我们可以确保: 当其他线程想要查看或修改一个状态时, 必须在我们的线程开始之前或者完成之后, 而不能在操作过程中.
原子操作
假设有操作a和b, 如果从执行a的线程的角度看, 当其他线程执行b时, 要么b全部执行完成, 要么一点也没有执行, 那么a和b就互为原子操作.
为了保证线程安全, "检查再运行"操作和"读-改-写"操作必须是原子操作.
锁的可重入特点
线程在试图获得它自己占有的锁时, 请求线程将会成功, 重进入意味着所有的请求都是基于"每线程", 而不是基于"每调用".
可重入的例子:
public class Supper{ public synchronized void doSomething(){...} } public class Child extends Super{ public synchronized void doSomething(){ ... super.doSomething(); } }
如果内部锁是不可重入的, 那么super.doSomething()将永远也无法得到Super的锁, 因为锁已经被子类占用了, 而可重入则可以避免这种死锁.
共享对象
同步同样还有一个重要而微妙的方面: 内存可见性. 我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态, 而且希望确保当一个线程修改了对象的状态之后, 他线程能够真正看到改变.
JVM允许将64位的读写划分两个32位的操作, 如果读写发生在不同的线程, 这样情况读取一个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位.
锁不仅是关于同步与互斥的, 也是关于内存可见的, 为了保证所有线程都能看到共享的, 可变变量的最新值, 读取和写入线程必须使用公共的锁进行同步.
volatile变量相对于synchronized而言, 只是轻量级的同步机制.
从内存可见性的角度来看, 写入volatile变量就像退出同步块, 读取volatile变量就像进入同步块.
加锁可以保证可见性与原子性; volatile变量只保证可见性.
逸出
如果一个对象还没有完成构造就发布了, 这种情况就是逸出.
不要让this引用在构造期间逸出.
一个导致this引用在构造期间逸出的常见错误, 是在构造函数中启动一个线程, 在构造函数中创建线程并没有错误, 但是最好不要立即启动它, 而是通过一个start()或者init()方法来启动之.
ThreadLocal
一个使用线程共享的方式使用Connection的例子:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ public Connection initialValue(){ return DriverManager.getConnection(url); } } public static Connection getConnection(){ return connectionHolder.get(); }
线程首次调用ThreadLocal.get()方法时, 会请求initialValue()提供一个初始值.
可以将ThreadLocal<T>看着map<Thread, T>它存储了与线程相关的值.
并发容器
采用并发容器替换同步容器, 这种做法以很小的风险带来了可扩展性显著提交
CopyOnWriteArrayList
CopyOnWriteArrayList是List相应的同步实现, 在多数操作为读取操作时会提高性能
CopyOnWriteArrayList是同步List的一个并发替代品(还有一种是CopyOnWriteSet), 通常情况下它提供了更好的并发性, 并避免了在迭代期间对容器的加锁和复制.多个线程可以对该容器进行迭代, 并且不会受到另一个或者多个想要修改容器的线程带来的干涉, 迭代的时候返回的元素严格与创建的时候一致, 不会考虑后续的修改.
在每次CopyOnWriteArrayList改变时都需要对底层数组进行一次复制, 因此当容器比较大时, 不是很合适, 而当容器迭代操作的频率远远高于对容器修改的频率2, 写入即复制容器是一个合适的选择.
CooncurrentMap
ConcurrentMap接口加入了对常见复合操作的支持, 比如"缺少即加入(putIfAbsent)", 替换和条件删除, 而且这些操作都是原子操作
CooncurrentMap使用了一个更加细化的锁机制, 名叫分离锁. 这个机制允许更深层次的共享访问. 任意数量的读取线程可以并发的访问Map, 读者和写者也可以并发的访问, 并且有限数量的写线程还可以并发修改Map, 结果是为并发带来更高的吞吐量, 同时几乎没有损失单线程访问的性能.
ConcurrentMap返回的迭代器具有弱一致性, 而并非及时失败的, 弱一致性的迭代器可以容许并发的修改, 当迭代器被创建时, 它会遍历已有的元素, 并且可以(但不保证)感应到在迭代器被创建后对容器的修改.
只有当你的程序需要独占访问中加锁时, ConcurrentMap 无法胜任.
Queue
BlockingQueue提供了可阻塞的put和take方法, 他们与可定时的offer和poll是等价的. 如果Queue已经满了, put方法会被阻塞直到有空间可用; 如果queue是空的, 那么take方法会被阻塞, 直到有元素可用. queue的长度可以有限, 也可以无限.
可以使用BlockingQueue的offer方法来处理这样一种场景: 如果条目不能被加入到队列里, 它会返回一个失败状态. 这样可以创建更多灵活的策略来处理超负荷工作, 比如减轻负载, 序列化剩余工作条目并写入硬盘, 减少生产者线程, 或者其他方法儿子生产者线程.
SynchronousQueue维护了一个没有存储空间的queue, 如果用洗盘子来比喻的话, 可以认为没有盘子架, 直接将洗好的盘子放到烘干机中. 因为是直接移交, 这样可以减少数据在生产者和消费者移动的延迟.
因为SynchronousQueue没有存储能力, 所以除非另一个线程已经准备好参与移交工作, 否则put和take会一直阻止, 这类队列只有在消费者充足的时候比较合适, 他们总是为下一个任务做好准备.
生产者-消费者模式带来了一些性能方面的提高. 生产者和消费者可以并发地执行, 如果一个受限于I/O, 另一个受限于CPU, 那么并发执行的全部产出会高于顺序执行的产出. 如果生产者和消费在不同层面并行执行, 那么紧密耦合会减弱并行性, 减少并行化的活动.
Deque(BlockingDeque)是一个双端队列是对Queue和BlockingQueue的扩展, 允许高效的在头和尾分别进行插入和删除, 其实现有ArrayDeque和LinkedBlockingDeque.
双端队列采用的是一种窃取的工作模式, 其原理是每一个消费者都有一个自己的双端队列, 如果一个消费者完成了自己的双端队列中的全部工作, 它可以偷取其他消费者的双端队列中末尾的任务. 由于消费者不会共享同一个队列, 因此相对于传统的生产者-消费者模式具有更高的可伸缩性. 而且即使一个工作者要访问另一个队列, 也是从末尾截取, 这样可以进一步降低对队列的争夺.
这里我google一个关于Deque的例子, 一看就明白了:
class Producer implements Runnable { private String name; private BlockingDeque<Integer> deque; public Producer(String name, BlockingDeque<Integer> deque) { this.name = name; this.deque = deque; } public synchronized void run() { for (int i = 0; i < 10; i++) { try { deque.putFirst(i); System.out.println(name + " puts " + i); Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Consumer implements Runnable { private String name; private BlockingDeque<Integer> deque; public Consumer(String name, BlockingDeque<Integer> deque) { this.name = name; this.deque = deque; } public synchronized void run() { for (int i = 0; i < 10; i++) { try { int j = deque.takeLast(); System.out.println(name + " takes " + j); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class BlockingDequeTester { public static void main(String[] args) { BlockingDeque<Integer> deque = new LinkedBlockingDeque<Integer>(5); Runnable producer = new Producer("Producer", deque); Runnable consumer = new Consumer("Consumer", deque); new Thread(producer).start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(consumer).start(); } }
Synchronizer(同步器)
Synchronizer包括semaphore, barrier, latch. 他们封装了状态, 而这些状态决定着线程执行到某一点时是通过还是被迫等待; 他们还提供操控状态的方法, 以及高效地等待Synchronizer进入到期望状态的方法.
Latch(闭锁)
一个闭锁工作起来就像一道大门: 直到闭锁达到终点状态之前, 门一直是关闭的, 没有线程通过, 在终点状态到来的时候, 门开了, 允许所有的线程通过. 一旦闭锁到达了终点状态, 它就不能再改变状态了, 所以它会永远保持敞开的状态.
CountDownLatch是闭锁的一个实现, 它的状态包括一个计数器, 初始化为一个正数, 用来表现需要等待的事件数. countDown方法对计数器做减操作, 表示一个事件已经发生了, 而await方法等待计数器达到零, 此时所有需要等待的时间都已经发生. 如果计数器入口时值为非零, await会一直阻塞知道计数器为零, 或者等待线程中断以及超时.
闭锁适用这样一种场景:需要计算在n个线程并发的情况下执行一个任务的时间, 如果我们简单的创建并启动线程, 那么先启动的就比后启动的具有领先优势, 并且根据活动线程数的增加或者减少, 这样的竞争度也在不断改变, 开始阀门能让控制线程同时释放所有工作线程, 结束阀门让控制线程能够等待最后一个线程完成任务.而不是顺序等待每一个线程结束.
举个例子就明白了:
public class TestHarness { public long timeTasks(int n, final Runnable task) throws Exception { final CountDownLatch startGate = new CountDownLatch(1); final CountDownLatch endGate = new CountDownLatch(n); for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { try { startGate.await(); // 所有线程运行到此被暂停, 等待一起被执行 try { task.run(); } finally { endGate.countDown(); } } catch (Exception e) { } }; }; t.start(); } long start = System.nanoTime(); startGate.countDown(); // 启动所有被暂停的线程 endGate.await(); // 等待所有线程执行完 long end = System.nanoTime(); return end - start; } public static void main(String[] args) { TestHarness th = new TestHarness(); Runnable r = new Runnable() { public void run() { System.out.println("running"); } }; try { th.timeTasks(10, r); } catch (Exception e) { e.printStackTrace(); } }
FutureTask
FutureTask的计算是通过Callable实现的, 它等价于一个可以携带结果的Runnable, 并且有三个状态:等待, 运行和完成. 完成包括所有计算以及任意的方式结束, 包括正常结束, 取消和异常, 一旦FutureTask进入完成状态, 它会永远停止这个状态上.
FutureTask.get()的行为依赖于任务的状态, 如果它已经完成, get可以立即结果, 否则会被阻塞知道任务转入完成状态, 然后会返回结果或者抛出异常.
Executor框架利用FutureTask来完成异步任务, 并可以用来进行任何潜在的耗时计算, 而且可以在真正需要计算结果之前就启动他们开始计算.
一个通过FutureTask来实现的高性能的缓存:
public interface Computable<K, V> { V compute(K arg); } public class ConcurrentCache<K, V> implements Computable<K, V> { public ConcurrentHashMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>(); public Computable<K, V> c; public ConcurrentCache(Computable<K, V> c) { this.c = c; } public V compute(final K arg) { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws Exception { System.out.println("begin compute..."); return c.compute(arg); } }; FutureTask< V> ft = new FutureTask<V>(eval); f = cache.putIfAbsent(arg, ft); // 在并发的情况下也只添加一个future if (f == null) { f = ft; ft.run(); } } try { return f.get(); } catch (Exception e) { // 如果执行失败, 必须干掉future cache.remove(arg); } return null; } public static void main(String[] args) { Computable<String, Integer> c = new Computable<String, Integer>() { public Integer compute(String arg) { System.out.println("computint..."); return Integer.valueOf(arg); } }; final ConcurrentCache<String, Integer> cc = new ConcurrentCache<String, Integer>(c); new Thread() { @Override public void run() { cc.compute("111"); } }.start(); new Thread() { @Override public void run() { cc.compute("111"); } }.start(); } }
Semaphore(信号量)
计数信号量用来控制能够同时访问某特定资源的活动的数量或者同时执行某一给定操作的数量. 技术信号量可以用来实现资源池或者给一个容器设定边界.
一个Semaphore管理一个有效的许可集, 许可的初始量通过构造函数传递给Semaphore, 活动能够获得许可, 并在使用之后释放许可, 如果已经没有可用的许可了, 那么acquire会被阻塞, 直到有可用的为止(或者直到被中断或者操作超时). release方法向信号量返回一个许可. 一个初始值为1的Semaphore可以用来充当mutex(互斥锁).
一个信号量的例子, 看了就明白了:
public class BoundedHashSet <T>{ private final Set<T> set; private final Semaphore sem; public BoundedHashSet(int n) { set = Collections.synchronizedSet(new HashSet<T>()); sem = new Semaphore(n); } public boolean add(T element) { try { sem.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = false; try { result = set.add(element); }finally { sem.release(); } return result; } public void remove(T o) { boolean result = set.remove(o); if (result) { sem.release(); } } public static void main(String[] args) { final BoundedHashSet<String> bhs = new BoundedHashSet<String>(3); for (int i = 0; i < 4; i++) { Thread t = new Thread() { @Override public void run() { bhs.add(System.currentTimeMillis() + ""); }; }; t.start(); } } }
Barrier(关卡)
关卡类似于闭锁, 他们能够阻塞一组线程, 直到某些事件发生, 其中关卡与闭锁的关键不同在于, 所有线程必须同时达到关卡点, 才能继续处理. 闭锁等待的是事件, 关卡等待其他线程. 关卡实现的是协议, 就像一些家庭成员指定商场中的集合地点:"我们每一个人6:00在麦当劳见, 到了以后不见不散, 之后我们再决定接下来做什么."
CyclicBarrier允许一个给定数量的成员多次集中在一个关卡点, 这在并行迭代算法中非常有用, 这个算法会把一个问题拆分成一系列相互独立的子问题, 当线程到达关卡点时, 调用await, await会被阻塞, 直到所有线程到达关卡点.
关卡通常用来模拟这种情况, 一个步骤的计算可以并行完成, 但是要求必须完成所有与一个步骤相关的工作后才能进入下一步.
Exchanger是关卡的另外一种形式, 它是一种两步关卡, 在关卡交汇点会叫唤数据, 当两方进行的活动不对称时, Exchanger是非常有用的, 比如当一个线程向缓冲写入一个数据, 这是另一个线程充当消费者使用这个数据.
一个关于使用CyclicBarrier的例子, 看了就明白了:
public class Cellular { private CyclicBarrier cb; private Worker[] workers; public Cellular() { int count = Runtime.getRuntime().availableProcessors(); workers = new Worker[count]; for (int i = 0; i < count; i++) { workers[i] = new Worker(); } cb = new CyclicBarrier(count, new Runnable() { public void run() { System.out.println("the workers is all end..."); } }); } public void start() { for (Worker worker : workers) { new Thread(worker).start(); } } private class Worker implements Runnable { public void run() { System.out.println("working..."); try { cb.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } public static void main(String[] args) { Cellular c = new Cellular(); c.start(); } }
笔记2
笔记3
评论
3 楼
micro123
2013-04-25
<a href='www.baidu.com'>很喜欢,你很棒</a>
2 楼
ribavnu
2013-01-06
想看这本书,一直没时间
1 楼
qingtian16265
2010-07-12
总结的很好,多谢了~~~最近正在看这本书!
发表评论
-
<异类>读书笔记
2013-03-06 07:54 0成功者能够获得更多的机会,从而能变得更为成功。税收愈减免,富人 ... -
《python学习手册》学习笔记
2013-03-11 22:25 3420python格式化传参数非常赞,用数字标明位置,值得java学 ... -
<万历十五年>读书笔记
2013-03-11 22:27 1529在网上下了一个电子书, 但是貌似跟万历十五年没啥关系, 都是讨 ... -
《鸟哥的linux私房菜》读书笔记(部分)
2013-03-11 22:27 2011x86是一种微机系统硬件架构,另一种是苹果的mac的架构 l ... -
《你的灯亮了吗》读书笔记
2013-03-06 07:20 1458这是一本原本写给程序员的书 本书的四个问题: 搞清问题的来源 ... -
《小狗钱钱》读书笔记
2013-03-06 07:17 1442一本非常不错的理财学习入门书, 以童话的形式, 儿童的思维方式 ... -
《我的奋斗》读书笔记
2012-04-14 22:03 2003文字写的很幽默, 故事也基本都是一些平常人的故事,看到了一个特 ... -
《Java Performance》书评
2012-01-15 18:32 2920原文: http://java.dzone.com/rev ... -
《程序员应该知道的97件事》读书笔记
2012-01-15 18:36 2339一本关于写代码的文 ... -
《影响力》读书笔记
2011-11-05 14:47 1797从书名上很可能以为 ... -
《浪潮之巅》读书笔记
2011-11-05 14:44 1336作为一个中国人通过分析硅谷高科技公司的一系列传奇, 总结出这 ... -
《黑客与画家》读书笔记
2011-11-05 13:37 1781以前看过《rework》, 觉得是每一个小型创业公司的创业宝 ... -
《乔布斯传》读书笔记
2011-10-18 08:53 2784在ipad上看完了这本书, 写的还不错, 里面没有无聊的八 ... -
《细说Java》读书笔记
2011-10-05 15:01 1947国人写的, 感觉是一 ... -
《敏捷估计与规划》读书笔记
2011-10-05 12:08 3140这本书断断续续看了很长时间, 内容非常不错, 基本涵盖了sc ... -
《怪诞心理学》读书笔记
2011-10-05 09:44 1778既然是怪诞, 那么整本书涉及的内容并不是我们平常司空见怪的一 ... -
《番茄工作法图解》读书笔记
2011-09-28 09:02 2349番茄工作法是时间管 ... -
《Java开发超级工具集》读书笔记
2011-09-28 08:59 2069"工欲善其事必先利其器", 在平时的开发 ... -
《敏捷迭代开发管理者指南》读书笔记
2011-09-24 13:09 2168这是一本关于迭代开发 ... -
《解析极限编程》读书笔记
2011-09-24 13:03 1743不知道是kent beck的语 ...
相关推荐
java并发编程实践笔记java并发编程实践笔记java并发编程实践笔记java并发编程实践笔记
java并发编程实战pdf学习笔记 总结了重要的知识点
java并发编程实践笔记资料.pdf
《Java并发编程实践》一书的个人读书笔记。主要列举包括各个章节的关键知识点,便于反复阅读和知识复习掌握。
Java是最先支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,因此Java开发者能常遇到上面描述的...这也是我想为Java并发技术而写这篇系列的原因。作为对自己的笔记,和对其他Java开发的追随者都可获益的。
当调用 start 启动线程时 Java 虚拟机会调 用该类的 run方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我 们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 ...
的管理,这种分离还在不同事务间划分了自然的分界线,在程序出现错误时可以很方便地进行恢复,还有利于提高程序的并发性。围绕任务执行来管理应用程序时,第一步要指明一个清晰的任务边界,理想情况下,任务是 独立...
线程安全就是对共享的、可变的状态进行管理,对象的状态就是它的数据,换句话说就是在不可控制的并发访问中保护数据。
使用java.util.concurrent类库构造安全的并发应用程序的基础。共享其实就是某一线程的数据改变对其它线程可见,否则就会出现脏数据。
在实践中,委托是创建线程安全类最有效的策略之一:用已有的线程安全类来管理所 有状态即可。
java中没有提供任何机制,来安全是强迫线程停止手头的工作,Thread.stop和 Thread.suspend方法存在严重的缺陷,不能使用。程序不应该立即停止,应该采用中断这种协作机制来处理,正确的做法是:先清除当前进程中的...
Linux面试专题及答案+ActiveMQ消息中间件面试专题+Java基础面试题+MySQL性能优化的21个最佳实践+微服务面试专题及答案+深入理解java虚拟机+设计模式面试专题及答案+开源框架面试专题及答案+并发编程及答案+Spring...
Java 并发学习笔记: 进程和线程, 并发理论, 并发关键字, Lock 体系, 原子操作类, 发容器 & 并发工具, 线程池, 并发实践 Java是一种面向对象的编程语言,由Sun Microsystems于1995年推出。它是一种跨平台的...
Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的核心理论做了下小...
Java并发编程.pdf JAVA核心知识点整理.pdf Java高级架构知识点整理.pdf Java高级架构面试知识点整理.pdf JVM与性能优化知识点整理.pdf MySQL性能调优与架构设计解析文档.pdf Nginx入门到实战.pdf springCloud笔记....
学习线程介绍Java多线程学习PDF格式Java并发编程的艺术.pdf JAVA并发编程实践.pdf图解Java多线程设计模式-第2版.pdf代码code1是《 Java并发编程的艺术》的源码ThreadDesignPatterns是《图解Java多线程设计模式》第1...
Java并发体系知识导图笔记.xmind JVM和性能优化.xmind JVM面试专题及答案.pdf kafka知识导图笔记.xmind Kafka面试专题及答案.pdf Linux面试专题及答案.pdf memcached面试专题及答案.pdf MongoDB面试专题及答案.pdf ...
2022java面试题、JVM面试题、多线程面试题、并发编程、Redis面试题、MySQL面试题、Java2022面试题、Netty面试题 一、内容概览 本次分享的资源涵盖了Java面试的各个方面,从基础知识到高级技术,从数据库到框架应用...
《实战Java高并发程序设计》笔记和源码笔记《实战Java高并发程序设计》中有很多代码范例,适合初学者通过实践入门并发编程,这本书有个问题就是前面的代码都用JDK7,第六章开始又用JDK8了笔者精心制作相关笔记并整理...