一.什么是线程安全
形式化的定义为,线程安全是指ADT或方法在多线程中要执行正确,不依赖于cpu的调度顺序。
要保证以下几点:
- 不违反spec,保持RI
- 与多少处理器,OS如何调度线程均无关
- 不需要在spec中强制要求client满足某种线程安全的义务
通俗的来说,如果把多线程看成是多个人,一个进程看成是一个项目。那么线程安全可以概括为以下两点:
- 其他人不要改变我不希望改变的数据
- 我在使用某个方法时,其他人尽量不要同时运行项目中的某个方法,如果产生交叉,也必须保证我的运行结果不会受你的操作的影响
我认为,多线程操作的理想就是让每个线程执行起来就跟单线程一样。
二.我们为什么要保证线程安全
这个就很好说明了,就比如,还是把多线程看成多个人,进程看成一个大项目,如果多个人一起管理一个成绩管理系统。假设这个成绩管理系统是多个平行宇宙的人一起使用的。
示例1:
假如你考试得了100分,你很开心的回家告诉父母,并索取奖励。但是父母在成绩管理系统上查询你的成绩时,你却只有30分。你父母笑着给你来了个混合双打,你哭丧着向时空管理局询问为什么这样。经过仔细查询,结果发现问题的根源是:另一个平行宇宙的你,成绩只有30分,覆盖了你原来的100分
这便是没有保证线程安全导致的,由此可见线程安全的重要性。
那么如何避免呢?
既然每个平行宇宙都有一个自己,那么我们只需要为每个平行宇宙的你单独创建一个变量用来存储成绩即可,而不是各个平行宇宙的人都使用同一个变量存储成绩。总结为:不允许数据的共享
这样,你考了100分该吃糖吃糖,另一个平行宇宙的你考了30分,该挨打挨打,互不干扰。
示例2:
假如不同平行宇宙的你姓名年龄甚至性别都有可能不同。而时空管理局规定:每个宇宙的公民都需要了解自己在其他平行宇宙的人格的姓名与年龄等信息。那么这些数据就不得不在宇宙管理系统中共享了。
有一天,你考试的时候忽然发现,老师说你与身份证上的信息不符,不允许你参加考试,结果按照0分处理。你又挨了一次父母的混合双打。你去时空管理局查询原因时,发现,原来是另一个平行宇宙的你擅自修改了你的姓名,年龄甚至性别。你气的不打一处来,声称要取消你身份信息的共享,但是时空管理局并不允许。
那么该怎么办呢?
既然必须共享,而且还不想让其他平行宇宙的人修改这些信息。那么我们直接保证数据不被允许修改不就好了嘛。总结为:共享不可变的数据类型
这样,你的姓名,年龄和性别等信息,其他宇宙的你只能了解,而不能够随意修改了。你又可以安心的考试了。
由上述例子可知,线程安全十分重要,如果无法保证线程安全,那么就有可能造成严重的后果,比如使奖励变成毒打。
三.我们如何保证线程安全
除了上述的两种方法:
- 不允许数据的共享
- 共享不可变的数据类型
我们还将介绍两种保证线程安全的方法:
- 使用线程安全的数据类型
- 使用锁机制
示例3:
在多线程中,List是很不安全的,如:我们想要往List中增加某个元素,那么我们首先要判断List中是否已经存在了这个元素,若不存在,再往里面添加该元素。
在单线程中,显然没有任何问题。但是在多线程中,如果两个线程添加某个元素时同时判断该元素是否存在,若List中没有该元素,那么此时两个线程中都会认为List中没有该元素,那么就都会往里面添加该元素。也就是说,这个元素被添加了两遍,如果有更多线程,那么该元素将会被添加更多遍,这自然不是我们希望的。
解决方法便是:使用线程安全的数据类型Collections.synchornizedList,这样便可以使操作变成原子的,也就不会存在上述的交叉问题了。总结为:使用线程安全的数据类型
示例4:
见以下代码:
List<String> list = Collections.synchornizedList(new ArrayList<String>());
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String ls = iter.next();
if(ls.equals("six"))
iter.remove();
}
尽管list已经使用了线程安全的数据类型,但是在多线程中,该方法仍然不安全,因为synchornizedList只能保证单个操作是原子的,如两个线程中的get()方法不会交叉。但不能保证两个线程中的get()和set()方法是不交叉的。
在上述代码中,如果线程1正在遍历list中的元素,而线程2此时触发了if判断语句,将其中一个元素remove了,那么线程1的结果就可能出现错误。
为了防止上述现象的发生,我们可以使用锁机制来确保同步。我们只要让iterator的遍历操作也变成原子的,那么就不会出现交叉现象。解决方法是:给iterator的遍历操作上把锁
见以下代码:
List<String> list = Collections.synchornizedList(new ArrayList<String>());
synchornized(list){
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String ls = iter.next();
if(ls.equals("six"))
iter.remove();
}
}
这样使用list本身对iterator的遍历操作进行上锁,即可保证遍历操作的原子性,也就不会出现交叉现象,保证了线程安全。
四.总结
在多线程操作时,我们要时时刻刻注意线程安全的保护,否则可能会出现意想不到的错误。保护线程安全主要有以下四种方法:
- 限制数据的共享
- 共享不可变数据类型
- 使用线程安全的数据
- 使用锁机制进行同步