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

JVM对不大于32位的基本类型的操作都是原子操作,所以读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,但它并不能保证一个线程写入的值对于另一个线程是可见的。因此在读或写原子数据时,使用线程同步是有必须要的,否则将时线程间数据不一致。

public class ThreadTest {
private static boolean stopRequested; //原子操作 public static void main(String[] args) throws Exception{
Thread thread = new Thread (new Runnable() {
public void run() {
int i=0;
while(!stopRequested) {
i++;
}
}
});
thread.start();
Thread.sleep(1000);
stopRequested = true;
}
}

上面这段代码中,由于boolean域的读和写操作都是原子操作,你可能期待这个程序运行大约1秒钟后,主线程将stopRequested设置为true,致使thread线程的循环终止。但事实上这个程序永远也不会停止:thread线程永远在循环。问题在于,thread的线程不能「看到」主线程对stopRequested所做的改变。

修正这个问题的一种方式是同步访问stopRequest域:

public class ThreadTest {
private static volatile boolean stopRequested; private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
} public static void main(String[] args) throws Exception{
Thread thread = new Thread (new Runnable() {
public void run() {
int i=0;
while(!stopRequested()) {
i++;
}
}
});
thread.start();
Thread.sleep(1000);
requestStop();
}
}

注意:写方法(requestStop)和读方法(stopRequested)都必须被同步,否则不起作用。(好像有一个就够了???)

第二种方式:使用volatile

private static volatile boolean stopRequested;

volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。


使用volatile时必须小心,考虑下面的代码:

private static volatile int nextNumber = 0;
public static int generateNumber() {
return nextNumber++;
}

这个方法的目的是确保每次调用都返回不同的值。虽然变量nextNumber是可原子访问的域,但它依然不能正常工作。问题在于,增量操作符「+」不是原子的。nextNumber++执行两项操作:首先它读取值,然后写回一个新值。若第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程获得同样的值。这就是「安全性失败」。

修正的办法是在方法的声明中增加synchronized修饰符或使用类AtomicLong,如:

private static final AtomicLong nextNumber = new AtomicLong();

public static long generateNumber() {
return nextNumber.getAndIncrement();
}

总之,当多个线程共享可变数据的时候,每个读或写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的「活性失败」和「安全性失败」。

67、避免过度同步

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

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要调用外来方法。即不要调用为了覆盖而设计的方法,或者是由客户端以函数的形式提供的方法(如,观察者模式)。如下面这个类,该类允许客户端在将元素添加到集合中时预定通知(观察者模式)。

import java.util.*;  

public class ObservableSet<E>{
private final Set<E> s;
private final List<SetObserver<E>> observers =
new ArrayList<SetObserver<E>>(); public ObservableSet(Set<E> s) {
this.s = s;
} //预定通知
public void addObserver(SetObserver<E> observer){
synchronized (observers) {
observers.add(observer);
}
} //取消通知
public boolean removeObserver(SetObserver<E> observer){
synchronized (observers) {
return observers.remove(observer);
}
} //通知所有观察者
private void notifyElementAdded(E element){
synchronized (observers) {
for(SetObserver<E> observer : observers){
observer.added(this, element);
}
}
} public boolean add(E element) {
boolean added = s.add(element);
if (added) {
notifyElementAdded(element);
}
return added;
} public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
set.addObserver(new SetObserver<Integer>() { @Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.print(element+" ");
if(element == 23)
set.removeObserver(this);
}
}); for(int i = 0; i < 100; i++){
set.add(i);
}
}
} //声明接口
interface SetObserver<E> {
void added(ObservableSet<E> set, E element);
}

上述代码将产生ConcurrentModificationException异常。在上面这段代码中,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中。而added方法最终会调用observers.remove方法。程序企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。

若我们用另一个线程来完成取消通知的操作,但是不直接调用removeObserver。如下面的代码使用了一个executor service:

set.addObserver(new SetObserver<Integer>() {
@Override
public void added(final ObservableSet<Integer> set, Integer element) {
System.out.print(element+" ");
if (element == 23) {
ExecutorService executorService = Executors.newSingleThreadExecutor(); final SetObserver<Integer> observer = this;
try {
executorService.submit(new Runnable() {
@Override
public void run() {
set.removeObserver(observer);
}
}).get();
} catch (ExecutionException ex) {
throw new AssertionError(ex.getCause());
}catch (InterruptedException ex) {
throw new AssertionError(ex.getCause());
}finally{
executorService.shutdown();
}
}
}
});

上面的代码将出现死锁。后台线程调用set.removeObserver,它企图锁定observers,但该锁已经被主线程持有。而主线程则一直在等待后台程序来完成对观察者的删除,这就是造成死锁的原因。

通过将外来的方法调用移出同步代码块可以有效的解决上述的死锁和异常问题。java1.5后,java类库提供了一个并发集合「CopyOnWriteArrayList」,它是ArrayList的一种变体,通过重新拷贝整个底层数组,实现所有的写操作。如:

private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>();

	public void addObserver(SetObserver<E> observer) {
observers.add(observer);
} public boolean removeObserver(SetObserver<E> observer) {
return observers.remove(observer);
} private void notifyElementAdded(E element) {
for (SetObserver<E> observer : observers) {
observer.added(this, element);
}
}

过度同步将影响程序的性能,原因:

  • 程序将失去并行的机会
  • cpu需要确保每个核有一个一致的内存视图而导致延迟
  • 限制了JVM优化代码的能力

总之,为了避免死锁和数据破坏,千万不要从同步区域内调用外来的方法。要尽量限制同步区域内部的工作量。当你设计一个可变类的时候,要考虑一下他们是否应该自己完成同步操作。只有当你有足够的理由一定要在内部同步类的时候,才可以这样做,同时还应该将这个决定清楚的写在文档中。

68、executor和task优先于线程

在java1.5后,java平台增加了「Executor Framework」,这是一个灵活的基于接口的任务执行工具。它创建了一个工作队列用于执行任务。如:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable {
@Override
public void run() {
.....
}
});
....
executor.shutdown(); //终止前允许执行以前提交的任务

若想让多个线程来处理这个队列中的任务,可以使用Executors.newCachedThreadPool()

不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在工作单元和执行机制是分开的。工作单元也称作「任务」。任务有两种:「Runnable」和「Callable」。执行机制一般为「executor service」。

69、开发工具优先于wait和notify

。。。。

直接使用wait和notify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言。没有理由在新代码中使用wait和notify,即使有,也是极少的。如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定要小心,以确保程序的活性。

70、线程安全性的文档化

线程安全性有多种级别。一个类为了可被多个线程安全的使用,必须在文档中清楚的说明它所支持的线程安全级别。

常见的线程安全级别有:

  • 不可变的(immutable)—这个类的实例是不可变的,不需要外部同步。如,「String」、「Long」和「BigInteger」。
  • 无条件的线程安全(thread-safe)—这个类的实例是可变的,但是其有足够的内部同步,无需任何外部同步。如,「Random」和「ConcurrentHashMap」。
  • 有条件的线程安全(thread-safe)—除了一些方法需要外部同步之外,其它与无条件的线程安全相同。如,Collections.synchronized返回的集合,它们的迭代器(iterator)要求外部同步。
  • 非线程安全(not thread-safe)—这个类的实例是可变的,为了并发的使用它们,每个方法都需要外部同步。如,「ArrayList」和「HashMap」等。

    -线程对立的(thread-hostile)—即使使用外部同步,这个类也不能被多个线程并发使用。线程对立的根源一般在于没有同步的修改静态数据。

对于有条件的线程安全必须在文档中指明哪个调用序列需要外部同步,还要指明为了线程同步必须获得哪把锁。如,「Collections.synchronizedMap」的文档


/**
* It is imperative that the user manually synchronize on the returned map
* when iterating over any of its collection views:
*/ Map<K, V> m = Collections.synchronizedMap(new HashMap<K, V>());
...
Set<K> s = m.keySet();
...
synchronized(m) { //同步m,不是s
for(K key : s) {
key.f();
}
}

对于无条件的线程安全类,应该考虑使用私有锁对象来代替同步的方法(把锁对象封装在它所同步的对象中)。这样可以防止客户端和子类的不同步干扰,如客户端超时的持有公有类的锁,将导致这个类的同步方法不能访问。

private final Object lock = new Object();
public void foo() {
synchronized(lock) { //使用私有锁对象代替同步方法
...
}
}

私有锁对象模式适用于那些专门为继承而设计的类,如这种类使用它的实例作为锁对象,子类可能很容易在无意中妨碍基类的操作。

71、慎用延迟初始化

「延迟初始化」是延迟到需要域的值时才将它初始化的行为。像大多数优化一样,对于「延迟初始化」,除非绝对必要,否则不要这样做

「延迟初始化」降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。在大多数情况下,正常的初始化要优先于延迟初始化。若利用延迟优化,就要使用同步访问方法。如:

//直接初始化
private final FieldType field = computeFieldValue(); //延迟初始化,必须同步
private FieldType field;
synchronized FieldType getField() {
if(field == null)
field = computeFieldValue();
return field;
}

若出于性能的考虑,需要对静态域使用延迟初始化,就是用lazy initialization holder class模式。如:

private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}

当getField方法第一次被调用时,FiledHolder类得到初始化。这种模式的好处在于,getField方法没有被同步,并且只执行一个域访问(原子操作),因此延迟初始化没有增加任何访问成本。

若出于性能的考虑,需要对实例域使用延迟初始化,就是用双重检查模式。如:

private volatile FieldType field; //volatile很重要
FieldType getField() {
if(field == null) {
synchronized(this) {
if(field == null) {
field = computeFieldValue();
}
}
}
}

总之,大多数域应该正常进行初始化,而不是延迟初始化。

72、不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。线程优先级是Java平台上最不可移植的特征了。Thread.yield的唯一用途是在测试期间人为地增加程序的并发性。

总之,不要让应用程序的正确性依赖于线程调度器,不要依赖Thread.yield或者线程优先级。否则,应用程序将既不健壮,也不具有可移植性。

73、避免使用线程组

不要使用线程组。如果你正在设计的一个类需要处理线程的逻辑组,可以使用线程池executor。

Effective java笔记(九),并发的更多相关文章

  1. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  2. effective java笔记之java服务提供者框架

    博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...

  3. Effective java笔记(二),所有对象的通用方法

    Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...

  4. effective java笔记之单例模式与序列化

    单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...

  5. Effective java笔记(八),异常

    57.只针对异常的情况才使用异常 try { int i = 0; while(true) range[i++].climb(); }catch(ArrayIndexOutOfBoundsExcept ...

  6. [Effective Java]第十章 并发

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  7. Effective java笔记7--线程

    一.对可共享数据的同步访问 synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块.正确地使用同步可以保证其他任何方法都不会看到对象处于不一致的状态中,还能保证通 ...

  8. Effective java笔记6--异常

    充分发挥异常的优点,可以提高一个程序的可读性.可靠性和可维护性.如果使用不当的话,它们也会带来负面影响. 一.只针对不正常的条件才使用异常 先看一段代码: //Horrible abuse of ex ...

  9. Effective java笔记5--通用程序设计

    一.将局部变量的作用域最小化      本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...

随机推荐

  1. GIT的认识

    说实话,在听到小伙伴们都说赶紧做作业的时候很茫然,连一点头绪都没有,根本不知道从何入手,但不能因为不会就不去做,于是还是拿起手机,找到小伙伴商量着做着,虽然等的过程很焦急,但还是注册成功了.而开始写对 ...

  2. C语言中的sizeof()

    sizeof,一个其貌不扬的家伙,引无数菜鸟竟折腰,小虾我当初也没少犯迷糊,秉着"辛苦我一个,幸福千万人"的伟大思想,我决定将其尽可能详细的总结一下. 但当我总结的时候才发现,这个 ...

  3. 拥抱.NET Core,学习.NET Core的基础知识补遗

    前言 .NET Core的新特性之一就是跨平台,但由于对之前框架的兼容导致编写一个.NET Core类库变得相当复杂,主要体现为相当多的框架目标和支持平台,今天我们就对.NET Core的跨平台特性进 ...

  4. 简单明了区分escape、encodeURI和encodeURIComponent

    一.前言 讲这3个方法区别的文章太多了,但是大部分写的都很绕.本文试图从实践角度去讲这3个方法. 二.escape和它们不是同一类 简单来说,escape是对字符串(string)进行编码(而另外两种 ...

  5. 类库间无项目引用时,在编译时拷贝DLL

    例一: xcopy $(TargetPath) $(SolutionDir)\Framework\HCSP.App\bin\Debug /y 例二: xcopy $(TargetPath) $(Sol ...

  6. MySQL 启动服务报错解决方案

    标签:ERROR! The server quit without updating PID file (/var/lib/mysql/localhost.localdomain.pid) 概述 文章 ...

  7. iOS block种类和切换

    block 分为三种 NSGlobalBlock,NSStackBlock, NSMallocBlock. NSGlobalBlock:类似函数,位于text段: NSStackBlock:位于栈内存 ...

  8. Azure PowerShell (12) 通过Azure PowerShell创建SSH登录的Linux VM

    <Windows Azure Platform 系列文章目录> 本章将介绍如何使用Azure PowerShell,创建SSH登录的Linux VM 前提要求: 1.安装Azure Pow ...

  9. JS实战 · 零碎笔记

    onclick:单击时触发事件 onmouseover:鼠标进入时触发事件 onmouseout:鼠标离开时触发事件   事件三要素:最基础的内容 事件源:有监听的HTML 标签,能响应事件的HTML ...

  10. C#设计模式-享元模式

    在软件开发过程,如果我们需要重复使用某个对象的时候,如果我们重复地使用new创建这个对象的话,这样我们在内存就需要多次地去申请内存空间了,这样可能会出现内存使用越来越多的情况,这样的问题是非常严重,然 ...