前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《七周七并发模型》阅读笔记(一)一、线程与锁——第一天二、线程与锁——第二天三、线程与锁——第三天

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

作者头像
阿杜
发布2018-08-06 11:43:24
6080
发布2018-08-06 11:43:24
举报
文章被收录于专栏:阿杜的世界阿杜的世界

一、线程与锁——第一天

线程与锁模型其实是对底层硬件运行过程的形式化,这种形式化既是该模型最大的优点,也是它最大的缺点。我们借助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的使用模式如下:

代码语言:javascript
复制
Lock lock = new ReentrantLock();
lock.lock();
try {
    《使用共享资源》
} finally {
      lock.unlock();
}

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

代码语言:javascript
复制
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创建时可以设置一个描述公平性的变量。什么是“公平”的锁?何时适合使用公平锁?使用非公平的锁会怎样? 根据官方文档中的解释:
代码语言:javascript
复制
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方法中发生了虚假唤醒——等待线程即使没有收到正确的信号,也能够执行后续的操作。
代码语言:javascript
复制
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的修改版本展示了这点:

代码语言:javascript
复制
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、知识点

  • 使用线程池,而不是直接创建线程
代码语言:javascript
复制
//线程池的大小设置为可用处理器数的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、自习

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016.04.03 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、线程与锁——第一天
    • 1、知识点
      • 2、自习
      • 二、线程与锁——第二天
        • 1、知识点
          • 2、自习
          • 三、线程与锁——第三天
            • 1、知识点
              • 2、自习
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档