java并发编程实战笔记与实践

简介{#intro}

线程带来的风险:

  1. 安全性问题
  2. 活跃性问题
  3. 性能问题

线程安全性{#threadSafe}

线程安全: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这>个类都能表现出正确的行为,那么就称这个类时线程安全的。

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别时对共享的和可变的状态访问     

线程安全的处理方法

  1. 共享不可变
    • 无状态对象一定是线程安全的
    • 不可变对象一定是线程安全的
  2. 可变不共享
    • 线程封闭
      • 栈封闭(局部变量)
      • ThreadLocal
  3. 共享可变
    • 同步(synchronized & Lock)
      • synchronized
        • synchronized(obj)、实例方法上加synchronized对obj对象加锁
        • 静态方法上加synchronized是对Class对象作为锁
        • 内置锁是互斥锁,且可重入。
      • Lock
        • lock()
        • tryLock()
        • lockInterruptly()

ps: 线程安全的程序不一定完全由线程安全类构成,线程安全类构成的程序不一定就是线程安全的,线程安全类中也可以 包含非线程安全的类

竟态条件:在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竟态条件。

常见的竟态条件类型:

  1. 先检查后执行
  2. 读取-修改-写入

AtomicReference

每个共享的和可变的变量都应该只有一个锁来保护,从而使维护人员知道是哪一个锁。

当执行时间按较长的计算或者可能无法快速完成的操作时(例如。网络I/O或控制台I/O)一定不要持有锁。

对象的共享

可见性: 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。 只要用数据在多个线程之间共享,就使用正确的同步

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性 当且仅当满足以下所有条件是,才应该使用volatile变量

发布一个对象是指:使对象能够在当前作用域之外的代码中使用 当某个不应该发布的对象被发布时,这种情况就被称为逸出   当发布某个对象时,在该对象的非私有域中引用的所有对象同样会被发布。   对一个类来说,外部方式是指行为并不完全由该类来规定的方法,包括其他类中定义的方法以及该类可以被改写的方法(既不是私有方法也不是终结方法)    当把一个对象传递给某个外部方法时,就相当于发布了这个对象   

不要在构造过程中使this引用逸出  

常见的错误:  

  1. 在构造函数中启动一个线程   
  2. 在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法)  

如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程  

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener(){
            public void onEvent(Event e){
                doSomeThing(e);
            }
        };
    }

    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListerner(safe.listener);
        return safe;
    }
}

线程封闭

可变不共享,如果仅在单线程内访问数据,就不需要同步了,这种技术被称为线程封闭   java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。            

  1. 栈封闭:对于基本类型的局部变量,由于任何方法都无法获得对基本类型的引用,因此java语言的这种语义确保了基本类型的局部变量始终封闭在线程内;   对于引用类型的栈封闭性,如果发布了引用,那么栈封闭性就被破坏,并导致对象逸出  
  2. ThreadLocal  

不变性:   不可变对象一定是线程安全的  

当满足以下条件时,对象才是不可变的:    

  1. 对象创建完之后其状态就不能修改    
  2. 对象的所有域都是final类型    
  3. 对象是正确创建的(创建期间没有this的逸出)    

正如“除非需要更高的可见性,。否则应将所有的域都声明未私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明未final域”也是一个良好的编程习惯    

安全的发布对象  

安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:    

  1. 在静态初始化函数中初始化一个对象引用  
  2. 将对象的引用保存到volatile类型的域或者AtomicReference对象中  
  3. 将对象的引用保存到某个正确构造对象的final类型域中  
  4. 将对象的引用保存到一个由锁保护的域中。  

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)   如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。     要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。            

对象的发布需求取决于它的可变性:    

  1. 不可变对象可以通过任意机制来发布  
  2. 事实不可变对象必须通过安全方式发布  
  3. 可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来    

java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。 1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。