同步容器

同步容器是指那些对所有的操作都进行加锁(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)多线程基础组件的更多相关文章

  1. Java并发编程实践读书笔记(1)线程安全性和对象的共享

    2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...

  2. Java并发编程实践读书笔记(5) 线程池的使用

    Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...

  3. Java并发编程实践(读书笔记) 任务执行(未完)

    任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元.   任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...

  4. Java并发编程实践读书笔记(4)任务取消和关闭

    任务的取消 中断传递原理 Java中没有抢占式中断,就是武力让线程直接中断. Java中的中断可以理解为就是一种简单的消息机制.某个线程可以向其他线程发送消息,告诉你“你应该中断了”.收到这条消息的线 ...

  5. Java并发编程实践读书笔记(3)任务执行

    类似于Web服务器这种多任务情况时,不可能只用一个线程来对外提供服务.这样效率和吞吐量都太低. 但是也不能来一个请求就创建一个线程,因为创建线程的成本很高,系统能创建的线程数量是有限的. 于是Exec ...

  6. Java并发编程实战 读书笔记(二)

    关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...

  7. Java并发编程实战 读书笔记(一)

    最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一  线程与进程   进程与线程的解释   个人觉 ...

  8. Java并发编程艺术读书笔记

    1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...

  9. Java并发编程实践

    最近阅读了<Java并发编程实践>这本书,总结了一下几个相关的知识点. 线程安全 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任 ...

随机推荐

  1. 一般处理程序获取Session方式

    今天写程序得时候遇到了一个问题:ajax在对ashx进行请求时如果按照 context.Request方式直接来获取值得话获取到得是空值,因此去网上搜了一下问题.现记录如下: ashx获取sessio ...

  2. [C#]使用dnSpy对目标程序(EXE或DLL)进行反编译修改并编译运行

    本文为原创文章.源代码为原创代码,如转载/复制,请在网页/代码处明显位置标明原文名称.作者及网址,谢谢! 本文使用的工具下载地址为: https://github.com/cnxy/dnSpy/arc ...

  3. Nginx+Tomcat+Redis实现持久会话

    使用开源web应用solo blog进行项目演示.前端使用Nginx作为负载均衡器,后端Tomcat连接Redis实现session存储.Redis的特点就是可以将session持久化.样才能真正实现 ...

  4. oracle数据库创建表,序列及添加代码案例

    create table cdpt( id number(6), name varchar2(30), constraint pk_id primary key(id) ); 更改数据库的“延迟段创建 ...

  5. IT连创业系列:App产品上线后,运营怎么搞?(上)

    又是一阵一阵的时光过去了,今夜,码的不是代码,是文字,继续和大伙分享创业的这一路历程. 话说,在突破技术的领域,IT连和IT恋上线后,慢慢走上运营这条路时,发现自己经常容易迷失. 毕竟,做为一名技术型 ...

  6. 《java.util.concurrent 包源码阅读》24 Fork/Join框架之Work-Stealing

    仔细看了Doug Lea的那篇文章:A Java Fork/Join Framework 中关于Work-Stealing的部分,下面列出该算法的要点(基本是原文的翻译): 1. 每个Worker线程 ...

  7. 关于apidoc文档生成不了的一个原因

    前几天在写完API后,写注释文档,然后很习惯的去用apidoc取生成注释文档,但是奇怪的事发生了,没有注释的内容,也没有报错:注释代码如下: /* * @api {get} /applet/:id 根 ...

  8. 【深度学习系列】用PaddlePaddle和Tensorflow进行图像分类

    上个月发布了四篇文章,主要讲了深度学习中的"hello world"----mnist图像识别,以及卷积神经网络的原理详解,包括基本原理.自己手写CNN和paddlepaddle的 ...

  9. async await Task

    一.使用Task 引用命名空间 using System.Threading.Tasks; 1.工厂方式 Task.Factory.StartNew(() => {Console.WriteLi ...

  10. spring boot 错误,求大神帮解决

    Exception in thread "main" java.lang.IllegalStateException: Failed to read Class-Path attr ...