九、并发 concurrent

第66条 同步访问共享的可变数据

多线程编程,可以充分发挥 多核CPU的性能。

保持即时的(基本类型的)变量可见性,使用volatile。非基本类型的使用 AtomicReference。
变量互斥修改使用Atomic-系列类型。

Java语言规范保证,除了long和double,读或写一个变量是原子的(atomic)即此操作是不可分隔的。但它不能保证一个线程写入的值对于另一个线程将是可见的。
Java语言规范中的内存模型(memory model)规定了一个线程所做的变化,何时以及如何变成对其他线程可见的。

解决并发的最佳办法:

  1. 不共享可变数据
  2. 共享不可变数据

深刻地理解正在使用的框架和类库非常重要,要搞清它们引入的线程。

安全发布对象引用的方法:

  1. 保存在静态域中
  2. volatile域
  3. final域
  4. 通过同步方法进行访问
  5. 做为并发集合中的元素

多个线程共享可变数据时,如果没有同步,可能产生最难以调试的问题,且问题是间歇性的,与时间有关,程序的行为在不同的vm上也可能不同。

第67条 不要过度同步

过度同步会导致性能降低、死锁,甚至不确定的行为。

在同步代码块中,不要调用外部方法(设计成将被覆盖的方法、由客户端以函数形式提供的方法),因为这样的外部方法对于我们是未知的,不知道该方法会做什么事情,也无法控制它。有可能导致程序异常、死锁或数据损坏。

通常,应该在同步区域内做尽可能少的工作。

过度同步对性能的坏影响:

  1. 获取锁所花费的CPU时间
  2. 失去了并行执行代码的机会
  3. 确保每个核都有一个一致辞的内存视图而导致的延迟
  4. 限制vm优化代码的能力

从性能上讲,在这个多核的时代,过度同步的实际成本不仅仅是获取锁所花费的CPU时间,而是指失去了并行执行代码的机会,会浪费掉多核的优势。以及因为需要确保每个核都有一个一致辞的内存视图而导致的延迟。限制vm优化代码的能力。

大部分时候,不要同步你的类,而是应该建立文档,注明它不是线程安全的。如需同步,由外部进行。
在设计时,只有确定一个类一定是用在多线程环境中,才考虑在类内部实现同步。

Java支持同步,但我们应该尽量少用、谨慎地用,在写synchronized关键字时,就要小心后面的代码实现了。

第68条 Executor 和 task 优先于线程

核心类 Executors 提供了静态工厂,创建各种线程池(thread pool)。
核心类 ThreadPoolExecutor 可以控制线程池操作的每个方面。

创建一个工作队列:

        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable(){
            @Override
            public void run() {
				// do sth
            }
        });
        executor.shutdown();

executorService可以完成很多事情:

  • 等待完成一项特殊任务
  • 等待一个任务集合中一个任务或所有任务完成(利用 invokeAny 或 invokeAll)
  • 等待executor service优雅地完成终止(利用 awaitTermination)
  • 可以在任务完成时逐个地获取这些任务的结果(利用 ExecutorCompletionService)

如何选择executor service:

  • 编写的是小程序,或是轻载服务器,使用newCachedThreadPool是个不错的选择,也不需要什么配置。
    如果是大负载的服务器,缓存线程池就不太适用了:在缓存线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就会创建一个新的线程。如果服务器负载太重,以致它所有的CPU都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况更糟。

  • 在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它包含固定数量的线程,或者为了最大限度地控制它,直接使用ThreadPoolExecutor类。

尽量不要自己写一个工作队列,也尽量不要直接使用线程。
现在关键抽象已不再是Thread了,以前它既是工作单元又是执行机制。现在工作单元和执行机制分开了。
工作单元 与 执行机制。以前两者都以Thread做为抽象。现在分开了。
工作单元,称作任务(Task)具体分为:Runnable 和 Callable。
执行任务的通用机制是 executor service。

站在任务的角度来看问题,让一个Executor service替你执行任务,在选择用哪个执行策略方面就获得了极大的灵活性。

从本质上讲,Executor Framework所做的工作是执行,犹如Collections Framework所做的工作是聚集(aggregation)一样。

java.util.Timer的代替:可以用Executor Framework中的ScheduledThreadPoolExecutor来代替。
ScheduledThreadPoolExecutor更加灵活。

  1. Timer只用一个线程来执行任务,在面对长期运行的任务时,会影响定时执行的准确性,有可能会有所延迟。
  2. 如果timer唯一的线程抛出未被捕获的异常时,timer就会停止执行。而使用线程池支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。

java.util.Timer ----> ScheduledThreadPoolExecutor
《java concurrency in practice》

第69条 并发工具优先于wait 和 notify

不要再使用wait和notify方法了,java.util.concurrent提供了更高级的并发工具来代替。
主要分为三类:Executor Framework、并发集合(Concurrent Collection)、同步器(Synchronizer)

并发集合:

为标准的集合接口(List、Queue、Map)提供了高性能的并发实现。这些实现在类内部自己管理同步,如果再在外部同步,只会使程序更慢。

同步的map实现,优先使用ConcurrentHashMap,而不是Collections.synchronizedMap或Hashtable,可以极大地提升并发应该程序的性能。更一般地,应该优先使用并发集合,而不是使用外部同步的集合。

有些集合接口通过阻塞操作进行了扩展,它们会一直等待到可以成功执行为止。如,BlockingQueue扩展了Queue接口,并添加了take等方法,从队列中删除并返回头元素,如果队列为空则等待。

这样阻塞队列就可以做为 工作队列(work queue),即生产----消费队列,一个或多个生产者线程(producer thread)在工作队列中添加项目,当工作项目可用时,一个或多个消费者线程(consumer thread)则从工作队列中取出并处理项目。

大多数Executor service实现(包括ThreadPoolExecutor)都使用BlockingQueue。

同步器(Synchronizer)

是一些使线程能够等待另一个线程的对象,允许他们协调动作。最常用的同步器是CountDownLatch和Semaphore。较不常的是CyclicBarrier和Exchanger。

对于间歇式的定时,应该优先使用System.nanoTime,而不是使用System.currentTimeMills。nanoTime更准确,且不受系统的实时时钟的调整所影响。

如果不得不使用wait和notify时:
wait方法用来使线程等待某个条件。它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。使用wait方法的标准模式:

synchronized (obj) {
	while ( condition is not met ) {
		obj.wait(); //(Releases lock, and reacquires on wakeup)
	}
	...//condition met, Perform action appropriate to condition
}

如上,始终应该使用wait循环模式来调用wait方法,这样会在等待之前和之后测试条件;永远不要在循环之外调用 wait 方法。

一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify请一定要小心,以确保程序活性(liveness)。而使用notifyAll代替notify可以避免来自不相关线程的意外或恶意等待。

第70条 线程安全性的文档化

当一个类的实例或者静态方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立约定的重要组成部分。如果没有文档说明并发性,使用者只能假设,如果假设错误就可能使程序缺少同步,或过度同步。

线程安全性级别:

  1. 不可变的 String Long BigInteger
  2. 完全线程安全 Random ConcurrentHashMap
  3. 部分线程安全 Collections.synchronized包装返回的集合,它们的迭代器iterator要求外部同步。
  4. 非线程安全 如 ArrayList HashMap

线程安全注解:Immutable、ThreadSafe、NotThreadSafe。有条件、无条件安全都合在ThreadSafe中

线程安全说明,根据情况可以写在类级别与方法级别上。

私有锁对象模式只能用在“完全线程安全”的类上。私有锁对象模式特别适合 专门为继承而设计的类。??回头再看

总结:每个类都应该利用字斟句酌的文档说明或 线程安全注解,清楚地表明它的线程安全性。部分线程安全的类必须在文档中指明“哪个方法需要外部同步,及执行时需要获取哪把锁”。如果是完全线程安全的类,考虑使用私有对象锁模式代替外部同步方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续版中灵活地对并发控制采用更加复杂的方法。

第71条 在并发环境下,尽量不要用延迟初始化

延迟初始化(lazy initialization)是延迟到需要域的值时,才将它初始化的行为。除非绝对必要,否则就不要这么做。

不得不用延迟初始化时:

  1. 如果出于性能的考虑需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。
private static class FieldHolder {
	static final FieldTyoe field = computeFieldValue();
} 

static FieldType getField() { return FieldHolder.field }
  1. 如果出于性能考虑需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom)这种模式避免了在域被初始化之后访问这个域时的锁定开销。
private volatile FieldType field;
FieldType getField() {
	FieldType result = field;
	if ( result == null ) { //first check(no locking)
		synchronized(this) {
			result = field;
			if ( result == null )  // second check (with locking)
				field = result = computeFieldValue();
		}
	}
	return result;
}
  1. 有时可以接受重复初始化的实例域,可以用上面的变体,单重检查模式(single-check idiom)
private volatile FieldType field;
FieldType getField() {
	FieldType result = field;
	if ( result == null ) {
		field = result = computeFieldValue();
	}
	return result;
}

第72条 不要依赖于线程调度器

当有多个线程可以运行时,由线程调试器(thread scheduler)决定哪些线程将会运行,以及运行多长时间。任何依赖于线程调试器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

最好的办法是确保可运行线程的平均数量不明显多玩处理器的数量。

如果线程没有在做有意义的工作,就不应该运行。对于Executor Framework,意味着适当地设置线程池的大小,并使任务保持适当地小,彼此独立。任务不应该太小,否则分配的开销也会影响性能。

不要使线程处于busy-wait状态,即反复检查一个共享对象是否符合条件,然后做某些事情。busy-wait会使程序易受调度器的影响,还会极大地增加处理器的负担,降低了同一机器上其他进程可以完成的有用工作量。反面例子如下:

private int count;

public void wait() {
	while(true) {
		synchronized (this) {
			if (count ==0) return;
		}
	}
}

不要使用Thread.yield 和 调整线程优先级 这两种方法。它们仅对调试器作些暗示,不能保证效果,且不具有移植性。

第73条 避免使用线程组

线程组已经过时,不要使用。

Thread.setUncaughtExceptionHandler方法可以把堆栈轨迹定向到一个handler中去,作日志导出。
如果正在设计的类,需要处理线程的逻辑组,可以参考一下executor线程池。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛克Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值