乐观锁和悲观锁

参考:
https://www.cnblogs.com/qjjazry/p/6581568.html
https://www.cnblogs.com/xuyuanjia/p/6027414.html

悲观锁(Pessimistic Locking):
总是假设最坏的情况发生,因此每次在取数据的时候就会加锁,操作完成后才释放锁。

乐观锁(Optimistic Locking):
假设大数据情况下不会发生数据冲突,因此在取数据的时候不加锁,只有在更新的时候判断该数据是否在此期间被修改过了。

1. Java中的悲观锁和乐观锁

  • 悲观锁
    Java中典型的悲观锁就是synchronized。

  • 乐观锁
    java.util.concurrent.atomic包下面的原子变量类就使用了乐观锁实现。
    Compare and Swap(CAS)是一种乐观锁的实现方式。
    CAS基本原理:CAS有三个要素:需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B),如果发现位置V的值和预期原值A相同,则将新值B更新到V处,否则不处理。

以AtomicInteger为例,看看CAS原理。

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

private static final Unsafe unsafe = Unsafe.getUnsafe()
AtomicInteger类使用sun.misc.Unsafe类的compareAndSwapInt()方法执行更新操作。Unsafe是JDK的内部工具类,通过调用Native方法(C/C++)可以直接读写内存、获得地址偏移值、锁定或释放线程等。

private volatile int value
将value声明为volatile,可保证线程间的数据可见性,但不能保证原子性。

private static final long valueOffset
valueOffset表示value字段相对于对象起始地址的偏移量,利用valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"))方法获得,该方法首先通过字段名value获取到该字段的Field,然后调用Unsafe的objectFieldOffset方法获取value字段相对于对象起始地址的偏移量。

public final int getAndIncrement()
该法的功能是获取值并+1,它调用unsafe.getAndAddInt(this, valueOffset, 1):

public final int getAndAddInt(Object arg0, long arg1, int arg3) {
    int arg4;
    do {
        arg4 = this.getIntVolatile(arg0, arg1);
    } while (!this.compareAndSwapInt(arg0, arg1, arg4, arg4 + arg3));
    return arg4;
}

public native int getIntVolatile(Object arg0, long arg1);

public final native boolean compareAndSwapInt(Object arg0, long arg1, int arg3, int arg4);

compareAndSwapInt()就是乐观锁的一种实现(CAS)。
期望值:getIntVolatile()方法获取主内存中的value值;
读写的位置:工作内存中对象+偏移量;
拟写入的新值:期望值+1
如果期望值和读写位置上值相同,则在读写位置上写入新值,返回true,否则不写入新值,返回false。

getAndAddInt()方法利用循环,不停的CAS直到写入成功为止。

CAS的缺陷:ABA问题
线程1从位置V上取出A,线程2从位置V上也取出A;
线程2将A改成了B,然后又改成了A;
线程1根据CAS操作时,发现期望值是A,和原值相同,则执行成功。
但这个过程存在问题,线程1感知不到A-B-A的过程。

解决:
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
该类本质上是在原来的CAS基础上加入了一个int类型的stamp(版本号),每次更新的时候检查当前Reference和期望的Reference是否相同,当前stamp和期望的stamp是否相同,如果相同则更新并返回true,否则啥也不做返回false。

2. 数据库中的悲观锁和乐观锁

建表(test)并插入一条数据:

id count
1 0
  • 无锁

封装Task:每次查出count值并+1更新

/**
 * 封装Task(无锁)
 */
public class TaskWithOutLock implements Runnable {

    @Override
    public void run() {
        int id = 1;
        int count = 0;
        Connection connection = DBUtil.getConnection();
        PreparedStatement psQuery = null;
        PreparedStatement psUpdate = null;

        for (int i = 0; i < 1000; i++) {
            try {
                // 查询数据
                String querySql = "select count from test where id = ?";
                psQuery = connection.prepareStatement(querySql);
                psQuery.setInt(1, id);
                ResultSet rs = psQuery.executeQuery();
                if (rs.next()) {
                    AtomicInteger atomicInteger = new AtomicInteger(rs.getInt("count"));
                    atomicInteger.getAndIncrement();
                    count = atomicInteger.get();
                }

                // 更新数据
                String updateSql = "update test set count = ? where id = ?";
                psUpdate = connection.prepareStatement(updateSql);
                psUpdate.setInt(1, count);
                psUpdate.setInt(2, id);
                Integer res = psUpdate.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        try {
            psUpdate.close();
            psQuery.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

注:由于count变量是各线程私有的,所以不用AtomicInteger也可以。

测试:多线程跑任务

    public static void main(String[] args) throws InterruptedException {
        int corePoolSize = 2;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, corePoolSize * 2, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());

        for (int i = 0; i < 2; i++) {
            TaskWithOutLock task = new TaskWithOutLock();
            pool.execute(task);
        }

        // 线程池不再接收新任务,但线程池中的任务继续执行
        pool.shutdown();

        // 阻塞当前线程直到 线程池中所有任务完成 或 超时 或 当前线程被中断
        pool.awaitTermination(5, TimeUnit.MINUTES);

        System.out.println("DONE!");
    }

结果:
数据库中count变为1993,可见在不加锁的情况下,计算结果与期望的2000不同。

  • 悲观锁
    封装Task:
/**
 * 封装Task(悲观锁)
 */
public class TaskPessimisticLock implements Runnable {

    @Override
    public void run() {
        int id = 1;
        int count = 0;
        Connection connection = DBUtil.getConnection();
        PreparedStatement psQuery = null;
        PreparedStatement psUpdate = null;

        for (int i = 0; i < 1000; i++) {
            try {
                // 开启事务
                connection.setAutoCommit(false);

                // 查询数据,加悲观锁
                String querySql = "select count from test where id = ? for update";
                psQuery = connection.prepareStatement(querySql);
                psQuery.setInt(1, id);
                ResultSet rs = psQuery.executeQuery();
                if (rs.next()) {
                    AtomicInteger atomicInteger = new AtomicInteger(rs.getInt("count"));
                    atomicInteger.getAndIncrement();
                    count = atomicInteger.get();
                }

                // 更新数据
                String updateSql = "update test set count = ? where id = ?";
                psUpdate = connection.prepareStatement(updateSql);
                psUpdate.setInt(1, count);
                psUpdate.setInt(2, id);
                Integer res = psUpdate.executeUpdate();

                // 提交事务
                connection.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        try {
            psUpdate.close();
            psQuery.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

(1)开启事务
(2)select count from test where id = ? for update打开悲观锁
(3)提交事务
注:悲观锁必须在事务中间,否则不生效。

测试:

    public static void main(String[] args) throws InterruptedException {
        int corePoolSize = 2;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, corePoolSize * 2, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());

        for (int i = 0; i < 2; i++) {
            TaskPessimisticLock task = new TaskPessimisticLock();
            pool.execute(task);
        }

        // 线程池不再接收新任务,但线程池中的任务继续执行
        pool.shutdown();

        // 阻塞当前线程直到 线程池中所有任务完成 或 超时 或 当前线程被中断
        pool.awaitTermination(5, TimeUnit.MINUTES);

        System.out.println("DONE!");
    }

结果:
count值为2000,与预期值相同。

  • 乐观锁
    修改表结构,加入version字段:
id count version
1 0 0

封装Task:

/**
 * 封装Task(乐观锁)
 */
public class TaskOptimisticLock implements Runnable {

    @Override
    public void run() {
        int id = 1;
        int count = 0;
        int version = 0;
        Connection connection = DBUtil.getConnection();
        PreparedStatement psQuery = null;
        PreparedStatement psUpdate = null;

        for (int i = 0; i < 1000; i++) {
            try {
                // 循环直到更新完成
                for (;;) {
                    // 查询数据
                    String querySql = "select count,version from test where id = ?";
                    psQuery = connection.prepareStatement(querySql);
                    psQuery.setInt(1, id);
                    ResultSet rs = psQuery.executeQuery();
                    if (rs.next()) {
                        AtomicInteger atomicInteger = new AtomicInteger(rs.getInt("count"));
                        atomicInteger.getAndIncrement();
                        count = atomicInteger.get();

                        AtomicInteger atomicVersion = new AtomicInteger(rs.getInt("version"));
                        version = atomicVersion.get();
                    }

                    // 更新数据
                    String updateSql = "update test set count = ?,version = ? where id = ? and version = ?";
                    int newVersion = version + 1;
                    psUpdate = connection.prepareStatement(updateSql);
                    psUpdate.setInt(1, count);
                    psUpdate.setInt(2, newVersion);
                    psUpdate.setInt(3, id);
                    psUpdate.setInt(4, version);
                    Integer res = psUpdate.executeUpdate();

                    if (res > 0) {
                        break;
                    }
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        try {
            psUpdate.close();
            psQuery.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在更新数据的时候判断version是否正确,如果正确则更新数据和version;否则循环判断。

测试:

    public static void main(String[] args) throws InterruptedException {
        int corePoolSize = 2;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, corePoolSize * 2, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());
 
        for (int i = 0; i < 2; i++) {
            TaskOptimisticLock task = new TaskOptimisticLock();
            pool.execute(task);
        }

        // 线程池不再接收新任务,但线程池中的任务继续执行
        pool.shutdown();

        // 阻塞当前线程直到 线程池中所有任务完成 或 超时 或 当前线程被中断
        pool.awaitTermination(5, TimeUnit.MINUTES);

        System.out.println("DONE!");
    }

结果:
count = 2000,version = 2000。和预期一致。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容