Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

多线程学习(一)之基础概念

2017-05-26
nivelle

核心概念

线程的安全性

线程安全:核心在于对状态访问进行管理,特别是对共享的和可变的状态的访问,一个对象是否需要是线程安全的,取决于它是否被多个线程访问,指的是在程序中访问对象的方式,而不是对象要实现的功能。当多个线程访问某个类时,这个类始终表现出真确的行为,那么就称这个类是线程安全的。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
 - 不在线程之间共享该状态变量
 - 将状态变量修改为不可变的变量
 - 在访问状态变量时使用同步
线程安全的类:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
对象的状态: 指的是存储在状态变量(例如实例或静态域)中的数据。对象的状态可能包括其他依赖对象的域。
无状态对象一定是线程安全的。


竞态条件: 最常见的竞态条件类型就是“先检查后执行”。首先观察到某个条件为真,然后根据这个观察结果采用相应的动作,但事实上,在你观察到这个结果以及开始相应操作时,观察结果可能变得无效,从而导致各种问题。

状态一致性:要想保持状态一致性,就需要在单个原子子操作中更新所有相关的状态变量。

内置锁: 用来支持原子性,同步代码块包括两部分:(1) 一个作为锁的对象引用 (2)作为这个锁保护的代码块

以关键字synchronized 来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁或者监视器器锁。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块是自动释放锁,而无论是正常的控制路径退出,还是通过从代码块中抛出异常退出。

java的内置锁相当于一种互斥体,这意味着最多只有一个线程能持有这种锁。


重入机制: 当某个线程请求一个由其他线程持有锁时,发出请求的线程就会阻塞。但是,如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作粒度是“线程”而不是“调用”。

重入机制的实现方式:为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所有。当线程请求一个未没有被任何线程持有的锁时,JVM将记下持有者,并且将获取的计数值置为1.如果同一个线程再次获得这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。


用锁来保护状态

如果使用同步来协调对某个变量的访问时,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量所有位置上都要使用同一个锁。但对象的内置锁与其状态之间并没有内在的联系。

当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象锁之后,只能阻止其他线程获得同一个锁.

对于可能被多个线程同时访问的可变状态变量,在访问它的时候都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

每个共享的和可变的变量只应该由一个锁来保护,从而使维护人员知道是哪一个锁。
常见加锁约定

将所有的可变状态封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会放生并发访问。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

活跃性与性能

原则:尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

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

对象的共享

可见性

单线程中,如果向某个变量写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。但是如果读写线操作在不同的线程中时,为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的顺序进行判断,几乎无法得出正确的结论。

非原子的64位操作:

java内存模型要求,变量的读取和写入操作必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。在多线程程序中使用共享可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明他们。


加锁与可见性

加锁的含义不仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。 image

Volatile变量

(1) 弱同步机制,用来确保将变量的更新操作通知到其他线程。

(2) volatile 变量不会被缓存在寄存器或者对其他处理不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile。

volatile变量通常用做某个操作完成、发生中断或者状态的标志。

加锁机制既可以确保可见性又确保原子性,而volatile变量只能确保可见性。

当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

  • 该变量不会与其他状态变量一起纳入不变性条件中

  • 在访问变量时不需要加锁

发布与逸出

发布:使对象能够在当前作用域之外的代码中使用,例如将一个指向该对象的引用保存到其他代码可以访问的地方或者在某个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。

逸出:当某个不应该发布的对象发布时,这种情况就叫逸出。

安全的对象构造过程

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

线程封闭

如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭,当某个对象封闭封闭在一个线程中使,这种方法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

一种常见的应用是JDBC的Connection对象。

栈封闭

栈封闭式线程封闭的一种特例。

栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中,他们位于执行线程的栈中,其他线程无法访问这个栈。

对于基本类型的局部变量,无论如何都不会破坏封闭性。由于任何方法都无法获得对基本类型的引用,因此java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。

ThreadLocal类

定义:这个类能使线程中的某个值与保存值得对象关联起来。ThreadLocal提供get与set方法,这些方法为每个使用该变量的线程都有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

** ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。**

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

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

通过将JDBC的链接保存到ThreadLocal对象中,每个线程都会拥有自己的链接。

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。

从概念来上讲可以将ThreadLocal视为包含了Map<Thread,T>对象,其中保存了特定于线程的值,但只是一种理解,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。

不变性


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

不变性对象很简单,他们只有一种状态,并且该状态由构造函数来控制。

不可变对象:

  • 对象创建以后状态就不能改变

  • 对象的所有域都是final类型

  • 对象是正确创建的(在对象创建期间,this引用没有逸出)

Final域

final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的),java内存模型中,final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

安全发布的常用模式

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

  • 在静态初始化函数中初始化一个对象引用

  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中

  • 将对象的引用保存到某个正确构造对象的final类型域中

通过线程容器实现线程同步:

  • 通过将一个键或值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程

  • 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将元素安全地发布到任何从这些容器中访问该元素的线程

  • 通过将某个元素放入BlockingQueue或者ConcurrentLikedQueue中,可以将元素安全地发布到任何从这些队列中访问元素的线程。

发布一个静态构造对象,最简单和最安全的方式是使用静态的初始化容器:

public static Holder holder = new Holder(42);

利用JVM在类的初始化阶段执行,利用了JVM的同步机制。

内容来自《java并发编程实战》 作者:Brian Goetz  Tim Peierls


上一篇 spring之事件

评论