iOS-线程安全探究

为什么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,这就像上面讲的有可能产生临界区,所以它不是线程安全的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,368评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,941评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,369评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,848评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,719评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,505评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,904评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,528评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,819评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,848评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,652评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,468评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,912评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,095评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,389评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,906评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,120评论 2 339

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,523评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,523评论 18 399
  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 863评论 0 1
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,432评论 1 15
  • 打开门, 有一股猛烈的寒风 袭面而来 一阵一阵抽打在身上 硬生生地疼 灰蒙蒙的天 滂沱的雨 每个人行色匆匆 我忘记...
    忘南川Lethe阅读 409评论 2 0