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

变量可见性和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 Concurrency in Practice.zip

    《Java Concurrency in Practice》是Java并发编程领域的一本经典著作,由Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowles和Doug Lea等专家共同编写。这本书深入探讨了Java平台上的多线程和并发编程,旨在...

    Java Concurrency In Practice Learning Note

    Java提供了`synchronized`关键字、`volatile`关键字以及`java.util.concurrent.locks`包下的锁(如`ReentrantLock`)来保证共享数据的可见性和一致性。理解它们的区别和使用场景是提升并发编程能力的重要步骤。例如...

    Java Concurrency In Practice.pdf

    《Java Concurrency In Practice》是一本关于Java并发编程的经典著作,由Brian Göetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes和Doug Lea共同编写。本书深入探讨了Java平台上的多线程编程技巧,...

    Concurrent_Programming+Java Concurrency in Practice+langspec

    首先,"Java Concurrency in Practice"是Java并发编程的经典之作,由Brian Goetz、Tim Peierls、Joshua Bloch、David Holmes和Doug Lea合著。这本书提供了一套实用的指导原则、设计模式和最佳实践,帮助Java开发者...

    Java Concurrency in Practice

    2. **线程安全与可见性**:讨论了Java内存模型(JMM),解释了volatile关键字的作用,以及如何确保线程间的变量可见性和数据一致性。 3. **同步机制**:详述synchronized关键字、wait()、notify()和notifyAll()方法...

    Java Concurrency in Practice Java并发编程

    《Java Concurrency in Practice》是Java并发编程领域的一本权威著作,由Brian Goetz、Tim Peierls、Joshua Bloch、David Holmes和Doug Lea等多位Java并发领域的专家共同编写。这本书深入探讨了Java平台上的多线程和...

    Java并发编程实践(Java Concurrency in Practice) (中英版)

    2. **同步机制**:详细阐述了Java中的同步工具,如synchronized关键字、volatile变量、java.util.concurrent包下的锁、条件变量和原子变量类。这部分还深入解析了死锁、活锁和饥饿现象,以及如何避免它们。 3. **...

    Java Concurrency in Practice CHM版本

    《Java Concurrency in Practice》是由Java并发库的主要开发者Doug Lea撰写的一本经典书籍,它深入探讨了Java编程中的多线程和并发编程技术。这本书是Java开发者掌握并发编程必备的参考文献,对于理解如何在Java环境...

    《Java Concurrency in Practice》源码

    《Java Concurrency in Practice》是Java并发编程领域的一本经典著作,由Brian Goetz、Tim Peierls、Joshua Bloch、David Holmes和Doug Lea合著。这本书深入探讨了如何在Java环境中有效地设计和实现多线程程序,强调...

    Java Concurrency in Practice-2006版

    除了讲述基础概念和同步机制,本书还着重介绍了Java内存模型(Java Memory Model),这是理解Java并发编程的关键部分,涉及到变量可见性、原子性、有序性等内存操作的行为。Java内存模型为程序员提供了清晰的并发...

    java concurrency in practice.rar

    - **volatile关键字**:确保变量的可见性,但不保证原子性。 - **Lock接口与ReentrantLock**:提供了比synchronized更细粒度的锁控制,支持公平锁和非公平锁。 - **java.util.concurrent.atomic包**:提供原子...

    3_Java_Concurrency_in_Practice_proglib_java_practice_

    3. **并发集合**:`ConcurrentHashMap`、`CopyOnWriteArrayList`等并发集合类允许在并发访问时提供更好的性能和安全性,避免了传统的同步容器可能导致的死锁问题。 4. **线程池**:书中深入介绍了`Executor`框架,...

    Addison.Wesley.Java.Concurrency.in.Practice.May.2006

    9. **Java内存模型**:解析Java内存模型(JMM),解释了变量可见性、有序性以及数据一致性的问题。 通过学习《Java并发实践》,开发者可以更好地理解和驾驭Java并发编程,从而编写出更高效、更可靠、更易于维护的...

    java-concurrency-in-practice:java并发精讲

    《Java并发精讲》课程是深入理解Java多线程编程的重要资源,涵盖了从基础到进阶的多个关键知识点。在Java编程中,处理并发问题是提升应用程序性能和响应速度的关键,也是开发者必须掌握的核心技能之一。 一、实现多...

Global site tag (gtag.js) - Google Analytics