原子变量
java.util.concurrent.atomic包定义了对单一变量进行原子操作的类。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换[compare-and-swap])。所有的类都提供了get和set方法,可以使用它们像读写volatile变量一样读写原子类。就是说,同一变量上的一个set操作对于任意后续的get操作存在happens-before关系。原子的compareAndSet方法也有内存一致性特点,就像应用到整型原子变量中的简单原子算法。
为了看看这个包如何使用,让我们返回到最初用于演示线程干扰的Counter类:
使用同步是一种使Counter类变得线程安全的方法,如SynchronizedCounter:
对于这个简单的类,同步是一种可接受的解决方案。但是对于更复杂的类,我们可能想要避免不必要同步所带来的活跃度影响。将int替换为AtomicInteger允许我们在不进行同步的情况下阻止线程干扰,如AtomicCounter:
并发集合
ConcurrentHashMap
使用的是一种更细粒度的锁,这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。
迭代的时候不会抛出ConcurrentModificationException,迭代器是拥有"弱一致性",而非"即时失败。size返回的是一个估计值。
ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的标准实现是ConcurrentSkipListMap,它是TreeMap的并发模式。ConcurrentSkipListMap和ConcurrentSkipListSet分别作为同步的SortedMap和SortedSet的并发替代品。
CopyOnWriteArrayList
在每次修改时,都会创建并重新发布一个新的容器副本,从而实现复制。“写入时复制”返回迭代器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。
并发随机数
在JDK7中,java.util.concurrent包含了一个相当便利的类,ThreadLocalRandom,当应用程序期望在多个线程或ForkJoinTasks中使用随机数时。 对于并发访问,使用TheadLocalRandom代替Math.random()可以减少竞争,从而获得更好的性能。 你只需调用ThreadLocalRandom.current(), 然后调用它的其中一个方法去获取一个随机数即可。下面是一个例子:
java.util.concurrent.atomic包定义了对单一变量进行原子操作的类。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换[compare-and-swap])。所有的类都提供了get和set方法,可以使用它们像读写volatile变量一样读写原子类。就是说,同一变量上的一个set操作对于任意后续的get操作存在happens-before关系。原子的compareAndSet方法也有内存一致性特点,就像应用到整型原子变量中的简单原子算法。
为了看看这个包如何使用,让我们返回到最初用于演示线程干扰的Counter类:
- public class Counter {
- private int c = 0;
- public void increment() {
- c++;
- }
- public void decrement() {
- c--;
- }
- public int value() {
- return c;
- }
- }
使用同步是一种使Counter类变得线程安全的方法,如SynchronizedCounter:
- public class SynchronizedCounter {
- private int c = 0;
- public synchronized void increment() {
- c++;
- }
- public synchronized void decrement() {
- c--;
- }
- public synchronized int value() {
- return c;
- }
- }
对于这个简单的类,同步是一种可接受的解决方案。但是对于更复杂的类,我们可能想要避免不必要同步所带来的活跃度影响。将int替换为AtomicInteger允许我们在不进行同步的情况下阻止线程干扰,如AtomicCounter:
- import java.util.concurrent.atomic.AtomicInteger;
- public class AtomicCounter {
- private AtomicInteger c = new AtomicInteger(0);
- public void increment() {
- c.incrementAndGet();
- }
- public void decrement() {
- c.decrementAndGet();
- }
- public int value() {
- return c.get();
- }
- }
并发集合
ConcurrentHashMap
使用的是一种更细粒度的锁,这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。
迭代的时候不会抛出ConcurrentModificationException,迭代器是拥有"弱一致性",而非"即时失败。size返回的是一个估计值。
ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的标准实现是ConcurrentSkipListMap,它是TreeMap的并发模式。ConcurrentSkipListMap和ConcurrentSkipListSet分别作为同步的SortedMap和SortedSet的并发替代品。
- import java.util.Map;
- public interface ConcurrentMap<K, V> extends Map<K, V> {
- /**
- * 仅当K没有相应的映射值才插入
- * @param key
- * @param value
- * @return
- */
- V putIfAbsent(K key, V value);
- /**
- * 仅当K被映射到V时才移除
- * @param key
- * @param value
- * @return
- */
- boolean remove(Object key, Object value);
- /**
- * 仅当 K被映射到oldValue时才替换为newValue
- * @param key
- * @param oldValue
- * @param newValue
- * @return
- */
- boolean replace(K key, V oldValue, V newValue);
- /**
- * 仅当K被映射到某个值时才替换为newValue
- * @param key
- * @param value
- * @return
- */
- V replace(K key, V value);
- }
CopyOnWriteArrayList
在每次修改时,都会创建并重新发布一个新的容器副本,从而实现复制。“写入时复制”返回迭代器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。
并发随机数
在JDK7中,java.util.concurrent包含了一个相当便利的类,ThreadLocalRandom,当应用程序期望在多个线程或ForkJoinTasks中使用随机数时。 对于并发访问,使用TheadLocalRandom代替Math.random()可以减少竞争,从而获得更好的性能。 你只需调用ThreadLocalRandom.current(), 然后调用它的其中一个方法去获取一个随机数即可。下面是一个例子:
- int r = ThreadLocalRandom.current().nextInt(4,77);
概述
几乎所有应用程序,都会使用某种形式的缓存。重用之前的计算结果,能降低延时,提高吞吐量,但却要消耗更多的内存。用内存“换”CPU。缓存看上去非常简单,然而简单的缓存可能会将性能瓶颈装变为可伸缩性瓶颈,即使缓存是用于提升单线程的性能。笔者会循序渐进的介绍缓存的使用方法演进。
模拟定义接口和功能
声明一个计算函数,使用泛型,输入是A,输出是V。然后我们实现这个接口,再开发一个包装器,可以缓存计算的结果。
接口:
实现:
使用HashMap和同步机制来初始化缓存
这种方法是最基本的缓存用法,是安全的。但是有一个明显的可伸缩性问题:每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用coumpute的线程可能被阻塞很长时间。如果有多个线程在排队等待还未计算出的结果,那么compute方法的计算时间可能比没有“记忆”操作的计算时间更长。
用ConcurrentHashMap替换HashMap
Memorizer2比Memorizer1有着更好的并发行为,ConcurrentHashMap是线程安全的,所以不需要同步compute方法。但是作为缓存仍然有问题----2个线程同时调用的compute的时候,可能会导致计算得到相同的值。因为缓存是用来避免相同的数据被计算多次,但对于更通用的缓存机制来说,这种情况是更糟糕的,对于提供单词初始化对象缓存来说,这个漏洞会存在安全风险。
Memorizer2问题在于,如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么很可能会重复这个计算。我们希望通过某种方法来表达“线程X正在计算f(1226)”这种情况,这样当另一个线程查找f(1226)时,他能够知道最高效的方法是等待线程X计算结束,然后去查询缓存“f(1226)的结果是多少”。
基于FutureTask的Memorizer封装器
FutureTask表示一个计算过程,这个过程可能已经计算完成,也可能正在进行。如果有结果可用,那么FutureTask.get将立即返回结果,否则它会一直阻塞,直到结果计算出来再将其返回。
Memorizer3进一步改进了代码。基于ConcurrentHashMap表现出了更好的并发性。但是他仍然有一个漏洞,就是2个线程计算相同值的漏洞。这个漏洞的概率远远小于Memorizer2的情况。但是compute方法中的if代码块仍然是非原子的“先检查再执行”操作。
基于原子操作putIfAbsent的改进
Memorizer4使用了putIfAbsent的原子方法,从而有效避免了Memorizer3的漏洞。但是这个缓存仍然存在问题。
缓存污染
缓存逾期
缓存清理
几乎所有应用程序,都会使用某种形式的缓存。重用之前的计算结果,能降低延时,提高吞吐量,但却要消耗更多的内存。用内存“换”CPU。缓存看上去非常简单,然而简单的缓存可能会将性能瓶颈装变为可伸缩性瓶颈,即使缓存是用于提升单线程的性能。笔者会循序渐进的介绍缓存的使用方法演进。
模拟定义接口和功能
声明一个计算函数,使用泛型,输入是A,输出是V。然后我们实现这个接口,再开发一个包装器,可以缓存计算的结果。
接口:
- package com.chinaso.phl;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public interface Computable<A, V> {
- V compute(A arg) throws InterruptedException;
- }
实现:
- package com.chinaso.phl;
- import java.math.BigInteger;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public class ExpensiveFunction implements Computable<String, BigInteger> {
- @Override
- public BigInteger compute(String arg) throws InterruptedException {
- return new BigInteger(arg);
- }
- }
使用HashMap和同步机制来初始化缓存
- package com.chinaso.phl;
- import java.util.HashMap;
- import java.util.Map;
- import net.jcip.annotations.GuardedBy;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public class Memorizer1<A, V> implements Computable<A, V> {
- @GuardedBy("this")
- private final Map<A, V> cache = new HashMap<A, V>();
- private final Computable<A, V> c;
- public Memorizer1(Computable<A, V> c) {
- this.c = c;
- }
- @Override
- public synchronized V compute(A arg) throws InterruptedException {
- V result = cache.get(arg);
- if (result == null) {
- result = c.compute(arg);
- cache.put(arg, result);
- }
- return result;
- }
- }
这种方法是最基本的缓存用法,是安全的。但是有一个明显的可伸缩性问题:每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用coumpute的线程可能被阻塞很长时间。如果有多个线程在排队等待还未计算出的结果,那么compute方法的计算时间可能比没有“记忆”操作的计算时间更长。
用ConcurrentHashMap替换HashMap
- package com.chinaso.phl;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public class Memorizer2<A, V> implements Computable<A, V> {
- private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
- private final Computable<A, V> c;
- public Memorizer2(Computable<A, V> c) {
- this.c = c;
- }
- @Override
- public V compute(A arg) throws InterruptedException {
- V result = cache.get(arg);
- if (result == null) {
- result = c.compute(arg);
- cache.put(arg, result);
- }
- return result;
- }
- }
Memorizer2比Memorizer1有着更好的并发行为,ConcurrentHashMap是线程安全的,所以不需要同步compute方法。但是作为缓存仍然有问题----2个线程同时调用的compute的时候,可能会导致计算得到相同的值。因为缓存是用来避免相同的数据被计算多次,但对于更通用的缓存机制来说,这种情况是更糟糕的,对于提供单词初始化对象缓存来说,这个漏洞会存在安全风险。
Memorizer2问题在于,如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么很可能会重复这个计算。我们希望通过某种方法来表达“线程X正在计算f(1226)”这种情况,这样当另一个线程查找f(1226)时,他能够知道最高效的方法是等待线程X计算结束,然后去查询缓存“f(1226)的结果是多少”。
基于FutureTask的Memorizer封装器
FutureTask表示一个计算过程,这个过程可能已经计算完成,也可能正在进行。如果有结果可用,那么FutureTask.get将立即返回结果,否则它会一直阻塞,直到结果计算出来再将其返回。
- package com.chinaso.phl;
- import java.util.Map;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.FutureTask;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public class Memorizer3<A, V> implements Computable<A, V> {
- private final Map<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();
- private final Computable<A, V> c;
- public Memorizer3(Computable<A, V> c) {
- this.c = c;
- }
- @Override
- public V compute(final A arg) throws InterruptedException {
- FutureTask<V> f = cache.get(arg);
- if (f == null) {
- Callable<V> eval = new Callable<V>() {
- @Override
- public V call() throws Exception {
- return c.compute(arg);
- }
- };
- FutureTask<V> ft = new FutureTask<V>(eval);
- f = ft;
- cache.put(arg, ft);
- ft.run(); // 这里调用的是c.compute(arg);
- }
- try {
- return f.get();
- } catch (ExecutionException e) {
- throw new InterruptedException(e.getMessage());
- }
- }
- }
Memorizer3进一步改进了代码。基于ConcurrentHashMap表现出了更好的并发性。但是他仍然有一个漏洞,就是2个线程计算相同值的漏洞。这个漏洞的概率远远小于Memorizer2的情况。但是compute方法中的if代码块仍然是非原子的“先检查再执行”操作。
基于原子操作putIfAbsent的改进
- package com.chinaso.phl;
- import java.util.concurrent.Callable;
- import java.util.concurrent.CancellationException;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ConcurrentMap;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.FutureTask;
- /**
- * @author piaohailin
- * @date 2014-4-23
- */
- public class Memorizer4<A, V> implements Computable<A, V> {
- private final ConcurrentMap<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();
- private final Computable<A, V> c;
- public Memorizer4(Computable<A, V> c) {
- this.c = c;
- }
- @Override
- public V compute(final A arg) throws InterruptedException {
- FutureTask<V> f = cache.get(arg);
- if (f == null) {
- Callable<V> eval = new Callable<V>() {
- @Override
- public V call() throws Exception {
- return c.compute(arg);
- }
- };
- FutureTask<V> ft = new FutureTask<V>(eval);
- // 只有第一个线程添加的时候才会为空,第二个线程此处会获取之前的FutureTask
- f = cache.putIfAbsent(arg, ft);
- if (f == null) {
- f = ft;
- ft.run(); // 这里调用的是c.compute(arg);
- }
- }
- try {
- return f.get();
- } catch (CancellationException e) {
- cache.remove(arg, f);
- return null;
- } catch (ExecutionException e) {
- throw new InterruptedException(e.getMessage());
- }
- }
- }
Memorizer4使用了putIfAbsent的原子方法,从而有效避免了Memorizer3的漏洞。但是这个缓存仍然存在问题。
缓存污染
缓存逾期
缓存清理
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
主要方法
public CountDownLatch(int count);
构造方法参数指定了计数的次数
public void countDown();
countDown方法,当前线程调用此方法,则计数减一
public void await() throws InterruptedException
awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0
示例代码
主要方法
public CountDownLatch(int count);
构造方法参数指定了计数的次数
public void countDown();
countDown方法,当前线程调用此方法,则计数减一
public void await() throws InterruptedException
awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0
示例代码
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.concurrent.CountDownLatch;
- /**
- *
- * @author piaohailin
- * @date 2014-5-6
- */
- public class CountDownLatchDemo {
- final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- public static void main(String[] args) throws InterruptedException {
- CountDownLatch latch = new CountDownLatch(2);//两个工人的协作
- Worker worker1 = new Worker("piaohailin", 5000, latch);
- Worker worker2 = new Worker("chinaso", 8000, latch);
- worker1.start();//
- worker2.start();//
- latch.await();//等待所有工人完成工作
- System.out.println("all work done at " + sdf.format(new Date()));
- }
- static class Worker extends Thread {
- String workerName;
- int workTime;
- CountDownLatch latch;
- public Worker(String workerName, int workTime, CountDownLatch latch) {
- this.workerName = workerName;
- this.workTime = workTime;
- this.latch = latch;
- }
- public void run() {
- System.out.println("Worker " + workerName + " do work begin at " + sdf.format(new Date()));
- doWork();//工作了
- System.out.println("Worker " + workerName + " do work complete at " + sdf.format(new Date()));
- latch.countDown();//工人完成工作,计数器减一
- }
- private void doWork() {
- try {
- Thread.sleep(workTime);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
阿姆达尔定律
阿姆达尔(Amdahl)定律是计算机系统设计的重要定量原理之一,于1967年由IBM360系列机的主要设计者阿姆达尔首先提出。该定律是指:系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。阿姆达尔定律实际上定义了采取增强(加速)某部分功能处理的措施后可获得的性能改进或执行时间的加速比。简单来说是通过更快的处理器来获得加速是由慢的系统组件所限制。
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了如下公式:
S=1/(a+(1-a)/n)
其中,a为串行计算部分所占比例,n为并行处理结点个数。这样,当a=0时,最大加速比s=n;当a=1时,最小加速比s=1;当n→∞时,极限加速比s→ 1/a,这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。这一公式已被学术界所接受,并被称做“阿姆达尔定律”(Amdahl law)。
阿姆达尔(Amdahl)定律是计算机系统设计的重要定量原理之一,于1967年由IBM360系列机的主要设计者阿姆达尔首先提出。该定律是指:系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。阿姆达尔定律实际上定义了采取增强(加速)某部分功能处理的措施后可获得的性能改进或执行时间的加速比。简单来说是通过更快的处理器来获得加速是由慢的系统组件所限制。
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了如下公式:
S=1/(a+(1-a)/n)
其中,a为串行计算部分所占比例,n为并行处理结点个数。这样,当a=0时,最大加速比s=n;当a=1时,最小加速比s=1;当n→∞时,极限加速比s→ 1/a,这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。这一公式已被学术界所接受,并被称做“阿姆达尔定律”(Amdahl law)。
如果在某算法中,一个线程的失败或挂起不会导致其他线程也失败挂起,那么这种算法就被称为非阻塞算法。
如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无所算法(Lock-Free)
如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无所算法(Lock-Free)