Java 锁机制了解一下

在多线程环境下,程序往往会出现一些线程安全问题,为此,Java提供了一些线程的同步机制来解决安全问题,比如:synchronized锁和Lock锁都能解决线程安全问题。

在多线程环境下,程序往往会出现一些线程安全问题,为此,Java提供了一些线程的同步机制来解决安全问题,比如:synchronized锁和Lock锁都能解决线程安全问题。

悲观锁和乐观锁

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

  • 悲观锁
  • 乐观锁

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

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

悲观锁应用

案例如下:

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();                }        //java学习交流:737251827  进入可领取学习资源及对十年开发经验大佬提问,免费解答!            });            thread.start();            threadList.add(thread);        }        for (Thread thread : threadList) {            thread.join();        }        System.out.println(count);    }}

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

乐观锁应用

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

在数据库表中,我们往往会设置一个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就失败了,它只能重新去获取版本号再进行更新,这就是乐观锁,我们并没有对程序和数据库进行任何的加锁操作,但它仍然能够保证线程安全。

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的值,则本次操作又会失败,程序重新进入循环。

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

手写一个自旋锁

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);    }//java学习交流:737251827  进入可领取学习资源及对十年开发经验大佬提问,免费解答!    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问题

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/133569.html

(0)

相关推荐

  • 从今天开始喝热水了

    我以前很少喝热水,喜欢喝冷水,特别是热天还喜欢喝冰水,因为长时间积累体内寒气太重了,每次来例假肚子还特别疼,量还减少。所以女孩子们一定要记住不喝冷水,要多喝热水。喝热水对于女孩子来...

    生活 2021年9月23日
  • “西营洲”亚健康更容易生病。每天应该采取哪些护理措施?

    如今人们生活节奏快,身体经常透支,这也是目前年轻人经常遇到的身体状况。有一种常见的疾病正在攻击每个人的健康,叫做亚健康。这是一个熟悉的症状。长期处于亚健康状态会让身体更容易出现疾病问题。亚健康人群需要尽快采取相应的治疗措施,需要尽快调整自己的身体状况。那么,应该采取哪些护理措施呢?让我们和边肖一起看看。

    生活 2021年12月16日
  • 奶粉里有香兰素,奶粉中的香兰素对婴儿的影响

    又是一年一度的双11购物季,你是不是计划趁着折扣优惠囤货?每年都会有年轻的家长或他们的长辈来咨询我,婴儿奶粉怎么选?因为他们根本搞不清各种成分差异到底意味着什么,如何取舍。

    生活 2021年11月11日
  • 网上药品黑市,打击精神药品网络黑市 莫让“互联网+医药”成为监管盲区

    据媒体报道,一些网络二手平台成为了精神类药物的黑市。三唑仑、氯氮平等国家严格管制的一、二类精神药品,在网上被任意售卖。不仅价格高出十几倍、几十倍,而且所售药品多无标签、成分不明。药品非常物,如此乱象对消费者生命健康的威胁细思极恐。网络已成生活的标配,某些网友生了病,懒得去医院问诊,就到网络上“求医问药”。网络药贩们抓住这样的心理,趁虚而入。在现实生活中,三唑仑、氯氮平都属于被严格监管的处方药。但在网络上,这些处方药被随意买卖,不仅真假难辨,而且缺少专业医生诊断。若不当服用,甚至会危及生命。互联网不是法外之地,更不该沦为黑市交易的帮凶。“互联网+”是行业发展趋势,“互联网+医药”需要规范化、制度化、体系化、法治化。互联网能够助力药业发展,但前提是要完善专有领域法律法规体系,并加强食药、网信等部门联动,加强网络监控、报警系统建设,严格落实网络平台主体责任。“互联网+医药”的积极作用值得肯定,但也需拉紧药品安全这根保险绳,切实让广大“互联网+医疗”消费者,既享受网络医疗服务的红利,又能避免被不法商贩毒害。(央广网评论员 李兆娣)

    科技 2021年10月28日