详解JUC并发编程之锁

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。但是现实并不是这样子的,所以JVM实现了锁机制,今天就叭叭叭JAVA中各种各样的锁。

1、自旋锁和自适应锁

自旋锁:在多线程竞争的状态下共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复阻塞线程并不值得,而是让没有获取到锁的线程自旋(自旋并不会放弃CPU的分片时间)等待当前线程释放锁,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改(jdk1.6之后默认开启自旋锁)。

自适应锁:为了解决某些特殊情况,如果自旋刚结束,线程就释放了锁,那么是不是有点不划算。自适应自旋锁是jdk1.6引入,规定自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该线程自旋获取到锁的可能性很大,会自动增加等待时间。反之就认为不容易获取到锁,而放弃自旋这种方式。

锁消除:锁消除时指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。意思就是:在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到那就可以把他们当作栈上的数据对待,认为他们是线程私有的,不用再加锁。

锁粗化:

  public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        System.out.println("拼接之后的结果是:>>>>>>>>>>>"+buffer);
    }
  @Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer 在拼接字符串时是同步的。但是在一系列的操作中都对同一个对象(StringBuffer )反复加锁和解锁,频繁的进行加锁解锁操作会导致不必要的性能损耗,JVM会将加锁同步的范围扩展到整个操作的外部,只加一次锁。

2、轻量级锁和重量级锁

这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁, 取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。轻量级锁是相对于重量级锁而言的。

轻量级锁加锁过程

在HotSpot虚拟机的对象头分为两部分,一部分用于存储对象自身的运行时数据,如Hashcode、GC分代年龄、标志位等,这部分长度在32位和64位的虚拟机中分别是32bit和64bit,称为Mark Word。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息。mark word中有两个bit存储锁标记位。

HotSpot虚拟机对象头Mark Word

存储内容 标志位 状态
对象哈希码,分代年龄 01 无锁
指向锁记录的指针 00 轻量级锁
指向重量级锁的指针 10 膨胀重量级锁
空,不需要记录信息 11 GC标记
偏向线程id,偏向时间戳,对象分代年龄 01 可偏向

在代码进入同步代码块时,如果此对象没有被锁定(标记位为01状态),虚拟机首先在当前线程的栈帧建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝,然后虚拟机使用CAS操作尝试将对象的Mark Word 更新为指向Lock Record的指针,如果操作成功了,那么这个线程就有了这个对象的锁,并且将Mark Word 的标记位更改为00,表示这个对象处于轻量级锁定状态。如果更新失败了虚拟机会首先检查是否是当前线程拥有了这个对象的锁,如果是就进入同步代码,如果不是,那就说明锁被其他线程占用了。如果有两个以上的线程争夺同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标记位变为10,后面等待的线程就要进入阻塞状态。

轻量级锁解锁过程

解锁过程同样使用CAS操作来进行,使用CAS操作将Mark Word 指向Lock Record 指针释放,如果操作成功,那么整个同步过程就完成了,如果释放失败,说明有其他线程尝试获取该锁,那就在释放锁的同时,唤醒被挂起的线程。

3、偏向锁

JVM 参数 -XX:-UseBiasedLocking 禁用偏向锁;-XX:+UseBiasedLocking 启用偏向锁。

        启用了偏向锁才会执行偏向锁的操作。当锁对象第一次被线程获取时,虚拟机会把对象头中的标记位设置为01,偏向模式。同时使用CAS操作获取到当前线程的线程ID存储到Mark Word 中,如果操作成功,那么持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不需要任何操作,直接进入。如果有多个线程去尝试获取这个锁时,偏向锁就宣告无效,然后会撤销偏向或者恢复到未锁定。然后再膨胀为重量级锁,标记位状态变为10。

4、可重入锁和不可重入锁

可重入锁就是一个线程获取到锁之后,在另一个代码块还需要该锁,那么不需要重新获取而可以直接使用该锁。大多数的锁都是可重入锁。但是CAS自旋锁不可重入。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 测试锁的重入性
 * @date 2021/12/30 22:09
 */
public class Test01 {
    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + "运行a方法");
        b();
    }
    private synchronized void b() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行b方法");
    }
    public static void main(String[] args) {
        Test01 test01 = new Test01();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0;i<10;i++){
            executorService.execute(() -> test01.a());
        }
    }
}

5、悲观锁和乐观锁

悲观锁总是悲观的,总是认为会发生安全问题,所以每次操作都会加锁。比如独占锁、传统数据库中的行锁、表锁、读锁、写锁等。悲观锁存在以下几个缺点:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。
  • 一个线程占有锁后,其他线程就得阻塞等待。
  • 如果优先级高的线程等待一个优先级低的线程,会导致线程优先级导致,可能引发性能风险。

乐观锁总是乐观的,总是认为不会发生安全问题。在数据库中可以使用版本号实现乐观锁,JAVA中的CAS和一些原子类都是乐观锁的思想。

6、公平锁和非公平锁

公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

非公平锁:非公平锁不需要按照申请锁的时间顺序来获取锁,而是谁能获取到CPU的时间片谁就先执行。非公平锁的优点是吞吐量比公平锁大,缺点是有可能导致线程优先级反转或者造成过线程饥饿现象(就是有的线程玩命的一直在执行任务,有的线程至死没有执行一个任务)。

synchronized中的锁是非公平锁,ReentrantLock默认也是非公平锁,但是可以通过构造函数设置为公平锁。

7、共享锁和独占锁

共享锁就是同一时刻允许多个线程持有的锁。例如Semaphore(信号量)、ReentrantReadWriteLock的读锁、CountDownLatch倒数闩等。

独占锁也叫排它锁、互斥锁、独占锁是指锁在同一时刻只能被一个线程所持有。例如synchronized内置锁和ReentrantLock显示锁,ReentrantReadWriteLock的写锁都是独占锁。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @description: 读写锁验证共享锁和独占锁
 * @author xiaojie
 * @date 2021/12/30 23:28
 * @version 1.0
 */
public class ReadAndWrite {
    static class ReadThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public ReadThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "这是共享锁。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁成功。。。。。。");
            }
        }
    }
    static class WriteThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public WriteThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.writeLock().lock();
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "这是独占锁。。。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁。。。。。。。");
            }
        }
    }
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock);
        WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock);
        readThred1.start();
        readThred2.start();
        writeThred1.start();
        writeThred2.start();
    }
}

8、可中断锁和不可中断锁

可中断锁只在抢占锁的过程中可以被中断的锁如ReentrantLock。

不可中断锁是不可中断的锁如java内置锁synchronized。

总结:

名称

优点

缺点

使用场景

偏向锁

加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步快的场景

轻量级锁

竞争的线程不会阻塞,提高了响应速度

如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能

追求响应时间,同步快执行速度非常快

重量级锁

线程竞争不适用自旋,不会消耗CPU

线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗

追求吞吐量,同步快执行速度较长

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

你可能感兴趣的:(详解JUC并发编程之锁)