Java并发编程实战笔记

2019-10-15  本文已影响0人  何何与呵呵呵

常见问题

哪些类是线程安全的?

哪些类不是线程安全的?

如何创建一个不可变类?

详见代码

如何安全的发布对象?

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方法来安全地发布:

但是安全发布的对象,虽然保证了内存可见性,但是不能保证其是不可变的。对象的发布需求取决于它的可变性:

如何安全的共享对象?

并发程序中使用和共享对象时,一些实用策略:

如何设计一个线程安全类?

设计线程安全类的3个基本要素:

如何扩展现有的线程安全类?

有哪些常见的非线程安全例子?

在多线程环境下,有哪些值得注意的问题?

多线程编程主要规则如下:

关键字

synchronized

volatile

final

ThreadLocal对象

常见的线程安全类使用

Vector和CopyOnWriteArrayList

Hashtable和ConCurrentHashMap

Collecctions.synchronizXXX

ConCurrentMap

任务执行

Executor

Executor是java类库中的任务抽象。其是基于生产者-消费者模式。提交任务的相当于生产者,执行任务的相当于消费者。

Callable与Future

Executor框架使用Runnable作为其基本的任务表示形式。但是Runnable的run方法不能返回一个值或者抛出受检查的异常。在解决有延迟的计算时,使用Callable与Future会带来更好的性能提升。

总结:通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。 当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大的好长,必须定义清晰的任务边界。 某些应用程序中存在着比较明显的任务边界,而在其他一些程序中,则需要进一步分析才能揭示出粒度更细的并行性。

任务的取消和关闭

健壮的程序可以很完善的处理失败、关闭和取消等过程。因此任务的正确关闭和取消很重要。

线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。Thread类中,interrupt方法能中断目标线程(设置中断状态)。
isInterrupted方法能返回目标线程的中断状态。静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这是清除中断状态的唯一方法。
阻塞库方法,如Thread.sleep和Object.wait,join等,都会检查线程何时中断,并且发现中断时提前返回。
它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。
当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断。这样,如果不触发InterruptException,那么中断状态将一直保持,直到明确地清除中断状态。
如何正确理解中断?中断并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。 在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。
如果在调用interrupted时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理—可以抛出InterruptedException,或通过再次调用interrupt来恢复中断状态。 通常,中断是实现取消的最合理方式。

线程池的使用

线程饥饿死锁

在线程池中,如果任务A依赖相同线程池中其它的任务,那么可能产生死锁。 只要线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或者条件,例如某个任务等待另一个任务的返回值或者执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

运行时间较长的任务

当线程池中的任务运行时间都比较长时,可能会影响本来应该运行比较短时间的任务的运行。 解决方法是:

线程池的大小设置多大比较合适?

线程池中线程何时创建和销毁?

线程池如何管理待处理的任务队列

线程池饱和策略

线程工厂

ThreadPoolExecutor定制化

ThreadPoolExecutor可扩展

循环和递归可以使用线程池来提高运算效率

总结: 对于并发执行的任务,Executor框架是一种强大且灵活的框架。 它提供了大量可调节的选型,例如创建和关闭线程的策略,处理队列任务的策略,处理过多任务的策略,并且提供了几个钩子方法来扩展它的行为。

活跃度危险

死锁

饥饿

糟糕的响应性

活锁

总结: 活跃性故障是非常严重的问题,因为当活跃性故障发生时,只有重启应用。 最常见的是锁顺序死锁。所以在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。 其中最好的解决办法是在程序中始终使用开放调用。这将大大减少需要同时持有多个锁的地方,也更容易发现这些地方。

性能与可伸缩性

可伸缩性指的是:当增加计算资源时(例如CPU,内存,存储容量或者I/O带宽),程序的吞吐量或者处理能力能够相应的增加

不用过度担心非竞争同步带来的开销,因为这个基本的机制已经非常快乐,并且JVM能够进行额外的优化以进一步降低或者消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。 发生锁竞争时会发生阻塞:JVM在实现阻塞行为时,可以采用#自旋等待(不断尝试,直到成功)#,或者通过操作系统挂起被阻塞的线程。选择哪种方式主要取决于阻塞的时间。

提高性能和可伸缩性的方式

CPU利用率检测

检测手段:

影响因素:

总结: 由于使用线程是为了充分利用多个处理器的计算能力,因此在并发程序性能的讨论中,通常更多地将侧重点防止吞吐量和可伸缩性上,而不是服务时间。 Amdahl定律告诉我们,程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例。 因为Java程序中串行操作的主要来源是独占方式的资源锁,因此通常可以通过以下方式来提升可伸缩性: - 减少锁的持有时间 - 降低锁的粒度 - 采用非独占的锁或非阻塞锁代替独占锁

性能测试

性能测试的指标

显示锁

ReentrantLock(重入锁)

ReadWriteLock(读写锁)

ReentrantLocksynchronized都是一种标准的互斥锁,但是对于大多数情况来说,互斥锁通常是一种过于强硬的加锁规则,因此也就不必要地限制了并发性。 主要原因是由于:互斥锁不仅限制了写/写,写/都还限制了读/读。而大多数数据结构大部分都是读操作,因此若能够放宽读操作的加锁请求,就能够提升程序的性能。

原子变量和非阻塞同步机制

java.util.concurrent包中许多类都提供了比synchronized机制更高的性能和可伸缩性,主要就是因为:原子变量和非阻塞的同步机制

总结: 非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用做一种"更好的volatile变量",从而为整数和对象引用提供原子的更新操作 非阻塞算法在设计及实现上比较困难,建议只使用已有的类库。

Java的内存模型

@NotThreadSafa
public class DoubleCheckedLocking {
    private static Resource resource;
    public static Resource getInstance() {
        if (resource == null){//非同步操作,有可能此时Resource未初始化完成,则有可能获取到一个失效的值。
            synchronized (DoubleCheckLocking.class) {
                if (resource == null) {
                    resource = new Resource();
                    //do something 
                }
            }
        }

        return resource;
    }
}
上一篇 下一篇

猜你喜欢

热点阅读