面试题答案
一键面试弱引用和强引用可能引发的潜在问题
- 野指针:
- 强引用:当对象被强引用持有,在对象释放后,若没有将强引用指针置为
nil
,则该指针会成为野指针。后续访问野指针会导致程序崩溃。例如,在多线程环境下,一个线程释放了对象,而另一个线程还持有指向该对象的强引用且未置nil
,就会出现这种情况。 - 弱引用:本身不会导致野指针问题,因为当对象释放时,所有指向它的弱引用会自动被设置为
nil
。但如果在对象即将释放前,某个线程通过弱引用访问对象并执行一些操作,而对象在另一个线程中马上被释放,可能会访问到已部分释放的对象,导致未定义行为。
- 强引用:当对象被强引用持有,在对象释放后,若没有将强引用指针置为
- 数据竞争:
- 强引用:在多线程环境下,多个线程同时对一个对象的强引用计数进行操作(例如
retain
和release
),可能会导致数据竞争。这会使得引用计数不准确,最终可能导致对象过早释放或过度保留,引发内存错误。 - 弱引用:虽然弱引用的维护由系统自动管理,一般不会出现类似强引用计数的数据竞争问题。但当多个线程通过弱引用访问对象并修改其属性时,如果对象属性没有适当的保护机制,就会出现数据竞争问题。
- 强引用:在多线程环境下,多个线程同时对一个对象的强引用计数进行操作(例如
解决方案及优缺点、适用场景
- 锁机制:
- 互斥锁(
NSLock
或pthread_mutex
):- 解决方案:在访问和修改对象引用计数或对象属性时,使用互斥锁来保证同一时间只有一个线程能执行相关操作。例如,在
retain
和release
操作前后加锁:
- 解决方案:在访问和修改对象引用计数或对象属性时,使用互斥锁来保证同一时间只有一个线程能执行相关操作。例如,在
- 互斥锁(
NSLock *lock = [[NSLock alloc] init];
// 假设 obj 是一个对象
[lock lock];
[obj retain];
[lock unlock];
// 其他操作
[lock lock];
[obj release];
[lock unlock];
- **优点**:实现简单,能有效避免数据竞争。
- **缺点**:会带来性能开销,特别是在高并发场景下,频繁的加锁解锁操作会降低程序的执行效率。
- **适用场景**:适用于并发访问频率不高,对性能要求不是极其苛刻的场景。
- 读写锁(
pthread_rwlock
):- 解决方案:对于读多写少的场景,使用读写锁。多个线程可以同时进行读操作,但写操作需要独占锁。例如,在读取对象属性时使用读锁,修改对象属性或引用计数时使用写锁:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
// 读操作
pthread_rwlock_rdlock(&rwlock);
// 读取对象属性
pthread_rwlock_unlock(&rwlock);
// 写操作
pthread_rwlock_wrlock(&rwlock);
// 修改对象属性或引用计数
pthread_rwlock_unlock(&rwlock);
- **优点**:能提高读操作的并发性能,在读多写少的场景下比互斥锁性能更好。
- **缺点**:实现相对复杂,且如果写操作频繁,可能会导致读线程长时间等待。
- **适用场景**:适用于读操作远多于写操作的场景,如缓存数据的读取和偶尔更新。
2. 原子属性:
- 解决方案:在定义对象属性时,使用
atomic
关键字修饰。例如:
@property (nonatomic, atomic, strong) SomeObject *obj;
- 优点:编译器会自动生成保证属性访问原子性的代码,能防止属性访问过程中的数据竞争。使用简单,不需要手动管理锁。
- 缺点:性能开销较大,因为每次访问属性都需要进行原子操作。并且
atomic
只能保证属性的原子性,不能保证对象内部数据的一致性。例如,对象有多个相关联的属性,修改其中一个属性时,atomic
不能保证其他属性的同步修改。 - 适用场景:适用于对简单属性的访问,对性能要求不是特别高,且只需要保证单个属性访问原子性的场景。
- 自动释放池(
NSAutoreleasePool
):- 解决方案:在多线程环境下,合理使用自动释放池可以控制对象的释放时机,减少野指针的出现概率。例如,在一个长时间运行的线程中,定期创建自动释放池来释放不再使用的对象:
while (1) {
@autoreleasepool {
// 线程中的操作,创建的对象会在自动释放池销毁时释放
}
}
- 优点:能有效管理内存,减少内存峰值,在一定程度上避免因对象长时间未释放导致的野指针问题(因为对象及时释放了)。
- 缺点:需要合理规划自动释放池的创建位置和生命周期,如果创建过于频繁可能会带来额外的性能开销。
- 适用场景:适用于线程中会大量创建临时对象的场景,如在一个循环中不断创建对象的线程。
- GCD(Grand Central Dispatch):
- 解决方案:使用
dispatch_queue
来串行化对对象的访问。例如,创建一个串行队列,所有对对象的操作都在这个队列中执行:
- 解决方案:使用
dispatch_queue_t queue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 对对象的操作,如 retain、release 或修改属性
});
- 优点:使用方便,利用 GCD 的高效调度机制,能有效避免数据竞争。而且 GCD 支持多核并行,在多核设备上性能较好。
- 缺点:如果使用不当,例如在队列中执行耗时操作,可能会影响主线程或其他队列的执行。
- 适用场景:适用于各种多线程场景,特别是需要在后台线程执行任务并保证数据一致性的场景。