Java并发实战

看完之后感觉Doug Lea写的书都是精品,不过有些地方翻译的确实不怎么样...

线程

线程比进程更轻,更易于共享数据,关于Java中的线程要了解:

  1. 线程安全
  2. 可见性
  3. 中断
  4. 关闭钩子
  5. 守护线程
  6. ThreadLocal
  7. 线程的开销

很长一段时间对线程安全的定义比较迷糊,下面这个定义感觉是比较准确的:

当多个线程访问某个类时,不管运行时环境采用何种调度方式,该类都能表现出正确的行为,那么就称这个类是线程安全的。

或者更简单地描述:

结果与调度的方式无关。

看似简单但坑却不少,比如多线程中可见性这种反直觉的东西:

private static boolean ready;
private static int number;
static class ReaderThread extends Thread {
    public void run() {
        while(!ready){
            Thread.yield();
        }
        System.out.println(number);
    }
}
public static void main(String[] args){
    new ReaderThread().start();
    number = 42;
    ready = true;
}

这里程序可能持续运行下去,因为ReaderThread可能永远看不到ready的值;程序也可能输出0,因为ReadyThread可能只看到写入ready的值:

在没有同步的情况下编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。

开启线程只需要一个start,而想让它停下来就没那么简单了,因为stop已经不建议再使用,原因是:

  1. 在任意位置抛出ThreadDeath
  2. 释放锁
  3. 关闭时需要同步,如果run是同步的那就永远都无法关闭

取而代之的是中断,但是中断并不代表停止,而是:

调用interrupt并不意味着立即停止目标线程的执行,而只是传递请求中断的消息。

但是比较靠谱的是在很多库中已经对中断做了处理:

  1. 在调用join、sleep、wai等阻塞返回后会抛出异常并擦除中断状态
  2. 在IO操作上阻塞时也会抛出异常并关闭流
  3. 在java.nio.channel.Selector上等待时也会立即返回

如果没有上面这些情况的话,调用中断不会对线程的执行造成半毛钱影响,如果你需要你的线程去响应中断,需要在合适的地方增加代码去判断并处理。

想让线程按照我们的意愿停止已经很麻烦了,但是如果它运行的时候还抛出莫名其妙的异常就更加恼火了,不过幸好还有:

<pre class="prettyprint">
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
</pre>

有时候我们需要在程序运行完成的时候做一些扫尾的工作,可以用关闭钩子来实现:

Runtime.getRuntime().addShutdownHook(new Thread(){
    public void run(){
        // your code
    }
})

只有JVM是正常结束的时候才会运行钩子,被暴力kill掉的时候就没机会执行了。既然讲到JVM的退出刚好可以看下Daemon和正常线程的区别:

当线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM将退出。

类似垃圾回收这些线程就得设置成守护线程,不能让他们影响了JVM的正常退出。在开启多线程的时候ThreadLocal真是一个神器,可以让很多信息的传递变得非常简单,具体可以看这里

最后,线程不仅在效率还是体验方面都带来了巨大的提升,单并非多多益善,增加线程也同时会增加一些开销:

  1. 上下文切换
  2. 内存同步(这个影响很小)
  3. 阻塞(竞争的同步可能需要操作系统的介入)

现在大家已经很少new Thread().start();这么玩了,大部分都会用线程池来管理,下面接着看:

线程池

创建新线程也是有开销了,所以可以将线程缓存下来重复利用,在使用线程池时需要注意:

  1. 阀值
  2. 饱和策略
  3. ThreadFactory

平时用到的大部分线程池的底层都是ThreadPoolExecutor,在初始化时有几个关键的阀值需要注意:

阀值 作用
corePoolSize 在execute时如果当先线程数小于该值则直接创建新线程来运行
maximumPoolSize 最大线程数
keepAliveTime Worker取任务的等待时长,超时后Worker就退出了

如果在execute的时候线程池已经满了,同时任务队列也满了,那么此时任务怎么办?这时候就饱和策略就要上场了:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

同时提供四种实现:

  1. AbortPolicy:抛异常
  2. CallerRunsPolicy:退回到调用者线程处理任务
  3. DiscardOldestPolicy:抛弃最老任务
  4. DiscardPolicy:抛弃

最后可以通过ThreadFactory来设置线程池中线程的属性:

public interfact ThreadFactory{
    Thread newThread(Runnable r);
}

在execute时如果需要创建新的线程就会调用该方法。

多个线程同时跑为了保证结果的准确锁是少不了的:

  1. 锁的实现
  2. 死锁
  3. 活锁
  4. 减少锁竞争

常用的锁有两种实现:

  1. synchronized
  2. concurrent

在之前concurrent要比synchronized快很多,但这个已经是很久之前的事情了,在synchronized的实现中通过偏向锁、轻量级锁优化了性能,而concurrent则是Doug Lea的作品。

相比较来说各有优劣:

synchronized缺点是不够灵活,但是你不需要费心释放锁;而concurrent使用很灵活,可以分别在不同的地方加锁、解锁,但是如果忘记解锁就悲剧了,一般用try-finally来做。

另外concurrent中实现了各种策略的锁:

作用
ReentrantLock 可重入独占锁
ReentrantReadWriteLock 可重入读写锁
Condition 条件
CountDownLatch 向下递减,到0时线程被唤醒
CyclicBarrier 有指定书目的线程await之后,一起开始执行
FutureTask 先提交任务,在get时阻塞
Semaphore 信号量

其实concurrent中核心是AQS框架,它将等待队列等操作都进行了封装,你只需要实现下面四个方法即可实现需要的锁:

  1. tryAcquire
  2. tryAcquireShared
  3. tryRelease
  4. tryReleaseShared

锁用的比较多的时候就容易产生死锁,一般原因分为下面几类:

  1. 锁顺序死锁
  2. 资源死锁
  3. 线程饥饿死锁:在同一个线程池中执行有互相依赖的任务时需要注意

可以用jstack –l pid来检测死锁。既然看到到了死锁,那就顺便看一下活锁:

不停地取出第一个任务,但是每次都会失败。

其实活锁看起来就是代码写的太挫了...用锁的时候除了导致程序的活跃性问题,还可能导致性能问题,减少锁开销的一般方法为:

  1. 缩小同步的范围,快进快出
  2. 减少锁的粒度
  3. 锁分段

用锁一定要慎重。

总结

多线程编程中还需要看看非阻塞算法Java内存模型,而真正在写代码的时候能写好确实是一件非常不容易的事情,通常需要通过很多代码的磨练总结在使用各个工具时候的一些基本原则,才能慢慢顺手一些。

更多文章可以看这里

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

推荐阅读更多精彩内容