《Effective Java》读书笔记 —— 并发

1.同步访问共享的可变数据

同步:同步不仅可以阻止一个线程看到对象处于不一致的状态之中,还可以保证进入同步方法或者同步代码块的每个线程,都看到一个锁保护的之前所有的修改效果。

Java 语言保证读或者写一个变量是原子的,除非这个变量的类型是 long 或者 double。虽然读写一个变量是原子的,但不能保证一个线程的写入值对于另一个线程将是可见的,归因于 Java 语言的内存模型(可能暂时存放在内存缓存或者寄存器),所以线程间同步也是必要的。

实例一

以下是是个不使用同步的例子,通过一个boolean域实现线程间通信,主线程通过一个静态变量控制后台线程何时停止循环。

private static boolean stopRequested;  // 共享数据
Thread backgroundThread = new Thread(new Runnable(){
    public void run() {
        int i = 0;
        while(!stopRequested) {
            i++;
        }
    }
});
backgroundThread.start();

sleep(1);    // 1s 后设置stopRequested,让另一个线程执行
stopRequested = true;

运行结果,循环永远不会结束,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested的改变。没有同步,不会考虑其他地方,所以编译器会有提升优化

while(!stopRequested) {
  i++;
}

转变为:

if (!stopRequested) {
   while(true) {
       i++;
   }
}

方案一:修改为同步方法数据

private static synchronized void requestStop {
    stopRequested = true;
}

private static synchronized boolean stopRequested() {
    return stopRequested;
}

方案二:stopRequested声明为 volatile ,性能更好,不使用锁。

private static volatile boolean stopRequested;
案例二

使用 volatile 要小心。volatile修饰符只能保证任何一个线程在读取该域的时候都是最近刚刚被写入的值,不能保证执行互斥方法。

private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
    return nextSerialNumber++;
}

由于增量操作符++,不是原子性的,generateSerialNumber方法内部其实执行了两项操作,首先读取值,然后写回一个新值,如果第二个线程在第一个线程读取旧值的期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号,这就是安全性失败,得到错误的结果。

方案一:generateSerialNumber 方法声明增加 synchronized 修饰符,可以删除变量的volatile修饰符

方案二:使用 AtomicLong 类

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber(){
    return nextSerialNum.getAndIncrement();
}
总结

当多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步。如果只需要线程间的交互通信,而不需要互斥,则可以使用volatile修饰符。如果线程间只读取数据,不会修改,那么事实上是不可变的,这种不需要同步机制。

2.避免过度同步

上一条是缺少同步的危险性,本条关注点相反,过度同步可能会导致性能降低、死锁、甚至不确定的行为。

在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成被覆盖的方法,或者是由客户端函数对象的形式提供的方法,这样的方法是外来的。因为外来的无法控制,在同步域中调用可能会导致异常、死锁或者数据损坏。

3.executor和task优先于线程

工作队列:允许客户端将后台异步处理的工作项目加入队列。

Executors 提供了静态工厂,可以创建任何你想要的大多数 executor

不应该编写自己的工作队列,而且尽量不要直接使用线程。

    * Executors.newSingleThreadExecutor();
        * 单个线程执行任务
    * Executors.newCachedThreadExecutor();
        * 缓冲池
        * 编写的是小程序,或者轻量级的服务器
    * Executors.newFixedThreadExecutor();
        * 大负载服务器,可以控制线程数据的线程池
    * Executors.newPoolExecutor();
        * 可以最大限度的控制线程池
    * ScheduledThreadPoolExecuror
        * timer只用一个线程执行任务,对于长期运行的任务,会影响都定时的准确性。
        * ScheduledThreadPoolExecuror支持多线程,更加灵活

使用demo

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable)   // 开始执行
executor.shutdown();            // 优雅地终止

4.并发工具优先于wait和notify

没有理由再使用,1.5以后Java提供更高级的并发工具

更高级的并发工具:

  • Executor Framework
  • 并发集合(Concurrent Collection)
  • 同步器(Synchronizer)
并发集合(Concurrent Collection)

并发工具为标准的集合接口(List、Queue、Map)提供了高性能的并发实现。

同步器(Synchronizer)

同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。

同步器包含:(后两种不常用)

  • CountDownLatch
    • 倒计数锁存器,允许一个或者多个线程等待一个或者多个其他线程来做某些事情
    • 构造器带有一个int,指允许所有在等待的线程被处理之前,必须在锁存器上调用 countDown 方法的次数
  • Semaphore
  • CyclicBarrier
  • Exchanger

5.线程安全性的文档化

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

几种线程安全性级别:

  • 不可变的:这个类的实例是不变的,所以,不需要外部的同步,包括String、Long、BitInteger
  • 无条件的线程安全:这个类的实例可变,但是这个类有足够的内部同步,可以被并发使用,包括Random、ConcurrentHashMap
  • 有条件的线程安全:一部分方法线程安全
  • 非线程安全:类可变,为了并发使用,客户端必须利用自己选择的外部同步包围每一个方法(或者调用序列),包括ArrayList和HashMap
  • 线程队里的:不能安全地被多个线程并发使用,即使有外部同步包围。线程对立的根源通常在于没有同步的修改静态数据,基本没有线程对立的

6.慎用延迟初始化

延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化,这种方法既适用于静态域,也适用于实例域。

优点:降低了初始化类或者创建实例的开销。

缺点:增加了访问被延迟初始化的域的开销。

多线程使用同一延迟初始化域,需要采用某种同步方法。

简单的同步get方法

大多数情况下,正常初始化要优先于延迟初始化,下面是正常初始化

private final FieldType field = computerFieldValue();

下面是支持多线程的延迟初始化

private FieldType field;
synchronized FieldType getField() {
    if (field == null) {
        field = computerFieldValue();
    }
    return field;
}
利用静态内部类实现懒加载

处于性能考虑,也可以使用一下方法

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

第一次调用getField,读取FieldHolder.field,导致FieldHolder类初始化。这种模式的好处在于,不需要对getField使用同步。VM在初始化类时,是同步访问的

双重检查模式

避免了在域被初始化之后访问这个域时的锁定开销。

private volatile Field field;
FieldType getField() {
    FieldType result = field;
    if (result == null) {
        synchronized(this) {
            result = field;
            if(result == null) {
                field = result = computerFieldValue();
            }
        }
    }
}
单重检查模式
private volatile Field field;
FieldType getField() {
    FieldType result = field;
    if(result == null) {
        field = result = computerFieldValue();
      }
    }
}
总结

大多数情况,都应该进行正常初始化,而不是延迟初始化。如果要使用,对于实例域,使用双重检查模式,对于静态域,使用功能静态内部类懒加载,对于可以接受重复初始化的实力域,可以考虑使用单重检查模式。

7.不要依赖于线程调度器

线程调度器:决定哪些线程将会运行以及运行多长时间。

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。不要依赖 Thread.yield 或者线程优先级

8.避免使用线程组

线程组:允许你同时把Thread的某些基本功能应用到一组线程上。

线程组并没有提供太多有用的功能,而且它们提供的许多功能还是有缺陷的。或者应该使用线程池。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,787评论 1 19
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,170评论 11 349
  • 并发系列的文章都是根据阅读《Java 并发编程的艺术》这本书总结而来,想更深入学习的同学可以自行购买此书进行学习。...
    小之丶阅读 1,035评论 1 7
  • 我试着扣开命运的窄门,把后面的日子拨一下,把我想要的东西放在每一天里,我会紧紧抓住不放手。如果我懂倒转回过...
    被解放的过来客阅读 441评论 0 2
  • 小学的时候,最要好的朋友是鸣。我们是同班,她就坐在我前排。 记忆中,我们的友谊应该是始于那条湖边的小路。刚上学那阵...
    Wendy_Yin阅读 407评论 4 7