《七周七并发模型》阅读笔记(一)一、线程与锁——第一天二、线程与锁——第二天三、线程与锁——第三天

一、线程与锁——第一天

线程与锁模型其实是对底层硬件运行过程的形式化,这种形式化既是该模型最大的优点,也是它最大的缺点。我们借助Java语言来学习线程与锁模型,不过内容也适用于其他语言。

1、知识点

线程与锁模型会带来三个主要的危害:竞态条件、死锁和内存可见性,本节提供了一些避免这些危害的准则:

  • 对共享变量的所有访问都需要同步化;(竞态条件
  • 读线程和写线程都需要同步化;(内存可见性
  • 按照约定的全局顺序来获取多把锁;(死锁
  • 当持有锁时尽量避免调用外星方法;(死锁
  • 应该尽可能缩短持有锁的时间;(死锁

2、自习

  • William Pugh的网站:Java内存模型
  • [http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html](JSR 133(Java内存模型)FAQ)
  • 深入理解Java内存模型-程晓明,这个系列的文章值得仔细研读
  • Java内存模型是如何保证对象初始化时线程安全的?是否必须通过加锁才能在线程之间安全地公开对象? (1)JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。 (2)新的 JMM 还寻求提供一种新的 初始化安全性 保证——只要对象是正确构造的(意即不会在构造函数完成之前发布对这个对象的引用,换句话说,不要让其他线程在其他地方能够看见一个构造期间的对象引用),然后所有线程都会看到在构造函数中设置的 final 字段的值,不管是否使用同步在线程之间传递这个引用。而且,所有可以通过正确构造的对象的 final 字段可及的变量,如用一个 final 字段引用的对象的 final 字段,也保证对其他线程是可见的。这意味着如果 final 字段包含,比如说对一个 LinkedList 的引用,除了引用的正确的值对于其他线程是可见的外,这个 LinkedList 在构造时的内容在不同步的情况下,对于其他线程也是可见的。 (3)在讲了如上的这段之后,如果在一个线程构造了一个不可变对象之后(对象仅包含final字段),你希望保证这个对象被其他线程正确的查看,你仍然需要使用同步才行。
  • 了解反模式“双重检查锁模式”(double-checked locking)以及为什么称之为反模式。 (1)程晓明的这篇文章——双重检查锁定与延迟初始化讲得十分清楚,关键在于:指令重排序导致在多线程情况下,其他线程可能访问到未初始化的对象。 (2)解决方案有二:用volatile修饰instance对象;采用Initialization On Demand Holder idiom方案,即基于类的初始化方案(关键是JVM在初始化类的时候需要获取一把锁)。 (3)选择方法:如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

二、线程与锁——第二天

内置锁虽然方便、灵活,但是也有很多限制:

  • 一个线程因为等待内置锁而进入阻塞后,就无法中断该线程了;
  • 尝试获取内置锁时,无法设置超时;
  • 获得内置锁,必须使用synchronized块;

Java 5之前,常常使用ReentrantLock锁代替synchronized关键字,因为ReentranLock锁可中断、可设置获取锁的超时时间、可实现细粒度加锁(链表上的交替锁)、可使用条件变量。

ReentrantLock的使用模式如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
    《使用共享资源》
} finally {
      lock.unlock();
}

并发编程有时需要等待某个事件发生,条件变量就是为这种情况而生的。使用条件变量的模式是:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
      while(!《条件为真》) {
         condition.await();
      }
    《使用共享资源》
} finally {
      lock.unlock();
}

1、知识点

ReentrantLock和java.util.concurrent.atomic突破了使用内置锁的限制,利用新的工具我们可以做到:

  • 在线程持有锁的时候中断它;
  • 设置线程获取锁的超时时间;
  • 按照任意顺序获取和释放锁;
  • 用条件变量等待某个条件为真;
  • 使用原子变量避免使用锁。

2、自习

  • ReentrantLock创建时可以设置一个描述公平性的变量。什么是“公平”的锁?何时适合使用公平锁?使用非公平的锁会怎样? 根据官方文档中的解释:
public ReentrantLock(boolean fair)
//Creates an instance of ReentrantLock with the given fairness policy.
//**Parameters:**
//fair - true if this lock should use a fair ordering policy

如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的;反之,则是非公平锁。 公平锁的性能不如非公平锁——公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配;另一方面,公平锁可以防止“饥饿”情况的产生,在以TPS为唯一指标的场景下,可以考虑使用公平锁。

  • 什么是ReentrantReadWriteLock?它与ReentrantLock有什么区别?适用于什么场景? ReentrantReadWriteLock的中文名称是读写锁,在多线程场景中,如果没有写线程在操作模板对象,读写锁允许多个读线程同时读。当对于某个数据结构的操作主要是读操作而只有少量的写操作时,就非常适合使用ReentrantReadWriteLock。
  • 什么是“虚假唤醒”(spurious wakeup)?什么时候会发生虚假唤醒?为什么符合规范的代码不用担心虚假唤醒? (1)线程有可能在没有调用过notify()和notifyAll()的情况下醒来; (2)查看如下代码,doWait方法中发生了虚假唤醒——等待线程即使没有收到正确的信号,也能够执行后续的操作。
public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

(3)为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(校注:这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。以下MyWaitNotify2的修改版本展示了这点:

public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}
  • 什么是AtomicIntegerFieldUpdater?它与AtomicInteger有什么区别?适用于什么场景? (1)AtomicIntegerFieldUpdater用于保证已经new出来的实例的原子性,AtomicInteger用于构造具备原子性的Integer实例。 (2)使用第三方库的时候,如果需要给第三方库提供的对象增加原子性,则使用AtomicIntegerFieldUpdater。

三、线程与锁——第三天

java.util.concurrent包不仅提供了第二天介绍的比内置锁更好的锁,还提供了一些通用高效、bug少的并发数据结构和工具。在实际使用中,较之自己实现解决方案,我们应更多地使用这些现成的工具。

1、知识点

  • 使用线程池,而不是直接创建线程
//线程池的大小设置为可用处理器数的2倍
int threadPoolSize = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
while(true) {
  Socket socket = server.accept();
  executor.execute(new ConnectionHandler(socket));
}
  • 使用CopyOnWriteArrayList让监听器相关的代码更简单高效;
  • 使用ArrayBlockingQueue让生产者和消费者之间高效协作;
  • ConcurrentHashMap提供了更好的并发访问。

2、自习

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非典型技术宅

Swift多线程:GCD进阶,单例、信号量、任务组1. dispatch_once,以及Swift下的单例2. dispatch_after3. 队列的循环、挂起、恢复4. 信号量(semaphore

31950
来自专栏大内老A

如何让普通变量也支持事务回滚?

有一次和人谈起关于事务的话题,谈到怎样的资源才能事务型资源。除了我们经常使用的数据库、消息队列、事务型文件系统(TxF)以及事务性注册表(TxR)等,还有那些资...

18880
来自专栏大闲人柴毛毛

轻量级线程池的实现

写在前面 最近因为项目需要,自己写了个单生产者-多消费者的消息队列模型。多线程真的不是等闲之辈能玩儿的,我花了两个小时进行设计与编码,却花了两天的时间调试与运...

53440
来自专栏积累沉淀

Java批处理

批处理 JDBC对批处理的操作,首先简单说一下JDBC操作sql语句的简单机制。 JDBC执行数据库操作语句,首先需要将sql语句打包成为网络字...

48950
来自专栏蜉蝣禅修之道

EJBCA使用之注册用户及创建证书

29840
来自专栏何俊林

阿里、华为、腾讯Java技术面试题精选

18950
来自专栏IT开发技术与工作效率

Redis 全中文总结

33240
来自专栏美团技术团队

高性能队列——Disruptor

背景 Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级)。...

612110
来自专栏码匠的流水账

java9系列(八)Multi-Release JAR Files

java9新支持了multi-release jar的功能,包括jar、javac、javap、jdeps等命令都能支持这个特性。所谓multi-release...

18220
来自专栏liuchengxu

[译]27个Jupyter Notebook小提示与技巧

Jupyter notebook, 前身是 IPython notebook, 它是一个非常灵活的工具,有助于帮助你构建很多可读的分析,你可以在里面同时保留代码...

29920

扫码关注云+社区

领取腾讯云代金券