java多线程(三) 之 对象的共享
2017-04-22 00:33
316 查看
同步代码块和同步方法可以确保原子的方式执行操作,但一种常见的误解是,认为关键字synchronized只能用于实现原子性或者确定”临界区”,同步还有另外一个重要的方面:内存可见性.我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生状态变化.
一. 可见性
糟糕的案例如下:这段代码,无法保证主线程写入的ready的值和number的值对于线程来说是可见的.
NoVisibility可能会持续的循环下去,因为线程可能永远看不到ready的值,一种更奇怪的现象是,NoVisibility 可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象称之为”重排序”.(本案例的原因在volatile节介绍)
在缺少同步的情况下,java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,此外,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中.
所以,在没有使用同步的情况下,编译器,处理器以及运行时,都可能对操作的执行顺序进行意想不到的调整!
类似上个例子中,统一的一个简单解决办法就是: 只要有数据在多个线程之间共享,就使用正确的同步.
1.失效数据:
上个例子中,当读线程查看ready变量时,可能会得到一个失效的值.
更糟糕的是,失效值可能不会同时出现: 一个线程可能获得某个变量的最值,而获得另一个变量的失效值.
线程不安全
线程安全
2.非原子的64位操作
最低安全性: 当线程在没有同步的情况下读去变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机变量.
最低安全性适用于绝大多数变量,但是存在一个例外,非volatile类型的64位数值变量.java内存模型要求,变量的读取操作和写入操作必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作.当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位.
因此,在多线程程序中,使用共享的long和double等类型的变量也是不安全的,除非使用关键字volatile来声明,或者使用锁保护起来.
3.加锁与可见性:
为什么在访问某个共享可变的变量时,要求所有线程在同一个锁上同步?
其实就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的.
因此,加锁的含义不仅仅局限与互斥行为,还包括了内存可见性.为了确保,所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步.
4.volatile关键字:用来确保将变量的更新操作通知到其他线程.
使用volatile关键子保证main线程中修改asleep的可见性.
当把变量声明为volatile类型后,编译器与运行都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值.
从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块.
为什么有了synchronized,还要有volatile?
在当前大多数处理器的架构上,读取volatile变量的开销只比读取volatile变量的开销略高一点.
为什么第一个案例NoVisibility 会出现奇怪的0,或者死循环?
对于服务器应用程序,无论是在开发阶段还是在测试阶段,当启动JVM时,一定要指定-server命令行选项.server模式的JVM将比client模式的JVM进行了更多的优化,例如: 将循环中未被修改的变量提升到循环外部,因此在client模式的JVM中能正确运行的代码,可能会在部署到server模式中运行失败.例如: 第一个案例中,会优化类似于下的运行结果:
volatile的局限性:
尽管volatile变量也可以用于表示其他的状态信息,但在使用的时候要非常小心.
例如: volatile语义不足以确保递增(count++)操作的原子性,除非你能确保只有一个线程对变量执行写操作.
而加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性.
volatile通常用作某个操作完成,发生中断或者状态标志.
当且满足一下所有条件时,才能使用volatile变量:
1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
2. 该变量不会与其他状态变量一起纳入不变性条件中
3. 在访问变量时不需要加锁
二. 发布与溢出
1.发布对象:
发布(Publish)一个对象是指使对象能够在当前作用域之外的代码中被使用
就是将这个对象公开,例如:
那么knowSecrets就是公开的一个对象,如果将一个Secret对象添加到集合knownSecrets中,同样会发布该Secret对象,因为任何代码都可以遍历该集合.
2.逸出:
数据逸出了它所在的作用域,例如:
states本身是一个私有变量,可是任何调用者都能修改这个数组的内容,这个本应该是私有的变量已经被发布了.
当发布一个对象的时候,在该对象的非私有域中引用的所有对象同样会被发布.
3.非常糟糕的隐式的this逸出:
下面的博客把隐式的this逸出介绍的非常通俗易懂.
http://blog.csdn.net/byluo/article/details/22291907
所以,永远不要在构造过程中使用this引用逸出.
4.安全对象的构造过程
使用工厂方法来防止this引用在构造过程中的逸出:
三. 线程的封闭:
当访问共享的可变数据时,通常需要使用同步,一种避免使用同步的方式就是不共享数据.如果仅在单线程访问数据,就不需要同步.这种技术称之为线程封闭.
1.Ad-hoc线程封闭
维护线程的封闭性职责完全由程序实现来承担.
2.栈封闭
只能通过局部变量访问对象
3.ThreadLocal类:
测试一下代码,你就知道它是用来干嘛的了,它可以为每一个使用同一个对象的它复制一个变量.使得多线程使用它互不干扰.
四. 线程的不变性:
不可变性条件:
例如下面的ThreeStooges类:
良好的编程习惯:
1) 尽量private域
2) 尽量final域
五. 安全发布
安全发布的常用模式:
安全发布的容器:
Hashtable,synchronizedMap,ConcurrentMap,Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList,synchronizedSet,BlockingQueue,ConcurrentLinkedQueue
并发编程的实用策略:
1. 线程封闭
2. 只读线程
3. 线程安全共享
4. 保护对象:使用锁来保护对象
一. 可见性
糟糕的案例如下:这段代码,无法保证主线程写入的ready的值和number的值对于线程来说是可见的.
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; } }
NoVisibility可能会持续的循环下去,因为线程可能永远看不到ready的值,一种更奇怪的现象是,NoVisibility 可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象称之为”重排序”.(本案例的原因在volatile节介绍)
在缺少同步的情况下,java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,此外,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中.
所以,在没有使用同步的情况下,编译器,处理器以及运行时,都可能对操作的执行顺序进行意想不到的调整!
类似上个例子中,统一的一个简单解决办法就是: 只要有数据在多个线程之间共享,就使用正确的同步.
1.失效数据:
上个例子中,当读线程查看ready变量时,可能会得到一个失效的值.
更糟糕的是,失效值可能不会同时出现: 一个线程可能获得某个变量的最值,而获得另一个变量的失效值.
线程不安全
public class MutableInteger { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
线程安全
public class SynchronizedInteger { private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
2.非原子的64位操作
最低安全性: 当线程在没有同步的情况下读去变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机变量.
最低安全性适用于绝大多数变量,但是存在一个例外,非volatile类型的64位数值变量.java内存模型要求,变量的读取操作和写入操作必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作.当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位.
因此,在多线程程序中,使用共享的long和double等类型的变量也是不安全的,除非使用关键字volatile来声明,或者使用锁保护起来.
3.加锁与可见性:
为什么在访问某个共享可变的变量时,要求所有线程在同一个锁上同步?
其实就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的.
因此,加锁的含义不仅仅局限与互斥行为,还包括了内存可见性.为了确保,所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步.
4.volatile关键字:用来确保将变量的更新操作通知到其他线程.
使用volatile关键子保证main线程中修改asleep的可见性.
static volatile boolean asleep; public static void main(String[] args) { new Thread(){ public void run() { while(!asleep){ yield(); } }; }.start(); asleep = true; }
当把变量声明为volatile类型后,编译器与运行都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值.
从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块.
为什么有了synchronized,还要有volatile?
在当前大多数处理器的架构上,读取volatile变量的开销只比读取volatile变量的开销略高一点.
为什么第一个案例NoVisibility 会出现奇怪的0,或者死循环?
对于服务器应用程序,无论是在开发阶段还是在测试阶段,当启动JVM时,一定要指定-server命令行选项.server模式的JVM将比client模式的JVM进行了更多的优化,例如: 将循环中未被修改的变量提升到循环外部,因此在client模式的JVM中能正确运行的代码,可能会在部署到server模式中运行失败.例如: 第一个案例中,会优化类似于下的运行结果:
boolean flag = ready while(!flag){ Thread.yield(); } System.out.println(number);
volatile的局限性:
尽管volatile变量也可以用于表示其他的状态信息,但在使用的时候要非常小心.
例如: volatile语义不足以确保递增(count++)操作的原子性,除非你能确保只有一个线程对变量执行写操作.
而加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性.
volatile通常用作某个操作完成,发生中断或者状态标志.
当且满足一下所有条件时,才能使用volatile变量:
1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
2. 该变量不会与其他状态变量一起纳入不变性条件中
3. 在访问变量时不需要加锁
二. 发布与溢出
1.发布对象:
发布(Publish)一个对象是指使对象能够在当前作用域之外的代码中被使用
就是将这个对象公开,例如:
public Set<Secret> knowSecrets;
那么knowSecrets就是公开的一个对象,如果将一个Secret对象添加到集合knownSecrets中,同样会发布该Secret对象,因为任何代码都可以遍历该集合.
2.逸出:
数据逸出了它所在的作用域,例如:
private String states = new String[]{"a","b","cd"}; public String[] getStates(){return states;}
states本身是一个私有变量,可是任何调用者都能修改这个数组的内容,这个本应该是私有的变量已经被发布了.
当发布一个对象的时候,在该对象的非私有域中引用的所有对象同样会被发布.
3.非常糟糕的隐式的this逸出:
public class ThisEscape{ public ThisEscape(EventSource source){ source.registerListener( new EventListener(){ public void onEvent(Event e){ doSomething(e); } }); } }
下面的博客把隐式的this逸出介绍的非常通俗易懂.
http://blog.csdn.net/byluo/article/details/22291907
所以,永远不要在构造过程中使用this引用逸出.
4.安全对象的构造过程
使用工厂方法来防止this引用在构造过程中的逸出:
//使用工厂方法来防止this引用在构造过程中逸出 public class SafeListener { private final EventListener<String> lis; private SafeListener(){ lis = e->{doSomething(e);}; } //静态工厂 public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener(); source.registListener(safe.lis); return safe; } private void doSomething(String e) { } } @FunctionalInterface interface EventListener<E>{ void onEvent(E e); } interface EventSource { void registListener(EventListener<?> e); }
三. 线程的封闭:
当访问共享的可变数据时,通常需要使用同步,一种避免使用同步的方式就是不共享数据.如果仅在单线程访问数据,就不需要同步.这种技术称之为线程封闭.
1.Ad-hoc线程封闭
维护线程的封闭性职责完全由程序实现来承担.
2.栈封闭
只能通过局部变量访问对象
public int loadTheArk(Collection<E> collection){ int num = 0; SortedSet<E> set; E a = null; //set和set里面的所有数据全部被封闭在了方法中,千万不要让他们逸出. set = new TreeSet<E>(); set.addAll(collection); for(E e:set){ if(a==null) a = e; else if(a.equals(e)) num++; } return num; }
3.ThreadLocal类:
测试一下代码,你就知道它是用来干嘛的了,它可以为每一个使用同一个对象的它复制一个变量.使得多线程使用它互不干扰.
public class ThreadLocalTest extends ThreadLocal<Integer>{ @Override protected Integer initialValue() { return 10; } public int add(){ set(get()+1); return get(); } public static void main(String[] args) { ThreadLocalTest tl = new ThreadLocalTest(); new ThreadTest(tl).start(); new ThreadTest(tl).start(); for(int i = 0;i<2000;i++) System.out.println("main"+"-->"+tl.add()); } } class ThreadTest extends Thread{ private ThreadLocalTest tl = null; public ThreadTest(ThreadLocalTest t) { tl = t; } @Override public void run() { for(int i = 0;i<2000;i++) System.out.println(this.getName()+"-->"+tl.add()); } }
四. 线程的不变性:
不可变性条件:
对象创建以后其状态就不能修改 对象的所有域都是final类型 对象是正确创建的(在对象创建时期,this引用没有逸出)
例如下面的ThreeStooges类:
import java.util.HashSet; import java.util.Set; public class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges(){ stooges.add("fly"); stooges.add("moon"); stooges.add("sky"); } public boolean isStooge(String name){ return stooges.contains(name); } }
良好的编程习惯:
1) 尽量private域
2) 尽量final域
五. 安全发布
安全发布的常用模式:
在静态初始化函数中初始化一个对象的引用 将对象的引用保存到volatile类型的域或者AtomicReferance对象中 将对象的引用保存到某个正确构造对象的final类型域中 将对象的引用保存到一个由锁保护的域中
安全发布的容器:
Hashtable,synchronizedMap,ConcurrentMap,Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList,synchronizedSet,BlockingQueue,ConcurrentLinkedQueue
并发编程的实用策略:
1. 线程封闭
2. 只读线程
3. 线程安全共享
4. 保护对象:使用锁来保护对象
相关文章推荐
- java多线程之多个线程访问共享对象和数据的方式
- 多个窗体直接共享、调用同一个对象(组件)
- Flex中SharedObject远程共享对象的使用
- 多个线程访问共享对象和数据的方式
- FluorineFx:远程共享对象(Remote SharedObjects)
- FCS编程之共享对象概念
- windows笔记-跨越进程边界共享内核对象【复制对象句柄】
- FMS3系列(六):使用远程共享对象(SharedObject)实现多人时时在线聊天(Flex | Flash)
- 共享文件时提示“将安全性信息应用到以下对象时发生错误”
- Java 并发编程(二)——对象共享
- 探索并发编程(三)------Java存储模型和共享对象
- Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock
- 共享跨越进程边界内核对象的方法?
- Flex中的本地共享对象--SharedObject
- java并发编程实战:对象的共享笔记
- 在Delphi与C++之间实现函数与对象共享
- Java多线程编程之ThreadLocal线程范围内的共享变量
- 技巧:多共享动态库中同名对象重复析构问题的解决方法
- Linux动态共享对象(动态链接库)装载过程
- 多个页面共享JAVASCRIPT 变量,对象,函数