在Java中,线程的安全实际上指的是内存的安全,这是由操做系统决定的。编程
目前主流的操做系统都是多任务的,即多个进程同时运行。为了保证安全,每一个进程只能访问分配给本身的内存空间,而不能访问别的、分配给别的进程的内存空间,这一安全特性是由操做系统保障的。可是线程却与进程不一样,由于在每一个进程的内存空间中都会有一块特殊的公共区域,一般被称为堆(内存),这块内存区域是进程内全部的线程均可以访问获得的,这个特性是线程之间通讯的一种方式,可是却会引起多个线程同时访问一块内存区域可能产生的一系列问题,这些问题被统称为线程的安全问题。promise
如何在Java中保证线程安全,也就是保证内存的安全,是一个重要的知识点。安全
使用局部变量保证线程安全(内存隔离法)多线程
在程序中,操做系统会为每一个线程分配专属的内存空间,一般被称为栈内存。栈内存是当前线程所私有的,其它线程无权访问,这是由操做系统保障的。那么,若是想要一些数据只能被某个线程访问的话,就能够把这些数据放入线程专属的栈内存中,其中最多见的就是局部变量,局部变量在线程执行方法时被分配到线程的栈内存中。并发
double avgScore(double[] scores) { double sum = 0; for (double score : scores) { sum += score; } int count = scores.length; double avg = sum / count; return avg; }
上面定义了一个算平均成绩的方法,其中的sum、count和avg都是局部变量,当有一个线程A来执行这个方法的时候,这些变量就会在A的栈内存中分配。若是在这时候有另一个B线程来执行这个方法,这些变量也会在B的栈内存中分配,可是B的栈内存中的这些变量和A的栈内存中的这些变量是相互独立的,并不会相互影响。编程语言
简单来讲,就是这些局部变量会在每一个独立线程私有的栈内存中分配一份,而因为线程的栈内存只能被当前线程本身访问,因此栈内存分配的这些变量不能被别的线程访问,也就不会有线程安全的问题了。而局部变量之因此是安全的,是由于它的使用范围仅仅局限于方法中,生命周期随着方法的执行从开始到结束。然而实际开发中却不可能仅仅将一个变量局限于一个方法中,老是要有一个变量被多个方法使用的状况,这时就又会产生线程安全的问题了。高并发
使用ThreadLocal类保证线程安全(标记隔离法)this
若是想要一个变量能被多个方法使用,一般是将变量定义为类的成员变量。而按照主流编程语言的规定,类的成员变量不能再被分配在线程的栈内存中,而应该分配在公共的堆内存中。这样,变量从单个线程的私有变成了多个线程的公有,要保证线程安全就须要想一些特殊的办法,其中的一个方法就是使用ThreadLocal类。使用ThreadLocal类修饰变量以后,每一个线程若是须要访问这个变量,都会拷贝一份出来,而后当前线程就只会访问这份拷贝。这样,由于每一个线程都只能访问本身拷贝的变量,即这些拷贝出来的变量是线程私有的,也就保证了线程的安全了。spa
class SugarFactory { ThreadLocal<String> sugar = new ThreadLocal<>(); String getSugar() { return sugar.get(); } }
上面是一个SugarFactory类,类中有一个ThreadLocal类型的成员变量sugar,每一个线程在运行时,若是须要用到sugar变量,就会从堆中拷贝一份sugar变量出来,存放到线程对象(Thread类的实例对象)的成员变量中去。线程类(Thread)是有一个相似于Map类型的成员变量专门用于存储ThreadLocal类型的数据的。操作系统
从逻辑的从属关系来理解,这些ThreadLocal类型的数据是属于Thread类的成员变量级别的。但若是是从逻辑的内存位置上来看,实际上这些ThreaLocal类型的数据仍是分配在公共区域的堆内存中。这种作法就相似于给该内存区域打上某种标记的作法,在堆内存中标记了这块内存是这个线程私有的。
完整地讲,就是当一个线程须要访问该ThredLocal类型变量的时候,就从堆内存中复制一份出来,并给这份复制打上一个标记,标记这份复制是该线程私有的。
使用常量保证线程安全(只读标记法)
咱们知道,在Java中的常量是只能被读取而不能被修改的,常量一般使用final修饰符进行修饰,这时候对于多线程来讲是安全的。
class MyPromise { final String promise = "i love you forever."; }
常量不会引发不经意被修改的问题,不管多少次读取,结果都是同样的。
使用悲观锁保证线程安全(加锁标记法)
悲观锁一般的理解就是互斥锁。所谓的悲观,指的就是悲观地认为必定会发生线程安全问题,因而就给公共的数据加上一把锁,若是一个线程想要访问该数据,就须要先获取该数据上所加的锁,才能访问该数据,而且在该线程没有释放锁以前,其余的线程是不可以访问该数据的,这样就保证了只有持有锁的线程可以访问该数据,也就保证了线程安全。
class LoveYou { double love = 100; final Lock lock = new Lock(); void increaseLove(double love) { lock.obtain(); this.love += love; lock.release(); } void decreaseLove(double love) { lock.obtain(); this.love -= love; lock.release(); } }
上面的代码中展现了一个这样的场景:我对你的爱初始值是100,若是你作了一些让我开心的事情,我对你的爱意就会增长;若是你作了一些让我难过的事情,我对你的爱意就会减小。由于你让我开心或难过老是反反复复的,若是把时间线拉得无限长,这就是一个并发的场景。增长爱意和减小爱意这两个方法被并发调用,它们共同操做总的爱意,而为了保证爱意的先后一致性,就须要在每次对数据进行操做以前先获取锁,操做完成以后再释放锁。
这种对数据进行加锁的作法,虽然可以很好地解决线程安全问题,可是锁的获取和释放是须要耗费资源的,若是在线程不多的状况下(并发不多),即线程安全问题发生几率较小的状况下,就很容易形成资源的浪费。
使用乐观锁(CAS)保证线程安全(状态比较法)
乐观锁是在并发量小的状况下对悲观锁的一种替代方案,具体是为了下降悲观锁可能产生的资源浪费。
所谓的乐观,指的就是乐观地认为数据在并发量小的状况下,被意外修改的可能性较小。
乐观锁一般的实现就是CAS(Compare and Swap,比较并交换)。假若有一个线程在操做数据,操做到一半想要休眠(挂起)了,而后它就会记录下当前数据的状态(当前数据值),而后就休眠(挂起)了。而后线程从新唤醒以后想要接着操做数据,这时候又担忧数据可能被修改了,因而就把线程休眠前保存的数据状态和如今的数据状态作一个比较,若是是同样的话,说明在线程休眠的过程当中数据没有被别的线程动过(也有可能数据已经被别的线程改过好多轮了,只是最后的数据和该线程休眠前的数据一致,这就是所谓的ABA问题),而后就能够接着完成线程还没完成的操做。若是数据先后不一致,则说明数据被修改,那么这时候线程前面的全部操做都要放弃,从头开始从新再处理一遍逻辑。
而后说一下ABA问题的解决方案,解决方案一般是给数据另外加一个做为标记的版本号字段,并规定每次修改数据都使版本号加1,就能有效判断数据到底有没有被修改过了。
最后再说一下乐观锁和悲观锁的使用场景。乐观锁一般适用在并发量较小的场景下,由于这种场景下数据被并发操做的几率很小,加互斥锁会浪费资源;而悲观锁一般适用在并发量很大的场景下,由于这种场景下数据被并发操做的几率很大,若是使用乐观锁的话,在每次数据被修改后线程都从头开始从新处理一遍逻辑,资源的消耗会远大于互斥锁的资源消耗,所以加互斥锁基本上是目前高并发场景的最优方案。
总结
线程的安全问题,从个人理解上,其实很大程度上是在于线程不是持续工做,而是会在工做的途中休眠所形成的,可是这个可能并无办法解决,由于这是操做系统所决定的,也能够说是CPU的运行机制所决定的,咱们只能从另外的入口去想办法解决问题。
"今天起了风,你站在风口,个人整个世界都是你的味道。"