悲观锁与乐观锁

149 阅读3分钟

悲观锁与乐观锁

悲观锁和乐观锁,是保证数据并发安全防止更新丢失的两种方法。

乐观锁和悲观锁

1、多线程并发,数据更新丢失

静态变量sCount线程不安全:

public class NoLock {
    private static int sCount = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程A让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    sCount++;
                }
            }
        }, "A").start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程B让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    sCount++;
                }
            }
        }, "B").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Main" + sCount);
    }
}

运行结果可能:

Main103

或可能:

Main200

2、悲观锁 Pessimistic Lock

认为程序并发严重,在读取数据的时候,认为其他线程会修改此数据,故加锁防止其他线程修改。 例子:使用synchronized加锁,保证数据线程安全。

public class PessimisticLock {
    private static int sCount = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程A让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    synchronized (PessimisticLock.class){
                        sCount++;
                    }
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程B让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    synchronized (PessimisticLock.class){
                        sCount++;
                    }
                }
            }
        }, "B").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Main" + sCount);
    }
}

运行结果:

Main200

2.1、synchronized缺点

synchronized加锁确保了线程安全,但却是以消耗性能为代价的,synchronized会让没有得到锁资源的线程进入BLOCKED状态,当争夺到锁资源后恢复为RUNNABLE状态,此过程涉及操作系统用户模式和内核模式的转换,代价较高。

3、乐观锁 Optimistic Lock

认为程序并发不严重,在读取数据的时候,其他线程不会修改,故不加锁;但在更新数据的时候,会判断其他线程是否更新此数据,如果失败会让线程不断去尝试更新。 例子:使用原子操作类AtomicInteger的底层实现CAS机制。

public class OptimisticLock {
    private static AtomicInteger sCount = new AtomicInteger(0);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程A让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    sCount.incrementAndGet();
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //线程B让sCount自增100次
                for (int i = 0; i < 100; i++) {
                    sCount.incrementAndGet();
                }
            }
        }, "B").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Main" + sCount);
    }
}

运行结果:

Main200

3.1、CAS机制

CAS机制,Compare And Swap,即比较并替换,使用基本操作数:内存地址,旧的预期值,将修改的新值。更新一个变量的时候,只有当变量的预期值和内存地址当中的实际值相同时,才会将内存地址对应的值修改为新值。

3.2、CAS自旋

当变量的预期值与内存地址当中的实际值不相同时,线程会重新获取内存地址的当前值,并重新计算将要修改的新值,这种尝试的过程被称为自旋。

3.3、CAS缺点

CAS的缺点 解释
CPU开销较大 在并发量比较高的情况下,如果多线程反复尝试更新某变量,会给CPU带来很大压力。
无法保证代码块的原子性 只能保证一个变量的原子性操作,而不能像synchronized一样保证整个代码块的原子性

欢迎关注Android技术堆栈,专注于Android技术学习的公众号,致力于提高Android开发者们的专业技能!

Android技术堆栈