Java并发编程实践读书笔记(2)多线程基础组件
同步容器
同步容器是指那些对所有的操作都进行加锁(synchronize)的容器。比如Vector、HashTable和Collections.synchronizedXXX返回系列对象:
可以看到,它的绝大部分方法都被加了同步(带个小时钟图标)。
虽然Vector这么劳神费力地搞了这么多同步方法,但在最终使用的时候它并不一定真的“安全”。
同步容器的复合操作不安全
啊?不是说Vector和HashTable是线程安全的吗?虽然Vector的方法增加了同步,但是像下面这种“先检查再操作”复合操作其实是不安全的:
//两个同步的原子操作合在一起就不再具有原子性了
public void getLast(Vector vector) {
int size = vector.size();
vector.get(size);
}
所以,以后再听说某个类是线程安全的,不能就觉得万事大吉了,应该留个心想想其安全的真正含义。
Vector和HashTable这些类的线程安全指的是它所提供的单个方法具有原子性,一个线程访问的时候其他线程不能访问。在进行复合操作时还需要咱们自己去保证线程安全:
public void getLast(Vector vector) {
//客户端显式锁保证符合操作的同步
synchronized (vector) {
int size = vector.size();
vector.get(size);
}
}
这种不安全的问题在遍历集合的时候仍然存在。Vector能做的仅仅就是在出现多线程访问导致集合内容冲突时(版本号与进入遍历之前不一样了)给一个异常提醒:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
一定要明白,ConcurrentModificationException异常的真正目的其实是在提醒咱的系统中存在多线程安全问题,需要我们去解决。不解决程序也能跑,但是指不定那天就见鬼了,这要靠运气。
书中还指出,像Vector这样把锁隐藏在代码端的设计,会导致客户端经常忘记去同步。即“状态与保护它的代码越远,程序员越容易忘记在访问状态时使用正确的同步”。这里的状态就是指的容器的数据元素。
即使同步容器在单方法的上能够做到“安全”,但是它会使CPU的吞吐量下降、降低系统的伸缩性,因此才有了下面的并发容器。
并发容器
JDK5针对于每一种同步容器,都设计了一个对应的并发容器,队列(Queue)和双向队列(Deque)是新增的集合类型。
ConcurrentHashMap使用分段锁运行多个线程并发访问
为了解决同步访问时的低吞吐量问题,ConcurrentHashMap使用了分段锁的方式,允许多个线程并发访问。
同步锁的机制是,使用16把锁,每一把锁负责1/16的散列桶的同步访问。你可能猜到了,如果存储的数据采用了一个糟糕的散列函数,那么ConcurrentHashMap的效果HashTable一样了。
ConcurrentHashMap也有它的问题,既然允许多个线程同时访问了,那么size()和isEmpty()方法的结果就不准确了。书中说这是一种权衡,认为多线程状态下size和isEmpty方法没有那么重要了。但是在使用ConcurrentHashMap是我们应该知道确实有这样的问题。
计算机世界里经常出现这样的权衡问题。是的,没有免费午餐,得到一些好处的同时就需要付出另外一些代价。典型的例子就是分布式系统中的CAP原则,即一致性、可用性和分区容错性三者不可兼得。
CopyOnWriteArrayList应用于读远大于写的场景
顾名思义,添加或修改时直接复制一个新的底层数组来存储数据。因为要复制,所以比较适合应用于写远小于读的场景。比如事件通知系统,注册和注销(写)的操作就远大于接收事件的操作。
阻塞队列应用于生产者-消费者模式
队列嘛就是一个存储单元,数据可以按序存入然后按序取出。关键在于阻塞。在生产者-消费者模式中。生成者可以往队列里存数据,消费者负责从队列里获取数据。阻塞的含义是当队列里没有数据时,消费者在take数据时会被阻塞,直到有生产者往队列里put了一个数据。相反,如果队列里的数据已经满了,那么生产者也只能等到消费者take走了一个数据之后才能put数据。
阻塞队列的两个好处:
1,使生产者和消费者解耦,他们之间不需要额外的直接对话通信;
2,阻塞队列可以协调生产者和消费者的速度,让较快的一方等待较慢的一方,不至于使未处理的消息累积过大;
阻塞方法与中断方法
个人总结:catch到InterruptException时要么继续往上抛,实在不能抛了就要标记当期线程为interrupt。
Thread.currentThread().interrupt();
切忌try-catch完了之后什么都不做,直接给和谐了。
同步工具类
CountDownLatch(计数器)-等待多个结果
当需要等待多个条件都满足时才执行下一步,就可以用Latch来做计数器:
public class CountDownLatchTest { public static void main(String[] args) throws InterruptedException { final Random r = new Random(); final CountDownLatch latch = new CountDownLatch(5);
for(int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(r.nextInt(5) * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " execute complated!");
latch.countDown();
} }).start();
}
System.out.println("Wait for sub thread execute...");
latch.await();
System.out.println("All Thread execute complated!");
} }
测试结果:
Wait for sub thread execute...
Thread- execute complated!
Thread- execute complated!
Thread- execute complated!
Thread- execute complated!
Thread- execute complated!
All Thread execute complated!
Semaphore(信号量)控制资源并发访问数量
Semaphore可以实现资源访问控制,在初始化时可以指定一个数量,这个数量表示可以同时访问资源的线程数。
也可以理解成许可证。访问资源前问Semaphore获取(acquire)访问许可,如果还有剩余的许可就能正常获取到,否则就会等待,知道有其他线程归还(release)许可了。
public static void main(String[] args) {
Semaphore s = new Semaphore(3);
for(int i = 0;i<10;i++) {
new Thread(new Runnable() { @Override
public void run() {
try {
System.out.println(System.currentTimeMillis() +":"+ Thread.currentThread().getName() + " waiting for Permit...");
s.acquire();
System.out.println(System.currentTimeMillis() +":"+ Thread.currentThread().getName() + " doing his job...");
Thread.sleep(5000);
s.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} }).start();
}
}
测试结果如下,可以看到每次同时执行的线程数永远只有3个:
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- doing his job...
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- doing his job...
:Thread- doing his job...
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- waiting for Permit...
:Thread- waiting for Permit... :Thread- doing his job...
:Thread- doing his job...
:Thread- doing his job... :Thread- doing his job...
:Thread- doing his job...
:Thread- doing his job... :Thread- doing his job...
当把Semaphore的许可数量设置为1时,Semaphore就变成了一个互斥锁。
Barrier(栅栏)实现并行计算
栅栏有着和计数器一样的功能,他们都可以等待一些线程执行完毕后再近些某项操作。
不同之处在于栅栏可以重置,它可以让多个线程同时到达某个状态或结果之后再继续往下一个目标出发。
并行计算时,各个子线程计算的速度可能不一样,需要等待每个线程计算完成之后再继续执行下一步计算:
垂直线表示计算状态,水平箭头的长度表示计算时间的差异。
public class BarrierTest { static int hours = ;
static boolean stopAll = false;
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(, new Runnable() {
@Override
public void run() {
System.out.println("every on stop,wait for a minute.");
hours++;
if(hours>) {
System.out.println("times up,Go off work!");
stopAll = true;
}
} });
Random r = new Random();
//barrier.
for(int i = ;i<;i++) {
new Thread(new Runnable() { @Override
public void run() {
while(!stopAll) {
System.out.println(Thread.currentThread().getName() + " is working...");
try {
Thread.sleep(r.nextInt() * );
barrier.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
} }).start();
}
} }
测试结果:
... ...
every on stop,wait for a minute.
Thread- is working...
Thread- is working...
Thread- is working...
every on stop,wait for a minute.
Thread- is working...
Thread- is working...
Thread- is working...
every on stop,wait for a minute.
Thread- is working...
Thread- is working...
Thread- is working...
every on stop,wait for a minute.
times up,Go off work!
另外还有一种叫Exchanger的Barrier,它可以用来做线程间的数据交换。
构建高效可伸缩的结果缓存
简单的用synchronize + HashMap实现结果缓存:
public class ComputeMemroyCache<T,V> { HashMap<T,V> cache = new HashMap<T,V>();
Computable<T,V> computable; public ComputeMemroyCache(Computable<T,V> computable) {
this.computable = computable;
} public synchronized V compute(T t) {
V result = cache.get(t);
if(result == null) {
result = computable.compute(t);
cache.put(t, result);
}
return result;
} }
public interface Computable<T,V> { public V compute(T t); }
这种缓存有时甚至比没有缓存还要糟糕:
如果计算的对象不多,那么系统仅仅是有个很长的热身阶段,否则的话,低命中率的缓存没有起到实际的作用,糟糕的同步反而使程序的吞吐量急剧下降。
如果去掉同步,并且使用ConcurrentHashMap,结果会好一点儿,但是还是会出现重复计算一个结果的情况。因为compute中有“先检查后计算”的行为(非原子操作)。
这里一个最严重的问题是,计算代码和客户度调用同步了,就是一定要计算到一个结果之后才往Map中缓存结果,如果计算时间过长,就会导致后面很多请求的堆积。下面的改进中使用了FutureTask来讲计算推迟到另外一个线程,从而可以立即将“正在计算”的动作存放都Map中:
public class FutureTaskComputeMemroyCache<T,V> { ConcurrentHashMap<T,FutureTask<V>> cache = new ConcurrentHashMap<T,FutureTask<V>>();
Computable<T,V> computable; public FutureTaskComputeMemroyCache(Computable<T,V> computable) {
this.computable = computable;
} public V compute(T t) throws InterruptedException, ExecutionException {
FutureTask<V> result = cache.get(t);
if(result == null) {
result = new FutureTask<V>(new Callable<V>() {
@Override
public V call() throws Exception {
return computable.compute(t);
}
});
cache.put(t, result);
result.run();
}
return result.get();
} }
还缺一点儿,上面的代码还是会存在重复计算的问题。还是因为“检查并计算”的复合操作!真是够烦人的。这里要记住:既然都使用了ConcurrentHashMap,那么在存取值的时候一定要记住是否还能简单的get,一定要考虑复合操作是否需要避免的问题。因为ConcurrentHashMap已经我们准备好了解决复合操作的putIfAbsent方法。使用了ConcurrentHashMap而没使用putIfAbsent那太可惜也太浪费。
public class FutureTaskComputeMemroyCache<T,V> { ConcurrentHashMap<T,FutureTask<V>> cache = new ConcurrentHashMap<T,FutureTask<V>>();
Computable<T,V> computable; public FutureTaskComputeMemroyCache(Computable<T,V> computable) {
this.computable = computable;
} public V compute(T t) throws InterruptedException, ExecutionException {
FutureTask<V> result = cache.get(t);
if(result == null) {
result = new FutureTask<V>(new Callable<V>() {
@Override
public V call() throws Exception {
return computable.compute(t);
}
});
FutureTask<V> existed = cache.putIfAbsent(t, result);
if(existed==null) {//之前没有启动计算时这里才需要启动
result.run();
}
}
return result.get();
} }
待完善
1,如果FutureTask计算失败,需要从缓存种移除;
2,缓存过期
这里仅尝试实现了缓存过期:
public class TimeoutFutureTaskComputeMemroyCache<T,V> { ConcurrentHashMap<T,ComputeFutureTask<V>> cache = new ConcurrentHashMap<T,ComputeFutureTask<V>>();
Computable<T,V> computable; public TimeoutFutureTaskComputeMemroyCache(Computable<T,V> computable) {
this.computable = computable;
} public V compute(T t) throws InterruptedException, ExecutionException {
ComputeFutureTask<V> result = cache.get(t);
if(result == null) {
result = new ComputeFutureTask<V>(new Callable<V>() {
@Override
public V call() throws Exception {
return computable.compute(t);
}
},1000 * 60);//一分钟超时
ComputeFutureTask<V> existed = cache.putIfAbsent(t, result);
if(existed==null) {//之前没有启动计算时这里才需要启动
result.run();
}else if(existed.timeout()) {//超时重新计算
cache.replace(t, existed, result);
result.run();
}
}
return result.get();
} class ComputeFutureTask<X> extends FutureTask<X>{ long timestamp;
long age; public ComputeFutureTask(Callable<X> callable,long age) {
super(callable);
timestamp = System.currentTimeMillis();
this.age =age;
} public long getTimestamp() {
return timestamp;
} public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
} public boolean timeout() {
return System.currentTimeMillis() - timestamp > age;
} } }
Java并发编程实践读书笔记(2)多线程基础组件的更多相关文章
- Java并发编程实践读书笔记(1)线程安全性和对象的共享
2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...
- Java并发编程实践读书笔记(5) 线程池的使用
Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...
- Java并发编程实践(读书笔记) 任务执行(未完)
任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元. 任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...
- Java并发编程实践读书笔记(4)任务取消和关闭
任务的取消 中断传递原理 Java中没有抢占式中断,就是武力让线程直接中断. Java中的中断可以理解为就是一种简单的消息机制.某个线程可以向其他线程发送消息,告诉你“你应该中断了”.收到这条消息的线 ...
- Java并发编程实践读书笔记(3)任务执行
类似于Web服务器这种多任务情况时,不可能只用一个线程来对外提供服务.这样效率和吞吐量都太低. 但是也不能来一个请求就创建一个线程,因为创建线程的成本很高,系统能创建的线程数量是有限的. 于是Exec ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- Java并发编程艺术读书笔记
1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...
- Java并发编程实践
最近阅读了<Java并发编程实践>这本书,总结了一下几个相关的知识点. 线程安全 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任 ...
随机推荐
- .NET读取Excel文件的三种方法的区别
ASP.NET读取Excel文件方法一:采用OleDB读取Excel文件: 把Excel文件当做一个数据源来进行数据的读取操作,实例如下: public DataSet ExcelToDS(strin ...
- C++中类的多态与虚函数的使用
C++的三大特性:封装.继承.多态.以前学的时候自己没去总结,记得在一本c++入门的书讲得还是比较清楚.今天上网找了一下多态,找到下面这篇文章写得比较清晰. http://pcedu.pconline ...
- Java多线程学习之线程池源码详解
0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...
- TweenMax动画库学习
之前在做HTML5移动端开发的时候,用的都是Animate.css,这个插件封装的的确很好,但是在做一些缓动方面的动画,它也有一定的不足之处,比如手要写一个连续的动画,需要不停的去重复写函数,使得代码 ...
- Python之matplotlib学习(一)
小试牛刀 在上一节已经安装好matplotlib模块,下面使用几个例子熟悉一下. 对应的一些文档说明: http://matplotlib.org/1.3.1/api/pyplot_summary.h ...
- Python之hashlib模块
hashlib 在做一个授权管理系统,需要生产动态生成密码,故使用hashlib >>> import time >>> import hashlib >&g ...
- linux top结果保存到文本上
[root@web-DB script]# cat top.sh # !/bin/bash today=`date +%Y%m%d%H%M` yesterday=`date -d "1 da ...
- Oracle漏洞分析(tns_auth_sesskey)
p216 Oracle漏洞分析: 开启oracle: C:\oracle\product\\db_1\BIN\sqlplus.exe /nolog conn sys/mima1234 as sysdb ...
- 老男孩最新Python全栈开发视频教程(92天全)重点内容梳理笔记 看完就是全栈开发工程师
为什么要写这个系列博客呢? 说来讽刺,91年生人的我,同龄人大多有一份事业,或者有一个家庭了.而我,念了次985大学,年少轻狂,在大学期间迷信创业,觉得大学里的许多课程如同吃翔一样学了几乎一辈子都用不 ...
- Redis 持久化之RDB和AOP
Redis 持久化之RDB和AOP Redis 有两种持久化方案,RDB (Redis DataBase)和 AOP (Append Only File).如果你先快速了解和使用RDB和AOP,可以直 ...