`
coolxing
  • 浏览: 870130 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
9a45b66b-c585-3a35-8680-2e466b75e3f8
Java Concurre...
浏览量:95963
社区版块
存档分类
最新评论

变量可见性和volatile, this逃逸, 不可变对象, 以及安全公开--Java Concurrency In Practice C03读书笔记

阅读更多

[本文是我对Java Concurrency In Practice第三章的归纳和总结, 也有部分语句摘自周志明所著的"深入理解java虚拟机".  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. ] 

线程安全包含2个方面: 原子性和可见性, java的同步机制都是围绕这2个方面来确保线程安全的.

 

可见性

理解可见性首先要清楚为什么多线程环境下会有可见性问题. 

现代CPU一般都使用读写速度很快的高速缓存来作为内存和CPU之间的缓冲, 高速缓存的引入可以有效的解决CPU和内存的速度矛盾, 但是也带来了新的问题: 缓存一致性. 在多CPU的系统中, 每个处理器都有自己的高速缓存, 而高速缓存又共享同一内存, 为了解决缓存一致性问题, 需要各个处理器访问缓存时都遵循一定的协议.

另外, 为了获得更好的执行效率, 处理器可能会对代码进行乱序执行优化, 处理器会在计算之后将乱序执行的结果进行重组, 保证该结果与顺序执行的结果是一致的, 但并不保证程序中各个语句计算的顺序与输入代码的顺序一致. java虚拟机在即时编译器中也有类似的指令重排序优化.

java内存模型规定了所有的变量都存储在主内存中, 除此之外每个线程都有自己的工作内存, 线程的工作内存中保存了被该线程使用到的变量的副本拷贝, 线程对变量的所有操作(读取, 赋值等)都必须在工作内存中进行, 而不能直接读写主内存中的变量. 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成.

由上可知, 一个线程修改了变量的值, 另一个线程并非总是能够及时获知最新的值, 这就是可见性问题的根源. 例如:

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) { 
	new ReaderThread().start(); 
	number = 42; 
	ready = true; 
    } 
}

由于指令重排序, 主线程中将ready赋值为true的操作可能发生在对number的赋值之前, 因此ReaderThread的输出结果可能为0. 又由于可见性, ReaderThread线程可能无法获知主线程对ready的修改, 那么ReaderThread的循环将不会停止. 也许在特定的机器上, 以上的"异常情况"很难出现, 实际上这取决于处理器架构和JVM实现, 以及运气.

 

synchronized与可见性

synchronized关键字不仅能够保证操作的原子性, 也能保证变量的可见性. JVM规范规定, 如果线程A和线程B通过同一把锁进行同步, 那么线程A在同步代码块中所做的修改对于线程B是可见的.

 

volatile与可见性

java的同步机制除了synchronized之外, 还有volatile.

如果使用volatile关键字修饰一个变量, 该变量就被声明为"易变的". JVM规范规定了任何一个线程修改了volatile变量的值都需要立即将新值更新到主内存中, 任何线程任何时候使用到volatile变量时都需要重新获取主内存的变量值, 而且volatile关键字隐含禁止进行指令重排序优化的语义. 以上的规范保证了volatile变量的线程可见性.

volatile是一种轻量级的同步机制, 不同于synchronized, volatile无法保证操作的原子性, 只能保证变量的可见性. 因此volatile关键字的使用是严格受限的, volatile关键字的正确使用必须同时满足以下条件:

1. 更改不依赖于当前值, 或者能够确保只会在单一线程中修改变量的值. 如果对变量的更改依赖于现有值, 就是一个race condition操作, 需要使用其他同步手段如synchronized将race condition操作转换为原子操作, 而volatile对原子性是无能为力的. 但是如果能够确保只会在单一线程中修改变量的值, 那么除了当前线程外, 其他线程不能更改变量的值, 此时race condition就不可能发生.

2. 变量不需要与其他状态变量共同参与不变约束. 比如start和end变量都被声明为volatile, 并且start和end组成不变约束start<end, 这样的不变约束是存在并发问题的:

private Date start;
private Date end;

public void setInterval(Date newStart, Date newEnd) {
	// 检查start<end是否成立, 在给start赋值之前不变式是有效的
	start = newStart;

	// 但是如果另外的线程在给start赋值之后给end赋值之前时检查start<end, 该不变式是无效的

	end = newEnd;
	// 给end赋值之后start<end不变式重新变为有效
}

volatile变量的典型应用场景是作为标记使用:

public class SocketThread extends Thread {
	public volatile boolean running = true;
	@Override
	public void run() {
		while (running) {
			// ...
		}
	}
}
 

64位数据(long和double类型)

JVM规范允许虚拟机将long和double类型的非volatile数据的读写操作划分为2次32位的操作来进行. 如果多个线程共享一个非volatile的long或double变量, 并且同时对该变量进行读取和修改, 那么某些线程可能会读取到一个既非原值, 也不是其他线程修改值的代表了"半个变量"的数值.

幸好几乎所有平台下的商用虚拟机几乎都选择把64为数据的读写操作作为原子操作来对待, 否则java程序员就需要在用到long和double变量时声明变量为volatile.

 

this逃逸

是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误, 因此应该避免this逃逸的发生.

this逃逸经常发生在构造函数中启动线程或注册监听器时, 如:

public class ThisEscape {
	public ThisEscape() {
		new Thread(new EscapeRunnable()).start();
		// ...
	}
	
	private class EscapeRunnable implements Runnable {
		@Override
		public void run() {
			// 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸
		}
	}
}

 在构造函数中创建Thread对象是没有问题的, 但是不要启动Thread. 可以提供一个init方法, 如:

public class ThisEscape {
	private Thread t;
	public ThisEscape() {
		t = new Thread(new EscapeRunnable());
		// ...
	}
	
	public void init() {
		t.start();
	}
	
	private class EscapeRunnable implements Runnable {
		@Override
		public void run() {
			// 通过ThisEscape.this就可以引用外围类对象, 此时可以保证外围类对象已经构造完成
		}
	}
}
  

线程限制

可以通过约定或者java内置的ThreadLocal将对象的访问限制在单一的线程上, 这样一来, 即使对象不是线程安全的, 也不会出现错误.

例如android的GUUI框架规定所有控件的更新都必须发生在主线程里, 因此即使android中的View组件不是线程安全的对象, 我们仍然无需担心会引发并发错误. 如果开发者没有遵循android控件对象的线程限制, 在程序运行时就会抛出异常. 

线程限制的另一个优点是可以防止死锁.

ThreadLocal多用于在线程中共享对象.

 

不可变对象

所有并发问题都是由于多个线程同时访问对象的某个可变属性引起的, 如果对象是不可变的, 那么所有的并发问题都将迎刃而解. 

所谓不可变对象是指对象一旦构造完成, 其所有属性就不能更改, 不可变对象显然都是线程安全的. 

对于不可变对象, 需要防止发生this逃逸.

如果需要对多个成员进行一项原子操作, 可以考虑使用这些成员构建一个不可变类. 例如:

public class CashedClass {
	private String cashedStr = "";
	private int cashedHashCode;
	
	public int hashCode(String str) {
		// 如果str是cashedStr, 就直接返回缓存的hashCode值
		if (str.equals(cashedStr)) {
			return cashedHashCode;
		} else {
			// 将cashedStr和hashCode值缓存起来
			cashedStr = str;
			cashedHashCode = cashedStr.hashCode();
			return cashedHashCode;
		}
	}
}

CashedClass不是一个线程安全的类, 因为对cashedStr和cashedHashCode的读写操作不具备原子性, 会发生race condition. 除了使用synchronized进行同步之外, 我们还可以使用不可变对象消除race condition:

public class CashedClass {
	// 使用一个volatile变量持有OneCashedValue对象
	private volatile OneCashedValue oneValue = new OneCashedValue("", 0);

	public int hashCode(String str) {
		int hashCode = oneValue.getStrHashCode(str);
		if (hashCode == -1) {
			hashCode = str.hashCode();
			// 对volatile变量的修改不依赖于当前值, 符合volatile的使用场景
			oneValue = new OneCashedValue(str, hashCode);
		}
		return hashCode;
	}

	/**
	 * 这是一个不可变类
	 */
	public class OneCashedValue {
		// 成员变量都是final的
		private final String str;
		private final int strHashCode;

		// 构造过程中不会发生this逃逸
		public OneCashedValue(String str, int strHashCode) {
			this.str = str;
			this.strHashCode = strHashCode;
		}

		public int getStrHashCode(String str) {
			if (!this.str.equals(str)) {
				// -1表示无效的hashCode值
				return -1;
			}
			return strHashCode;
		}
	}
}
  

公开成员

对象可以通过方法传参, 方法返回值, 非private修饰等方式公开对象的成员, 使得对象之外的代码可以访问相应的成员.

对象的成员一旦公开, 就需要保证多线程环境下所公开成员的可见性, 这就是所谓的安全的公开. 公开一个成员变量的前提是, 该成员变量没有参与任何不变式约束, 且该成员变量没有非法值, 因为一旦公开, 我们无法保证外部修改变量后变量仍然满足不变式约束和未取非法值. 在满足前提的条件下,  可以通过以下方式安全的公开对象的成员:

1. 线程限制. 如果限制对象只可由单一的线程访问, 那么无论公开哪个成员, 都不会产生并发问题.

2. 公开不可变成员. 如果对象的某个成员是不可变的, 那么公开该成员不会产生并发问题.

3. 公开事实上的不可变成员. 如果对象的某个成员是可变的, 但约定访问该成员的所有线程不要去修改这个成员, 那么该成员是事实上不可变的. 这种场景下公开该成员不会产生并发问题.

4. 公开线程安全的成员. 线程安全的成员内部会妥善并发问题, 因此公开线程安全的成员是恰当的.

5. 公开可变的非线程安全的成员. 这就要求所有访问该成员的线程使用特定的锁进行同步.

7
0
分享到:
评论
4 楼 hapjin 2015-09-22  
您好,请问下您的最后一个JAVA示例中:
   public class OneCashedValue { 
        // 成员变量都是final的 
        private final String str; 
        private final int strHashCode; 
 
        // 构造过程中不会发生this逃逸 
        public OneCashedValue(String str, int strHashCode) { 
            this.str = str; 
            this.strHashCode = strHashCode; 
        }

是因为把 str 和 strHashCode 声明成 final 类型的之后,在构造函数OneCashedValue中 就不会发生this 逃逸了吗?
能详细解释下此处不会发生this逃逸的原因吗???打扰了。谢谢。
3 楼 breadviking 2015-06-09  
volatile 能保证 单个读/写的原子性
2 楼 逐客叫我 2014-10-16  
有些深奥,还有些看不懂。
1 楼 heipacker 2013-09-27  
第二段代码中的start、end要加上volatile修饰符

相关推荐

    Java并行(3):可见性重访之锁、Volatile与原子变量1

    1. 过期数据 2. 锁的可见性 4. 原变量 1. JSL 第三版 2. Java Concurrency in Practice

    java多线程笔记

    Java线程:概念与原理 2 一、操作系统中线程和进程的概念 2 二、Java中的线程 3 三、Java中关于线程的名词解释 3 四、线程的状态转换和生命周期 4 Java线程:创建与启动 7 Java线程:线程名称的设定及获取 10 Java...

    Java volatile与AQS锁内存可见性

    从JUC中的AQS引入,讲解Java volatile与AQS锁内存可见性

    Java并发编程系列- volatile

    Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;Java并发编程系列- volatile;

    java volatile 关键字实战

    java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java volatile 关键字实战java ...

    java多线程编程总结

    Java线程:volatile关键字 Java线程:新特征-线程池 Java线程:新特征-有返回值的线程 Java线程:新特征-锁(上) Java线程:新特征-锁(下) Java线程:新特征-信号量 Java线程:新特征-阻塞队列 Java线程:新特征-...

    java多线程安全性基础介绍.pptx

    java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...

    Java多线程编程总结

    Java 线程系列博文总结word化,编目如下,欢迎互相学习交流: Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:...

    volatile变量详解

    容易忽略的变量声明,但是很重要 volatile的作用: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.

    Java并发:volatile内存可见性和指令重排

     Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。  工作内存中保存了主内存...

    Java并发编程实战

    3.5.2 不可变对象与初始化安全性 3.5.3 安全发布的常用模式 3.5.4 事实不可变对象 3.5.5 可变对象 3.5.6 安全地共享对象 第4章 对象的组合 4.1 设计线程安全的类 4.1.1 收集同步需求 4.1.2 依赖状态的操作 ...

    Java 线程总结

    Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:线程的交互 Java线程:线程的调度-休眠 Java线程:线程的调度-优先级 ...

    Java线程:volatile关键字

    主要讲述java线程volatile关键字

    java7hashmap源码-Concurrency:这是用来学习java多线程的

    java7 hashmap源码 concurrency 项目介绍 并发编程 可见性-volatile 通过内存屏障和禁止重排序优化实现 1.对volatile变量写操时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量...1.不可变对象 满足条件 a

    Java 并发编程实战

    3.5.2 不可变对象与初始化安全性 3.5.3 安全发布的常用模式 3.5.4 事实不可变对象 3.5.5 可变对象 3.5.6 安全地共享对象 第4章 对象的组合 4.1 设计线程安全的类 4.1.1 收集同步需求 4.1.2 依赖状态的操作 ...

    volatile变量

    一个定义为volatile的变量是说这变量可能会被 意想不到地改变,这样,编译器就不会去假设这 个变量的值了。精确地说就是,优化器在用到这 个变量时必须每次都小心地重新读取这个变量的 值,而不是使用保存在寄存器里...

Global site tag (gtag.js) - Google Analytics