为什么CFRunLoopRef是线程安全的,而基于此的NSRunLoop却不是线程安全的呢?
线程安全时多线程领域的问题,线程安全可言简单的理解为一个方法或者一个实例可言在多线程环境中使用而不会出现问题。
为什么会出现线程不安全?
在同一个进程中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。例如:同一内存区(变量,数组、对象)、系统(数据库,webServices等)或者文件。实际上,这些问题只有在多个线程向这些资源做了改变,比如写入删除才可以能发生,只要资源部不发生变化,多个线程读取相同的资源就是安全。
多线程同时执行下面代码可能会出错:Java
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
如果线程A和线程B同时执行这个Counter对象的add(),我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码看作单挑指令来执行的,而是按照下面的顺序:
1、从内存获取 this.count 的值放到寄存器;
2、将寄存器中值增加value
3、将寄存器中值写回内存
如果线程A和线程B交错执行会发生什么:
this.count = 0;
A:读取this.count 到一个寄存器(0)
B:读取this.count 到一个寄存器 (0)
B:将寄存器的值加2
B:将寄存器(2)回写到内存。this.count 现在等于2
A:将寄存器的值加3
A:将寄存器(3)回写到内存,this.count 现在等3
两个线程分别+2,+3到count变量上,两个线程执行结束后变量的值应该等于5.然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0.然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后回写内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程的交叉执行结果是无法预料的。
竞态条件&临界区
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就存在竞态条件。导致竞态条件发生的代码区称为临界区。上例中的add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。
共享资源
允许被多个线程同时执行的代码称为线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源会引发竞态条件。
局部变量
局部变量存储在线程自己的栈中。局部变量永远不会被多个线程共享。所以基础类型那个局部变量是线程安全的,如:
public void someMethod{
long threadSafeInt = 0;
threadSafeInt ++;
}
局部对象引用
上面提到的局部变量是一个基本类型,如果局部变量是一个对象类型呢?对象的局部引用和基础类型不一样。尽管饮用本身没有被共享,但是引用的对象并没有存储在线程的栈内,所有对象都存放在共享堆中,所以对对象的局部引用,有可能是不安全的。怎样才能保证线程安全的呢?如果在某个方法中创建的对象不会被其他方法或者全局变量获得,或者说函数中创建的对象没有逃出此函数的范围,那么他就是线程安全。例如:
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject)
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
上例中LocalObject对象么有被方法返回,也米有被传递给someMethod()方法外的对象,始终在someMethod()中,每个执行someMethod()的线程都会穿件自己的localObject对象,并赋值给localObject引用。因此这里的LocalObject是线程安全的。事实上,整个someMethod都是线程安全的。即使将LocalObject作为参数传递给同一类的其他方法,他仍然是线程安全的。当然,如果LocalObject通过其某些方法传递给其他的线程,那就是不安全的了。
对象的成员对象存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那么这个代码就不是线程安全的。比如:
public class NotThreadSafe{
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text)
}
}
如果两个线程同时调用NotThreadSafe实例的add()方法,就会有竞态条件:
NotThreadSafe shareInstance = new NotThreadSafe();
new Thread(new MyRunnable(shareInstanc)).start();
new Thread(new MyRunnable(shareInstanc)).start();
public class MyRunnable implement Runnable {
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run (){
this.instance.add("some text");
}
}
注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。
当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
现在两个线程都有自己单独的NotThreadSafe对象,访问的不是同一资源,不满足竞态条件,是线程安全的。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。
1、线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的.
如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。资源可以是对象,数组,文件,数据库连接,套接字等等
2、注意即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:
线程1检查记录X是否存在。检查结果:不存在
线程2检查记录X是否存在。检查结果:不存在
线程1插入记录X
线程2插入记录X
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录。
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。
不可变的共享资源
当多个 线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
如果你需要对ImmutableValue类的实例进行操作,如添加一个类似于加法的操作,我们不能对这个实例直接进行操作,只能创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:
public class ImmutableValue{
。。。。
public ImmutableValue add(int valueToAdd){//累方法
return new ImmutableValue(this.value + valueToAdd);
}
}
请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。
判断是否是线程安全的需要深入了解对象的内部实现。
public void Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用,因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,多个线程访问Calculator实例时仍可通过setValue()和add()方法改变它的状态,因此Calculator类不是线程安全的。
换句话说:ImmutableValue类是线程安全的,但使用它的类则不一定是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。也就是iOS中的类方。
回到开篇的问题,CFRunLoopRef是线程安全的,这个需要看到CFRunLoopRef对象的实现。CFRunloopRef是Apple维护的CoreFoundation,没有不允许创建一个新对象,只有两个获取Runloop的对象 CFRunLoopGetMain()和CFRunLoopGetCurrent()。它们的内部实现如下:
staticCFMutableDictionaryRefloopsDic;
/// 访问 loopsDic 时的锁
staticCFSpinLock_tloopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef_CFRunLoopGet(pthread_tthread){
OSSpinLockLock(&loopsLock);
if(!loopsDic){
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic=CFDictionaryCreateMutable();
CFRunLoopRefmainLoop=_CFRunLoopCreate();
CFDictionarySetValue(loopsDic,pthread_main_thread_np(),mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRefloop=CFDictionaryGetValue(loopsDic,thread));
if(!loop){
/// 取不到时,创建一个
loop=_CFRunLoopCreate();
CFDictionarySetValue(loopsDic,thread,loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(...,thread,loop,__CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
returnloop;
}
CFRunLoopRefCFRunLoopGetMain(){
return_CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRefCFRunLoopGetCurrent(){
return_CFRunLoopGet(pthread_self());
}
可以看到生成的对象是加锁的,这样就避免被改变了。NSRunLoop可以初始化一个对象,可以生成一个新的runloop,这就像上面讲的有可能产生临界区,所以它不是线程安全的。