Java并发编程实战的作品目录

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程实战的作品目录相关的知识,希望对你有一定的参考价值。

参考技术A

对本书的赞誉
译者序
前 言
第1章 简介
1.1 并发简史
1.2 线程的优势
1.2.1 发挥多处理器的强大能力
1.2.2 建模的简单性
1.2.3 异步事件的简化处理
1.2.4 响应更灵敏的用户界面
1.3 线程带来的风险
1.3.1 安全性问题
1.3.2 活跃性问题
1.3.3 性能问题
1.4 线程无处不在
第一部分 基础知识
第2章 线程安全性
2.1 什么是线程安全性
2.2 原子性
2.2.1 竞态条件
2.2.2 示例:延迟初始化中的竞态条件
2.2.3 复合操作
2.3 加锁机制
2.3.1 内置锁
2.3.2 重入
2.4 用锁来保护状态
2.5 活跃性与性能
第3章 对象的共享
3.1 可见性
3.1.1 失效数据
3.1.2 非原子的64位操作
3.1.3 加锁与可见性
3.1.4 Volatile变量
3.2 发布与逸出
3.3 线程封闭
3.3.1 Ad-hoc线程封闭
3.3.2 栈封闭
3.3.3 ThreadLocal类
3.4 不变性
3.4.1 Final域
3.4.2 示例:使用Volatile类型来发布不可变对象
3.5 安全发布
3.5.1 不正确的发布:正确的对象被破坏
3.5.2  不可变对象与初始化安全性
3.5.3 安全发布的常用模式
3.5.4 事实不可变对象
3.5.5 可变对象
3.5.6 安全地共享对象
第4章 对象的组合
4.1 设计线程安全的类
4.1.1 收集同步需求
4.1.2 依赖状态的操作
4.1.3 状态的所有权
4.2 实例封闭
4.2.1 Java监视器模式
4.2.2 示例:车辆追踪
4.3 线程安全性的委托
4.3.1 示例:基于委托的车辆追踪器
4.3.2 独立的状态变量
4.3.3 当委托失效时
4.3.4 发布底层的状态变量
4.3.5 示例:发布状态的车辆追踪器
4.4 在现有的线程安全类中添加功能
4.4.1 客户端加锁机制
4.4.2 组合
4.5 将同步策略文档化
第5章 基础构建模块
5.1 同步容器类
5.1.1 同步容器类的问题
5.1.2 迭代器与Concurrent-ModificationException
5.1.3 隐藏迭代器
5.2 并发容器
5.2.1 ConcurrentHashMap
5.2.2 额外的原子Map操作
5.2.3 CopyOnWriteArrayList
5.3 阻塞队列和生产者-消费者模式
5.3.1 示例:桌面搜索
5.3.2 串行线程封闭
5.3.3 双端队列与工作密取
5.4 阻塞方法与中断方法
5.5 同步工具类
5.5.1 闭锁
5.5.2 FutureTask
5.5.3 信号量
5.5.4 栅栏
5.6 构建高效且可伸缩的结果缓存
第二部分 结构化并发应用程序
第6章 任务执行
6.1 在线程中执行任务
6.1.1 串行地执行任务
6.1.2 显式地为任务创建线程
6.1.3 无限制创建线程的不足
6.2 Executor框架
6.2.1 示例:基于Executor的Web服务器
6.2.2 执行策略
6.2.3 线程池
6.2.4 Executor的生命周期
6.2.5 延迟任务与周期任务
6.3 找出可利用的并行性
6.3.1 示例:串行的页面渲染器
6.3.2 携带结果的任务Callable与Future
6.3.3 示例:使用Future实现页面渲染器
6.3.4 在异构任务并行化中存在的局限
6.3.5 CompletionService:Executor与BlockingQueue
6.3.6 示例:使用CompletionService实现页面渲染器
6.3.7 为任务设置时限
6.3.8 示例:旅行预定门户网站
第7章 取消与关闭
第8章 线程池的使用
第9章 图形用户界面应用程序
第三部分 活跃性、性能与测试
第10章 避免活跃性危险
第11章 性能与可伸缩性
第12章 并发程序的测试
第四部分 高级主题
第13章 显式锁
第14章 构建自定义的同步工具
第15章 原子变量与非阻塞同步机制
第16章 Java内存模型
附录A 并发性标注
参考文献

Java并发编程实战之互斥锁

文章目录

Java并发编程实战之互斥锁

之前在《Java并发编程实战基础概要》中提到了原子性这一个概念,一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”。 那么原子性问题到底改如何解决呢?

如何解决原子性问题?

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。所以本质来说,解决原子性问题,是要保证中间状态对外不可见。(这一点需要细细品一品)

原子性问题的源头是线程切换,多个线程同时操作同一个变量。这样就会出现线程冲突的问题。所以需要一种机制保持在多核CPU下,同一个时刻只有一个线程更改某个共享变量。(其实没有共享变量,也不会存在并发问题),所以说如果我们能够保证对共享变量的修改是互斥的,那么就能保证原子性了。

锁模型

一谈到互斥,我们很自然就会想到了锁。首先我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则呢就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()

这个过程非常像办公室里高峰期抢占坑位,每个人都是进坑锁门(加锁),出坑开门(解锁),如厕这个事就是临界区。

上面的例子虽然挺形象的,但是容易忽略锁的两个很重要的点,分别是我们锁的是什么?和我们想要保护的又是什么?

  • 第一个问题,锁的到底是什么?我们单纯锁的是门吗?这么理解也没有错,但是想一想,实际上我们想要锁的是对这个厕所的使用,因为你不可能锁上这个厕所的门,去上另一个厕所,这样没任何意义。所以锁跟你想保护的东西是有一个对应关系的。所以对应到编程世界中,锁的其实是对共享变量的访问。
  • 第二个问题,我们想要保护的是什么?我们保护的其实就是我们即将要使用的这个厕所。对应到编程世界中,保护的就是共享变量。

所以对应编程世界中,锁和资源是有一个对应关系的,所以锁的模型如下:

Java synchronized 关键字

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块


class X 
  // 修饰非静态方法
  synchronized void foo() 
    // 临界区
  
  // 修饰静态方法
  synchronized static void bar() 
    // 临界区
  
  // 修饰代码块
  Object obj = new Object()void baz() 
    synchronized(obj) 
      // 临界区
    
  
  

我们可以通过synchronized关键字对我们的临界区进行加锁和解锁。但是我们从代码中并没有看到这个加锁和解锁的动作,这是因为这些操作是由Java编译器为我们加上的。

我们可以利用javap命令来查看生成的字节码文件,就可以看出来Java编译器会为我们synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock()

下面通过javap命令查看下面代码生成的字节码文件

public void method() 
     synchronized (this) 
         System.out.println("start");
     
 

字节码文件如下:

图中的monitorenter对应的就是加锁,而monitorexit对应的就是解锁

至于为什么会有两个monitorexit指令呢?

是因为对于synchronized关键字而言,javac在编译时,会生成对应的monitorentermonitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

参考: 《Java锁synchronized关键字学习系列之重量级锁》

synchronized锁的是代码块还是锁的是对象?从上面我们可以总结出synchronized锁的其实是对象

synchronized 里的加锁 lock() 和解锁 unlock() 锁定的对象是什么?

下面的代码我们看到只有 synchronized修饰代码块的时候,锁定了一个obj 对象,那 synchronized修饰方法的时候锁定的是什么呢?


class X 
  // 修饰非静态方法
  synchronized void foo() 
    // 临界区
  
  // 修饰静态方法
  synchronized static void bar() 
    // 临界区
  
  // 修饰代码块
  Object obj = new Object()void baz() 
    synchronized(obj) 
      // 临界区
    
  
  
  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X
class X 
  // 修饰静态方法
  synchronized(X.class) static void bar() 
    // 临界区
  

  • 当修饰非静态方法的时候,锁定的是当前实例对象 this
class X 
  // 修饰非静态方法
  synchronized(this) void foo() 
    // 临界区
  

到这里我们引申出另一个问题,当我们锁住了对象的时候,对象身上发生了什么变化,jvm如何知道这个对象被“锁“住了,关于这个题外话这里不多赘述,可以参考:《Java锁synchronized关键字学习系列之CAS和对象头》

Java synchronized 关键字 只能解决原子性问题?

并发会产生三大问题

  1. 原子性问题
  2. 可见性问题
  3. 有序性问题

前面我们一直在说,锁可以解决原子性问题,Java synchronized 关键字只能解决原子性问题吗?

答案肯定是否定的,前面在《Java并发编程实战基础概要》 说到了Java内存模型规范了Java虚拟机(JVM)如何提供按需禁用缓存和编译优化的方法。这些方法包括:volatile、synchronized和final关键字,以及Java内存模型中的Happens-Before规则

所以synchronized关键字还可以解决可见性问题(可以参考Happens-Before的锁定规则:对一个锁的解锁操作 Happens-Before于后续对这个锁的加锁操作)。但是以synchronized关键字不能完全解决有序性问题,因为synchronized关键字不能避免指令重排,所以我们在之前《Java并发编程实战基础概要》双重检验的单例模式中,必须加volatile来避免因为发生指令重排,返回错误实例。

如何正确使用Java synchronized 关键字?

正确使用Java synchronized 关键字主要是关注在synchronized锁定的对象跟受保护资源的关系。如何理解呢?举一个例子:

class SafeCalc 
  static long value = 0L;
  synchronized long get() 
    return value;
  
  synchronized static void addOne() 
    value += 1;
  

从上面代码我们可以看出来synchronized 关键字锁定的是两个不同的对象,在之前我们讲过,synchronized 关键字修饰非静态方法的时候,锁定的是当前实例对象 this。而当修饰静态方法的时候,锁定的是当前类的 Class 对象。所以我们现在相当于用两个锁保护一个资源(一个共享变量value)。

从上图可以看出来,由于get() 方法和addOne()方法是两把不同的锁,说明执行addOne()方法的过程中可以执行get() 方法,并发性不能得到保证,所以这两临界区并不是互斥的,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。

再举一个例子,下面这个例子是否正确使用synchronized 关键字

class SafeCalc 
  long value = 0L;
  long get() 
    synchronized (new Object()) 
      return value;
    
  
  void addOne() 
    synchronized (new Object()) 
      value += 1;
    
  

答案很明显是错误的使用,synchronized 关键字锁定的new object每次在内存中都是新对象,所以每次锁的都不是同一个对象,怎么做到互斥呢?

所以要真正使用好互斥锁,必须深入分析锁定的对象和受保护资源的关系。

锁和受保护资源的合理关联关系

直接给出结论:受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源。

我们来举一个用一把锁来保护多个资源的例子。

class Account 
  // 账户余额  
  private Integer balance;
  // 账户密码
  private String password;

  // 取款
  synchronized void withdraw(Integer amt) 
    if (this.balance > amt)
        this.balance -= amt;
      
   
  // 查看余额
  synchronized Integer getBalance() 
     return balance;
  

  // 更改密码
  synchronized void updatePassword(String pw)
    this.password = pw;
   
  // 查看密码
  synchronized String getPassword() 
     return password;
  

从上面代码可以看出来,我们是使用当前实例this来管理Account类中所有的资源。所以会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的,所以不会有并发问题。但是却会产生另一个问题,就是性能太差了。

我们可以稍微修改一下,使用两把锁,让取款和修改密码是可以并行的,因为这两个行为互不干扰。

class Account 
  // 锁:保护账户余额
  private final Object balLock
    = new Object();
  // 账户余额  
  private Integer balance;
  // 锁:保护账户密码
  private final Object pwLock
    = new Object();
  // 账户密码
  private String password;

  // 取款
  void withdraw(Integer amt) 
    synchronized(balLock) 
      if (this.balance > amt)
        this.balance -= amt;
      
    
   
  // 查看余额
  Integer getBalance() 
    synchronized(balLock) 
      return balance;
    
  

  // 更改密码
  void updatePassword(String pw)
    synchronized(pwLock) 
      this.password = pw;
    
   
  // 查看密码
  String getPassword() 
    synchronized(pwLock) 
      return password;
    
  

用不同的锁对受保护资源进行精细化管理,能够提升性能,这种锁还有个名字,叫细粒度锁。所以我们上锁的时候需要考虑锁的粒度。

所以我们在上锁的时候,应该分析多个资源的关系。如果资源之间没有关系,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。

死锁

前面说到使用细粒度锁可以提高并行度,是性能优化的一个重要手段。但是使用细粒度锁是有代价的,这个代价就是可能会导致死锁。

死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

下面举一个死锁的例子:

public class T 
    private Object o1 = new Object();
    private Object o2 = new Object();

    public void m1() 
        synchronized (o1) 
            try 
                Thread.sleep(10000);
             catch (InterruptedException e) 
                e.printStackTrace();
            

            synchronized (o2) 
                System.out.println("如果出现这句话表示没有死锁");
            
        

    

    public void m2() 
        synchronized(o2) 

            synchronized (o1) 
                System.out.println("如果出现这句话表示没有死锁");
            

        

    
    public static void main(String[] args) 
        T t=new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    

上面这个例子死锁是怎么发生的呢?假设当线程1持有锁对象o1,然后当线程2持有锁对象o2的时候;然后线程1需要对对象o2加锁,但是因为线程2已经对对象o2加锁了,所以线程1需要等待线程2解除锁占用。然后线程2同样需要对对象o1加锁,但是因为线程1已经对对象o1加锁了,所以线程2同样要等待线程1解除锁占用。所以现在就出现了线程1和线程2互相在等待对方解除锁占用,于是就出现了死锁。

预防死锁

那我们如何去预防死锁呢?

那如何避免死锁呢?要避免死锁就需要分析死锁发生的条件,只有以下这四个条件都发生时才会出现死锁:

  • 互斥:一个资源每次只能被一个进程(或者线程)使用。进程(或者线程)对所分配到的资源不允许其他进程(或者线程)进行访问,若其他进程(或者线程)访问该资源,只能等待,直至占有该资源的进程(或者线程)使用完成后释放该资源
  • 占有且等待:进程(或者线程)获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程(或者线程)占有,此时请求阻塞,但又对自己获得的资源保持不放
  • 不可抢占:是指进程(或者线程)已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 循环等待:线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

所以我们想要避免死锁,其实只要破坏掉上面其中一个条件即可。但是第一个条件互斥是没办法破坏的,因为我们用锁的初衷就是为了互斥,所以我们需要从其他三个条件下手。

破坏占有且等待条件

要破坏这个条件,可以一次性申请所有资源。上面的例子一次性申请所有的资源,就相当于一次性加锁了o1和o2对象,解锁的时候也是一次性解锁了o1和o2对象,所以上面的例子可以改成下面这种方式

public class T 
    private Object o1 = new Object();

    public void m1() 
        synchronized (o1) 
            try 
                Thread.sleep(10000);
             catch (InterruptedException e) 
                e.printStackTrace();
            

            System.out.println("如果出现这句话表示没有死锁");
        

    

    public void m2() 
        synchronized (o1) 
                System.out.println("如果出现这句话表示没有死锁");
            
    
    public static void main(String[] args) 
        T t=new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    

上面这种方式直接使用了一个锁,这种肯定是不会有死锁的。

或者我们还是锁定两个不同的对象,我们还可以这么改造

class M 

  private List<Object> list = new ArrayList<>();

  public synchronized boolean lock(Object o1, Object o2) 
    if (list.contains(o1) || list.contains(o2)) 
      return false;
     else 
      list.add(o1);
      list.add(o2);
    
    return true;
  

  public synchronized void unlock(Object o1, Object o2) 
    list.remove(o1);
    list.remove(o2);
  


class T 

  private Object o1 = new Object();
  private Object o2 = new Object();
  private M m = new M();

  public void m1() 
    while (!m.lock(o1, o2)) 

    
    try 
      synchronized (o1) 
        try 
          Thread.sleep(10000);
         catch (InterruptedException e) 
          e.printStackTrace();
        

        synchronized (o2) 
          System.out.println("如果出现这句话表示没有死锁");
        
      
     finally 
      m.unlock(o1, o2);
    

  

  public void m2() 
    while (!m.lock(o1, o2)) 

    
    try 
      synchronized (o2) 
        synchronized (o1) 
          System.out.println("如果出现这句话表示没有死锁");
        

      
     finally 
      m.unlock(o1, o2);
    

  

  public static void main(String[] args) 
    T t = new T();
    new Thread(t::m1).start();
    new Thread(t::m2).start();
  


从上面代码可以看出来,我们抽取了一个类M来同时申请多个资源,从而破坏了占有且等待条件

破坏不可抢占条件

破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。Java 在语言层次确实没有解决这个问题,但是java.util.concurrent 这个包下面提供的 Lock类中的tryLock(long, TimeUnit) 方法,可以帮我们在一段时间尝试获取锁,所以可以轻松解决这个问题的

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源,这样就不会出现两个线程交错加锁的情况。上面的情况就是因为我们申请资源其实不是顺序的,也就是加锁不是顺序的,T1加锁的是o1然后o2,T2加锁的是o2然后o1。 如果T1和T2都是加锁o1然后o2,其实就不会有这种问题。

class T 
  private Object o1 = new Object();
  private Object o2 = new Object();

  public void m1() 
    synchronized (o1) 
      try 
        Thread.sleep(10000);
       catch (InterruptedException e) 
        e.printStackTrace();
      

      synchronized (o2) 
        System.out.println("如果出现这句话表示没有死锁");
      
    

  

  public void m2() 
    synchronized(o1) 

      synchronized (o2) 
        System.out.println("如果出现这句话表示没有死锁");
      

    

  
  public static void main(String[] args) 
    T t=new T();
    new Thread(t::m1).start();
    new Thread(t::m2).start();
  

总结

但实际上开发过程中的案例肯定不会像我们举例的这么简单,具体问题具体分析,但是我们还是需要从这三个条件出发,去破坏掉我们这三个条件,才能够避免死锁的问题。

用 synchronized 实现等待 - 通知机制

前面我们在讲死锁的破坏占用且等待条件的时候,使用了一个死循环的方式来循环等待

while (!m.lock(o1, o2)) 


这种方案,在并发冲突大的场景(也就是可能很久都获取不到锁)不适用,因为这种场景下可能要循环上万次才能获取到锁,太消耗 CPU 了。

那有没有更好的方案呢?那就是使用"等待 - 通知机制"。怎么理解"等待 - 通知机制"呢?你可以类比于医院排队叫号。如果没有排队叫号系统,每个人都需要去问医生是不是轮到我了。而有了排队叫号,病人只需要等着医生把你叫过来,病人和医生是不是都省心省力了。

而在编程世界中,一个完整的“等待 - 通知机制”是这样的:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。

那在Java的世界中如何实现的“等待 - 通知机制”? Java 语言内置的 synchronized 配合 wait()notify()notifyAll() 这三个方法就能轻松实现。

我们看方法名称就可以知道,wait()顾名思义就是让线程等待,而、notify()notifyAll()就是唤醒线程。

那么wait()的实现机制是怎样的?

在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java 对象的 wait() 方法就能够满足这种需求。如上图所示,当调用 wait() 方法后,当前线程就会被阻塞,并且进入到右边的等待队列中,这个等待队列也是互斥锁的

以上是关于Java并发编程实战的作品目录的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程实战基础概要

Java并发编程实战之互斥锁

Java并发编程实战之互斥锁

《java并发编程实战》

汪大神Java多线程编程实战

Java并发编程实战 04死锁了怎么办?