返回

深入浅出话MIT 6.1810 Lab 8:剖析多核处理器上的锁

后端

一、锁的本质与类型

在并发编程中,锁是一种至关重要的机制,用于控制多线程对共享资源的访问,防止数据不一致和程序崩溃。

当一个线程获取锁后,它可以独占访问共享资源,直到释放锁为止。这就像一个人获得了一把钥匙,可以进入一个房间,而其他人都需要等待直到他离开才能进入。

有两种基本类型的锁:

  • 互斥锁: 只允许一个线程同时访问共享资源,就像只有一把钥匙的房间,一次只能一个人进入。
  • 读写锁: 允许多个线程同时读取共享资源,但只能有一个线程同时写入共享资源,就像一个图书馆,许多人可以同时阅读,但一次只能有一个书写员写书。

二、锁的应用场景

锁在并发编程中广泛应用,以下是一些常见的应用场景:

  • 多线程访问共享数据结构,例如链表或哈希表,防止数据混乱。
  • 多线程访问共享设备,例如打印机或文件系统,防止设备冲突。
  • 多线程访问数据库,防止数据更新冲突。

三、锁的使用与优化

使用锁是一把双刃剑。虽然它可以防止数据不一致,但也可能会导致性能下降,就像一个人拿着钥匙,所有人都得排队等他出来一样。

因此,在使用锁时,需要权衡利弊,尽量减少锁的使用。以下是一些优化锁使用的策略:

  • 使用细粒度的锁: 只对需要同步的资源加锁,就像只锁住需要使用的房间,而不是整个房子。
  • 避免长时间持有锁: 在不使用锁时立即释放锁,就像用完钥匙后立即放回钥匙盘,而不是揣在兜里。
  • 使用锁升级技术: 在需要时将读锁升级为写锁,就像在图书馆,当需要写书时,可以把读者的借书证升级为写书证。
  • 使用非阻塞锁: 避免线程在等待锁时被阻塞,就像使用自动门,不需要等门卫开门,直接可以进去。

四、小结

锁是并发编程中的必备工具,合理使用锁可以避免死锁和争用,提升并发编程的性能和效率。就像交通信号灯,可以协调车辆通行,锁也可以协调线程对资源的访问。

在实际项目中,需要根据具体情况选择合适的锁类型,并采用适当的锁优化策略。就像不同的交通场景需要不同的信号灯配置,不同的并发编程场景也需要不同的锁策略。

示例代码

// 使用互斥锁保护共享资源
private final Object lock = new Object();

public void accessSharedResource() {
    synchronized (lock) {
        // 对共享资源进行操作
    }
}

// 使用读写锁保护共享资源
private final ReadWriteLock lock = new ReentrantReadWriteLock();

public void readSharedResource() {
    lock.readLock().lock();
    try {
        // 读取共享资源
    } finally {
        lock.readLock().unlock();
    }
}

public void writeSharedResource() {
    lock.writeLock().lock();
    try {
        // 写入共享资源
    } finally {
        lock.writeLock().unlock();
    }
}

常见问题解答

  1. 什么是死锁?
    死锁是指两个或多个线程相互等待对方的锁,导致所有线程都无法继续执行,就像两个孩子互相抢玩具,谁也不肯放手,最后都哭了起来。

  2. 什么是争用?
    争用是指多个线程同时尝试获取同一把锁,导致线程长时间等待,就像一群人排队等电梯,电梯来了大家一窝蜂挤进去,互相推搡。

  3. 如何避免死锁?
    避免死锁的方法包括:避免嵌套锁、使用超时机制、采用死锁检测和恢复机制,就像交通信号灯可以防止死锁,定时器可以防止电梯拥堵,监控系统可以检测和解决死锁。

  4. 如何减少争用?
    减少争用的方法包括:使用细粒度的锁、使用非阻塞锁、采用乐观并发控制,就像使用自动门可以减少排队时间,使用预订机制可以减少冲突,乐观并发控制可以减少锁的使用。

  5. 锁和同步有什么区别?
    锁是一种实现同步的机制,同步是指协调多个线程对共享资源的访问,就像锁是交通信号灯,同步是交通管理。