Java多线程 各种锁(一篇全搞懂)

Java多线程 锁

1、乐观锁与悲观锁

(1)悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

(2)乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
在这里插入图片描述
CSA算法详解以及他们的使用场景和优缺点:
上一篇博客:Java多线程 乐观锁、悲观锁、以及CAS算法

2、公平锁与非公平锁

(1)公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁

优点:等待锁的线程不会饿死
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

图解:
在这里插入图片描述
(2)非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景

优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程

缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁

图解:
在这里插入图片描述

3、可重入锁与不可重入锁

(1)可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

(2)是指在同一个线程在外层方法获取锁的时候,不能再进入该线程的内层方法会自动获取锁。如(非可重入锁NonReentrantLock)。

ReentrantLock和NonReentrantLock的实现原理:
其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

synchronized 实现是由jvm底层实现。

4、独享锁与共享锁

(1)独享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。(例如ReentrantReadWriteLock中的写锁)

(2)共享锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。(例如ReentrantReadWriteLock中的读锁)

Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
结构如下图:

1、写锁是一个独享锁,所以我们看一下ReentrantReadWriteLock中tryAcquire(arg)的实现:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
 }

获取:
(1)获取同步状态,并从中分离出低16为的写锁状态
(2)如果同步状态不为0,说明存在读锁或写锁
(3)如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
(4)如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
(5)如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
(6)将当前线程设置为写锁的获取线程

释放的过程与独占锁基本相同。

2、读锁是一个共享锁,获取读锁的步骤如下:

(1)获取当前同步状态
(2)计算高16为读锁状态+1后的值
(3)如果大于能够获取到的读锁的最大值,则抛出异常
(4)如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
(5)如果上述判断都通过,则利用CAS重新设置读锁的同步状态
读锁的获取步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。

5、自旋锁 VS 适应性自旋锁

(1)自旋锁
让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

缺点:
它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间;如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

(2)适应自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

补充:在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,其中AQSde CLHlock在把线程封装成节点后插入CLH后便在做自旋操作。

6、无锁 、 偏向锁、量级锁 和 重量级锁(难点)

前言:
我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级

四种锁状态对应的的Mark Word内容:
在这里插入图片描述
(1)无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功
CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

(2)偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

(3)轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

(4)重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

在这里插入图片描述

  • 10
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然可以!下面是一篇关于Java多线程中的""的博客: ## Java多线程中的Java多线程编程中,是一种重要的同步机制,用于保护共享资源的访问。使用可以防止多个线程同时对共享资源进行修改,从而避免数据不一致的问题。 ### 1. 什么是是一种同步机制,它可以让多个线程按照一定的顺序访问共享资源。在Java中,可以是隐式的,也可以是显式的。 - 隐式:通过关键字`synchronized`来实现,它可以用于修饰方法或代码块。当一个线程进入被`synchronized`修饰的方法或代码块时,它会自动获取,并在执行完毕后释放。 - 显式:通过`java.util.concurrent.locks`包中的Lock接口及其实现类来实现,如ReentrantLock。显式需要手动获取和释放,在使用上更加灵活。 ### 2. 的作用 的主要作用是保护共享资源的访问,它可以解决多线程并发访问时可能引发的以下问题: - 竞态条件(Race Condition):当多个线程同时访问共享资源,并且对其进行写操作时,可能会导致数据的不一致。 - 临界区(Critical Section):当多个线程同时访问共享资源,并且对其进行读写操作时,可能会导致数据的不一致。 - 死(Deadlock):当多个线程相互等待对方释放时,可能会导致程序无法继续执行。 ### 3. 的类型 Java中常用的类型包括: - 内置(Intrinsic Lock):也称为监视器(Monitor Lock),是由关键字`synchronized`来实现的。内置是基于对象的,每个对象都有一个用于同步的内置,当一个线程获取了该后,其他线程必须等待。 - 重入(Reentrant Lock):是`java.util.concurrent.locks`包中的一个显式实现类,它具有与内置类似的功能,但提供了更高级的特性,如可重入、公平和超时等。 - 读写(Read-Write Lock):也是`java.util.concurrent.locks`包中的一个显式实现类,它区分了读操作和写操作,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。 ### 4. 的使用示例 下面是一个使用内置`synchronized`来实现线程安的示例: ```java public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } ``` 在上述示例中,`increment()`和`getCount()`方法都被`synchronized`修饰,这意味着同一时间只能有一个线程执行这些方法,从而保证了`count`变量的访问安。 ### 5. 的注意事项 在使用时,需要注意以下事项: - 避免死:在获取的时候,要确保能够及时释放,避免多个线程相互等待对方释放而导致死。 - 避免饥饿:要确保所有线程都有公平获取的机会,避免某个线程一直无法获得而导致饥饿。 - 的粒度:要选择合适的粒度,尽量减小的范围,以提高程序的并发性能。 - 的性能:显式相对于内置,通常具有更高的性能,但使用不当可能导致性能问题。要根据实际情况选择合适的。 ### 总结 Java多线程编程中重要的同步机制,用于保护共享资源的访问。它可以解决竞态条件、临界区和死等问题。在使用时,需要注意避免死和饥饿,选择合适的粒度和类型,以及权衡的性能。 希望本篇博客对你有所帮助!如有任何问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值