在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个 共享、可变资源 的状况,这种资源能够是:一个变量、一个对象、一个文件等。特别注意两点:数据库
简单的说,若是你的代码在单线程下执行和在多线程下执行永远都能得到同样的结果,那么你的代码就是线程安全的。那么,当进行多线程编程时,咱们又会面临哪些线程安全的要求呢?又是要如何去解决的呢?
编程
跟数据库事务的原子性概念差很少,即一个操做(有可能包含有多个子操做)要么所有执行(生效),要么所有都不执行(都不生效)。缓存
关于原子性,一个很是经典的例子就是银行转帐问题:安全
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程可以当即看到。可见性问题是好多人忽略或者理解错误的一点。多线程
CPU从主内存中读数据的效率相对来讲不高,如今主流的计算机中,都有几级缓存。每一个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会当即更新该缓存,但并不必定会当即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤为是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。并发
这一点是操做系统或者说是硬件层面的机制,因此不少应用开发人员常常会忽略。app
有序性指的是,程序执行的顺序按照代码的前后顺序执行。如下面这段代码为例:工具
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们必定彻底按照此顺序执行。
性能
处理器为了提升程序总体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。学习
讲到这里,有人要着急了——什么,CPU不按照个人代码顺序执行代码,那怎么保证获得咱们想要的效果呢?实际上,你们大可放心,CPU虽然并不保证彻底按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
线程之间共享堆空间,在编程的时候就要格外注意避免竞态条件。危险在于多个线程同时访问相同的资源并进行读写操做。当其中一个线程须要根据某个变量的状态来相应执行某个操做的以前,该变量极可能已经被其它线程修改。
死锁:指两个或两个以上的进程(或线程)在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
关于死锁发生的条件:
活锁:是指线程1可使用资源,但它很礼貌,让其余线程先使用资源,线程2也可使用资源,但它很绅士,也让其余线程先使用资源。这样你让我,我让你,最后两个线程都没法使用资源。
关于“死锁与活锁”的比喻:
饥饿:是指若是线程T1占用了资源R,线程T2又请求封锁R,因而T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。而后T4又请求封锁R,当T3释放了R上的封锁以后,系统又批准了T4的请求......,T2可能永远等待。
也就是,若是一个线程由于CPU时间所有被其余线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是由于它得不到CPU运行时间的机会。
关于“饥饿”的比喻:
在Java中,下面三个常见的缘由会致使线程饥饿,以下:
1.高优先级线程吞噬全部的低优先级线程的CPU时间
2.线程被永久堵塞在一个等待进入同步块的状态,由于其余线程老是能在它以前持续地对该同步块进行访问
3.线程在等待一个自己(在其上调用wait())也处于永久等待完成的对象,由于其余线程老是被持续地得到唤醒
解决饥饿的方案被称之为“公平性” – 即全部线程均能公平地得到运行机会。在Java中实现公平性方案,须要:
在Java中实现公平性,虽Java不可能实现100%的公平性,依然能够经过同步结构在线程间实现公平性的提升。
首先来学习一段简单的同步态代码:
若是有多个线程调用doSynchronized()方法,在第一个得到访问的线程未完成前,其余线程将一直处于阻塞状态,并且在这种多线程被阻塞的场景下,接下来将是哪一个线程得到访问是没有保障的。
改成使用锁方式替代同步块,为了提升等待线程的公平性,咱们使用锁方式来替代同步块:
注意到doSynchronized()再也不声明为synchronized,而是用lock.lock()和lock.unlock()来替代。下面是用Lock类作的一个实现:
注意到上面对Lock的实现,若是存在多线程并发访问lock(),这些线程将阻塞在对lock()方法的访问上。另外,若是锁已经锁上(校对注:这里指的是isLocked等于true时),这些线程将阻塞在while(isLocked)循环的wait()调用里面。要记住的是,当线程正在等待进入lock() 时,能够调用wait()释放其锁实例对应的同步锁,使得其余多个线程能够进入lock()方法,并调用wait()方法。
这回看下doSynchronized(),你会注意到在lock()和unlock()之间的注释:在这两个调用之间的代码将运行很长一段时间。进一步设想,这段代码将长时间运行,和进入lock()并调用wait()来比较的话。这意味着大部分时间用在等待进入锁和进入临界区的过程是用在wait()的等待中,而不是被阻塞在试图进入lock()方法中。
在早些时候提到过,同步块不会对等待进入的多个线程谁能得到访问作任何保障,一样当调用notify()时,wait()也不会作保障必定能唤醒线程。所以这个版本的Lock类和doSynchronized()那个版本就保障公平性而言,没有任何区别。
但咱们可以改变这种状况,以下:
下面将上面Lock类转变为公平锁FairLock。你会注意到新的实现和以前的Lock类中的同步和wait()/notify()稍有不一样。重点是,每个调用lock()的线程都会进入一个队列,当解锁时,只有队列里的第一个线程被容许锁住FairLock实例,全部其它的线程都将处于等待状态,直到他们处于队列头部。以下:
首先注意到lock()方法不在声明为synchronized,取而代之的是对必需同步的代码,在synchronized中进行嵌套。
还需注意到,QueueObject实际是一个semaphore。doWait()和doNotify()方法在QueueObject中保存着信号。这样作以免一个线程在调用queueObject.doWait()以前被另外一个线程调用unlock()并随之调用queueObject.doNotify()的线程重入,从而致使信号丢失。queueObject.doWait()调用放置在synchronized(this)块以外,以免被monitor嵌套锁死,因此另外的线程能够解锁,只要当没有线程在lock方法的synchronized(this)块中执行便可。
最后,注意到queueObject.doWait()在try – catch块中是怎样调用的。在InterruptedException抛出的状况下,线程得以离开lock(),并需让它从队列中移除。
经常使用的保证Java操做原子性的工具是锁和同步方法(或者同步代码块)。使用锁,能够保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。
与锁相似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例:
不管使用锁仍是synchronized,本质都是同样,经过锁或同步来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。
基础类型变量自增(i++)是一种常被新手误觉得是原子操做而实际不是的操做。Java中提供了对应的原子操做类来实现该操做,并保证原子性,其本质是利用了CPU级别的CAS指令。因为是CPU级别的指令,其开销比须要操做系统参与的锁的开销小。AtomicInteger使用方法以下:
Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会当即被更新到内存中,而且将其它线程缓存中对该变量的缓存设置成无效,所以其它线程须要读取该值时必须从主内存中读取,从而获得最新的值。
volatile适用场景:volatile适用于不须要保证原子性,但却须要保证可见性的场景。一种典型的使用场景是用它修饰用于中止线程的状态标记。以下所示:
在这种实现方式下,即便其它线程经过调用stop()方法将isRunning设置为false,循环也不必定会当即结束。能够经过volatile关键字,保证while循环及时获得isRunning最新的状态从而及时中止循环,结束线程。
上文讲过编译器和处理器对指令进行从新排序时,会保证从新排序后的执行结果和代码顺序执行的结果一致,因此从新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。
除了从应用层面保证目标代码段执行的顺序性外,JVM还经过被称为happens-before原则隐式地保证顺序性。两个操做的执行顺序只要能够经过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不做任何保证,可对其进行任意必要的从新排序以获取高效率。
happens-before原则(先行发生原则),以下:
1.平时项目中使用锁和synchronized比较多,而不多使用volatile,难道就没有保证可见性?
2.锁和synchronized为什么能保证可见性?
3.既然锁和synchronized便可保证原子性也可保证可见性,为什么还须要volatile?
4.既然锁和synchronized能够保证原子性,为何还须要AtomicInteger这种的类来保证原子操做?
5.还有没有别的办法保证线程安全?
6.synchronized便可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别?