Java并发编程实战读书笔记三

 第七章 取消和关闭

Java没有提供任何机制来安全的终止线程,虽然 Thread.stop 和 suspend 等方法提供了这样的机制,但由于存在着一些严重的陷,因此应该避免使用

7.1任务取消

 7.1.1 中断

取消任务中生产者使用了队列的put操作导致阻塞后任务永远无法取消

 

interrupt()、interrupted()、isInterrupted() 的区别

区别1,interrupted()属于类方法,而interrupt()和isInterrupted()属于对象方法。
区别2:
interrupted():返回当前线程的中断标志位,并设置中断标志位false;
interrupt():设置线程对象的中断标志位为true;
isInterrupted():返回线程对象的中断标志位。
关于,当线程满足两个条件,阻塞状态和中断标志为ture,则会抛出InterruptedException异常,并且会自动将中断标志位设置为false,阻塞方法,一般有,Thread.sleep(…),Object.wait(…),join(…),中断不会停止线程。如果不触发InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。调用interrupt并不意味着立即停止自标线程正在进行的工作,而只是传递了请求中断的消息。

在使用静态的 interrupted 时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted 时返回了 true,那么除非你想屏蔽这个中断,否则必须对它进行处理一一可以抛出InterruptedException,或者通过再次调用interrupt 来恢复中断状态

通常,中断是实现取消的最合理方式

通过Future取消任务

7.1.6处理不可中断的阻塞 

包括一下

通过改写interrupt方法将非标准的取消操作封装在Thread中

 7.1.7 采用 newTaskFor 来封装非标准的取消

 

7.2.3“毒丸”对象

采用毒丸关闭线程服务

7.2.4 示例:只执行一次的服务

ExecutorService.submit();//返回Future

ExecutorService.execute();//返回void

关闭线程池标准方法:

shutdown():用于关闭启动线程,如果不调用该语句,jvm不会关闭。

awaitTermination():用于等待子线程结束,再继续执行下面的代码。该例中我设置一直等着子线程结束

  1. service.shutdown();

  2. service.awaitTermination();

7.2.5 shutdownNow 的局限性

 

 

 

7.3 处理非正常的线程终止(线程抛异常)

线程应该在 try-catch 代码块中调用这些任务,这样就能捕获那些未检查的异常了,或者也可以使用 ty-finally 代码块来确保框架能够知道线程非正常退出的情况,并做出正确的响应。

主动方法解决未检查异常

UncaughtExceptionHandler 接口,它能检测出某个线程由于未捕获的异常而终结的情况

 

7.4 JVM关闭

7.4.1 关闭钩子

在正常关闭中,JVM 首先调用所有已注册的关闭钩子 (Shutdown Hook)。关闭钩子是指通过 Runtime.addShutdownHook 注册的但尚未开始的线程

关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源

 7.4.2守护线程

主线程创建的所有线程是普通线程

守护线程和非守护线程的区别?

Java有两种线程:守护线程(Daemon)和 用户线程(User),用户线程也叫普通线程。早在JDK1.5的时候,就规定了当所有非守护线程退出时,JVM才会退出。

1、用户线程:当所有非守护线程退出时,JVM才会退出。
2、守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。

setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为Daemon模式,false为User模式。setDaemon(boolean on)方法必须在线程启动之前调用,当线程正在运行时调用会产生异常。isDaemon方法将测试该线程是否为守护线程。

值得一提的是,当你在一个守护线程中产生了其他线程,那么这些新产生的线程不设置Daemon属性的时候,都将是守护线程,如果设置了Daemon属性,新产生的线程则根据设置的属性决定是守护线程还是用户线程,用户线程同理。

总结:
(1)不存在用户线程的时候jvm会自动退出
(2)通过setDaemon设置用户线程和守护线程
(3)用户线程中新产生的线程默认是用户线程(不设置Daemon属性的时候)
(4)守护线程中新产生的线程默认是守护线程(不设置Daemon属性的时候)
(5)通过isDaemon查看是用户线程还是守护线程

第八章  线程池使用

8.2 设置线程池大小

要设置线程池的大小并不困难,只需要避免“过大”和“过小”这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的 CPU 和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。如果线程池过小,那么将导致许多空闲的处理资源

8.3 配置ThreadPoolExecutor

8.3.1线程的创建与销毁

线程池大小设置为0的情况

8.3.2 管理队列任务

newFixedThreadPool和 newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。

一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但它又带来了新的问题:当队列填满后,新的任务该怎么办? (有许多饱和策略[SaturationPolicy] 可以解决这个问题。请参见 8.3.3 节在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低 CPU 的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。

对于非常大的或者无界的线程池,可以通过使用 SynchronousQueue 来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue 不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor 将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被首先放在队列中,然后由工作者线程从队列中提取该任务。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在 newCachedThreadPool工广方法中就使用了 SynchronousQueue。

当使用像 LinkedBlockingQueue 或 ArrayBlockingQueue 这样的FIFO(先进先出)队列时,任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockingQuewe,这个队列将根据优先级来安排任务。任务的优先级是通过自然顺序或Comparator(如果任务实现了Comparable)来定义的

只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。此时应该使用无界的线程池,例如 newCachedThreadPool 

8.3.3 饱和策略

AbortPolicy、CallerRunsPolicy、DiscardPolicy 和 DiscardOldestPolicy

使用Semaphore 来控制任务的提交速率

 8.3.4 线程工厂

8.3.5在调用构造函数后再定制 ThreadPoolExecutor,比如set方法

通过unconfigurableExecutorService方法能防止 ExecutorService被修改,中间增加了一个包装类,只暴露ExecutorService的方法,隐藏了域

8.4 扩展ThreadPoolExecutor

示例:给线程池添加统计信息

在程序清单8-9的TimingThreadPool中给出了一个自定义的线程池,它通过beforeExecute、afterExecute 和 terminated 等方法来添加日志记录和统计信息收集。为了测量任务的运行时间,beforeExecute 必须记录开始时间并把它保存到一个 afterExecute 可以访问的地方。因为这些方法将在执行任务的线程中调用,因此 beforeExecute 可以把值保存到一个ThreadLocal变量中,然后由 afterExecute 来读取。在 TimingThreadPool中使用了两个AtomicLong 变量,分别用于记录已处理的任务数和总的处理时间,并通过 terminated 来输出包含平均任务时间的日志消息。

第九章 图形用户界面应用程序

第十章 避免活跃性危险

10.1 死锁

数据库如何解决死锁?JVM无法解决死锁

 10.1.2动态的锁顺序死锁

通过System.identityHashCode方法计算hashcode,修改锁顺序处理死锁问题 

 

 10.1.5 资源死锁

比如大量线程连接池等待

10.2 死锁的避免与诊断

10.2.1 支持定时的锁

Lock类中的tryLock可以设置超时时间

10.3 其他活跃性危险

10.3.1 饥饿

要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级

10.3.3 活锁

 第11章 性能与可伸缩性

11.2.1例在各种架中隐的行部分

多个线程从queue中取元素是一个串行过程

增加线程时,ConcurrentLinkedQueue性能优于synchronized Linkedlist

11.3.1 上下文切换

 11.3.2 内存同步

同步操作的性能开销包括多个方面。在 synchronized 和 volatile 提供的可见性保证中可能会使用一些特殊指令,即内存栅栏(Memory Barrier)。内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行管道。内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序的

没有作用的同步操作会被JVM优化,因为其他线程无法重复获得锁

 JVM的锁消除优化

11.4.3 锁分段

        锁分段。例如,在 ConcurrentHashMap 的实现中使用了一个包含 16 个锁的数组,每个锁保护所有散列桶的 1/16,其中第 N个散列桶第(N mod 16)个锁来设散列函数具有合理的分布性,并且关键字能够实现均匀分布,那么这大约能把对于锁的请求减少到原来的1/16。正是这项技术使得 ConcurrentHashMap 能够支持多达16 个并发的写人器。(要使得拥有大量处理器的系统在高访问量的情况下实现更高的并发性,还可以进一步增加锁的数量,但仅当你能证明并发写入线程的竞争足够激烈并需要突破这个限制时,才能将锁分段的数量超过默认的 16 个。
        锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。通常,在执行一个操作时最多只需获取一个锁,但在某些情况下需要加锁整个容器,例如当 ConcurrentHashMap 需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合中时,就需要获取分段所集合中所有的锁。 

基于散列的锁分段的代码实现

 11.4.4 避免热点域

size()等计数方法的优化 

 11.4.5 一些替代独锁的方法

ReadWriteLock

11.4.6 监测CPU的利用率

当测试可伸缩性时,通常要确保处理器得到充分利用。一些工具,例如 UNIX 系统上的vmstat和mpstat,或者 Windows 系统的 perfmon,都能给出处理器的“忙碌”状态。 

 11.6 减少上下的开销

锁竞争剧烈会导致频繁的上下文切换

日志输出使用单独的线程处理性能更好,减少了业务线程IO,减少了上下文切换

程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例。因为 Java 程序中串行操作的主要来源是独占方式的资源锁,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。

第十二章 并发程序的测试

吞吐量:指一组并发任务中已完成任务所占的比例

响应性:指请求从发出到完成之间的时间(也称为延迟。

可伸缩性:指在增加更多资源的情况下(通常指 CPU),吞量(或者缓解短缺)的提升情况。

12.1.6 产生更多的交操作

测试的时候可以使用Thread.yield来产生更多的交替操作,更容易测试出错误 

12.3 避免性能测试的陷阱

12.3.1 垃圾回收会影响性能测试结果

12.3.2 动态编译

编译会对性能测试产生影响

动态编译、垃圾回收以及自动优化等操作都影响与时间相关的测试结果。

第十三章  显式锁

13.1 Lock 与 ReentrantLock

 13.1.1 轮询锁与定时锁

使用tryLock尝试获取锁,失败则重试

 定时锁

tryLock(long time, TimeUnit unit)

13.1.2 可中断的锁获取操作 

13.3 公平性

 非公平锁的性能高于公平锁

 在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B请求这个锁。由于这个锁已被线程 A 持有,因此B 将被起。当A释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B 获得锁的时刻并没有推迟C 更早地获得了锁,并且吞吐量也获得了提高。

公平锁的适用情况:

当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。在这些情况下,“插队”带来的吞吐量提升(当锁处于可用状态时,线却还处于被唤醒的过程中)则可能不会出现。

13.5 读写锁

允许多个读操作同时进行,但每次只允许一个写操作

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}
ReadWriteLock的一些实现方式

 一种读写锁ReentrantReadwriteLock的使用

第十四章 构建自定义的同步工具

14.1.1示例:将前提条件的失败传递给调用者

Queue和BlockingQueue的区别

Queue 提供了上述两种选择,即 poll 方法能够在队列为时返回 ull,而remove 方法则抛出一个异常但Queue 并不适合在生产者-消费者设计中使用BlockingQueue 中的操作只有当队列处于正确状态时才会进行处理,否则将阻塞,因此当生产者和消费者并发执行时,BlockingQueue 才是更好的选择。 

14.1.3 条件队列

使用 wait 和 notifyAll 来实现一个有界缓存

 14.2.4 通知

14.3 显式的 Condition 对象

ReentrantLock 要求在调用 signal 或 signalAl时应该持有 Lock,但在 Lock 的具体实现中,在构造 Condition时也可以不满足这个需求。

14.4 Synchronizer剖析

事实上,它们在实现时都使用了一个共同的基类,即 AbstractQueuedSynchronizer(AQS)这个类也是其他许多同步类的基类。AQS 是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS 很容易并且高效地构造出来。不仅 ReentrantLock 和 Semaphore是基于AQS 构建的,还包括 CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和 FutureTask

14.5 AbstractQueuedSynchronizer

 使用 AbstractQueuedSynchronizer 实现的二元闭锁

第十五章  原子变量与非阻塞同步机制

15.1 锁的劣势

当线程在锁上发生竞争时,智能的JVM 不一定会挂起线程,而是根据之前获取操作中对锁的持有时间长短来判断是使此线程挂起还是自旋等待。

与锁相比,volatile 变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换或线程调度等操作。然而,volatile 变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的复合操作。因此,当一个变量依赖其他的变量时,或者当变量的新值依赖于旧值时,就不能使用 volatile 变量。这些都限制了 volatile 变量的使用,因此它们不能用来实现一些常见的工具,例如计数器或互斥体(mutex)。例如,虽然自增操作(++i) 看起来像一个原子操作,但事实上它包含了3 个独立的操作一一获取变量的当前值,将这个值加 1,然后再写人新值。为了确保更新操作不被丢失,整个的读一改一写操作必须是原子的。到目前为止,我们实现这种原子操作的唯一方式就是使用锁定方法,如第2章的 Counter 所示。

15.2 硬件对并发的支持

乐观锁:对于细粒度的操作,还有另外一种更高效的方法,也是一种乐观的方法,通过这种方法可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试(也可以不重试)。

现代处理器:在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment)以及交换(Swap)等指令,这些指令足以实现各种互斥体,而这些互斥体又可以实现一些更复杂的并发对象。现在,几乎所有的现代处理器中都包含了某种形式的原子读-改写指令,例如比较并交换( Compare-and-Swap)或者关联加载/条件存储(Lad-Linked/StoreConditional)。操作系统和JVM 使用这些指来实现锁和并发的数据结构,但在 Java 5.0之前在 Java类中还不能直接使用这些指令。

15.2.1 比较并交换

CAS语义

 当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值而其他线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同:当获取锁失败时,线程将被挂起),而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争 CAS 时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。@这种灵活性就大大减少了与锁相关的活跃性风险(尽管在一些不常见的情况下仍然存在活锁风险)

15.2.2 非阻塞的计数器

 CAS主要缺点:它将使调用者处理竞争问题(通过重试、回退、放弃),而在锁中能自动处理竞问题(程在获得锁之前将一直阻塞)

15.3 原子变量类

AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。

15.3.1 原子变量是一种“更好的 volatile

 15.3.2  性能比较:锁与原子变量

锁与原子变量在不同竞争程度上的性能差异很好地说明了各自的优势和劣势。在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效地避免竞争。(在单 CPU 的系统上,基于 CAS 的算法在性能上同样会超过基于锁的算法,因为 CAS 在单 CPU 的系统上通常能执行成功,只有在偶然情况下,线程才会在执行读-改 写的操作过程中被其他线程抢占执行。在图15-1 和图 15-2 中都包含了第三条曲线,它是一个使用 ThreadLocal 来保存 PRNG状态的PseudoRandom。这种实现方法改变了类的行为,即每个线程都只能看到自己私有的伪随机数字序列,而不是所有线程共享同一个随机数序列,这说明了,如果能够避免使用共享状态,那么开销将会更小。我们可以通过提高处理竞争的效率来提高可伸缩性,但只有完全消除竞争,才能实现真正的可伸缩性

15.4 非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法

15.4.2 非阻塞的链表

ConcurrentLinkedQueue算法实现

 

15.4.4 ABA问题 

第十六章 Java内存模型

16.1.2 重排序 

 16.1.3 Java内存模型简介

Happens-Before规则

 

16.2.3 安全始化模式

 16.2.4双重检查加锁

常见错误,线程可能看到一个被部分构造的Resource,通过加volatile

 使用final

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值