java并发编程实战——读书笔记

 value++; 包含三个独立操作:读取value,将value加1,并将计算结果写入value
如果错误的假设程序中的操作将按照某种特定顺序来执行,那么会存在各种可能的危险。

框架中如果有多线程并发性,那使用框架的应用程序代码也会遇到并发性问题,在代码中会访问应用程序的状态,所有访问这些状态的代码都应该考虑线程安全问题。
Timer\Servlet 、JSP \ RMI远程方法调用\Swing 和AWT 都会引入线程安全问题。

同步:synchronized, volatile,显式锁,原子变量
 解决同步问题:1不在线程之间共享该状态变量,2将状态变量修改为不可变的变量,3在访问状态变量时使用同步。

程序状态的封装性越好,就越容易实现线程安全。

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么久称这个类是线程安全的。

在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。

无状态对象一定是线程安全的。
先检查后执行 是一种很典型的竞态条件。

内置锁是可重入的,就是同一个线程可以继续获取自己持有的锁。锁计数器+1,退出同步块时锁计数器-1,如果锁计数器为0,则该锁没有被任何线程持有。

JAVA的锁以线程为粒度,POSIX的锁以“调用”为粒度。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

应该尽量把执行时间较长又不影响同步状态的操作从同步代码块中分离出去,从而在这些操作执行过程中,其他线程可以访问共享方法。
在获取和释放锁操作上都需要一定的开销,因此同步代码块也不要分得过细。
同步代码块安全性必须保证,简单性和性能之间需要找到某种平衡。

当执行时间较长的计算或者可能无法快速完成的操作时(I/O),一定不要持有锁。

JVM的“重排序”,可能使多线程程序出现不可预料的问题。

非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。可能会读取到某个值的高32位和另一个值得低32位。64位操作系统上读写64位数值是原子操作。

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。

volatile可以用来标示一些重要的程序生命周期事件的发生。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。 count++ volatile不能确保原子性。
当且仅当满足以下条件时才使用volatile:
  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。
封装能够使得对程序的正确性进行分析变得可能,并且更难破坏设计约束条件。
不要在构造函数中使this引用逸出。因为当前this尚未构造完毕。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。

线程封闭:
应用程序服务器提供的连接池是线程安全的。
局部变量和ThreadLocal类是线程封闭的。
使用线程封闭技术,可以把某个特定的子系统实现为单线程子系统。
ThreadLocal的get方法第一次调用时,会执行initialValue来获取初始值。
当某个频繁执行的操作需要一个临时对象,而同时又希望避免在每次执行时都重新分配该临时对象,这时就可以使用ThreadLocal。防止对可变的单实例变量或全局变量进行共享。当线程终止后,ThreadLocal的值会作为垃圾回收。
不变性:
不可变对象一定是线程安全的。
Volatile Cached Factorizer 实现线程安全。
安全发布:
在未被正确发布的对象中存在两个问题。手续,除了发布对象的线程外,其他线程可以看到的Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。然后,更糟糕的情况是,线程看到Holder引用的值时最新的,但Holder状态的值却是失效的。情况变得更加不可预测的是,某个线程的第一次读取域时得到失效值,而再次读取这个域时会得到一个更新值。
Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他对象线程可见。
  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。
线程封闭、只读共享、线程安全共享、保护对象。

实例封闭:
deepCopy并不只是用unmodifiableMap来包装Map的,因为这只能防止容器对象被修改,而不能防止调用者修改保存在容器中的可变对象。基于同样的原因,如果只是通过拷贝构造函数来填充deepCopy中的HashMap,那么同样是不正确的,因为这样做只是复制了只想Point对象的引用,而不是Point对象本身。
由于每次调用getLocation就要复制数据,虽然车辆的实际位置发生了变化,但返回的信息却保持不变。

如果一个类是由多个独立的线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。如果含有复合操作,则这个类必须提供自己的加锁机制以保证这些复合操作都是源自操作,除非整个复合操作都可以委托给状态变量。

扩展方法比直接将代码添加到类中更脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变化,那么子类会被破坏。
通过组合的办法,为现有的类添加一个原子操作,更好。

在设计同步策略时需要考虑多个方面,例如:将哪些变量声明为volatile类型,哪些变量用锁来保护,哪些锁保护哪些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作必须是原子操作等。
java.text.SimpleDateFormat不是线程安全的。
将同步策略文档化。
Collections.synchronizedXxx 实现线程安全的方式:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器,并在副本上进行迭代。会存在显著的性能开销。这种方式的好坏取决于对个因素:容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用平率,以及在响应时间和吞吐量等方面的需求。

标准容器的toString方法将迭代容器,并在每个元素上调用toString来生成容器内容的格式化表示。
通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。

ConcurrentHashMap使用一种粒度更细的加锁机制来实现更大程度的分享,分段锁。在这种机制下,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发的访问Map,并且一定数量的写入线程可以并发的修改Map。在并发访问环境下将实现更高的吞吐量,而在单线程下损失非常小的性能。它提供的迭代器不会跑出COncurrentModificationException,因此不需要在迭代过程中对容器加锁。
对于一些需要在整个Map上进行计算的方法,例如Size和isEmpty,这些方法的雨衣被略微减弱了以反映容器的并发特性。由于size返回的结果在计算时可能已经过期了,它是集上只是一个估计值。get、put、containKey和remove的性能更强了。
CopyOnWriteArrayList 的迭代器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。 可以用在事件通知系统中。

阻塞队列生产者消费者模式。 有时需要调整生产者线程数量和消费者线程数量之间的比率,从而实现更高的资源利用率。
put会阻塞,offer不会阻塞会返回一个状态。
在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具,它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
BlockingQueue、LinkedBlockingQueue、ArrayBlockingQueue是FIFO队列。PriorityBlockingQueue优先级队列。
SynchronousQueue 不会为队列中元素维护存储空间,它维护一组线程。这些线程在等待把元素加入或移除队列。put和take会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付工作时,才适合使用同步队列。
SynchronousQueue  它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、 事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
生产者和消费者可以并发的执行,如果一个是I/O密集型,另一个是CPU密集型,那么并发执行的吞吐率。如果生产者和消费者的并行度不同,那么将它们紧密耦合在一起会把整体并行度降低为二者中更小的并行度。
BlockingDeque 双端队列 的使用场景是工作密取, 当执行某个工作时可能导致出现更多的工作的场景。网页爬虫、搜索图算法,垃圾回收阶段对堆进行标记等。

CountDownLatch 闭锁,确保某个计算在其需要的所有资源都被初始化之后才继续执行。确保某个服务在其所依赖的所有其他服务都已经启动之后才启动。等待直到某个操作的所有参与者都就绪再继续执行。
CountDownLatch是一种灵活的闭锁实现,可以使一个或多个线程等待一组事件的发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。

FutureTask
Future.get的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则get将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。
 Semaphore:用来控制同时访问某个特定资源的操作熟练,或者同时执行某个特定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。
Semaphore管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可,并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可。release方法将返回一个许可给信号量。初始值为1的Semaphore可以用做互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。

在不涉及I/O操作或共享数据访问的计算问题中,当现实数量为CPU或CPU+1时将获得最优的吞吐量。更多的线程并不会带来任何帮助,甚至在某种程度上会降低性能,因为多个线程将会在CPU和内存等资源上发生竞争。

CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。如果对await的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都将终止并抛出BrokenBarrierException。如果成功通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,可以用来选举。
将一个问题分解成一定数量的子问题,为每个子问题分配一个线程来进行求解,之后再将所有结果合并起来。

当线程A调用Exchange对象的exchange()方法后,他会陷入阻塞状态,直到线程B也调用了exchange()方法,然后以线程安全的方式交换数据,之后线程A和B继续运行。
当两方执行不对称的操作时,Exchanger会非常有用。比如一个线程向缓冲区写入数据,另一个线程从缓冲区读取数据。将满的缓冲区与空的缓冲区交换。

你可能感兴趣的:(并发,并发,java,多线程)