在大多数软件应用中,线程的数量都不止一个,多线程程序处在一个多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变,这就将“线程安全”的问题提上了议程。那么,如何确保线程的安全呢?
线程安全
一般说来,确保线程安全的方法有这几个:竞争与原子 *** 作、同步与锁、可重入、过度优化。
竞争与原子 *** 作
多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多 *** 作被 *** 作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的 *** 作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。
因此,为了避免出现多线程 *** 作数据的出现异常,Linux系统提供了一些常用 *** 作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。
同步与锁
为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。
可重入
一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
过度优化
在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。
我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整 *** 作volatile变量的指令顺序。
在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。线程安全主要在体现在这三个方面:
1原子性:提供互斥访问,同一时刻只能有一个线程对数据进行 *** 作,(atomic,synchronized);
2可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
所以想保证线性安全的话只要从这三个方面入手就可以了。解决这个问题通常有两种方法(个人认为)
一:使用synchronized关键字,这个大家应该都很熟悉了,不解释了;
二:使用CollectionssynchronizedList();使用方法如下:
假如你创建的代码如下:List<Map<String,Object>> data=new ArrayList<Map<String,Object>>();
那么为了解决这个线程安全问题你可以这么使用CollectionssynchronizedList(),如:
List<Map<String,Object>> data=CollectionssynchronizedList(new ArrayList<Map<String,Object>>());
其他的都没变,使用的方法也几乎与ArrayList一样,大家可以参考下api文档;
额外说下 ArrayList与LinkedList;这两个都是接口List下的一个实现,用法都一样,但用的场所的有点不同,ArrayList适合于进行大量的随机访问的情况下使用,LinkedList适合在表中进行插入、删除时使用,二者都是非线程安全,解决方法同上(为了避免线程安全,以上采取的方法,特别是第二种,其实是非常损耗性能的)。你首先要弄明白为什么要实现一个线程安全的NSMutabeArray?线程安全的NSMutabeArray只是个手段,真正的目的是什么?为了实现消息队列?网络 *** 作?还是其它?分析之后,绝大多数情况下,可以将问题简化。
因为没有更多信息,假设还是需要数组。简单的方式,是用一个类比如叫ThreadSafetyArray,将NSMutabeArray包装起来。之后ThreadSafetyArray提供插入和删除的函数。而要遍历,就提供一个walk函数,walk函数传入一个block。
ThreadSafetyArray的所有函数,使用锁或者diapatch_queue保证线程安全。这样你并不需要让NSMutabeArray线程安全,只需要让ThreadSafetyArray线程安全。public class SingDemo{
private static SingDemo demo = new SingDemo();
private SingDemo(){
}
//加入锁
public synchronized SingDemo getInstance(){
return demo;
}
}1
Confinement 限制数据共享。 将可变数据限制在单一线程内部,避免竞争。核心思想就是线程之间不共享可变数据类型。
2
Immutable 将可变数据类型改为Immutable类型。 避免多线程间的race condition。
3
Threadsafe data type 共享线程安全的可变数据。 如果必须要在多线程间使用mutable的数据类型,必须要使用线程安全的数据类型。在JDK的类文档中,记录着是否线程安全。如List,Set,Map等集合类,都是线程不安全的。
4
Synchronization 通过锁的机制共享不安全的可变数据。
并发编程三要素(线程的安全性问题体现在):
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个 *** 作要么 全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行 重排序)
出现线程安全问题的原因:
线程切换带来的原子性问题
缓存导致的可见性问题
编译优化带来的有序性问题
解决办法:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在 *** 作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行 *** 作,如果没有其他线程争用共享数据,那 *** 作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步 *** 作称为非阻塞同步。
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步 *** 作去保证正确性,因此会有一些代码天生就是线程安全的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)