史上最全 Java 中各种锁的介绍

摘要:
锁被设计为强制执行互斥的并发控制策略。公平锁获取示例1/**2*true表示ReentrantLock 3*/4privateReentrantLock=newReentrantLock的公平锁;56publicvoidtestFail(){7try{8lock.lock();9System.out.println;10}最终{11lock.unlock();12}13}14 publicstaticvoidmain{15FairLockTestfairLock=newFairLockTest();16Runnablerunable=()-˃{17System.out.println();18fairLock.testFail(();19};20Thread[]threadArray=新线程[10];21for{22threadArray[i]=newThread;23}24for{25threadArray[i].start();26}27}操作结果:1线程-1启动2线程-1获得锁3线程-3启动4线程-3获得锁5线程-5启动6线程-5获得锁7线程-2启动8线程-2获得锁9线程-4启动10线程-4获得锁11线程-6启动12线程-6获得锁13线程-10启动14线程-8启动15线程-10获得锁16线程-9启动17线程-7启动18线程-8获得锁19线程-9获得锁20线程-7获得锁如果获得锁的顺序与线程启动的顺序一致,这称为公平锁。因此,需要根据实际情况进行选择。

什么是锁

在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
     锁通常需要硬件支持才能有效实施。这种支持通常采取一个或多个原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap"”。这些指令允许单个进程测试锁是否空闲,如果空闲,则通过单个原子操作获取锁。

公平锁

  • 定义:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。

  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

  • 公平锁获取锁例子

 1 /**
 2     *     true 表示 ReentrantLock 的公平锁
 3     */
 4    private ReentrantLock lock = new ReentrantLock(true);
 5
 6    public   void testFail(){
 7        try {
 8            lock.lock();
 9            System.out.println(Thread.currentThread().getName() +"获得了锁");
10        }finally {
11            lock.unlock();
12        }
13    }
14    public static void main(String[] args) {
15        FairLockTest fairLock = new FairLockTest();
16        Runnable runnable = () -> {
17           System.out.println(Thread.currentThread().getName()+"启动");
18            fairLock.testFail();
19        };
20        Thread[] threadArray = new Thread[10];
21        for (int i=0; i<10; i++) {
22            threadArray[i] = new Thread(runnable);
23        }
24        for (int i=0; i<10; i++) {
25            threadArray[i].start();
26        }
27    }

运行结果

 1Thread-1启动
 2Thread-1获得了锁
 3Thread-3启动
 4Thread-3获得了锁
 5Thread-5启动
 6Thread-5获得了锁
 7Thread-2启动
 8Thread-2获得了锁
 9Thread-4启动
10Thread-4获得了锁
11Thread-6启动
12Thread-6获得了锁
13Thread-10启动
14Thread-8启动
15Thread-10获得了锁
16Thread-9启动
17Thread-7启动
18Thread-8获得了锁
19Thread-9获得了锁
20Thread-7获得了锁

看到结果里面获得锁的顺序和线程启动顺序是一致的,这就是公平锁。

非公平锁

  • 定义:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

  • 优点:非公平锁性能高于公平锁性能,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

  • 缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

  • 非公平锁列子:只需要将上面公平锁的代码改为new ReentrantLock(false);
    运行结果

 1Thread-1启动
 2Thread-0启动
 3Thread-2启动
 4Thread-3启动
 5Thread-4启动
 6Thread-8启动
 7Thread-7启动
 8Thread-6启动
 9Thread-1获得了锁
10Thread-0获得了锁
11Thread-5启动
12Thread-5获得了锁
13Thread-2获得了锁
14Thread-3获得了锁
15Thread-4获得了锁
16Thread-8获得了锁
17Thread-7获得了锁
18Thread-6获得了锁
19Thread-9启动
20Thread-9获得了锁

线程启动顺序是1、0、2、3、 4、 8 、7 、6 、5 、9,获得锁的顺序却是1 、0 、5 、2 、3 、4  、8 、7 、6 、9,这就是非公平锁,它不保证先排队尝试去获取锁的线程一定能先拿到锁。

重入锁

  • 定义:- 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁
    下面是一个synchronized重入锁的列子:

 1public class ReentrantLockTest {
 2
 3
 4    public static void main(String[] args){
 5        for (int i = 0; i < 10; i++) {
 6           new Thread(() -> A()).start();
 7        }
 8    }
 9    public static   synchronized void  A(){
10        System.out.println(Thread.currentThread().getName());
11        B();
12    }
13    public static synchronized void  B(){
14        System.out.println(Thread.currentThread().getName());
15    }
16}

输出:

1Thread-1
2Thread-1
3Thread-0
4Thread-0

A方法和B方法同时输出了线程名称,表明即使递归使用synchronized也没有发生死锁,证明其是可重入的。

读写锁

百度百科定义的读写锁是:

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

  • 与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥,读写互斥,写写互斥,而一般的独占锁是:读读互斥,读写互斥,写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。

  • Java里面ReentrantReadWriteLock读写锁特性
        公平选择性: 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
        重进入: 读锁和写锁都支持线程重进入。
        锁降级: 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

乐观锁、悲观锁

  • 乐观锁:乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用“数据版本机制”或“CAS操作”来实现。

  • 悲观锁: 悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。典型的数据库的查询 for update

    • 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

    • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

    • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对jdk1.7 及以前的ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

偏向锁、轻量级锁、重量级锁

 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
 

  • 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

  • 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

  • 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

独享锁、共享锁

  •  独享锁是指该锁一次只能被一个线程所持有。

  •  共享锁是指该锁可被多个线程所持有。
    对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

    参考文章
    https://blog.csdn.net/qiuwenjie123/article/details/79950532
    https://segmentfault.com/q/1010000009659039
    https://blog.csdn.net/qq_43519310/article/details/100107346
    https://blog.csdn.net/u010648018/article/details/79750608
    https://www.cnblogs.com/hustzzl/p/9343797.html
    http://ifeve.com/locks/
    http://ifeve.com/locks/

免责声明:文章转载自《史上最全 Java 中各种锁的介绍》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Element UI 中scope用法Hadoop源码分析5: RPC基本线程下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

1. Redis是属于多线程还是单线程?不同版本之间有什么区别?

Redis是普及率最高的技术之一,不仅是面试会被经常问到,在工作中也是非常常用的。下面我们就深入的了解一下Redis,顺便会介绍一些常见的面试题。 问题:Redis是属于单线程还是多线程? 不同版本的Redis是不同的,在Redis4.0之前,Redis是单线程运行的,但是单线程并不代表效率就低,像Nginx、Nodejs也是单线程程序,但是它们的效率并不...

Android Looper详解

在Android下面也有多线程的概念,在C/C++中,子线程可以是一个函数, 一般都是一个带有循环的函数,来处理某些数据,优先线程只是一个复杂的运算过程,所以可能不需要while循环,运算完成,函数结束,线程就销毁。对于那 些需要控制的线程,一般我们都是和互斥锁相互关联,从而来控制线程的进度,一般我们创建子线程,一种线程是很常见的,那就是带有消息循环的线程...

多线程中如何取消任务

大多数情况下,任务运行完后会自动结束。然而,有时我们希望提前结束任务或线程,可能是因为用户取消了操作,或者应用程序需要被快速关闭。但是,Java并没有提供任务机制来安全地终止线程。虽然如此,但Java提供了线程中断,中断是一种协作机制,能使一个线程终止另一个线程的当前工作。 我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享数据处于不一致的...

Spring Boot中使用Java线程池ExecutorService

1. 认识java线程池 1.1 在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.需处理的任务的数量大 1.2 使用线程池的好处: 1.减少在创建和销毁线程上所花的时间以及系统资源的开销 2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存 1.3 线程池包括以下四个基本组成部分: 1、线程池管理器(ThreadPool...

记一次调bug的过程:windows下查找java应用程序CPU与内存过高

最近写了一个多线程程序,并发量峰值有五六千,甚至八九千个线程。经过几番调试,程序终于能够正常运行起来了,而实际上“正常运行”的背后却是“暗藏玄机”。在程序运行4、5个小时之后,会发现电脑机箱非常热,风扇转地异常快。打开任务管理器,发现程序的CPU飙到了90%上下,内存占用4G左右。程序看似正常,但检查log文件就会发现有问题,数据丢失地非常多。 我猜测可能...

Python与Golang协程异同

背景知识 这里先给出一些常用的知识点简要说明,以便理解后面的文章内容。 进程的定义: 进程,是计算机中已运行程序的实体。程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。 线程的定义: 操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 进程和线程的关系: 一条线程指的是进程中一个单一顺序的控制流,一个进程...