看并发代码时经常遇到公平锁、可重入锁、乐观锁这些名字。它们描述的是不同方面,同一把锁可以同时属于其中几类。

锁住的是哪个对象

synchronized 的互斥范围取决于锁对象。两个实例上的同步方法分别锁住各自的 this,静态同步方法锁住类对象。方法名字相同,并不意味着使用同一把锁。

public class LockObjectDemo {
    private final Object lock = new Object();
    private int count;

    public void add() {
        synchronized (lock) {
            count++;
        }
    }

    public int get() {
        synchronized (lock) {
            return count;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockObjectDemo demo = new LockObjectDemo();
        Thread a = new Thread(() -> {
            for (int i = 0; i < 1000; i++) demo.add();
        });
        Thread b = new Thread(() -> {
            for (int i = 0; i < 1000; i++) demo.add();
        });
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(demo.get());
    }
}

结果为 2000。这里把锁定义成 private final,避免外部代码拿它做同步,也避免运行中更换锁对象。用字符串常量或装箱后的缓存对象作为锁,可能意外地与其他代码共用同一个对象。

几种分类

分类关注的问题
可重入、不可重入已经持锁的线程能否再次获取
公平、非公平新来的线程是否需要遵守排队规则
独占、共享同一时刻能否允许多个线程持有
悲观、乐观操作前先排他,还是提交时检查冲突

synchronized 和 ReentrantLock 都支持重入。否则同步方法调用同一对象的另一个同步方法时,就可能等自己释放锁。

公平锁倾向于照顾已经等待的线程,非公平锁允许一定程度的插队。公平不代表操作系统一定按顺序调度,也不代表性能一定更好。唤醒和调度有开销,非公平策略有时可以减少切换,但等待时间也可能更不均匀。

读写锁把访问分成读和写:多个读者可以同时进入,写操作则需要独占。共享的是读取权限,不是允许读者随意修改共享对象。

乐观处理与自旋

悲观处理通常先获取锁,再读写数据。乐观处理先根据旧值计算,提交时检查旧值有没有变化,例如 CAS;失败后可以重试,也可以放弃。

乐观处理不等于永远不使用锁,也不是“读多写少就一定更快”。冲突频繁时,反复重试同样会耗费 CPU。

自旋说的是等待方式:暂时不挂起线程,循环尝试获取资源。等待很短时可能省掉挂起和恢复的开销;持锁线程长时间不释放时,空转反而浪费 CPU。实际使用时,还要考虑超时、中断和公平性等问题。

同一组共享数据的读写需要遵守一致的加锁规则。如果两个入口各锁各的对象,就起不到互斥作用。

最后修改:2018 年 01 月 20 日
如果觉得我的文章对你有用,请随意赞赏