《java并发编程实战》笔记

Posted Stay hungry,stay foolish.

tags:

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

  最近在看《java并发编程实战》,希望自己有毅力把它读完。

  线程本身有很多优势,比如可以发挥多处理器的强大能力、建模更加简单、简化异步事件的处理、使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题。看下面例子:

public class UnsafeSequence {
    private int value;
    
    /*返回一个唯一的数值*/
    public int getNext(){
        return value++;
    }
}

  UnsafeSequence的问题在于,如果执行时机不对,那么两个线程在调用getNext时会得到相同的值,图1给出了这种错误情况。虽然递增运算value++看上去是单个操作,但事实上它包含三个独立的操作:  读取value、将value加1、将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使它们得到相同的值,并都将这个值加1。结果就是,在不同线程的调用中返回了相同的值。

 

在UnsafeSequence中说明的是一种常见的并发安全问题,称为竞态条件。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

 

再举个例子,延迟初始化中的竞态条件:

public class LazyInitRace {
    private HashMap<String, String> instance = null;
    
    public HashMap<String, String> getInstance(){
        if (instance == null) {
            instance = new HashMap<String, String>();
        }
        
        return instance;
    }
}

  在LazyInitRace中包含一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance,A看到instance为空,因而创建一个新的HashMap实体,B同样需要判断instance是否为空,此时的instance是否为空,要取决于不可预测的时序,如果当B检查时,instance也为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例。

  java提供了锁机制来解决这一问题,但这些终归只是一些机制,要编写线程安全的代码,其核心在于要对对象的状态进行管理。

  对象的状态是指存储在状态变量(例如实例或者静态域)中的数据。

一、线程封闭

  如果一个对象无状态,它一定是线程安全的。  

public class StatelessServlet implements Servlet {
    public void service(ServletRequest request, ServletResponse response)
            throws ServletException, IOException {
        int i = 1;
        i++;
        ...
    }
}

  与大多数servlet相同,StatelessServlet是无状态的:它即不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中(这块需要对jvm内存分配有基础了解),并且只能由正在执行的线程访问。线程之间没有共享状态,由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

  像上面的例子,仅在线程内访问数据,自然也就安全,这种技术称为线程封闭。java提供了一些机制来实现线程封闭,例如局部变量(上面的例子)和ThreadLocal类。

  1.栈封闭

  也就是局部变量,这块要理解为什么局部变量是线程安全的。jvm运行时的数据分配如图2所示。

  

  java虚拟机栈是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这部分会抛出两种异常:如果线程请求的栈深度大于虚拟机允许的栈深度,抛出StackOverflowError;如果栈扩展时无法申请到足够内存,抛出OutOfMemoryError异常。

  2.ThreadLocal类

  ThreadLocal对象通常用于防止对可变的单实例变量或者全局变量进行共享。例如JDBC的Connection对象,JDBC并不要求Connection对象必须是线程安全的。伪代码如下:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    };
};

public static Connection getConnection(){
    return connectionHolder.get();
}

 

二、用锁来保护状态

  1.内置锁

  java提供了一种内置的锁机制来支持原子性:同步代码块。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象,一般不要这么做,这样会影响效率。

  synchronized(lock){

  //访问或修改由锁保护的共享状态

  }

  每一个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。

  对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁以后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式的创建锁对象。

  开发中常见的内置锁的使用方法是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态饿代码路径进行同步,使得在该对象上不会发生并发访问,例如,Vector和其他的同步集合类。

  2.Volatile变量

  同步还有另外一层意思:我们不仅希望防止某个线程正在使用对象状态而另一个线程正在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。java提供了一种削弱的同步机制,即volatile变量,用来确保将变量的更新操作通知其他线程。

  volatile变量的典型用法:  

volatile boolean asleep;
    ...
        while(!asleep){
            ...
        }

volatile变量通常用做某个操作完成,发生中断或者作为状态的标志。volatile的语义不足以确保递增操作的原子性。也就是说,加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

关于volatile后补:

  这块的讲解不是很详细,这里重新整理下,首先要达成一个共识:

  1、每个线程都有自己的线程存储空间

  2、线程何时同步本地存储空间的数据到主存是不确定的。

  正是由于这种不确定性,一个线程修改了数据其他线程不能及时看到,而使用volatile以后,做了如下事情

  1、每次修改volatile变量都会同步到主存中  

  2、每次读取volatile变量的值都强制从主存读取最新的值(强制JVM不可优化volatile变量,如JVM优化后变量读取会使用cpu缓存而不从主存中读取)

  通过直接读取主存保证了可见性,无论哪个线程读取volatile类型变量都是最新数据。但是这不意味着volatile修饰的变量是线程安全的,多线程交替执行还是会存在数据不一致的问题。看到某个成员变量被修饰成volatile类型,可以理解为下面代码的行为:

public class SynchronizedInteger{
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized void setValue(int value) {
        this.value = value;
    }
}

 

  

三,不可变对象

  如果一个对象在被创建后其状态就不能被修改,那么这个对象是不可变对象,所以,不可变指的是状态不可变。不可变对象一定是线程安全的

  书中给出了一个判断不可变对象的原则:

  • 对象创建以后其状态不能修改(听着像废话)
  • 对象的所有域都是final类型
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)

  例子:

public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();
    
    public ThreeStooges(){
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }
    
    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}

  尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域来访问,最后一个要求是“正确的构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。

  至此,区分3个概念:

  (1)无状态对象:无成员变量,一定线程安全

  (2)不可变对象:一定线程安全,有状态,但状态不可变

  (3)可变对象:线程不安全,状态可变

 

总之,这部分的核心是理解对象的状态。

以上是关于《java并发编程实战》笔记的主要内容,如果未能解决你的问题,请参考以下文章

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

Java并发编程实战读书笔记之死锁

Java并发编程实战读书笔记之死锁

java并发编程实战读书笔记之FutureTask

java并发编程实战读书笔记之FutureTask

java并发编程实战笔记