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不推荐的,这样会使程序变得很 ...
随机推荐
- Fun<>,匿名方法,Lambda表达式 冒泡排序C#
大头文 分享,进步 冒泡排序C#实现,使用委托,包括三种方式:Fun<>,匿名方法,Lambda表达式 冒泡排序是一种简单的排序方法,适合于小量数字排序,对于大量数字(超过10个),还有更 ...
- web实现QQ第三方登录
开放平台-web实现QQ第三方登录 应用场景 web应用通过QQ登录授权实现第三方登录. 操作步骤 1 注册成为QQ互联平台开发者,http://connect.qq.com ...
- Java业务原子性的一种实现(key 独占访问)
开发过程中,有时候为了解决多线程竞争问题需要加锁,通常锁定的对象是class,object,method,但在特定时候我们需要更细粒度的加锁,也就是根据不同输入参数来锁定不同的资源,这样只有调用此方法 ...
- jQuery的ajax对WebApi和OData的封装
基于jQuery的ajax对WebApi和OData的封装 WebApi 的使用带来了一个显著的特点,对type有一定的要求.一般ajax的type无非就是两种,GET和POST.如果用JSONP来跨 ...
- ExtJs--15--Ext.is*各种类型推断的方法,简单看源代码就能够明确了
/** * Returns true if the passed value is empty, false otherwise. The value is deemed to be empty if ...
- AngularJS应用开发思维之1:声明式界面
这篇博客之前承接上一篇:http://www.cnblogs.com/xuema/p/4335180.html 重写示例:模板.指令和视图 AngularJS最显著的特点是用静态的HTML文档,就可以 ...
- Android 2.3 版本中链接边框问题解决
在做移动互联网开发的过程中,同样需要考虑到移动终端(如手机.平板)的不同版本浏览器兼容问题,在Android 2.3 版本的默认浏览器中有一个bug-会自动给所有链接文本在点击操作过程中加黄色或绿色边 ...
- dozer-初识
1.简介 dozer是一种JavaBean的映射工具,类似于apache的BeanUtils.但是dozer更强大,它可以灵活的处理复杂类型之间的映射.不 但可以进行简单的属性映射.复杂的类型 ...
- datatables表格
datatables表格 并不是所有的后台开发都有美工和前端工程师来配合做页面,为了显示数据并有一定的美感,jQuery的DataTables插件对于像我这样的前端菜鸟来说真是雪中送炭,当然对于专业的 ...
- Cocos2d-x Lua 阅读Csv文件,使用数据更方便
在我的书或出售之前,我的源代码,有Csvshadow文件. 也许这是偏见.我与工作将是最长的轮廓Csv,所以,我会帮助不大喜欢它的游戏. Csv文件,非常格式easy,也就是说,一个数据线,字段之间用 ...