浅谈Java锁机制

目录
  • 1、悲观锁和乐观锁
  • 2、悲观锁应用
  • 3、乐观锁应用
  • 4、CAS
  • 5、手写一个自旋锁

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锁机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java多线程锁机制相关原理实例解析

    上下文:程序运行需要的环境(外部变量) 上下文切换:将之前的程序需要的外部变量复制保存,然后切换到新的程序运行环境 系统调用:(用户态陷入操作系统,通过操作系统执行内核态指令,执行完回到用户态)用户态--内核态--用户态:两次上下文切换 线程wait()方法:将自身加入等待队列,发生了一次上下文切换 notify()方法:将线程唤醒,也发生了上下文切换 Java线程中的锁:偏向锁.轻量级锁.重量级锁. 注意:偏向锁和轻量级锁都没有发生竞争,重量级锁发生了竞争. 偏向锁:可重入和经常使用某一个线程

  • Java并发编程之显式锁机制详解

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁.而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用.本文主要涉及以下一些内容: 接口Lock的基本组成成员 可重入锁Re

  • java synchronized 锁机制原理详解

    目录 前言: 1.synchronized 的作用: 2.synchronized 底层语义原理: 3. synchronized 的显式同步与隐式同步: 3.1.synchronized 代码块底层原理: 3.2.synchronized 方法底层原理: 4.JVM 对 synchronized 锁的优化: 4.1.锁升级:偏向锁->轻量级锁->自旋锁->重量级锁 4.1.1.synchronized 的 Mark word 标志位: 4.1.2.锁升级过程: 4.2.锁消除: 4.3

  • Java锁机制Lock用法示例

    本文实例讲述了Java锁机制Lock用法.分享给大家供大家参考,具体如下: package com.expgiga.JUC; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 一.用于解决多线程安全问题的方式: * 1.同步代码块 synchronized 隐式锁 * 2.同步方法 synchronized 隐式锁 * 3.同步锁Lock (jdk1.5以后

  • Java 多线程同步 锁机制与synchronized深入解析

    打个比方:一个object就像一个大房子,大门永远打开.房子里有很多房间(也就是方法).这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法).房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间.另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人.所有的东西就这么多了,下面我们看看这些东西之间如何作用的. 在此我们先来明确一下我们的前提条件.该对象至少有一个synchronized方法,否则这个key还有啥意义.当然也就不会有我们的这个主题了. 一

  • Java线程并发中常见的锁机制详细介绍

    随着互联网的蓬勃发展,越来越多的互联网企业面临着用户量膨胀而带来的并发安全问题.本文着重介绍了在java并发中常见的几种锁机制. 1.偏向锁 偏向锁是JDK1.6提出来的一种锁优化的机制.其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作.也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式.在JVM中使用-XX:+UseBiasedLocking pac

  • Java的锁机制:synchronized和CAS详解

    目录 一 为什么要用锁 二 synchronized怎么实现的 三 CAS来者何人 四synchronized和CAS孰优孰劣 轻量级锁 重量级锁 总结 提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点.比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识. 一 为什么要用锁 我们使用多线程肯定是为了提

  • 详解java中的互斥锁信号量和多线程等待机制

    互斥锁和信号量都是操作系统中为并发编程设计基本概念,互斥锁和信号量的概念上的不同在于,对于同一个资源,互斥锁只有0和1 的概念,而信号量不止于此.也就是说,信号量可以使资源同时被多个线程访问,而互斥锁同时只能被一个线程访问 互斥锁在java中的实现就是 ReetranLock , 在访问一个同步资源时,它的对象需要通过方法 tryLock() 获得这个锁,如果失败,返回 false,成功返回true.根据返回的信息来判断是否要访问这个被同步的资源.看下面的例子 public class Reen

  • 浅谈Java锁机制

    目录 1.悲观锁和乐观锁 2.悲观锁应用 3.乐观锁应用 4.CAS 5.手写一个自旋锁 1.悲观锁和乐观锁 我们可以将锁大体分为两类: 悲观锁 乐观锁 顾名思义,悲观锁总是假设最坏的情况,每次获取数据的时候都认为别的线程会修改,所以每次在拿数据的时候都会上锁,这样其它线程想要修改这个数据的时候都会被阻塞直到获取锁.比如MySQL数据库中的表锁.行锁.读锁.写锁等,Java中的synchronized和ReentrantLock等. 而乐观锁总是假设最好的情况,每次获取数据的时候都认为别的线程不

  • 浅谈Java锁的膨胀过程以及一致性哈希对锁膨胀的影响

    目录 1.锁优化 1.1.锁消除 1.2.锁粗化 1.3.自旋锁 1.4.自适应自旋锁 1.5.锁膨胀 2.锁膨胀实战 2.1.jol工具 2.2.锁膨胀测试代码 2.3.输出分析 2.4.锁释放 3.一致性哈希对锁膨胀的影响 4.锁性能测试 1.锁优化 在JDK6之前,通过synchronized来实现同步效率是很低的,被synchronized包裹的代码块经过javac编译后,会在代码块前后加上monitorenter和monitorexit字节码指令,被synchronized修饰的方法则

  • 浅谈Java 代理机制

    目录 一.常规编码方式 二.代理模式概述 三.静态代理 3.1.什么是静态代理 3.2.代码示例 四.Java 字节码生成框架 五.什么是动态代理 六.JDK 动态代理机制 6.1.使用步骤 6.2.代码示例 七.CGLIB 动态代理机制 7.1.使用步骤 7.2.代码示例 八.什么情况下使用动态代理 九.静态代理和动态代理对比 十.总结 一.常规编码方式 在学习代理之前,先回顾以下我们的常规编码方式:所有 interface 类型的变量总是通过向上转型并指向某个实例的. 1)首先,定义一个接口

  • 浅谈Java内省机制

    目录 概念 JavaBean 内省 相关API 代码案例:获取属性相关信息 内省属性的注意事项 完整代码 概念 JavaBean 在实际编程中,我们常常需要一些用来包装值对象的类,例如Student. Employee.Order,这些 类中往往没有业务方法,只是为了把需要处理的实体对象进行封装,有这样的特征: 属性都是私有的: 有无参的public构造方法: 对私有属性根据需要提供公有的getXxx方法以及setXxx方法: 比如:属性名称为name,则有getName方法返回属性name值,

  • 浅谈Java并发编程之Lock锁和条件变量

    简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

  • 浅谈Java非阻塞同步机制和CAS

    什么是非阻塞同步 非阻塞同步的意思是多个线程在竞争相同的数据时候不会发生阻塞,从而能够在更加细粒度的维度上进行协调,从而极大的减少线程调度的开销,从而提升效率.非阻塞算法不存在锁的机制也就不存在死锁的问题. 在基于锁的算法中,如果一个线程持有了锁,那么其他的线程将无法进行下去.使用锁虽然可以保证对资源的一致性访问,但是在挂起和恢复线程的执行过程中存在非常大的开销,如果锁上面存在着大量的竞争,那么有可能调度开销比实际工作开销还要高. 悲观锁和乐观锁 我们知道独占锁是一个悲观锁,悲观锁的意思就是假设

  • 浅谈Java并发中ReentrantLock锁应该怎么用

    目录 1.重入锁 说明 2.中断响应 说明 3.锁申请等待限时 tryLock(long, TimeUnit) tryLock() 4.公平锁 说明 源码(JDK8) 重入锁可以替代关键字 synchronized . 在 JDK5.0 的早期版本中,重入锁的性能远远优于关键字 synchronized , 但从 JDK6.0 开始, JDK 在关键字 synchronized 上做了大量的优化,使得两者的性能差距并不大. 重入锁使用 ReentrantLock 实现 1.重入锁 package

  • 浅谈Java分布式架构下如何实现分布式锁

    01分布式锁运用场景 互联网秒杀,抢优惠卷,接口幂等性校验.咱们以互联网秒杀为例. @RestController @Slf4j publicclassIndexController{ @Autowired privateRedissonredission; @Autowired privateStringRedisTemplatestringRedisTemplate; @RequestMapping("/deduct_stock") publicStringdeductStock(

  • 浅谈Java垃圾回收机制

    一.什么是垃圾 java中,什么样的对象是垃圾?有人说:没有被引用的对象就是垃圾对象.我一开始对此也是深信不疑的,但是当年我这么回答面试官的时候,得到的是一个大大的白眼. 判断一个对象是否是垃圾,有两种算法,一种是引用计数法,但是,这种方法解决不了循环引用的问题. /**循环问题*/ public class Demo{ public Demo instance; public static void main(String[] args) { Demo a=new Demo(); Demo b

  • 浅谈java如何实现Redis的LRU缓存机制

    目录 LRU概述 使用LinkedHashMap实现 使用LinkedHashMap简单方法实现 双链表+hashmap LRU概述 最近使用的放在前面,最近没用的放在后面,如果来了一个新的数,此时内存满了,就需要把旧的数淘汰,那为了方便移动数据,肯定就得使用链表类似的数据结构,再加上要判断这条数据是不是最新的或者最旧的那么应该也要使用hashmap等key-value形式的数据结构. 使用LinkedHashMap实现 package thread; import java.util.Link

随机推荐