浅谈Java锁机制

 更新时间:2021-09-30 20:48:51   作者:佚名   我要评论(0)

目录1、悲观锁和乐观锁2、悲观锁应用3、乐观锁应用4、CAS5、手写一个自旋锁1、悲观锁和乐观锁
我们可以将锁大体分为两类:

悲观锁

1、悲观锁和乐观锁

我们可以将锁大体分为两类:

  • 悲观锁
  • 乐观锁

顾名思义,悲观锁总是假设最坏的情况,每次获取数据的时候都认为别的线程会修改,所以每次在拿数据的时候都会上锁,这样其它线程想要修改这个数据的时候都会被阻塞直到获取锁。比如MySQL数据库中的表锁、行锁、读锁、写锁等,Java中的synchronizedReentrantLock等。

而乐观锁总是假设最好的情况,每次获取数据的时候都认为别的线程不会修改,所以并不会上锁,但是在修改数据的时候需要判断一下在此期间有没有别的线程修改过数据,如果没有修改过则正常修改,如果修改过则这次修改就是失败的。常见的乐观锁有版本号控制、CAS算法等。

2、悲观锁应用

案例如下:

public class LockDemo {

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 1000; ++j) {
                    count++;
                }
            });
            thread.start();
            threadList.add(thread);
        }
        // 等待所有线程执行完毕
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println(count);
    }
}

在该程序中一共开启了50个线程,并在线程中对共享变量count进行++操作,所以如果不发生线程安全问题,最终的结果应该是50000,但该程序中一定存在线程安全问题,运行结果为:

48634

若想解决线程安全问题,可以使用synchronized关键字:

public class LockDemo {

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                // 使用synchronized关键字解决线程安全问题
                synchronized (LockDemo.class) {
                    for (int j = 0; j < 1000; ++j) {
                        count++;
                    }
                }
            });
            thread.start();
            threadList.add(thread);
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println(count);
    }
}

将修改count变量的操作使用synchronized关键字包裹起来,这样当某个线程在进行++操作时,别的线程是无法同时进行++的,只能等待前一个线程执行完1000次后才能继续执行,这样便能保证最终的结果为50000。

使用ReentrantLock也能够解决线程安全问题:

public class LockDemo {

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        Lock lock = new ReentrantLock();
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                // 使用ReentrantLock关键字解决线程安全问题
                lock.lock();
                try {
                    for (int j = 0; j < 1000; ++j) {
                        count++;
                    }
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
            threadList.add(thread);
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println(count);
    }
}

这两种锁机制都是悲观锁的具体实现,不管其它线程是否会同时修改,它都直接上锁,保证了原子操作。

3、乐观锁应用

由于线程的调度是极其耗费操作系统资源的,所以,我们应该尽量避免线程在不断阻塞和唤醒中切换,由此产生了乐观锁。

在数据库表中,我们往往会设置一个version字段,这就是乐观锁的体现,假设某个数据表的数据内容如下:

+----+------+----------+ ------- +
| id | name | password | version |
+----+------+----------+ ------- +
|  1 | zs   | 123456   |    1    |
+----+------+----------+ ------- +

它是如何避免线程安全问题的呢?

假设此时有两个线程A、B想要修改这条数据,它们会执行如下的sql语句:

select version from e_user where name = 'zs';

update e_user set password = 'admin',version = version + 1 where name = 'zs' and version = 1;

首先两个线程均查询出zs用户的版本号为1,然后线程A先执行了更新操作,此时将用户的密码修改为了admin,并将版本号加1,接着线程B执行更新操作,此时版本号已经为2了,所以更新肯定是失败的,由此,线程B就失败了,它只能重新去获取版本号再进行更新,这就是乐观锁,我们并没有对程序和数据库进行任何的加锁操作,但它仍然能够保证线程安全。

4、CAS

仍然以最开始做加法的程序为例,在Java中,我们还可以采用一种特殊的方式来实现它:

public class LockDemo {

    static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 1000; ++j) {
                    // 使用AtomicInteger解决线程安全问题
                    count.incrementAndGet();
                }
            });
            thread.start();
            threadList.add(thread);
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println(count);
    }
}

为何使用AtomicInteger类就能够解决线程安全问题呢?

我们来查看一下源码:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

count调用incrementAndGet()方法时,实际上调用的是UnSafe类的getAndAddInt()方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

getAndAddInt()方法中有一个循环,关键的代码就在这里,我们假设线程A此时进入了该方法,此时var1即为AtomicInteger对象(初始值为0),var2的值为12(这是一个内存偏移量,我们可以不用关心),var4的值为1(准备对count进行加1操作)。

首先通过AtomicInteger对象和内存偏移量即可得到主存中的数据值:

var5 = this.getIntVolatile(var1, var2);

获取到var5的值为0,然后程序会进行判断:

!this.compareAndSwapInt(var1, var2, var5, var5 + var4)

compareAndSwapInt()是一个本地方法,它的作用是比较并交换,即:判断var1的值与主存中取出的var5的值是否相同,此时肯定是相同的,所以会将var5+var4的值赋值给var1,并返回true,对true取反为false,所以循环就结束了,最终方法返回1。

这是一切正常的运行流程,然而当发生并发时,处理情况就不太一样了,假设此时线程A执行到了getAndAddInt()方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

线程A此时获取到var1的值为0(var1即为共享变量AtomicInteger),当线程A正准备执行下去时,线程B抢先执行了,线程B此时获取到var1的值为0,var5的值为0,比较成功,此时var1的值就变为1;这时候轮到线程A执行了,它获取var5的值为1,此时var1的值不等于var5的值,此次加1操作就会失败,并重新进入循环,此时var1的值已经发生了变化,此时重新获取var5的值也为1,比较成功,所以将var1的值加1变为2,若是在获取var5之前别的线程又修改了主存中var1的值,则本次操作又会失败,程序重新进入循环。

这就是利用自旋的方式来实现一个乐观锁,因为它没有加锁,所以省下了线程调度的资源,但也要避免程序一直自旋的情况发生。

5、手写一个自旋锁

public class LockDemo {

    private AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        // 获取当前线程对象
        Thread thread = Thread.currentThread();
        // 自旋等待
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    public void unlock() {
        // 获取当前线程对象
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
    }

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                lockDemo.lock();
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
                lockDemo.unlock();
            });
            thread.start();
            threadList.add(thread);
        }
        // 等待线程执行完毕
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println(count);
    }
}

使用CAS的原理可以轻松地实现一个自旋锁,首先,AtomicReference中的初始值一定为null,所以第一个线程在调用lock()方法后会成功将当前线程的对象放入AtomicReference,此时若是别的线程调用lock()方法,会因为该线程对象与AtomicReference中的对象不同而陷入循环的等待中,直到第一个线程执行完++操作,调用了unlock()方法,该线程才会将AtomicReference值置为null,此时别的线程就可以跳出循环了。

通过CAS机制,我们能够在不添加锁的情况下模拟出加锁的效果,但它的缺点也是显而易见的:

  • 循环等待占用CPU资源
  • 只能保证一个变量的原子操作
  • 会产生ABA问题

到此这篇关于浅谈Java锁机制的文章就介绍到这了,更多相关Java锁机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
  • Java的锁机制:synchronized和CAS详解
  • java synchronized 锁机制原理详解
  • Java多线程锁机制相关原理实例解析
  • Java锁机制Lock用法示例
  • Java并发编程之显式锁机制详解
  • 详解java中的互斥锁信号量和多线程等待机制
  • Java线程并发中常见的锁机制详细介绍
  • Java 多线程同步 锁机制与synchronized深入解析

相关文章

  • 浅谈Java锁机制

    浅谈Java锁机制

    目录1、悲观锁和乐观锁2、悲观锁应用3、乐观锁应用4、CAS5、手写一个自旋锁1、悲观锁和乐观锁 我们可以将锁大体分为两类: 悲观锁
    2021-09-30
  • java 如何为文件及文件夹添加权限

    java 如何为文件及文件夹添加权限

    目录java 为文件及文件夹添加权限java 修改文件所有者及其权限1.设置所有者2.ACL文件权限3.POSIX文件权限java 为文件及文件夹添加权限 /**
    2021-09-30
  • Python教程自己实现软件加密功能

    Python教程自己实现软件加密功能

    目录原理加密操作:解密操作:生成随机密钥:加密单元:解密单元: 加密文本文件解密文件 基础知识 在 Python 中异或操作符为:^,也可以记
    2021-09-30
  • 基于maven的三种packaging方式

    基于maven的三种packaging方式

    maven的三种packaging方式 pom是maven依赖文件 jar是java普通项目打包 war是java web项目打包 pom:打出来可以作为其他项目的maven依赖
    2021-09-30
  • SpringMVC跨服务器上传文件中出现405错误的解决

    SpringMVC跨服务器上传文件中出现405错误的解决

    目录SpringMVC跨服务器上传文件中出现405错误重点来了~SpringMVC跨服务器上传文件中出现405错误 下面是 应用服务器 的代码 package com.it
    2021-09-30
  • 5种 JavaScript 方式实现数组扁平化

    5种 JavaScript 方式实现数组扁平化

    目录一、数组扁平化概念 二、实现1. reduce2. toString & split3. join & split4. 递归5. 扩展运算符一、数组扁平化概念 数组扁平化是指
    2021-09-30
  • Java杂谈之如何优化写出漂亮高效的代码

    Java杂谈之如何优化写出漂亮高效的代码

    目录命名中的不一致方案中的不一致代码中的不一致总结大部分程序员对于一致性本身的重要性是有认知的。但通常来说,大家理解的一致性都表现在
    2021-09-30
  • JavaScript实现轮播图案例

    JavaScript实现轮播图案例

    本文实例为大家分享了JavaScript实现轮播图效果的具体代码,供大家参考,具体内容如下 运用定时器所写成的一个简单的轮播图,直接看代码,如
    2021-09-30
  • python学习字符串驻留与常量折叠隐藏特性详解

    python学习字符串驻留与常量折叠隐藏特性详解

    下面是Python字符串的一些微妙的特性,绝对会让你大吃一惊。 案例一: 案例二: 案例三: 很好理解, 对吧&#63; 说明: 这些行为是由于 Cpy
    2021-09-30
  • 纯JS将table表格导出到excel的方法

    纯JS将table表格导出到excel的方法

    html <div > <button type="button" onclick="getXlsFromTbl('tableExcel','myDiv')">IE导出Excel方法</button> <button type="bu
    2021-09-30

最新评论