多线程系列提高 (3)-- 对象的共享:在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。然而,当读操作和写操作在不同的线程中执行时,就是另外一种情况。我们无法确保执行读操作的线程能够适时的看到其他线程写入的值,为了确保多个线程之间对内存心如操作的可见性,必须使用同步机制。
下面以代码为例:
- package MultiThreading;
- import java.io.Reader;
- /** * Created by L_kanglin on 2017/4/13. */
- //在没有同步的情况下共享变量public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); } } public static void main(String[] args) throws InterruptedException { ReaderThread thread=new ReaderThread(); thread.start(); //休眠1s thread.sleep(1000); number=42; ready=true; }}
在代码中,主线程和读线程都将访问共享变量 ready 和 number。主线程启动读线程,然后将 number 设为 42,并将 ready 设为 true,读线程一直循环直到发现 ready 的值变为 true,然后输出 number 的值。虽然 NoVisibility 看起来会输出 42,单事实上可能输出 0,或者根本无法终止。这是因为在 diamante 中没有使用足够的同步机制,因此无法保证主线程写入的 ready 值和 number 值对于读线程来说是可见的。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其它线程来说是都是可见的。否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是一个失效值。
二、volatile 变量
volatile 变量是一种稍弱的同步机制,用来确保将变量的更新操作通知到其它线程。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其它处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。由上面的一段话可以总结到 volatile 的功能主要有两个:(1) 内存的可视性机制:读取 volatile 类型的变量时总会返回最新写入的值。(2) 禁止指令重排序:JVM 内存模型允许编译器对操作顺序进行指令重排序,有上层的 Java–>c/c++–> 字节码—> 汇编机器语言,才操作完成。此时为了提高运行效率,可能会进行重排序,而 volatile 类型的变量则不会执行这个操作,因为已经告诉了编译器这个变量是共享的,不会与其它内存操作一起重排序。
因此在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量一种比 synchronized 关键字更轻量级的同步机制。
重点理解:volatile 变量对可见性的影响比 volatile 变量本身更为重要。当线程 A 首先写入一个 volatile 变量并且线程 B 随后读取该变量时,在写入 volatile 变量之前对A可见的所有变量的值,在 B 读取了 volatile 变量后,对 B 也是可见的。因此,从内存可见性的角度来看,写入 volatile 变量相当于退出同步代码块,而读取 volatile 变量就相当于进入同步代码块。
volatile 的局限性 volatile 的语义不足以确保递增操作 (count++) 的原子性(读–改–写),除非你能确保只有一个线程对变量执行写操作。注意:加锁机制既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用 volatile 变量:(1) 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。(2) 该变量不会与其它状态变量一起纳入不变性条件中。(3) 在访问变量时不需要加锁。
三、ThreadLocal 类
线程封闭:当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭。它是实现线程安全性的最简单的方式之一。当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。线程封闭在 Java 中主要有三种实现方式 (1)Ad-hoc 线程封闭:维护线程封闭性的职责完全由程序实现来承担。(2) 栈封闭:只能通过局部变量才能访问对象(3)ThreadLocal 类:这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal 提供了 get 和 set 等访问接口或方法,这些方法为每个使用该变量的线程都保存有一份独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。
- /** * Created by L_kanglin on 2017/4/13. */
- public class connectionHolder {
- private static ThreadLocal connectionHolder = new ThreadLocal() {
- public Connection initialValue() {
- return DriverManager.getConnection(DB_URL);
- }
- };
- public static Connection getConnection() {
- return connectionHolder.get();
- }
- }
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。例如,在 Java 5.0 之前,Integer.toString()方法使用 ThreadLocal 对象来保存一个 12 字节大小的缓存区,用于对结果进行格式化,而不是使用共享的静态缓存区 (这需要使用锁机制) 或者在每次调用时都分配一个新的缓存区。当某个线程初次调用 ThreadLocal.get 方法时,就会调用 initialValue 来获取初始值。从概念上看,你可以将 ThreadLocal 视为包含了 Map
四、不变性
满足同步需求的另一种方法是使用不可变对象,如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数创建的,只要它们的状态不改变,那么这些不变性条件就得以维持。注意:不可变对象一定是线程安全的。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: http://www.92to.com/bangong/2017/04-14/20447799.html