一步一步掌握线程机制(六)---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()这个方法来返回应该在线程第一次访问此变量时返回的值。我们还可以通过继承自这个类来让子线程继承父线程的局部变量。