阶段1

事情的起因是同事写了这样一段代码。

@synchronized(@"test synchronized"){NSLog(@"do something");
}

于是我指出这样应该是锁不住的,因为 synchronized 锁的是对象,而每次创建的字符串都是新对象,所以锁不住。

同事跟我说,“no,no,no”,你太天真了,编译器会优化字符串,像这种写在代码里的字符串,会被放在ios包的常量字符串里,终生只有一个地址。还给我祭出了ipa包内容截图。

于是我自己写了段测试代码

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(@"test synchronized"){[NSThread sleepForTimeInterval:3];NSLog(@"1");}});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(@"test synchronized"){NSLog(@"2");}});
2018-07-19 10:19:43.029043+0800 TestJsPatch[4988:1322177] 1
2018-07-19 10:19:43.029133+0800 TestJsPatch[4988:1322179] 2

看来真的是这样。

阶段2

针对上面的问题,我想着写死在代码里的纯字符串会被编译器优化,那如果新创建的 NSString 对象,是不是就锁不住了呢。于是我测试了下面的代码。

NSString *string1 = [[NSString alloc] initWithString:@"test synchronized"];NSString *string2 = [[NSString alloc] initWithString:@"test synchronized"];NSLog(@"%p", &string1);NSLog(@"%p", &string2);dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(string1){[NSThread sleepForTimeInterval:3];NSLog(@"1");}});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(string2){NSLog(@"2");}});
2018-07-19 10:22:03.083801+0800 TestJsPatch[4994:1323439] 0x16f2b9868
2018-07-19 10:22:03.083857+0800 TestJsPatch[4994:1323439] 0x16f2b9860
2018-07-19 10:22:06.089249+0800 TestJsPatch[4994:1323509] 1
2018-07-19 10:22:06.089360+0800 TestJsPatch[4994:1323510] 2

神奇的事情发生了,string1 和 string2 的地址明显是不一样的,为什么还是能锁住呢。有了阶段1的经验,在这里,有一个猜想是,虽然 string1 和 string2 对象的地址不一样,但是他们指向的内容地址是一样的,还是 "test synchronized" 的地址。

后来又加了两句这样的打印。

NSLog(@"%p", string1);
NSLog(@"%p", string2);2018-07-19 10:25:24.797033+0800 TestJsPatch[5000:1325160] 0x102b30a80
2018-07-19 10:25:24.797042+0800 TestJsPatch[5000:1325160] 0x102b30a80

发现他们指向的内容地址果然是一样的。那么这里就存在两个问题

  1. 这个指向的内容的地址是否就是 "test synchronized" 常量的地址呢
  2. synchronized 锁的是内容地址而非对象地址,这个可否从代码里找到根据

问题1

接下来我们去看 "test synchronized" 常量的地址是什么呢,通过Hopper可以看到

字符串在包里的地址是

0000000100024a80

再看看程序打印出来string1和string2的地址

0x102b30a80

不一样,有点纳闷。这时候又请教了组里的一位大神,大神给解释说,ios程序的安装就好像是把安装包的内容搬到了内存里。安装包里的地址和内存里的地址肯定是不一样的,但他们相对于起始位置的偏移应该是一样的。于是下面开始找安装包和内存各自的起始位置。

安装包的起始位置也可以从Hopper中看到,在Hopper中将位置拉到安装包的起始处,可以看到如下地址。

可以看到安装包的起始位置是。

0000000100000000

那内存的起始位置怎么看到,可以在Xcode中使用命令image list能列出整个程序image的内容。

(lldb) image list
[  0] 294BD955-9C66-3433-AFBC-DA4A79560B66 0x0000000102b0c000 /tmp/xcode/TestJsPatch-cvdefnzlhcjogbdlyqafsspkkxzw/Build/Products/Release-iphoneos/TestJsPatch.app/TestJsPatch /tmp/xcode/TestJsPatch-cvdefnzlhcjogbdlyqafsspkkxzw/Build/Products/Release-iphoneos/TestJsPatch.app.dSYM/Contents/Resources/DWARF/TestJsPatch
[  1] B15E536A-7107-32DA-BFAF-ECE44C5685E4 0x0000000102ccc000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/usr/lib/dyld
[  2] BBB23B9E-FD65-3AB5-B873-85910ABE5B95 0x00000001929a7000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/System/Library/Frameworks/Photos.framework/Photos
[  3] CC396CA7-A9D1-33D4-898E-573CC46EC982 0x0000000183983000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/usr/lib/libz.1.dylib
[  4] E53F9393-BFC8-3EF5-8520-B0FE6B193183 0x000000018440d000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation

可以看出起始位置是

0x0000000102b0c000

那么我看减一下,看看偏移是否一致呢

0x102b30a80 - 0x0000000102b0c000 = 0000000100024a80 - 0000000100000000

可以看到,是一样的,也就是说,即使是用静态字符串初始化的NSString,他们指向的内容依然是一样的。

问题2

对于问题2,synchronized 锁的是内容地址而非对象地址,这个可否从代码里找到根据。就需要去翻阅ios的代码了,首先我们需要搞清楚@synchronized这个语法糖,到底调用的是什么方法,从Xcode中打开Debug -> Debug Workflow -> Always Show Disassembly,在断点调试的时候可以可以看到汇编代码。

可以看到@synchronized编成汇编后如下

->  0x10067a838 <+12>:  ldr    x0, [x0, #0x20]0x10067a83c <+16>:  bl     0x1006941b0               ; symbol stub for: objc_retain0x10067a840 <+20>:  mov    x19, x00x10067a844 <+24>:  bl     0x100694210               ; symbol stub for: objc_sync_enter0x10067a848 <+28>:  nop    0x10067a84c <+32>:  ldr    x0, #0x2118c              ; (void *)0x00000001b6025858: NSThread0x10067a850 <+36>:  nop    0x10067a854 <+40>:  ldr    x1, #0x20ae4              ; "sleepForTimeInterval:"0x10067a858 <+44>:  fmov   d0, #3.000000000x10067a85c <+48>:  bl     0x100694174               ; symbol stub for: objc_msgSend0x10067a860 <+52>:  adr    x0, #0x1e280              ; @"'1'"0x10067a864 <+56>:  nop    0x10067a868 <+60>:  bl     0x100693f40               ; symbol stub for: NSLog0x10067a86c <+64>:  mov    x0, x190x10067a870 <+68>:  bl     0x10069421c               ; symbol stub for: objc_sync_exit0x10067a874 <+72>:  mov    x0, x190x10067a878 <+76>:  ldp    x29, x30, [sp, #0x10]0x10067a87c <+80>:  ldp    x20, x19, [sp], #0x200x10067a880 <+84>:  b      0x1006941a4               ; symbol stub for: objc_release0x10067a884 <+88>:  mov    x20, x00x10067a888 <+92>:  mov    x0, x190x10067a88c <+96>:  bl     0x10069421c               ; symbol stub for: objc_sync_exit0x10067a890 <+100>: mov    x0, x200x10067a894 <+104>: bl     0x100693fd0               ; symbol stub for: _Unwind_Resume

@synchronized对应的代码就是objc_sync_enter和objc_sync_exit,接下来我们去ios runtime的源码里找对应的实现,源码可以从https://opensource.apple.com/source/objc4/中下载,下载之后搜索objc_sync_enter,代码是在objc-sync.mm中。

int objc_sync_enter(id obj)
{int result = OBJC_SYNC_SUCCESS;if (obj) {SyncData* data = id2data(obj, ACQUIRE);assert(data);data->mutex.lock();} else {// @synchronized(nil) does nothingif (DebugNilSync) {_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");}objc_sync_nil();}return result;
}

这个方法其实比较简单,通过 id2data 方法返回一个 SyncData 对象,然后调用 SyncData的 mutex 锁,如果传进来的 obj 是 nil 的话,这个锁就没有效果。看来重点在id2data 方法中。

id2data 主要是生成一个 SyncData 对象,关于 id2data 方法,这篇文章解释的很清楚剖析@synchronizd底层实现原理。简单来说,就是两层cache机制,能保证synchronized对同一个对象只会锁一次,并且还能适当加快效率。

其实对于问题2,我们只要看生成的 SyncData 存的是什么东西就行了。

result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);

从以上代码就可以看出,SyncData 存的是 object 指向的地址,而非 object 本地的地址。

阶段3

后来我又好奇了,在阶段2是一层对象,指针指向常量池里的字符串,那如果我用两层对象呢,比如如下这。

NSString *string1 = [[NSString alloc] initWithString:@"test synchronized"];NSString *string2 = [[NSString alloc] initWithString:@"test synchronized"];NSLog(@"%p", &string1);NSLog(@"%p", &string2);NSLog(@"%p", string1);NSLog(@"%p", string2);NSString *string3 = [[NSString alloc] initWithString:string1];NSString *string4 = [[NSString alloc] initWithString:string2];NSLog(@"%p", &string3);NSLog(@"%p", &string4);NSLog(@"%p", string3);NSLog(@"%p", string4);

string3 和 string4 分别指向了 string1 和 string2,然后又指向了常量字符串,打印出的内容如下。

2018-07-19 10:46:38.288084+0800 TestJsPatch[5030:1333710] 0x16d79d868
2018-07-19 10:46:38.288128+0800 TestJsPatch[5030:1333710] 0x16d79d860
2018-07-19 10:46:38.288135+0800 TestJsPatch[5030:1333710] 0x102684a80
2018-07-19 10:46:40.813209+0800 TestJsPatch[5030:1333710] 0x102684a80
2018-07-19 10:46:40.813298+0800 TestJsPatch[5030:1333710] 0x16d79d858
2018-07-19 10:46:40.813309+0800 TestJsPatch[5030:1333710] 0x16d79d850
2018-07-19 10:46:40.813318+0800 TestJsPatch[5030:1333710] 0x102684a80
2018-07-19 10:46:40.813326+0800 TestJsPatch[5030:1333710] 0x102684a80

如下可以看出,虽然经过了两层指针转换,但他们指向的内容地址依然一样,所以对 synchronized 的效果也是一样的。

阶段4

上面的例子都是用常量字符串直接初始化 NSString,所以可能编译器有优化,那么如果我用 initWithFormat 来初始化会怎么样呢。

NSString *string3 = [[NSString alloc] initWithFormat:@"%@", @"test synchronized"];NSString *string4 = [[NSString alloc] initWithFormat:@"%@", @"test synchronized"];NSLog(@"%p", &string3);NSLog(@"%p", &string4);NSLog(@"%p", string3);NSLog(@"%p", string4);dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(string3){[NSThread sleepForTimeInterval:3];NSLog(@"1");}});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(string4){NSLog(@"2");}});
2018-07-19 10:53:34.314633+0800 TestJsPatch[5048:1338079] 0x16ba39858
2018-07-19 10:53:34.314677+0800 TestJsPatch[5048:1338079] 0x16ba39850
2018-07-19 10:53:34.314684+0800 TestJsPatch[5048:1338079] 0x1c4453980
2018-07-19 10:53:34.314691+0800 TestJsPatch[5048:1338079] 0x1c44539e0
2018-07-19 10:53:34.315006+0800 TestJsPatch[5048:1338159] 2
2018-07-19 10:53:37.319756+0800 TestJsPatch[5048:1338155] 1

可以看出 initWithFormat 并没有进行常量字符串的优化,而是新创建了一个对象。 @synchronized 也就失效了。

结论

常量字符串在编译时会被放在常量池里,也就是 Section __cfstring 中,如果是用 initString 方式初始化 NSString,则 NSString 的内容还是指向这块地址的。但是如果用 initWithFormat 的方式初始化 NSString,则会创建一个新的对象。所以在日常使用中,如果用常量字符串初始化 NSString,应该优先考虑 initString 方法。同时也应该注意 @synchronized 的使用范围,防止 @synchronized 失效。

synchronized猎奇相关推荐

  1. 【java线程】锁机制:synchronized、Lock、Condition

    [Java线程]锁机制:synchronized.Lock.Condition 原创 2013年08月14日 17:15:55 标签:Java /多线程 74967 http://www.infoq. ...

  2. java static 可见性_Java多线程 synchronized与可见性的关系以及可见性问题总结

    作者:七里香的编程之路 出自:OSCHINA 原文:my.oschina.net/u/4098550/blog/4548274 能保证可见性的措施 除了volatile 可以让变量保证可见性外.hap ...

  3. 你真的掌握了并发编程volatile synchronized么?

    先看代码: import java.util.concurrent.atomic.AtomicInteger;/**** @author xialuomantian*/ public class Ne ...

  4. Java使用字节码和汇编语言同步分析volatile,synchronized的底层实现

    关于怎么查看字节码的五种方法参考本人另一篇文章<Java以及IDEA下查看字节码的五种方法> 查看汇编语言汇编码 说要看汇编还是很有必要的,因为有些地方比如加锁其实还是通过汇编实现的,只看 ...

  5. java并发vol_java 并发中 volitile、synchronized和lock的比较(一)

    1.volitile和(synchronnized.lock) 首先比较volitile和synchronnized,volitile线程不安全,但是synchronized则是线程安全的. voli ...

  6. synchronized底层原理_你用过synchronized吗?它的底层原理是什么?Java经典面试题来了...

    并发编程已经成为程序员必备技能 作为Java程序员,不懂得并发编程显然已经不能满足市场需求了,尤其是在面试过程中将处于被动地位,也有可能面试将就此终结. 那么作为Java开发者的你,日常虽然可以基于J ...

  7. 面试题-自旋锁,以及jvm对synchronized的优化

    背景 想要弄清楚这些问题,需要弄清楚其他的很多问题. 比如,对象,而对象本身又可以延伸出很多其他的问题. 我们平时不过只是在使用对象而已,怎么使用?就是new 对象.这只是语法层面的使用,相当于会了一 ...

  8. JAVA多线程之Synchronized、wait、notify实例讲解

    一.Synchronized synchronized中文解释是同步,那么什么是同步呢,解释就是程序中用于控制不同线程间操作发生相对顺序的机制,通俗来讲就是2点,第一要有多线程,第二当多个线程同时竞争 ...

  9. ReentrantLock与synchronized

    1.ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的 ...

最新文章

  1. c# 定位内存快速增长_c#如何避免内存分配瓶颈以提高多线程性能
  2. 项目收获与体会_践行“十个一”在劳动中收获成长——青岛六十六中高二年级学农实践活动圆满结束...
  3. effective C++ 读书笔记(0-2)
  4. oracle课程设计摘要,Oracle程序设计课程设计概要(doc 35页)
  5. Day08- team、iptables、firewall
  6. chm帮助文档编辑_零成本轻松制作帮助文档方法分享
  7. 事件处理介绍(简要学习笔记十七)
  8. 步进电机只能一个方向转?
  9. linux检查python安装情况,使用Python检测Linux服务器连接状态
  10. [Giveaway] 来自AnyBizSoft、4Media、EASEUS的限时免费软件
  11. dynamips tutorial
  12. LaTeX 在线编辑器(LaTeX online editors)
  13. 关于学习的认知方法 | 九七的奇思妙想
  14. python爬股指期货数据_新浪期货数据接口API(实时数据/历史数据)
  15. python遍历循环和无限循环结构_Python --- 程序的循环结构
  16. processing制作动态山水背景
  17. 使用 Python 地图绘制工具 -- folium 全攻略
  18. vue的增删改查(elementUI)
  19. 安装算量软件解决造价人的痛点难点
  20. 独立开发并发布自己的一款手游——SpaceWar

热门文章

  1. c语言不安全库_C语言中不安全的库函数及解决方案
  2. python特效电子相册_用Python和Conky做个电子相册,美化你的Linux桌面
  3. 运筹学与计算机知识,计算机、数学、运筹学等领域的36个重要算法
  4. 根据概率分布随机采样python_PR Sampling Ⅱ:马尔可夫链蒙特卡洛 MCMC及python实现...
  5. python 组合数_python – 查找两个数组元素的最大有效组合数
  6. MATLAB保存数据为dat格式,[转载] Matlab中的数据以.txt或.dat格式保存
  7. c语言程序设计中&是什么,C语言程序设计是什么
  8. Linux中b设备是什么,linux-将新设备添加到b??trfs卷中,但是可用...
  9. SpringSecurity Basic Authentication
  10. Flask session API