服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 浅谈Java锁机制

浅谈Java锁机制

2022-01-25 00:56随身电源 Java教程

在多线程环境下,程序往往会出现一些线程安全问题,为此,Java提供了一些线程的同步机制来解决安全问题,比如:synchronized锁和Lock锁都能解决线程安全问题。下面小编就来详细介绍该知识点,需要的朋友可以参考一下

1、悲观锁和乐观锁

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

  • 悲观锁
  • 乐观锁

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

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

2、悲观锁应用

案例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,但该程序中一定存在线程安全问题,运行结果为:

?
1
48634

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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也能够解决线程安全问题:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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字段,这就是乐观锁的体现,假设某个数据表的数据内容如下:

?
1
2
3
4
5
+----+------+----------+ ------- +
| id | name | password | version |
+----+------+----------+ ------- +
1 | zs   | 123456   |    1    |
+----+------+----------+ ------- +

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

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

?
1
2
3
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中,我们还可以采用一种特殊的方式来实现它:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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类就能够解决线程安全问题呢?

我们来查看一下源码:

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

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

?
1
2
3
4
5
6
7
8
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对象和内存偏移量即可得到主存中的数据值:

?
1
var5 = this.getIntVolatile(var1, var2);

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

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

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

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

?
1
2
3
4
5
6
7
8
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、手写一个自旋锁

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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锁机制内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7013272218679541773

延伸 · 阅读

精彩推荐
  • Java教程20个非常实用的Java程序代码片段

    20个非常实用的Java程序代码片段

    这篇文章主要为大家分享了20个非常实用的Java程序片段,对java开发项目有所帮助,感兴趣的小伙伴们可以参考一下 ...

    lijiao5352020-04-06
  • Java教程升级IDEA后Lombok不能使用的解决方法

    升级IDEA后Lombok不能使用的解决方法

    最近看到提示IDEA提示升级,寻思已经有好久没有升过级了。升级完毕重启之后,突然发现好多错误,本文就来介绍一下如何解决,感兴趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    这篇文章主要介绍了Java使用SAX解析xml的示例,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程小米推送Java代码

    小米推送Java代码

    今天小编就为大家分享一篇关于小米推送Java代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧...

    富贵稳中求8032021-07-12
  • Java教程xml与Java对象的转换详解

    xml与Java对象的转换详解

    这篇文章主要介绍了xml与Java对象的转换详解的相关资料,需要的朋友可以参考下...

    Java教程网2942020-09-17
  • Java教程Java BufferWriter写文件写不进去或缺失数据的解决

    Java BufferWriter写文件写不进去或缺失数据的解决

    这篇文章主要介绍了Java BufferWriter写文件写不进去或缺失数据的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望...

    spcoder14552021-10-18
  • Java教程Java8中Stream使用的一个注意事项

    Java8中Stream使用的一个注意事项

    最近在工作中发现了对于集合操作转换的神器,java8新特性 stream,但在使用中遇到了一个非常重要的注意点,所以这篇文章主要给大家介绍了关于Java8中S...

    阿杜7482021-02-04
  • Java教程Java实现抢红包功能

    Java实现抢红包功能

    这篇文章主要为大家详细介绍了Java实现抢红包功能,采用多线程模拟多人同时抢红包,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙...

    littleschemer13532021-05-16