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不推荐的,这样会使程序变得很 ...
随机推荐
- POJ1201-Intervals(差动限制)
Intervals Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 20786 Accepted: 7866 Descri ...
- 【高德地图API】从零开始学高德JS API(八)——地址解析与逆地址解析
原文:[高德地图API]从零开始学高德JS API(八)——地址解析与逆地址解析 摘要:无论是百度LBS开放平台,还是高德LBS开放平台,其调用量最高的接口,必然是定位,其次就是地址解析了,又称为地理 ...
- js调用wcf 的SOA
jquery 调用wcf 的SOA架构,将三层架构运用到SOA的架构中来 经过前面3天的学习,我想大家应该对SOA的架构有了初步的了解,其实 SOA与三层架构并不冲突,而是三层架构的升级版. 来看下传 ...
- 【百度地图API】如何制作可拖拽的沿道路测距
原文:[百度地图API]如何制作可拖拽的沿道路测距 摘要: 地图测距,大家都会,不就map.getDistance麼.可是,这只能测任意两点的直线距离,用途不够实际啊.比如,我想测试北京天安门到北京后 ...
- Webserver管理系列:3、Windows Update
微软的操作系统可以使用用户过程中发现了一些漏洞,因此,他们经常发布一些系统补丁.因此,我们需要自己主动安装更新功能后,打开系统. 默认的更新功能未开启自己主动: 开启自己主动更新功能后.Windows ...
- MSSQL发现第五到数据的第十
第二十数据查询数据库,第十条数据,两起案件: 1,ID是连接的,当然这样的情况比較好查.直接SELECT就能够了.取ID大于5小于10就能够了, 这样的情况比較少. 2.ID不是连接的.假设要取第五条 ...
- Java初认识--函数和数组
一.函数 1.函数的定义 函数就是定义在类中的具有特定功能的一段独立小程序,函数也称为方法. java中最小的功能单元就是函数. 2.函数的格式 修饰符 返回值类型 函数名(参数类型 形式参数1,参数 ...
- MySQL 升级方法指南大全
原文:MySQL 升级方法指南大全 通常,从一个发布版本升级到另一个版本时,我们建议按照顺序来升级版本.例如,想要升级 MySQL 3.23 时,先升级到 MySQL 4.0,而不是直接升级到 MyS ...
- 在windows下用C语言写socket通讯实例
原文:在windows下用C语言写socket通讯实例 From:Microsoft Dev Center #undef UNICODE #define WIN32_LEAN_AND_MEAN #in ...
- log4e插件的安装和使用
1.首先下载log4e小工具.放入myeclipse10安装文件夹D:\Program Files (x86)\myEclipse10\MyEclipse Blue Edition 10\dropin ...