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不推荐的,这样会使程序变得很 ...
随机推荐
- measureChildren作品
无论是在改写View依然是ViewGroup什么时候.特别ViewGrop什么时候,通常是不可避免的重写onMeasure方法,我们一定会调用setMeasuredDimension()将測量好的宽高 ...
- 通信网Project之——单源单宿最短路问题
最主要的Vertex类: #ifndef VERTEX_H #define VERTEX_H #include <climits> #include <cstddef> #de ...
- Mediator - 中介者模式
定义 用一个中介对象来封装一系列的对象的交互.中介者使各对象不须要显示地相互使用,从而使其耦合松散,并且能够独立的改变他们之间的交互. 案例 比方有一个图像界面,在界面上有一个输入框LineEdit, ...
- C语言库函数大全及应用实例四
原文:C语言库函数大全及应用实例四 [编程资料]C语言库函数大全及应用实例四 couble fmod (double x, dou ...
- 谢绝艳照门 - 手把手教你把当今很hit的家庭监控IP Camera变得网络安全起来
IP Camerars现在已经越来越便宜了,很多人都可以买得起,并且大家也乐意去购买,因为它们的确是用来监控你在高房价的中国购买的爱巢的非常便利的设备.当然,配套的监控应用也层出不穷,从通用的家庭安全 ...
- or1200于IMMU分析
以下摘录<步骤吓得核心--软-core处理器的室内设计与分析>一本书 1 IMMU结构 OR1200中实现IMMU的文件有or1200_immu_top.v.or1200_immu_tlb ...
- xfire集成spring构建webservice
前言:xfire.spring都是比较流行的技术,这里就不再赘述他们各自的优点:本文着重介绍xfire和spring的整合,不会做太深入的探究. 服务端 1. web.xml配置 spring配置部分 ...
- 基于Asterisk的VoIP开发指南——(2)Asterisk AGI程序编写指南
原文:基于Asterisk的VoIP开发指南--(2)Asterisk AGI程序编写指南 5. Asterisk AGI程序编写指南 5.1概述 很多时候,我们需要在拨号方案中做某些业务逻辑的判断或 ...
- Android 使用Gson解析json案例具体解释
一.眼下解析json有三种工具:org.json(Java经常使用的解析),fastjson(阿里巴巴project师开发的),Gson(Google官网出的),解析速度最快的是Gson,下载地址:h ...
- .NET 开源了,Visual Studio 开始支持 Android 和 iOS 程序编写并自带 Android 模拟器
.NET 开源了,Visual Studio 开始支持 Android 和 iOS 程序编写并自带 Android 模拟器 北京时间今天凌晨的 Connect(); 大会上,多少程序员的假想成为现实. ...