(一)建立线程html
要想明白线程机制,咱们先从一些基本内容的概念下手。java
线程和进程是两个彻底不一样的概念,进程是运行在本身的地址空间内的自包容的程序,而线程是在进程中的一个单一的顺序控制流,所以,单个进程能够拥有多个线程。算法
还有就是任务和线程的区别。线程彷佛是进程内的一个任务,准确点讲,任务是由执行线程来驱动的,而任务是附着在线程上的。编程
1、如今正式讲讲线程的建立。canvas
正如咱们前面讲的,任务是由执行线程驱动的,没有附着任务的线程根本就不能说是线程,因此咱们在建立线程的时候,将任务附着到线程上。数组
所谓的任务,对应的就是Runnable,咱们要在这个类中编写相应的run()方法来描述这个任务所要执行的命令,接着就是将任务附着到线程上。像是这样:安全
Thread thread = new Thread(new Runnable(){ @Override public void run(){ ... } });
接着咱们只要经过start()启动该Thread就行。服务器
2、线程的启动数据结构
一种方式就是上面使用的:建立一个Thread的子类,而后实现run()方法,接着一样是经过start()来开启它。注意,Thread的子类只能承载一个任务。多线程
另一种方式就是经过Executor(执行器)来实现。
Executor会在客户端和任务之间提供一个间接层,由这个间接层来执行任务,而且容许管理异步任务的执行,而无需经过显式的管理线程的生命周期。
ExecutorService exec = Executors.newCachedThreadPool();
exec.executor(new RunnableClass);
其中,CachedThreadPool是一种线程池。
3、线程池
线程池在多线程处理技术中是一个很是重要的概念,它会将任务添加到队列中,而后在建立线程后自动启动这些任务。线程池的线程都是后台线程,每一个线程都是用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。线程池中的线程数目是有一个最大值,但这并不意味着只能运行这样多的线程,它的真正意思是同时可以运行的最大线程数目,因此能够等待其余线程运行完毕后再启动。
线程池都有一个线程池管理器,用于建立和管理线程池,还有一个工做线程,也就是线程池中的线程。咱们必须提供给线程池中的工做线程一个任务,这些任务都是实现了一个任务接口,也就是Runnable。线程池还有一个重要的组成:任务队列,用于存放没有处理的任务,这就是一种缓冲机制。
经过线程池的介绍,咱们能够知道,使用到线程池的状况就是这样:须要大量的线程来完成任务,而且完成任务的时间比较短,就像是咱们如今的服务器,同时间接受多个请求而且处理这些请求。
java除了上面的CachedThreadPool,还有另外一种线程池:FixedThreadPool。CachedThreadPool会在执行过程当中建立与所需数量相同的线程,而后在它回收旧线程的时候中止建立新的线程,也就是说,它每次都要保证同时运行的线程的数量不能超过所规定的最大数目。而FixedThreadPool是一次性的预先分配所要执行的线程,像是这样:
ExecutorService exec = Executors.newFixedThreadPool(5);
就是不管要分配的线程的数目是多少,都是运行5个线程。这样的好处是很是明显的,就是用于限制线程的数目。CachedThreadPool是按需分配线程,直到有的线程被回收,也就是出现空闲的时候才会中止建立新的线程,这个过程对于内存来讲,代价是很是高昂的,由于咱们不知道实际上须要建立的线程数量是多少,只会一直不断建立新线程。
看上去彷佛FixedThreadPool比起CachedThreadPool更加好用,但实际上使用更多的是CachedThreadPool,由于通常状况下,不管是什么线程池,现有线程都有可能会被自动复用,而CachedThreadPool在线程结束的时候就会中止建立新的线程,也就是说,它能确保结束掉的线程的确是结束掉了,不会被从新启动,而FixedThreadPool没法保证这点。
接下来咱们能够看看使用上面两种线程池的简单例子:
public void main(String[] args){ ExecutorService cachedExec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++){ cachedExec.execute(new RunnableClass); } cachedExec.shutdown(); ExecutorService fixedExec = Executors.newFixedThreadPool(3); for(int i = 0; i < 5; i++){ fixedExec.execute(new RunnableClass); } fixedExec.shutdown(); }
CachedThreadPool会不断建立线程直到有线程空闲下来为止,而FixedThreadPool会用3个线程来执行5个任务。
在java中,还有一种执行线程的模式:SingleThreadExecutor。顾名思义,该执行器只有一个线程。它就至关于数量为1的FixedThreadPool,若是咱们向它提交多个任务,它们就会按照提交的顺序排队,直到上一个任务执行完毕,由于它们就只有一个线程能够运行。这种方式是为了防止竞争,由于任什么时候刻都只有一个任务在运行,从而不须要同步共享资源。
(二)Thread的生命周期
以前讲到Thread的建立,那是Thread生命周期的第一步,其后就是经过start()方法来启动Thread,它会执行一些内部的管理工做而后调用Thread的run()方法,此时该Thread就是alive(活跃)的,并且咱们还能够经过isAlive()方法来肯定该线程是否启动仍是终结。
一旦启动Thread后,咱们就只能执行一个方法:run(),而run()方法就是负责执行Thread的任务,因此终结Thread的方法很简单,就是终结run()方法。仔细查看文档,咱们会发现里面有一个方法:stop(),彷佛能够用来中止Thread,可是这个方法已经被废除了,由于它存在着内部的竞争。
咱们常常须要一个不断执行的Thread,而后在某个特定的条件下才会终结它,方法有不少,但最经常使用的有设定标记和中断Thread两种方式。
咱们将以前例子中的Thread改写一下:
public class RandomCharacterGenerator extends Thread implements CharacterSource { static char[] chars; static String charArray = "abcdefghijklmnopqrstuvwxyz0123456789"; static { chars = charArray.toCharArray(); } private volatile boolean done = false; Random random; CharacterEventHandler handler; public RandomCharacterGenerator() { random = new Random(); handler = new CharacterEventHandler(); } public int getPauseTime() { return (int) (Math.max(1000, 5000 * random.nextDouble())); } @Override public void addCharacterListener(CharacterListener cl) { handler.addCharacterListener(cl); } @Override public void removeCharacterListener(CharacterListener cl) { handler.removeCharacterListener(cl); } @Override public void nextCharacter() { handler.fireNewCharacter(this, (int) chars[random.nextInt(chars.length)]); } public void run() { while(!done){ nextCharacter(); try { Thread.sleep(getPauseTime()); } catch (InterruptedException ie) { return; } } } public void setDone(){ done = true; } }
如今咱们多了一个标记:done,这样咱们就能够在代码中经过调用setDone()来决定何时中止该Thread。这里使用了volatile关键字,它主要是为了同步。这点会放在同步这里讲。
设定标记的最大问题就是咱们必须等待标记的状态,这样就会形成延迟。固然,这种延迟是没法避免的,但必须想办法缩短到最小。因而,中断Thread这种方法就有它的发挥地方了。
咱们能够经过interrupt()方法来中断Thread,该方法会形成两个反作用:
1.它会致使任何的阻塞方法都会抛出InterruptedException,咱们必须强制性的捕获这个错误哪怕咱们根本就不须要处理它,这也是java的异常处理机制让人诟病的一个地方。
2.设定Thread对象内部的标记来指示此Thread已经被中断了,像是这样:
public void run(){
while(!isInterrupted()){
...
}
}
虽然没法避免延迟,可是延迟已经被缩短了。
不管是采用标记仍是中断的方法,咱们之因此没法消除延迟的缘由是咱们没法肯定是检查标记先仍是调用方法先,这就是所谓的race condition,是线程处理中永远没法避免的话题。
Thread不只能够被终结,还能够暂停,挂起和恢复。
Thread本来有suspend()方法和resume()方法来执行挂起和恢复,但它们和stop()出于一样的缘由,都被废除了。
咱们能够经过sleep()方法来挂起Thread,当在指定的时间后,它就会自动恢复。严格意义上讲,sleep并不等同于suspend,真正的suspend应该是由一个线程来挂起另外一个线程,可是sleep只会影响当前的Thread。要想真正实现挂起和恢复,咱们可使用等待和通知机制,但这个机制最大的问题就是咱们的Thread必须使用该技术来编写。
Thread在终结后,若是有可能,咱们还须要对它进行善后。即便Thread已经被终结了,可是其余对象只要还持有它的引用,它们就能够调用该Thread的资源,这也会致使该Thread没法被回收。
但咱们有时候仍是但愿继续保持该Thread的引用,由于咱们想要判别它是否真的已经完成了工做,可使用join()方法。
join()方法会被阻塞住直到Thread完成它的run()方法,可是这个存在风险:第一次对join()方法的调用可能会一直被阻塞住很长时间直到Thread真正完成,因此,通常状况下咱们仍是使用isAlive()方法来判断。
因为咱们能够经过实现一个Runnable接口来定义咱们的任务,因此在判断所在线程是否已经中断的时候,就有一个问题:该任务尚未绑定到任何线程上。咱们能够经过currentThread()方法来得到当前Thread的引用,接着调用isInterrupted()来判断线程是否中断。
如今开始进入线程编程中最重要的话题---数据同步,它是线程编程的核心,也是难点,就算咱们理解了数据同步的基本原理,可是咱们也没法保证可以写出正确的同步代码,但基本原理是必须掌握的。
要想理解数据同步的基本原理,首先就要明白,为何咱们要数据同步?
public class CharacterDisplayCanvas extends JComponent implements CharacterListener { protected FontMetrics fm; protected char[] tmpChar = new char[1]; protected int fontHeight; public CharacterDisplayCanvas() { setFont(new Font("Monospaced", Font.BOLD, 18)); fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont()); fontHeight = fm.getHeight(); } public CharacterDisplayCanvas(CharacterSource cs) { this(); setCharacterSource(cs); } public void setCharacterSource(CharacterSource cs) { cs.addCharacterListener(this); } public synchronized void newCharacter(CharacterEvent ce) { tmpChar[0] = (char) ce.character; repaint(); } public Dimension preferredSize() { return new Dimension(fm.getMaxAscent() + 10, fm.getMaxAdvance() + 10); } protected synchronized void paintComponent(Graphics gc) { Dimension d = getSize(); gc.clearRect(0, 0, d.width, d.height); if (tmpChar[0] == 0) { return; } int charWidth = fm.charWidth((int) tmpChar[0]); gc.drawChars(tmpChar, 0, 1, (d.width - charWidth) / 2, fontHeight); } }
仔细查看上面的代码,咱们就会发现,有两个方法的前面多了一个新的关键字:synchronized。让咱们看看这两个方法为何要添加这个关键字。
newCharacter()用于显示新字母,而paintComponent()负责调整和重画canvas。这两个方法存在着race condition,也就是竞争,由于它们访问的是同一份数据,最重要的是它们是由不一样的线程所调用的,这就致使咱们没法保证它们的调用是按照正确的顺序来进行,可能在newCharacter()方法未被调用前paintComponent()方法就已经从新绘制canvas。
之因此产生竞争,除了这两个方法访问的是同一份数据以外,还和它们是非automic有关。一个程序若是被认为是automic,那么就表示它是没法被中断的,不会有中间状态。使用synchronized,就能保证该方法没法被中断,那么其余线程就没法在该方法没有完成前调用它。
结合对象锁的知识,咱们能够简单的讲解一下synchronized的原理:一个线程若是想要调用另外一个线程的synchronized方法,并且该方法正在被其余线程调用,那么这个线程就必须等待,等待其余线程释放该方法所在的对象的锁,而后得到该锁执行该方法。锁机制可以确保同一时间只有一个线程可以调用该方法,也就能保证只有一个线程可以访问数据。
还记得咱们以前经过使用标记来结束线程的时候,将该标记用volatile修饰?若是咱们不用volatile,又能使用什么方法呢?
若是单单只是上面的知识,咱们可能会想到利用synchronized来同步run()和setDone(),由于就是这两个方法在竞争done这个数据。可是这样存在很大的问题:run()会在done没有被设置true前永远不会结束,可是done标记却要等到run()方法结束后才能由setDone()方法进行设置。
这就是一个死锁,永远解不开的锁。
产生死锁的缘由有不少,像是上面这种状况就是一个典型的表明,主要缘由就是run()方法的scope(范围)太大。所谓的scope,指的是获取锁到释放锁的时间,而run()方法的scope是一个循环,除非done设置为true。这种须要依赖其余线程的方法来结束执行的方法,若是将整个方法设置为同步,就会出现死锁。
因此,最好的方法就是将scope缩小。
咱们能够不用对整个方法进行同步,而是对须要访问的数据进行同步,也就是对done使用volatile。
要想理解volatile的工做原理,咱们必须清楚变量的加载机制。java的内存模型容许线程可以在local memory中持有变量的值,因此这也就致使某个线程改变该变量的值时,其余线程可能不会察觉到该变量的变化。这种状况只是一种可能,并不表明必定会出现,但像是循环执行这种操做,就增长了这种可能。
因此,咱们要作的事情其实很简单,就是让线程从同一个地方取出变量而不是本身维护一份。使用volatile,每次使用该变量都要从主存储器中读取,每次改变该变量时,也要存入主存储器,并且加载和存储都是automic,不管是不是long或者double变量(这两种类型的存储是非automic的)。
值得注意的,run()方法和setDone()方法自己就是automic,由于setDone()方法仅有一个存储操做,而run()方法也只有一个读取操做,其他部分根本就须要该值保持不变,也就是说,这两个方法其实自己就不存在竞争。
但让人更加困惑的是,volatile自己的存在如今也引发人们的关注:它到底有没有必要?
volatile是以moot point(未决点)来实现的:变量永远都从主存储器中读取,但这也只是JDK 1.2以前的状况,如今的虚拟机实现使得内存模式愈来愈复杂,并且也获得了极大的优化,而且这种趋势只会一直持续下去。也就是说,基于内存模式的volatile可能会由于内存模式的不断优化而逐渐变得没有意义。
volatile的使用是有局限的,它仅仅解决因内存模式而引起的问题,并且只能用在对变量的automic操做上,也就是访问该变量的方法只能够有单一的加载或者存储。但不少方法都是非automic,像是递增或者递减操做,就容许存在中间状态,由于它们自己就是载入,变动和存储的简化而已,也就是所谓的syntactic sugar(语法糖)。
咱们大概能够这样理解volatile的使用条件:强迫虚拟机不要临时复制变量,哪怕咱们在许多状况下都不会使用它们。
volatile是否能够运用在数组上,让整个数组中的全部元素都被同步呢?凡是使用java的人都会对这样的幻想嗤之以鼻,由于实际状况是只有数组的引用才会被同步,数组中的元素不会是volatile的,虚拟机仍是能够将个别元素存储于local的寄存器中,没有任何方法能够指定数组的元素应该以volatile的方式来处理。
咱们上面的同步问题是发生在展现随机数字与字母的显示组件,如今咱们继续将功能完善:玩家能够输入所显示的字母,而且正确就会得分。
(四)同步方法和同步块
在以前例子的基础上,咱们增长新的功能:根据正确与不正确的响应来显示玩家的分数。
public class ScoreLabel extends JLabel implements CharacterListener { private volatile int score = 0; private int char2type = -1; private CharacterSource generator = null, typist = null; 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 synchronized void resetGenerator(CharacterSource newCharactor) { if (generator != null) { generator.removeCharacterListener(this); } generator = newCharactor; if (generator != null) { generator.addCharacterListener(this); } } public synchronized 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(); } } }
这里咱们将newCharacter()方法用synchronized进行同步,是由于这个方法会被多个线程调用,而咱们根本就不知道哪一个线程会在何时调用这个方法。这就是race condition。
变量的volatile没法解决上面的多线程调度问题,由于这里的问题是方法调度的问题,并且更加可怕的是,须要共享的变量很多,其中有些变量是做为条件判断,这就会致使在这些条件变量没有正确的设置前,有些线程已经开始启动了。
这并非简单的将这些变量设置为volatile就能解决的问题,由于就算这些变量的状态不对,其余线程依然可以启动。
这里有几个方法的同步是须要引发咱们注意的:resetScore(),resetGenerator()和resetTypist()这几个方法是在从新启动时才会被调用,彷佛咱们不须要为此同步它们:其余线程这时根本就没有开始启动!!
可是咱们仍是须要同步这些方法,这是一种防卫性的设计,保证整个Class全部相关的方法都是线程安全的。遗憾的是,咱们必须这样考虑,由于多线程编程的最大问题就是咱们永远也不知道咱们的程序会出现什么问题,因此,任何可能会引发线程不安全的因素咱们都要尽可能避免。
这也就引出咱们的问题:如何可以对两个不一样的方法同步化以防止多个线程在调用这些方法的时候影响对方呢?
对方法作同步化,可以控制方法执行的顺序,由于某个线程上已经运行的方法没法被其余线程调用。这个机制的实现是由指定对象自己的lock来完成的,由于方法须要访问的对象的lock被一个线程占有,但值得注意的是,所谓的对象锁其实并非绑定在对象上,而是对象实例上,若是两个线程拥有对象的两个实例,它们均可以同时访问该对象,
同步的方法如何和没有同步的方法共同执行呢?
全部的同步方法都会执行获取对象锁的步骤,可是没有同步的方法,也就是异步方法并不会这样,因此它们可以在任意的时间点被任意的线程执行,而无论究竟是否有同步方法在执行。
关于对象锁的话题天然就会引出一个疑问:静态的同步方法呢?静态的同步方法是没法获取对象锁的,由于它没有this引用,对于它的调用是不存在对象的。但静态的同步方法的确是存在的,那么它又是怎样运做的呢?
这须要另外一个锁:类锁。
咱们能够从对象实例上得到锁,也能从class(由于class对象的存在)上得到锁,即便这东西其实是不存在的,由于它没法实现,只是帮助咱们理解的概念。值得注意的是,由于一个class只有一个class对象,因此一个class只有一个线程能够执行同步的静态方法,并且与对象的锁毫无相关,类锁能够再对象锁外被独立的得到和释放,一个非静态的同步方法若是调用同步的静态方法,那么它能够同时得到这两个锁。
提供synchronized关键字的目的是为了让对象中的方法可以循序的进入,大部分数据保护的需求均可以由这个关键字实现,但在更加复杂的同步化状况中仍是太简单了。
在java这个对象王国里,难道真的是没有Lock这个对象的容身之处吗?答案固然是不可能的,J2SE 5.0开始提供Lock这个接口:
private Lock scoreLock = new ReentrantLock(); public void newCharacter(CharacterEvent ce){ if(ce.source == generator){ try{ scoreLock.lock(); if(char2type != -1){ score--; setScore(); } char2type = ce.character; }finally{ scoreLock.unlock(); } } else{ try{ scoreLock.lock(); if(char2type != ce.character){ score--; } else{ score++; char2type = -1; } setScore(); }finally{ scoreLock.unlock(); } }
Lock这个接口有两个方法:lock()和unlock(),咱们能够在开始的时候调用lock(),而后在结束的时候调用unlock(),这样就能有效的同步化这个方法。
咱们能够看到,其实使用Lock接口只是为了让Lock更加容易被管理:咱们能够存储,传递,甚至是抛弃,其他和使用synchronized是同样的,但更加灵活:咱们能够在有须要的时候才获取和释放锁,由于lock再也不依附于任何调用方法的对象,咱们甚至可让两个对象共享同一个lock!也可让一个对象占有多个lock!!
使用Lock接口,是一种明确的加锁机制,以前咱们的加锁是咱们没法掌握的,咱们没法知道是哪一个线程的哪一个方法得到锁,但能确保同一时间只有一个线程的一个方法得到锁,如今咱们能够明确得的把握这个过程,灵活的设置lock scope,将一些耗时和具备线程安全性的代码移出lock scope,这样咱们就能够写出高效并且线程安全的程序代码,不用像以前同样,为了防止未知错误必须对全部相关方法进行同步。
使用lock接口,能够方便的利用它里面提供的一些便利的方法,像是tryLock(),它能够尝试取得锁,若是没法获取,咱们就能够执行其余操做,而不是浪费时间在等待锁的释放。tryLock()还能够指定等待锁的时间。
synchronized不只能够同步方法,它还能够同步一个程序块:
public void newCharacter(CharacterEvent ce){ if(ce.source == generator){ synchronized(this)[ if(char2type != -1){ score--; setScore(); } char2type = ce.character; } } else{ synchronized(this){ if(char2type != ce.character){ score--; } else{ score--; char2type = -1; } setScore(); } } }
若是是为了缩小lock的范围,咱们依然仍是可使用synchronized而不是使用lock接口,并且这种方式才是更加常见的,由于使用lock接口时咱们须要建立新的对象,须要异常管理。咱们能够lock住其余对象,如被共享的数据对象。
选择synchronized整个方法仍是代码块,都没有什么问题,但lock scope仍是尽量的越小越好。
考虑到newCharacter()这个方法里面出现了策略选择,咱们能够对它进行重构:
private synchronized void newGeneratorCharacter(int c){ if(char2type != -1){ score--; setScore(); } char2type = c; } private synchronized void newTpistCharacter(int c){ if(char2type != c){ score--; } else{ score++; char2type = -1; } setScore(); } public synchronized void newCharacter(CharacterEvent ce){ if(ce.source == generator){ newGeneratorCharacter(ce.character); } else{ newTypistCharacter(ce.character); } }
咱们会注意到,两种策略方法都要用synchronized锁住,但真的有必要吗?由于它们是private,只会在该对象中使用,没有理由要让这些方法获取锁,由于它们也只会被对象内的synchronized方法调用,而这时已经得到锁了。可是咱们仍是要这样作,考虑到之后的开发者可能不知道调用这些方法以前须要获取锁的状况。
因而可知,java的锁机制远比咱们想象中要聪明:它并非盲目的在进入synchronized程序代码块时就开始获取锁,若是当前的线程已经得到锁,根本就没有必要等到锁被释放仍是去获取,只要让synchronized程序段运行就能够。若是没有获取锁,也就不会将它释放掉。这种机制之因此可以运行是由于系统会保持追踪递归取得lock的数目,最后会在第一个取得lock的方法或者代码块退出的时候释放锁。
这就是所谓的nested lock。
以前咱们使用的ReentrantLock一样支持nested lock:若是lock的请求是由当前占有lock的线程发出,内部的nested lock就会要求计数递增,调用unlock()就会递减,直到计数为0就会释放该锁。但这个是ReentrantLock才具备的特性,其余实现了Lock这个接口的类并不具备。
nested lock是很是重要的,由于它有利于避免死锁的发生。死锁的发生远比咱们想象中要更常见,像是方法间的相互调用,更加常见的状况就是回调,像是Swing编程中依赖事件处理程序与监听者的窗口系统,考虑一下监听者常常变更的状况,同步简直就是一个恶梦!!
Synchronized没法知道lock被递归调用的次数,可是使用ReentrantLock能够作到这点。咱们能够经过getHoldCount()方法来得到当前线程对lock所要求的数量,若是数量为0,表明当前线程并未持有锁,可是还不能知道锁是自由的,咱们必须经过isLocked()来判断。咱们还能够经过isHeldByCurrentThread()来判断lock是否由当前的线程所持有,getQueueLength()能够用来取得有多少个线程在等待取得该锁,但这个只是预估值。
在多线程编程中常常讲到死锁,可是即便没有涉及到同步也有可能会产生死锁。死锁之因此是个问题,是由于它会让程序没法正确的执行,更加可怕的是,死锁是很难被检测的,特别是多线程编程每每都会是一个复杂的程序,它可能永远也不会被发现!!
更加悲哀的是,系统没法解决死锁这种状况!
最后一个问题是关于公平的授予锁。
咱们知道,锁是要被授予线程的,可是应该按照什么依据来授予呢?是按照先到先得吗?仍是服务请求最多?或者是对系统最有利的形式来授予?java的同步行为最接近第三种,由于同步并非用来对特殊状况授予锁,它是通用的,因此没有理由让锁按照到达的顺序来授予,应该是由各实现所定义在底层线程系统的行为所决定,但ReentrantLock提供了一种选项能够按照先进先出的顺序获取锁:new ReentrantLock(true),这是为了防止发生锁饥饿的现象。
咱们能够根据本身的具体实现来决定这种公平。
最后,咱们来总结一下:
1.对于同时涉及到静态和非静态方法的同步状况,使用lock对象更加容易,由于lock对象无关于使用它的对象。
2.将整个方法同步化是最简单的,可是这样范围会变大,让确实没有必要的程序段无效率的持有锁。
3.若是涉及到太多的对象,使用同步块机制也是有问题的,同步块没法解决跨方法的锁范围。
(五)等待与通知机制
在以前咱们关于中止Thread的讨论中,曾经使用过设定标记done的作法,一旦done设置为true,线程就会结束,一旦为false,线程就会永远运行下去。这样作法会消耗掉许多CPU循环,是一种对内存不友好的行为。
java中的对象不只拥有锁,并且它们自己就能够经过调用相关方法使本身成为等待者和通知者。
Object对象自己有两个方法:wait()和notify()。wait()会等待条件的发生,而notify()会通知正在等待的线程此条件已经发生,它们都必须从synchronized方法或块中调用。
这种等待-通知机制的目的到底是为什么?
等待-通知机制是一种同步机制,但它更像是一个通讯机制,可以让一个线程与另外一个线程在某个特定条件下进行通讯。可是,该机制却没有指定特定条件是什么。
等待-通知机制可否取代synchronized机制吗?固然不行,等待-通知机制并不会解决synchronized机制可以解决的竞争问题,实际上,这二者是相互配合使用的,并且它自己也存在竞争问题,这是须要经过synchronzied来解决的。
private boolean done = true; public synchronized void run(){ while(true){ try{ if(done){ wait(); }else{ repaint(); wait(100); } }catch(InterruptedException e){ return; } } } public synchronized void setDone(boolean b){ done = b; if(timer == null){ timer = new Thread(this); timer.start(); } if(!done){ notify(); } }
这里的done已经不是volatile,由于咱们不仅是设定个标记值,咱们还须要在设定标记的同时自动发送一个通知。因此,咱们如今是经过synchronized来保护对done的访问。
run()方法不会在done为false时自动退出,它会经过调用wait()方法让线程在这个方法中等待,直到其余线程调用notify()方法。
这里有几个地方值得咱们注意。
首先,咱们这里经过使用wait()方法而不是sleep()方法来使线程休眠,由于wait()方法须要线程持有该对象的同步锁,当wait()方法执行的时候,该锁就会被释放,而当收到通知的时候,线程须要在wait()方法返回前从新得到该锁,就好像一直都持有锁同样。这个技巧是由于在设定与发送通知以及测试与取得通知之间是存在竞争的,若是wait()和notify()在持有同步锁的同时没有被调用,是彻底没有办法保证此通知会被接收到的,而且若是wait()方法在等待前没有释放掉锁,是不可能让notify()方法被调用到,由于它没法取得锁,这也是咱们之因此使用wait()而不是sleep()的另外一个缘由。若是使用sleep()方法,此锁就永远不会被释放,setDone()方法也永远不会执行,通知也永远不会送出。
接着就是这里咱们对run()进行同步化。咱们以前讨论过,对run()进行同步是很是危险的,由于run()方法是绝对不可能会完成的,也就是锁永远不会被释放,可是由于wait()自己就会释放掉锁,因此这个问题也被避免了。
咱们会有一个疑问:若是在notify()方法被调用的时候,没有线程在等待呢?
等待-通知机制并不知道所送出通知的条件,它会假设通知在没有线程等待的时候是没有被收到的,由于这时它也只是返回且通知也被遗失掉,稍后执行wait()方法的线程就必须等待另外一个通知。
上面咱们讲过,等待-通知机制自己也存在竞争问题,这真是一个讽刺:本来用来解决同步问题的机制自己居然也存在同步问题!其实,竞争并不必定是个问题,只要它不引起问题就行。咱们如今就来分析一下这里的竞争问题:
使用wait()的线程会确认条件不存在,这一般是经过检查变量实现的,而后咱们才调用wait()方法。当其余线程设立了该条件,一般也是经过设定同一个变量,才会调用notify()方法。竞争是发生在下列几种状况:
1.第一个线程测试条件并确认它须要等待;
2.第二个线程设定此条件;
3.第二个线程调用notify()方法,这并不会被收到,由于第一个线程尚未进入等待;
4.第一个线程调用wait()方法。
这种竞争就须要同步锁来实现。咱们必须取得锁以确保条件的检查和设定都是automic,也就是说检查和设定都必须处于锁的范围内。
既然咱们上面讲到,wait()方法会释放锁而后从新获取锁,那么是否会有竞争是发生在这段期间呢?理论上是会有,但系统会阻止这种状况。wait()方法与锁机制是紧密结合的,在等待的线程尚未进入准备好能够接收通知的状态前,对象的锁其实是不会被释放的。
咱们的疑问还在继续:线程收到通知,是否就能保证条件被正确的设定呢?抱歉,答案不是。在调用wait()方法前,线程永远应该在持有同步锁时测试条件,在从wait()方法返回时,该线程永远应该从新测试条件以判断是否还须要等待,这是由于其余的线程一样也可以测试条件并判断出无需等待,而后处理由发出通知的线程所设定的有效数据。但这是在只有一个线程在等待通知,若是是多个线程在等待通知,就会发生竞争,并且这是等待-通知机制所没法解决的,由于它能解决的只是内部的竞争以防止通知的遗失。多线程等待最大的问题就是,当一个线程在其余线程收到通知后再收到通知,它没法保证这个通知是有效的,因此等待的线程必须提供选项以供检查状态,并在通知已经被处理的情形下返回到等待的状态,这也是咱们为何老是要将wait()放在循环里面的缘由。
wait()也会在它的线程被中断时提早返回,咱们的程序也必需要处理该中断。
在多线程通知中,咱们如何确保正确的线程收到通知呢?答案是不行的,由于咱们根本就没法保证哪个线程可以收到通知,可以作到的方法就是全部等待的线程都会收到通知,这是经过notifyAll()实现的,但也不是真正的唤醒全部等待的线程,由于锁的问题,实质上全部的线程都会被唤醒,可是真正在执行的线程只有一个。
之因此要这样作,多是由于有一个以上的条件要等待,既然咱们没法确保哪个线程会被唤醒,那就干脆唤醒全部线程,而后由它们本身根据条件判断是否要执行。
等待-通知机制能够和synchronized结合使用:
private Object doneLock = new Object(); public void run(){ synchronized(doneLock){ while(true){ if(done){ doneLock.wait(); }else{ repaint(); doneLock.wait(100); } }catch(InterruptedException e){ return; } } } public void setDone(boolean b){ synchronized(doneLock){ done = b; if(timer == null){ timer = new Thread(this); timer.start(); } if(!done){ doneLock.notify(); } } }
这个技巧是很是有用的,尤为是在具备许多对对象锁的竞争中,由于它可以在同一时间内让更多的线程去访问不一样的方法。
最后咱们要介绍的是条件变量。
J2SE5.0提供了Condition接口。Condition接口是绑定在Lock接口上的,就像等待-通知机制是绑定在同步锁上同样。
private Lock lock = new ReentrantLock(); private Condition cv = lockvar.newCondition(); public void run(){ try{ lock.lock(); while(true){ try{ if(done){ cv.await(); }else{ nextCharacter(); cv.await(getPauseTime(), TimeUnit.MILLISECONDS); } }catch(InterruptedException e){ return; } } }finally{ lock.unlock(); } } public void setDone(boolean b){ try{ lock.lock(); done = b; if(!done){ cv.signal(); }finally{ lock.unlock(); } } }
上面的例子好像是在使用另外一种方式来完成咱们以前的等待-通知机制,实际上使用条件变量是有几个理由的:
1.条件变量在使用Lock对象时是必须的,由于Lock对象的wait()和notify()是没法运做的,由于这些方法已经在内部被用来实现Lock对象,更重要的是,持有Lock对象并不表示持有该对象的同步锁,由于Lock对象和对象所关联的同步锁是不一样的。
2.Condition对象不像java的等待-通知机制,它是被建立成不一样的对象,对每一个Lock对象均可以建立一个以上的Condition对象,因而咱们能够针对个别的线程或者一群线程进行独立的设定,也就是说,对同一个对象上全部被同步化的在等待的线程都得等待相同的条件。
基本上,Condition接口的方法都是复制等待-通知机制,可是提供了避免被中断或者能以相对或绝对时间来指定时限的便利。
前面咱们已经讲过如何让对象具备Thread安全性,让它们可以在同一时间在两个或以上的Thread中使用。Thread的安全性在多线程设计中很是重要,由于race condition是很是难以重现和修正的,咱们很难发现,更加难以改正,除非将这个代码的设计推翻来过。
同步最大的问题不是咱们在须要同步的地方没有使用同步,而是在不须要同步的地方使用了同步,致使效率极度低下。因此,咱们要想办法限制同步,由于无谓的同步比起无谓的运算还更加让人无语。
可是否有办法彻底避免同步呢?
在有些状况下是能够的。咱们可使用以前的volatile关键字来解决这个问题,由于volatile修饰的变量是被完整的存储的,在读取它们的时候,可以确保它们是有效的,也就是最近一次存入的值。但这也是能够避免同步的惟一状况,若是有多个线程同时访问同一份数据,就必须明确的同步化全部对该数据的访问以防止各类race condition。
为何没法彻底避免呢?
每组线程都有本身的一组寄存器,但系统将某个线程分配给CPU时,它会把该线程持有的信息加载到CPU的寄存器中,在分配不一样的线程给CPU前,它会将寄存器的信息保存下来,因此线程之间毫不会共享保存在寄存器中的数值,可是经过使用volatile,咱们能够确保变量不会保持在寄存器中,这点咱们在以前的文章中已经说过了,这就可以确保变量是真正的共享于线程之间。可是同步为何可以解决这个问题呢?由于当虚拟机进入synchronized方法或者synchronized块的时候,它必须从新加载本来已经缓冲到自有寄存器上的数据,也就是存入到主存储器中。
也就是说,除了使用volatile和同步,咱们就没有方法保证被线程共享的数据在访问上的安全性,但事实证实,volatile并非值得推荐的解决方法,因此也只剩下同步了。
既然这样,咱们惟一可以作到的就是学会恰当的使用同步。
同步的目的就是防止race condition致使数据在不一致或者变更中间状态被使用到,这段期间会禁止线程间的竞争。但这个保证会由于一个微妙的问题而变得不可信:线程间可能在同步的程序代码运行前就开始竞争。
并非全部的race condition都应该避免,只有在无Thread安全性的程序段中的race condition才会被认为是问题。咱们可使用两种方法来解决这个问题:使用synchronized程序代码来防止race condition的发生或者将程序设计成无需同步(或仅使用最少的同步)就具备Thread安全性。
对于第二种方法,咱们应该尽量缩小同步的范围并从新组织程序代码以便让具备Thread安全性的段落可以被移出synchronized块以外,这点很是重要,若是有足够的程序代码可以被移出synchronized块以外,咱们甚至根本就不须要进行同步。
咱们可使用volatile来减小同步,可是volatile只能针对单一的载入或者存储操做,但不少状况下都不是这样子,因此它的使用是比较不常见的。
J2SE 5.0提供了一组atomic class来处理更加复杂的状况。相对于只能处理单一的atomic操做,这些class可以让多个操做被atomic地对待,这样咱们也就有可能不须要同步就能实现同步机制所作到的一切。
咱们能够用AtomicInteger,AtomicLong,AtomicBoolean和AtomicReference这四个class实现的四个基本的atomic类型来处理integer,long,boolean和对象。这些class都提供了两个构造器:默认的构造器的值为0,false,false或者null,另外一个构造器是以程序设计者所指定的值来初始化和建立变量。set()和get()这两个方法就提供了volatile变量所具备的的功能:可以atomic的设定与取得值,由于它们可以确保数据的读写是从主存储器上运行的。可是这些class还提供了更多的操做来适应volatile没法解决的状况。
getAndSet()可以在返回初始值的时候atomic的设定变量成新值,彻底不须要任何的同步lock。compareAndSet()与weakCompareAndSet()是有条件的修改程序的方法,这两个方法都要取用两个参数:在方法启动时预期数据所具备的的值,以及要把数据所设定成的值。它们都只会在变量具备预期值的时候才会设定成新值,若是当前值不等于预期值,该变量就不会被从新赋值而且返回false。这两个方法之间有什么区别吗?第二个方法少了一项保证:若是方法返回的值false,该变量不会被变更,可是这并不表示现有值不是预期值,也就是说,这个方法无论初始值是不是预期值均可能会没法更新改值。
incrementAndGet(),decrementAndGet(),getAndIncrement()和getAndDecrement()提供了前置递增,前置递减,后递增和后递减,之因此有这些方法,是由于这些操做都不是atomic的。
addAndGet()和getAndAdd()提供了"前置"和"后置"的运算符给指定值的加法运算,它们可以让程序对变量增或者减一个指定值,包括了负值,因此咱们就不须要一个相对的减法运算。
atomic package目前没有实现atomic字符或者浮点变量,可是咱们可使用AtomicInteger来处理字符,就像是字符常量同样,可是使用atomic的浮点数须要atomic带有只读浮点数值的受管理对象。咱们也没有实现atomic数组,并无功能可以对整个数组作atomic化的变更,最多就是经过使用AtomicInteger,AtomicLong和AtomicReference的数组来模型化,可是数组的大小必须在构造时就指定好,而且在操做过程当中必须提供索引。至于Boolean的atomic数组,一样也能够经过AtomicInteger来实现。
atomic package还有两个类:AtomicMarkableReference和AtomicStampedReterence。这两个类可以让mark或stamp跟在任何对象的引用上,更精确点讲,AtomicMarkableReference提供了一种包括对象引用结合boolean的数据结构,而AtomicStampedReference提供了一种包括对象引用结合integer的数据结构。
其实,atomic class的这些方法在本质上是同样的。在使用的时候,get()方法须要咱们传入一个数组做为参数,stamp或者mark被存储在数组的第一个元素而引用被正常返回。其余的get()方法就只是返回引用,mark或者stamp。
set()与compareAndSet()方法须要额外的参数来表明mark或者stamp。最后,这些class都带有attemptMark()或attemptStamp()方法,用来依据期待的引用设定mark或者stamp。
到了这里,咱们也许会欣喜的将每一个程序或者class改为只用atomic变量,事实上,这种尝试并不仅是替换变量那么简单。atomic class并非同步工具的直接替代品,它们的使用会让咱们的程序设计更加复杂,就算只是一些简单的class也是这样。
咱们来举一个例子:
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版本是一致的,这个目的也是重构的基本原则:在不影响代码外在表现下对代码进行内在的修改,也是面向对象的核心思想。
变量代换是最简单的操做,咱们只要将以前所使用的变量替换成atomic变量。像是咱们这里就能够将resetScore()中的score和char2type这两个变量修改为atomic变量。但有意思的是,将这两个变量一块儿变动的动做并非atomic地完成,仍是有可能会让char2type变量的变动在完成前就变动了score。这彷佛是个问题,但实际上并非这样,由于咱们还保持住这个类在synchronized版本上的语义。要记住,同步的目的是为了消除有问题的race condition,有一些race condition根本就不是问题。咱们再来举个例子:resetScore()和newCharacter()方法都是synchronized,但这也只是意味着二者不会同时运行,被拖延住的newCharacter()方法的调用仍是可能会由于到达的顺序或者取得lock的顺序而延迟运行,因此打字输入的事件可能会等到resetScore()方法完成后才会被传递,但这时传递到的只是个已通过时的事件,这些也是出于一样的缘由:在resetScore()方法中同时变动两个变量这个动做并无被atomic地处理。
第二个修改是变动算法。
咱们来看看resetGenerator()和resetTypist()这两个方法的新的实现。以前咱们对这两个方法所作的,就是尝试将二者的同步lock分离。这的确是个不错的主意,这两个方法都没有变更score或者char2type变量,事实上,它们甚至也没有变更到相互共享的变量,由于resetGenerator()方法和resetTypist()的同步lock只是用来保护此方法不受多个Thread同时地调用。可是,若是只是简单的将generator变量变成AtomicReference,那么以前咱们解决过的问题都会从新引起。
之因此会是这样,是由于resetGenerator()这个方法封装的状态并不仅是generator变量的值,让generator变量变成AtomicReference表示咱们知道对该变量的操做是atomic地发生,但当咱们要从resetGenerator()方法中彻底的删除掉同步时,咱们还必须确保整个由此方法所封装住的状态仍是一致的,而这些所谓的状态,包括了在字母源产生器上ScoreLabel对象的登记(this对象),在这个方法完成后,咱们要确保this对象只有被登记过一次到惟一一个产生器上,也就是被分配到generator的instance变量上的那一个。
想一下这个具体的情景:现有的产生器是generatorA,某个线程以generatorB产生器调用resetGenerator(),而另外一个线程以称为generatorC的产生器来调用此方法。
咱们以前的代码是这样的:
if (generator != null) { generator.removeCharacterListener(this); } generator = newCharactor; if (generator != null) { generator.addCharacterListener(this); }
这段代码最大的问题就是:两个线程同时要求generatorA删除this对象,实际上它会被删除两次,ScoreLabel对象一样也会加入generatorB和generatorC。这两个结果都是错的。
可是咱们前面使用了synchronized来防止这样的错误,但若是是没有使用synchronized的前提下:
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对象所监听的那一个,而且是惟一的一个。
可是这里会有一个反作用:交换以后监听器会从旧的数据来源中被删除掉,且监听器会在交换前被加入到新的数据来源,它如今有可能接收到既不是现有的产生器也不是打字输入来源所产出的字符。以前newCharacter()方法会检查来源是否为产生器的来源,并在不是的时候会假设来源是打字输入来源。如今就再也不是这样,newCharacter()方法必须在处理它以前确认字母的来源,它也必须忽略掉来自不正确的监听器的字母。
因此咱们的最后一步就是从新尝试操做:
@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()这个方法的修改是最大的,由于它如今必需要丢弃任何不是来自于所属来源的事件。
重点是打字输入事件的处理。咱们须要检查输入的字母是不是正确的,若是不是,玩家就会被惩罚,这是经过atomic地递减分数来完成的。若是字母被正确的输入,玩家没法被当即的给予奖赏,相对的,char2type变量要先被更新,分数只有在char2type被正确更新时才会更新。若是更新的操做失败了,这表明着在咱们处理此事件时,有其余事件已经被其余线程处理过了,而且是成功处理完。
什么叫成功的处理完?它表示咱们必须从新处理事件,由于咱们是基于这样的假设:假设正在使用的该变量值不会被变动而且程序代码完成时也是这样,全部已经被咱们设定为具备特定值的变量就确实应该是那个值,但由于这与其余线程冲突,这些假设也就被破坏了。经过从新尝试处理事件,就好像从未遇到冲突同样。
因此咱们才须要将这段代码封装在一个无穷循环里:程序不会离开循环直到事件被成功处理掉。显然在多个事件间存在race condition,循环会确保没有一个事件会被漏掉或者处理了超过一次。只要咱们确实只处理有效的事件一次,事件被处理的顺序就不重要了,由于在处理完每一个事件后,数据就会保持在无缺的状态。实际上,当咱们使用同步的时候,也是同样的情形:多个事件并无以指定的顺序进行,它们只是根据授予lock的顺序来进行。
整个代码如:
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(); } } }
atomic变量的目的只是避免同步的性能因素,可是为何它在无穷循环中的时候还比较快呢?从技术上来说,答案确定不是那个无穷循环,额外的循环只会发生在atomic操做失败的时候,这是由于与其余线程发生冲突。要发生一个真正的无穷循环,就须要无穷的冲突。若是使用同步,这也会是一个问题:无数的线程要访问lock一样也会让程序没法正常操做。另外一个方面,实际上,atomic class与同步间的性能差别一般不是很大。
因此,咱们有必要平衡同步和atomic变量的使用。在使用同步的时候,线程在取得lock前 会被block住而不能执行。这可以让程序由于其余的线程被阻挡不能执行而atomic地来运行。当使用atomic变量的时候,线程是可以并行的运行相同的代码,atomic变量的目的不是消除不具备线程安全性的race condition,它们的目的是要让程序代码具备线程安全性,因此就不用特意去防止race condition。
使用atomic变量正是应了这样的一句老话:天下没有白吃的午饭!咱们避开了同步,可是在所运行的工做量上却付出了代价。这就是所谓的乐观同步:咱们的程序代码抓住保护变量的值并做出在这瞬间没有其余修改的假设,而后程序代码就计算出该变量的新值并尝试更新该变量。若是有其余线程同时修改了这个变量,这个更新就失败而且程序必须从新执行这些步骤,而且使用变量的最新修改过的值。
上面例子中出现了数据交换,也就是在取得旧值的同时atomic地设定新值的能力。这是经过使用getAndSet()方法来完成的,使用这个方法就能确保只有一个线程可以取得并使用该值。
若是有更复杂的数据交换时该如何处理?若是值的设定要依据该旧值又该如何处理?
咱们能够经过把get()和compareAndSet()方法放在循环中来处理。get()方法用来取得旧值,以计算新值,而后再经过使用compareAndSet()方法来设定新值----只有在旧值没有被更动的时候才会设定为新值,若是这个方法失败,整个操做能够从新进行,由于当前的线程在失败时都没有动到任何数据。虽然调用过get()方法,计算过新值,数据的交换并非被个别地atomic,若是它成功,这个顺序能够被认为是atomic的,由于它只在没有其余线程变更该值时才会成功。
compareAndSet()这个方法处理的其实就是所谓比较与设定这种状况。它是只有在当前值是预期值的时候才atomic地设定值的能力,这个方法是在atmoic层提供条件支持能力的重要方法。,它甚至可以用来实现出由mutex所提供的同步能力。
若是是更加复杂的比较该如何处理?若是比较是要依据旧值或者外部值该如何处理?
咱们依然能够经过把get()和compareAndSet()方法放在循环中来处理。由于数据交换和比较实际上是差很少的,惟一的区别就是get()方法取得旧值的目的是为了用来比较或者只用来完成atomic地交换,而复杂的比较是能够用来观察是否要继续操做。
虽然atomic class可用的数据类型列表数量是至关大的,但它依然不是完整的。它不能支持字符和浮点数据类型,虽然支持通常对象类型,可是没有对更复杂的对象类型提供支持,像是String这类对象就具备不少方便的操做。可是,咱们是在JAVA中编程,也就是面向对象编程,咱们彻底能够将数据类型封装进只读的数据对象中来对任何新类型实现atomic的支持,而后此数据对象经过改变atomic引用到新的数据对象,就能够被atomic地变更。但这也仅仅在嵌入数据对象的值不会被任何方式变更的时候才有效。任何对数据对象的变更必须只能经过改变引用到不一样的对象来完成,旧对象的值是不变的。全部由数据对象所封装的值,无论是直接仍是非直接,必须是只读的才能使这个技巧有效的运做。
因此,咱们是不可能atomic地改变浮点数值,可是咱们能够atomic地改变对象引用到不一样的浮点数值,只要浮点数值是只读的,它就具备线程安全性。
这就是咱们实现的浮点数值的atomic class:
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是同样的。
这一样也是须要在值没有以任何方式直接改变的状况下才会有效。任何对数据对象的改变是经过改变引用到不一样的对象上来完成的,也就是所谓的额数据交换,旧的对象值必须没有被变更过,无论是直接仍是间接封装的值都必须是只读的才能让这个技巧有效运做。
明白了这点,咱们也清楚的知道了以高级atomic数据类型来执行大量的数据变动,咱们会用到大量的临时对象,由于为了确保全部的操做是atomic的,咱们必须在临时变量上来完成全部的计算,而且全部的值都是使用数据交换来作atomic地变动。实际上,对象的建立远比咱们想象中要多得多:对每一个事务动做都须要建立一个新的对象,每一个atomic地比对和设定操做在失败而必须从新尝试的时候也须要建立新的对象。
因此,咱们必须明白一件事:使用atomic变量会让咱们的代码很是复杂,咱们必须在它与使用同步间取得平衡:是否能够接受全部临时对象的建立?此技巧是否比同步好?
咱们最后来说一下Thread的局部变量。
任何线程均可以在任意的时间定义该线程私有的局部变量,而其余线程能够定义相同的变量以建立该变量自有的拷贝。这就意味着线程的局部变量没法用在线程间共享状态,对某个线程私有的变量所作的改变并不会反映在其余线程所持有的拷贝上,但这意味着对该变量的访问觉不须要同步化,由于它不可能让多个线程同时访问。
咱们能够利用java.lang.ThreadLocal这个类来模型化:
public class ThreadLocal<T>{ protected T initialValue(); public T get(); public void set(T value); public void remove(); }
通常状况下,咱们都是subclass这个ThreadLocal并覆写initialValue()这个方法来返回应该在线程第一次访问此变量时返回的值。咱们还能够经过继承自这个类来让子线程继承父线程的局部变量。
事实上,ThreadLocal由于性能很是差的缘由咱们不多使用到,可是它在实现一些线程问题的时候仍是很是有用的,像是Android的消息队列,就是使用到了这点。