- 线程带来的问题:a)安全性问题b)活跃性问题c)性能问题
- 要编写线程安全的代码其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问
- Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,”同步”这个术语还包括volatile类型的变量,显示锁以及原子变量
- 在编写并发应用程序时,一种正确的编程方法是:首先使代码正确运行,然后在提高代码的速度。
- 完全有线程安全类构成的程序并不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类
- 线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的
- 无状态对象一定是线程安全的
- 无状态对象、原子性、竟态条件、符合操作
- 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竟态条件。最常见的竟态条件类型就是”先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作
- 计数器,可以通过现有的线程安全类实现如AtomicLong
- 在实际情况中,应尽可能地使用现有的线程安全对象(如AtomicLong)来管理类的状态
- 同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
- 重入意味着获取锁的操作粒度是”线程”,而不是”调用”
- 每个共享的和可变的变量都应该只由一个锁来保护,从而是维护人员知道是那一个锁
- 一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问,如Vector
- 并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护
- 对于每个包含多个变量的不可变性条件,其中涉及的所有变量都需要由同一个锁来保护
- 无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性或性能问题,当执行时间较长的计算或者可能无法快速完成的操作时(如I/O),一定不要持有锁
- 只要有数据在多个线程间共享,就使用正确的同步
- 可见性问题,产生失效值,非原子的64位操作问题,使用volatile声明或同步保护
- 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
- Volatile变量的正确的使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)
- 调试提示,在启动JVM时指定 –servcr命令,将进行更多优化,比如将循环中未被修改的变量提升到循环外部,发现无线循环
- 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性
- 当且仅当满足一下所有条件时,才应该使用volatile变量:a)对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值b)该变量不会与其他状态变量一起纳入不变性条件中c)在访问变量时不需要加锁
- 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭
- 线程封闭技术:Ad-hoc/栈封闭/ThreadLocal类
- 满足同步需求的另一种方法是使用不可变对象:某个对象在被创建后其状态就不能被修改。不可变对象只有一种状态,且有构造函数来控制
- 不可变:a)状态不可修改b)所有域都是final类型c)正确的构造过程
- 可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步
- 要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:a)在静态初始化函数中初始化一个对象引用b)将对象的引用保存到volatile类型的域或者AtomicReferance对象中c)将对象的引用保存到某个正确构造对象的final类型域中d)将对象的引用保存到一个由锁保护的域中
- 通过容器安全发布对象:a)通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中b)通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中c)通过将某个元素放入BlockingQuere或者ConcurrentLinkedQuere中
- 当获得对象的一个引用时,需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁,是否可以修改它的状态,或者只能读取它
- 在并发程序中使用和共享对象时,可以使用一些实用的策略:a)线程封闭b)只读共享c)线程安全共享d)保护对象
- 在设计线程安全类的过程中,需要包含一下三个基本要素:a)找出构成对象状态的所有变量b)找出约束状态变量的不变性条件c)建立对象状态的并发访问管理策略
- 等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密关联
- 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁
- 使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过共有方法来访问锁
- 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量
- Synchronized、volatile或者任何一个线程安全类都对应于某种同步策略,用于在并发访问时确保数据的完整性
- 在设计同步策略时需要考虑多个方面,例如,将哪些变量声明为volatile类型,哪些变量用锁来保护,哪些锁保护那些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作必须是原子操作等。
- servletContext、Httpsession或dataSource等的线程安全性
- 同步容器将所有对容器状态的访问都穿行化,以实现它们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重减低
- 通过并发容器来代替同步容器,可以极大的提高伸缩性并降低风险;ConcurrentHashMap、CopyOnWriteArrayList、CopyonWriteArraySet、BlockingQueue
- 阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)
- 闭锁可以延迟线程的进度直到其到达终止状态,可以用来确保某些活动直到其他活动都完成偶才继续执行
- FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算
- 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量,可以用于实现资源池,例如数据库连接池
- 栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行,闭锁用于等待事件,而栅栏用于等待其他线程
- 并发技巧:a)可变状态是至关重要的b)尽量将域声明为final类型,除非需要它们是可变的c)不可变对象一定是线程安全的d)封装有助于管理复杂性e)用锁来保护每个可变变量f)当保护同一个不变性条件中的所有变量时,要使用同一个锁g)在执行复合操作期间,要持有锁h)如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题i)不要故作聪明的推断出不需要使用同步j)在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的k)将同步策略文档化
- 在线程池中执行任务比为每个任务分配一个线程优势更多:a)重用线程,分摊在线程创建和销毁过程中产生的巨大开销b)请求到达时,工作线程已存在,不会由于等待创建线程而延迟任务的执行,提高了响应性c)通过调整线程池大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败
- 通过使用Executor,可以实现各种调优、管理、监视、记录日志、错误报告和其他功能,如果不使用任务执行框架,那么要增加这些功能是很困难的
- Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor
- 在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议:a)”已请求取消”标志
- 对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点),通常,中断是实现取消的最合理方式
- 最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出,在必要时进行清理,通知某个所有者线程已经退出
- 任务不会在其自己拥有的线程中执行,而是在某个服务(如线程池)拥有的线程中执行,这就是大多数可阻塞的库函数都只是抛出interruptedException作为中断响应,它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作
- 当取消一个生产者-消费者操作时,需要同时取消生产者和消费者
- 关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程
- 线程可分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。普通线程与守护线程之间的差异仅在于当线程退出时发生的操作
- 死锁:过度加锁可能导致”锁顺序死锁”,使用线程池和信号量来限制对资源的使用,可能会导致”资源死锁”
- 在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁
- 有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间
- Amdahl定律告诉我们,程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例
- 提升可伸缩性可通过:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁
- 当某个类第一次被加载时,JVM会通过解释字节码的方式来执行它。在某个时刻,如果一个方法运行的次数足够多,那么动态编译器会将它编译为机器代码,当编译完成后,代码的执行方式将从解释执行变为直接执行
- 当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。在这些情况下,”插队”带来的吞吐量提升则可能不会出现
- 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized
- 读/写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行
- 可以使用java语言和类库提供的底层机制来构造自己的同步机制,包括内置的条件队列、显示的Condition对象以及AbstractQueuedSynchronized框架
- 当使用条件等待时(Object.wait或Condition.await):a)通产都有一个条件谓词-包括一些对象状态的测试,线程在执行前必须首先通过这些测试b)在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试c)在一个循环中调用wait d)确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量e)当调用wait、notify或notifyAll等方法时,一定要持有与条件队列相关的锁f)在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁
- 活跃性故障:死锁、活锁、丢失的信号。丢失的信号:线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词
- 大多数情况下应该有限选择notifyAll而不是单个的notify。只有同时满足两个条件时,才能用单一的notify而不是notifyAll:a)所有等待线程的类型都相同,只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作b)单进单出,在条件变量上的每次通知,最多只能唤醒一个线程来执行
- 对于每个依赖状态的操作,以及每个修改其他操作依赖状态的操作,都应该定义一个入口协议和出口协议,入口协议就是该操作的条件谓词,出口协议则包括:检查被该操作修改的所有状态变量,并确认它们是否使用某个其他的条件谓词变为真,如果是,则通知相关的条件队列
时间: 2024-10-19 03:17:47