Atomic变量和Thread局部变量
Atomic变量和Thread局部变量
前面我们已经讲过如何让对象具有Thread安全性,让它们能够在同一时间在两个或以上的Thread中使用。Thread的安全性在多线程设计中非常重要,因为race condition是非常难以重现和修正的,我们很难发现,更加难以改正,除非将这个代码的设计推翻来过。
同步最大的问题不是我们在需要同步的地方没有使用同步,而是在不需要同步的地方使用了同步,导致效率极度低下。所以,我们要想办法限制同步,因为无谓的同步比起无谓的运算还更加让人无语。
但是否有办法完全避免同步呢?
在有些情况下是可以的。我们可以使用之前的volatile关键字来解决这个问题,因为volatile修饰的变量是被完整的存储的,在读取它们的时候,能够确保它们是有效的,也就是最近一次存入的值。但这也是可以避免同步的唯一情况,如果有多个线程同时访问同一份数据,就必须明确的同步化所有对该数据的访问以防止各种race condition。
为什么无法完全避免呢?
每组线程都有自己的一组寄存器,但系统将某个线程分配给CPU时,它会把该线程持有的信息加载到CPU的寄存器中,在分配不同的线程给CPU前,它会将寄存器的信息保存下来,所以线程之间绝不会共享保存在寄存器中的数值,但是通过使用volatile,我们可以确保变量不会保持在寄存器中,这点我们在之前的文章中已经说过了,这就能够确保变量是真正的共享于线程之间。但是同步为什么能够解决这个问题呢?因为当虚拟机进入synchronized方法或者synchronized块的时候,它必须重新加载原本已经缓冲到自有寄存器上的数据,也就是存入到主存储器中。
也就是说,除了使用volatile和同步,我们就没有方法保证被线程共享的数据在访问上的安全性,但事实证明,volatile并不是值得推荐的解决方法,所以也只剩下同步了。
既然这样,我们唯一能够做到的就是学会恰当的使用同步。
同步的目的就是防止race condition导致数据在不一致或者变动中间状态被使用到,这段期间会禁止线程间的竞争。但这个保证会因为一个微妙的问题而变得不可信:线程间可能在同步的程序代码运行前就开始竞争。
public class ScoreLabel extends JLabel implements CharacterListener {
private volatile int score = 0;
private int char2type = -1;
private CharacterSource generator = null, typist = null;
private Lock scoreLock = new ReentrantLock(); public ScoreLabel(CharacterSource generator, CharacterSource typist) {
this.generator = generator;
this.typist = typist;
if (generator != null) {
generator.addCharacterListener(this);
}
if (typist != null) {
typist.addCharacterListener(this);
}
} public ScoreLabel() {
this(null, null);
} public void resetGenerator(CharacterSource newCharactor) {
try {
scoreLock.lock();
if (generator != null) {
generator.removeCharacterListener(this);
}
generator = newCharactor;
if (generator != null) {
generator.addCharacterListener(this);
}
} finally {
scoreLock.unlock();
}
} public void resetTypist(CharacterSource newTypist) {
if (typist != null) {
typist.removeCharacterListener(this);
typist = newTypist;
}
if (typist != null) {
typist.addCharacterListener(this);
}
} public synchronized void resetScore() {
score = 0;
char2type = -1;
setScore();
} private synchronized void setScore() {
SwingUtilities.invokeLater(new Runnable() { @Override
public void run() {
setText(Integer.toString(score));
}
});
} @Override
public synchronized void newCharacter(CharacterEvent ce) {
if (ce.source == generator) {
if (char2type != -1) {
score--;
setScore();
}
char2type = ce.character;
} else {
if (char2type != ce.character) {
score--;
} else {
score++;
char2type = -1;
}
setScore();
}
}
}
为了修改这个类,我们需要三个修改:简单的变量代换,算法的变更和重新尝试操作,每一个修改都要保持class的synchronized版本语义的完整,而这些都是依赖于程序代码所有的效果,所以我们必须确保程序代码的最终效果和synchronized版本是一致的,这个目的也是重构的基本原则:在不影响代码外在表现下对代码进行内在的修改,也是面向对象的核心思想。
if (generator != null) {
generator.removeCharacterListener(this);
}
generator = newCharactor;
if (generator != null) {
generator.addCharacterListener(this);
}
这段代码最大的问题就是:两个线程同时要求generatorA删除this对象,实际上它会被删除两次,ScoreLabel对象同样也会加入generatorB和generatorC。这两个结果都是错的。
if (newGenerator != null) {
newGenerator.addCharacterListener(this);
}
oldGenerator = generator.getAndSet(newGenerator);
if (oldGenerator != null) {
oldGenerator.removeCharacterListener(this);
}
当它被两个线程同时调用时,ScoreLabel对象会被generatorB和generatorC登记,各个线程随后会atomic地设定当前的产生器,因为它们是同时运行,可能会有不同的结果:假设第一个线程先运行,它会从getAndSet()中取回generatorA,然后将ScoreLabel对象从generatorA的监听器中删除,而第二个线程从getAndSet()中取回generatorB并从generatorB的监听器删除ScoreLabel。如果第二个线程先运行,变量会稍有不同,但结果永远会是一样的:不管哪一个对象被分配给genrator的instance变量,它就是ScoreLabel对象所监听的那一个,并且是唯一的一个。
@Override
public synchronized void newCharacter(CharacterEvent ce) {
int oldChar2type;
if (ce.source == generator.get()) {
oldChar2type = char2type.getAndSet(ce.character);
if (oldChar2type != -1) {
score.decrementAndGet();
setScore();
}
} else if (ce.source == typist.get()) {
while (true) {
oldChar2type = char2type.get();
if (oldChar2type != ce.character) {
score.decrementAndGet();
break;
} else if (char2type.compareAndSet(oldChar2type, -1)) {
score.incrementAndGet();
break;
}
}
setScore();
}
newCharacter()这个方法的修改是最大的,因为它现在必须要丢弃任何不是来自于所属来源的事件。
public class ScoreLabel extends JLabel implements CharacterListener {
private AtomicInteger score = new AtomicInteger(0);
private AtomicInteger char2type = new AtomicInteger(-1);
private AtomicReference<CharacterSource> generator = null;
private AtomicReference<CharacterSource> typist = null; public ScoreLabel(CharacterSource generator, CharacterSource typist) {
this.generator = new AtomicReference<CharacterSource>();
this.typist = new AtomicReference<CharacterSource>(); if (generator != null) {
generator.addCharacterListener(this);
}
if (typist != null) {
typist.addCharacterListener(this);
}
} public ScoreLabel() {
this(null, null);
} public void resetGenerator(CharacterSource newGenerator) {
CharacterSource oldGenerator;
if (newGenerator != null) {
newGenerator.addCharacterListener(this);
}
oldGenerator = generator.getAndSet(newGenerator);
if (oldGenerator != null) {
oldGenerator.removeCharacterListener(this);
}
} public void resetTypist(CharacterSource newTypist) {
CharacterSource oldTypist;
if (newTypist != null) {
newTypist.addCharacterListener(this);
}
oldTypist = typist.getAndSet(newTypist);
if (oldTypist != null) {
oldTypist.removeCharacterListener(this);
}
} public synchronized void resetScore() {
score.set(0);
char2type.set(-1);
setScore();
} private synchronized void setScore() {
SwingUtilities.invokeLater(new Runnable() { @Override
public void run() {
setText(Integer.toString(score.get()));
}
});
} @Override
public synchronized void newCharacter(CharacterEvent ce) {
int oldChar2type;
if (ce.source == generator.get()) {
oldChar2type = char2type.getAndSet(ce.character);
if (oldChar2type != -1) {
score.decrementAndGet();
setScore();
}
} else if (ce.source == typist.get()) {
while (true) {
oldChar2type = char2type.get();
if (oldChar2type != ce.character) {
score.decrementAndGet();
break;
} else if (char2type.compareAndSet(oldChar2type, -1)) {
score.incrementAndGet();
break;
}
}
setScore();
}
}
}
如果是更加复杂的比较该如何处理?如果比较是要依据旧值或者外部值该如何处理?
public class AtomicDouble extends Number{
private AtomicReference<Double> value;
public AtomicDouble(){
this(0.0);
} public AtomicDouble(double initVal){
value = new AtomicReference<Double>(new Double(initVal));
} public double get(){
return value.get().doubleValue();
} public void set(double newVal){
value.set(new Double(newVal));
} public boolean compareAndSet(double expect, double update){
Double origVal, newVal; newVal = new Double(update);
while(true){
origVal = value.get(); if(Double.compare(origVal.doubleValue(), expect) == 0){
if(value.compareAndSet(origVal, newVal)){
return true;
}else{
return false;
}
}
}
} public boolean weakCompareAndSet(double expect, double update){
return compareAndSet(expect, update);
} public double getAndSet(double setVal){
Double origVal, newVal; newVal = new Double(setVal);
while(true){
origVal = value.get();
if(value.compareAndSet(origVal, newVal)){
return origVal.doubleValue();
}
}
} public double getAndAdd(double delta){
Double origVal, newVal; while(true){
origVal = value.get();
newVal = new Double(origVal.doubleValue() + delta);
if(value.compareAndSet(origVal, newVal)){
return origVal.doubleValue();
}
}
} public double addAndGet(double delta){
Double origVal, newVal; while(true){
origVal = value.get();
newVal = new Double(origVal.doubleValue() + delta);
if(value.compareAndSet(origVal, newVal)){
return newVal.doubleValue();
}
}
} public double getAndIncrement(){
return getAndAdd((double)1.0);
} public double getAndDecrement(){
return addAndGet((double)-1.0);
} public double incrementAndGet(){
return addAndGet((double)1.0);
} public double decrementAndGet(){
return addAndGet((double)-1.0);
} public double getAndMultiply(double multiple){
Double origVal, newVal; while(true){
origVal = value.get();
newVal = new Double(origVal.doubleValue() * multiple);
if(value.compareAndSet(origVal, newVal)){
return origVal.doubleValue();
}
}
} public double multiplyAndGet(double multiple){
Double origVal, newVal; while(true){
origVal = value.get();
newVal = new Double(origVal.doubleValue() * multiple);
if(value.compareAndSet(origVal, newVal)){
return newVal.doubleValue();
}
}
}
}
到现在为止,我们还只是对个别的变量做atomic地设定,还没有做到对一群数据atomic地设定。如果是这样,我们就必须通过创建封装这些要被变动值的对象来完成,之后这些值就可以通过atomic地变动对这些值的atomic引用来做到同时地改变。这样的运行方式其实和上面实现的AtomicDouble是一样的。
public class ThreadLocal<T>{
protected T initialValue();
public T get();
public void set(T value);
public void remove();
}
一般情况下,我们都是subclass这个ThreadLocal并覆写initialValue()这个方法来返回应该在线程第一次访问此变量时返回的值。我们还可以通过继承自这个类来让子线程继承父线程的局部变量。
Atomic变量和Thread局部变量的更多相关文章
- 一步一步掌握线程机制(六)---Atomic变量和Thread局部变量
前面我们已经讲过如何让对象具有Thread安全性,让它们能够在同一时间在两个或以上的Thread中使用.Thread的安全性在多线程设计中非常重要,因为race condition是非常难以重现和修正 ...
- Java:全局变量(成员变量)与局部变量
分类细则: 变量按作用范围划分分为全局变量(成员变量)和局部变量 成员变量按调用方式划分分为实例属性与类属性 (有关实例属性与类属性的介绍见另一博文https://blog.csdn.net/Drag ...
- Lua的五种变量类型、局部变量、全局变量、lua运算符、流程控制if语句_学习笔记02
Lua的五种变量类型.局部变量.全局变量 .lua运算符 .流程控制if语句 Lua代码的注释方式: --当行注释 --[[ 多行注释 ]]-- Lua的5种变量类型: 1.null 表示 ...
- 【Java】使用Atomic变量实现锁
Atomic原子操作 在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类 Java从JDK1.5开始提供了java.uti ...
- es6中的let声明变量与es5中的var声明变量的区别,局部变量与全局变量
自己通过看typescript官方文档里的let声明,与阮一峰老师翻译的的es6学习文档,总结以下三点 1.var声明可以多次重复声明同一个变量,let不行 2.let变量只在块级作用域里面有效果,v ...
- C/C++语言中变量作用域:局部变量,全局变量,文件级变量
C/C++语言中的变量分为全局变量和局部变量. 这样的划分方式的根据是变量的可见范围或者叫做作用域. 1 局部变量 局部变量指的是定义在{}中的变量,其作用域也在这个范围内.尽管常见的局部变量都是定义 ...
- java入门---变量类型&类变量&局部变量&实例变量&静态变量
在Java语言中,所有的变量在使用前必须声明.声明变量的基本格式如下: type identifier [ = value][, identifier [= value] ...] ; ...
- 牛客网Java刷题知识点之全局变量(又称成员变量,分为类变量和实例变量)、局部变量、静态变量(又称为类变量)
不多说,直接上干货! 定义类其实就是在定义类中的成员.成员:成员变量<-->属性,成员函数<-->行为. 局部变量在方法内部声明,并且只能在方法内部使用,在外层的方法被调用时被 ...
- 3.ruby语法基础,全部变量,实例变量,类变量,局部变量的使用和注意的要点
1.ruby的全局变量的概念和Java的全局变量的概念不同, ruby的全局变量是以$符号开头的,如果给全局变量的初始化值为nil会出现警告. 赋值给全局变量,这是ruby不推荐的,这样会使程序变得很 ...
随机推荐
- c# 替换非法字符
保存文件的时候,文件名不允许非法字符. public string ReplaceSpecialCharacter(string str) { List<ch ...
- What is HHVM?
What is HHVM? HHVM is an open-source virtual machine designed for executing programs written in Hack ...
- asp.net mvc3 数据验证(二)——错误信息的自定义及其本地化
原文:asp.net mvc3 数据验证(二)--错误信息的自定义及其本地化 一.自定义错误信息 在上一篇文章中所做的验证,在界面上提示的信息都是系统自带的,有些读起来比较生硬.比如: ...
- ASP.NET MVC中对Model进行分步验证的解决方法
原文:ASP.NET MVC中对Model进行分步验证的解决方法 在我之前的文章:ASP.NET MVC2.0结合WF4.0实现用户多步注册流程中将一个用户的注册分成了四步,而这四个步骤都是在完善一个 ...
- JS自动化测试 单元测试之Qunit
前言 因为公司开发了一套javascript SDK需要测试,在网上找了很久,找到了JQuery团队开发的QUnit,和基于JUnit的JsUnit,还有一些还没有看,先讲讲QUnit吧 下载 登录J ...
- div、ul、li等无法居中问题,text-align无效 margin auto无效
很简单.如果是div,直接把div换成: <table align="center"> <tr> <td> ...
- 调试javascript
Chrome 控制台 如何调试javascript 上面的文章已经大致介绍了一下console对象具体有哪些方面以及基本的应用,下面简单介绍一下如何利用好chrome控制台这个神器好好调试javasc ...
- iOS文档序列化(对象归档)
对象归档: 概念: 对象归档是指将对象写入文件保存在硬盘,当再次又一次打开程序时,能够还原这些对象.也称:对象序列化.对象持久化. 数据持久性的方式(事实上就是3类) 1,NSKeyedArchive ...
- leetcode第二题--Median of Two Sorted Arrays
Problem:There are two sorted arrays A and B of size m and n respectively. Find the median of the two ...
- NCache实现Oracle数据与分布式缓存数据同步的3个步骤
多层次结构的应用程序是目前发展的趋势,这种程序都需要庞大的数据库支持.而数据传输的能力直接影响程序性能,成为程序可扩展性的瓶颈.因此很多开发者开始在程序中使用内存分布式缓存来提高程序性能. 同时,内存 ...