Java并发与多线程(3)——Java中的锁

三、Java中的锁

  • 三、Java中的锁
    • 3.1 JVM中的对象内存布局
      • 3.1.1 MarkWord
      • 3.1.2 指向类的指针:
      • 3.1.3 数组长度:
    • 3.2 Java中Monitor对象
    • 3.3 Java中共享数据
      • 3.3.1 不可变
      • 3.3.2 绝对线程安全
      • 3.3.3 相对线程安全
      • 3.3.4 线程兼容
      • 3.3.4 线程对立
    • 3.4 线程安全的实现
      • 3.4.1 互斥同步(阻塞同步)
        • 3.4.1.1 synchronized关键字
        • 3.4.1.2 ReentrantLock
        • 3.4.1.3 synchronized与ReentrantLock对比
      • **3.4.2 非阻塞同步**
        • 3.4.3.1 CPU原子指令
        • 3.4.3.2 CAS原理
        • 3.4.3.3 CAS的逻辑漏洞:ABA问题
      • **3.4.3 无同步操作**
    • 3.5 锁
      • 3.5.1 锁的分类
      • 3.5.2 悲观锁与乐观锁
        • 3.5.2.1 概念
        • 3.5.2.2 对比
        • 3.5.2.3 常见示例
      • 3.5.3 互斥锁与自旋锁、适应性自旋锁
        • 3.5.3.1 概念
        • 3.5.3.2 区别
        • 3.5.3.3 常见示例
      • 3.5.4 公平锁与非公平锁
        • 3.5.4.1 概念
        • 3.5.4.2 区别
        • 3.5.4.3 常见示例
      • 3.5.5 可重入锁与不可重入锁
        • 3.5.5.1 概念
        • 3.5.5.2 区别
        • 3.5.5.3 常见示例
      • 3.5.6 共享锁与排他锁(读锁与写锁)
        • 3.5.6.1 概念
        • 3.5.6.2 区别
        • 3.5.6.3 常见示例
      • 3.5.7 可中断锁与不可中断锁
        • 3.5.7.1 概念
        • 3.5.7.2 区别
        • 3.5.7.3 示例
    • 3.6 synchronized、Reentrant详解
      • 3.6.1 synchronized实现机制
        • 3.6.1.1 WaitSet、EntryList、cxq
        • 3.6.1.2 为什么说synchronized是非公平锁
        • 3.6.1.3 无锁状态、偏向锁、轻量级锁、重量级锁
        • 3.6.1.4 锁升级(锁膨胀)
        • 3.6.1.5 wait()、notify()/notifyAll()
      • 3.6.2 ReentrantLock的实现机制
        • 3.6.2.1 AQS(抽象队列同步器 )基本介绍
        • 3.6.2.2 AQS两种资源共享方式
        • 3.6.2.3 AQS实现核心
        • 3.6.2.4 为什么ReentrantLock是互斥的?
        • 3.6.2.5 为什么ReentrantLock是可重入的?
        • 3.6.2.6 为什么ReentrantLock可以实现公平锁?
        • 3.6.2.7 Condition的await()、signal()/signalAll()使用
      • 3.6.3 synchronized与ReentrantLock总结
        • ReentrantLock的优势:
      • 3.6.4 Object与Condition等待的不同
    • 3.7 volatile关键字
      • 3.7.1 volatile关键字作用
      • 3.7.2 volatile与synchronized区别
    • 3.8 CountDownLatch(线程计数器)
        • 主要方法:
        • 示例:t1等待t2执行5次后继续执行
    • 3.9 CyclicBarrier(回环栅栏)
        • 主要方法:
        • 示例
        • CyclicBarrier与CountDownLatch的区别
    • 3.10 Semaphore
        • 主要方法:
        • 示例

三、Java中的锁

3.1 JVM中的对象内存布局

HotSpot虚拟机中一个Java对象在内存中由对象头实例数据对齐字节填充部分组成。其中对象头又分为三部分,MarkWord、指向类的指针和数组长度(只有数组对象有)。如下:

Java并发与多线程(3)——Java中的锁_第1张图片

3.1.1 MarkWord

MarkWord用于存储对象自身的运行时数据,如哈希码和GC分代年龄等,是实现轻量级锁和偏向锁的关键;

MarkWord在32位和64位虚拟中分别占用32bit和64bit;

由于是与对象自身数据无关的额外存储成本,MrakWord被设计为非固定的动态数据结构,会根据对象的状态复用自己的存储空间,以便在极小的空间存储更多的信息。对象的状态主要有未锁定、轻量级锁定、重量级锁定、GC标记、可偏向等状态。

Java并发与多线程(3)——Java中的锁_第2张图片)

3.1.2 指向类的指针:

用于存储指向方法区对象类型数据的指针,Java虚拟机通过该指针确定该对象是哪个类的实例。

3.1.3 数组长度:

数组对象会有一个记录数组长度的部分。

3.2 Java中Monitor对象

https://zhuanlan.zhihu.com/p/356010805

3.3 Java中共享数据

Java语言中各种操作共享的数据有五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

3.3.1 不可变

不可变的对象一定是线程安全的。使用final修饰的(严格来说是JVM里没有发生this引用逃逸的)。

// 在多线程中不需要考虑value的安全性
private final int value = 1;

3.3.2 绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施。这个定义十分严格,在JavaAPI中标注线程安全的类大多数都不是绝对的线程安全。例如Vector是一个线程安全的容器,它的addget()size()等方法都被synchronized修饰。但是也不代表它是绝对线程安全,例如:

public class VectorDemo {

    public static void main(String[] args) {
        Vector<Integer> vector = new Vector<>();
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        Thread removeThread = new Thread(() -> {
            for (int i = 9; i >= 0; i--) {
                vector.remove(i);
            }
        });

        Thread printThread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(vector.get(i));
            }
        });
        removeThread.start();
        printThread.start();
    }
}

上述代码运行会报ArrayIndexOutOfBoundsException

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.base/java.util.Vector.get(Vector.java:780)
	at org.numb.concurrency.chapter03.VectorDemo.lambda$main$1(VectorDemo.java:20)
	at java.base/java.lang.Thread.run(Thread.java:834)

因为一个线程去移除vector元素,一个又去获取元素,导致获取元素时此元素已被删除。

3.3.3 相对线程安全

对象单次的操作是线程安全的,这也是通常意义上所讲的线程安全。如VectorHashTablesynchronizedCollection()方法包装的集合。

3.3.4 线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发的环境中安全使用。如ArrayListHashMap等。

3.3.4 线程对立

线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。线程对立通常是有害的,典型的例子是Thread类suspend()resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险。

3.4 线程安全的实现

实现线程安全的方法主要有:1、互斥同步;2、非阻塞同步、3、无同步方案

3.4.1 互斥同步(阻塞同步)

同步是指对个线程并发访问共享数据时,保证共享数据在同一时刻只被一条(或者一些)线程使用。互斥是实现同步的一种手段,临界区互斥量信号量都是常见的互斥实现方式。是一种悲观的同步措施。

3.4.1.1 synchronized关键字

synchronized关键字经过Javac编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码。这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,就以这个对象的引用作为reference;如果没有明确指定,那将synchronized修饰的方法类型,来决定是取代吗所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

Java并发与多线程(3)——Java中的锁_第3张图片

执行monitorenter指令时,首先尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加1,而执行monitorexit时就将计数器减一。一旦计数器为0锁即被释放,获取锁失败当前线程就被阻塞等待,直到请求锁定的对象持有它的线程释放为止。

  • 被synchronized修饰的同步块对同一条线程来说是可重入的。
  • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。
  • synchronized是一种重量级锁,且是非公平的

3.4.1.2 ReentrantLock

ReentrantLockLock接口的最常见实现。基于Lock接口,用户能够以非块结构来实现互斥同步。ReentrantLocksynchronized增加的功能有:等待可中断、可实现公平锁和可以绑定多个条件

  • 等待可中断

    当持有锁的线程长时间不释放锁时,等待的线程可选择放弃

  • 可实现公平锁

    ReentrantLock默认是非公平锁,可以通过带布尔值的构造函数实现公平锁

  • 锁可以绑定多个条件

    一个ReentrantLock对象可以同时绑定多个Condition对象

3.4.1.3 synchronized与ReentrantLock对比

  • 性能

    JDK 5及以前版本ReentrantLock效率高于synchronized,JDK 6针对synchronized做了大量优化,现在性能基本持平。

  • synchronized优势

    1. synchronized是java原生的语法,更加基础,在synchronized功能满足需求时优先使用。
    2. synchronized的,即使抛出异常也可以由JVM释放,而ReentrantLock必须在finally代码块中释放锁
    3. JVM可以在线程和对象元数据中记录synchronized锁的相关信息,而JVM很难知道ReentrantLock由哪些线程持有,增加了问题定位难度。
  • ReentrantLock优势

    ReentrantLock功能比synchronized更强大,可以实现等待可中断、可实现公平锁和可以绑定多个条件。

3.4.2 非阻塞同步

​ 阻塞同步在对共享数据进行加锁后,会导致用户态与内核态的切换、维护锁计数器和检查线程是否需要唤醒等开销。而非阻塞同步是基于冲突检测的乐观并发策略,即先进行操作,然后检测如果有没有其他线程竞争共享数据,则成功,如果有竞争则不断尝试,直至没有竞争的共享数据,即无锁编程。

3.4.3.1 CPU原子指令

​ 这种策略前提是操作和冲突检测必须具备原子性,而原子性的保证如果再依赖加锁和互斥同步实现就失去了意义,所以必须依赖硬件实现,即可以通过一条CPU指令就能完成,这类指令有

  • 测试并设置(Test-and-Set)

  • 获取并增加(Fetch-and-Increment)

  • 交换(Swap)

  • 比较并交换(Compare-and-Swap,即CAS)

  • 加载链接/条件储存(Load-Linked/Store-Conditional,即LL/SC)

3.4.3.2 CAS原理

​ CAS指令需要三个操作数,分别为内存位置V、旧的预期值A和新的预期值B。CAS执行时当且仅当V符合预期A时,处理器才会用B来更新V的值,否则不更新。不管是否更新V值,都会返回V的旧值。上述操作是一个原子操作,执行期间不会被其他线程打断。

3.4.3.3 CAS的逻辑漏洞:ABA问题

CAS存在一个逻辑漏洞,又称为ABA问题,即如果一个变量V初次读取的时候是A,并且在准备赋值的时候检查它仍为A,但是无法保证这个期间它的值曾经被修改过

Java并发与多线程(3)——Java中的锁_第4张图片

J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类AtomStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前这个类非常鸡肋,大部分情况下ABA不会影响程序并发的正确性。如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。

3.4.3 无同步操作

要保证线程安全不一定非要采用同步,只需保证共享数据的正确性即可,有些代码本身就是线程安全的如:

  • 可重入代码
  • 线程本地存储:如ThreadLocal。

3.5 锁

3.5.1 锁的分类

Java并发与多线程(3)——Java中的锁_第5张图片

3.5.2 悲观锁与乐观锁

3.5.2.1 概念

  • 悲观锁:当出现竞争关系时,对共享数据进行加锁
  • 乐观锁:当出现竞争关系时不会去加锁,而是通过CAS、版本号等进行判断,并不断重试

3.5.2.2 对比

  • 悲观锁都会进行加锁性能较差,乐观锁不会加锁性能好。
  • 乐观锁不能保证每次都成功,获取失败时会一直重试或一段时间后抛异常

3.5.2.3 常见示例

  • 悲观锁synchronized关键字、Lock的实现类(ReentrantLockReentrantReadWriteLock
  • 乐观锁java.util.concurrent.atomic包下面的AtomicBooleanAtomicIntegerAtomicReference

下面实现一个简单生产者消费者模型,一个线程对num++,另一个线程判断是奇数则消费

  1. 不加锁时

    class Demo{
        private static Integer num = 0;
    	public static void noLockScene() {
    
            Thread t1 = new Thread(() -> {
                while (true) {
                    if (num % 2 == 1) {
                        try {
                            TimeUnit.MICROSECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("thread 1, num = " + num);
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                while (true) {
                    try {
                        TimeUnit.MICROSECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num++;
                }
    
            });
            t1.start();
            t2.start();
        }
    }
    

    输出如下:由于未加锁,竞争导致逻辑混乱

    thread 1, num = 2
    thread 1, num = 5
    thread 1, num = 6
    thread 1, num = 8
    
  2. synchronized悲观锁

    class Demo{
    	private static byte[] lock = new byte[0];
        public static void synchronizedScene() {
            Thread t1 = new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        if (num % 2 == 1) {
                            try {
                                TimeUnit.MICROSECONDS.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println("thread 1, num = " + num);
                        }
                    }
                }
    
            });
    
            Thread t2 = new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        try {
                            TimeUnit.MICROSECONDS.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        num++;
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    

    输出:这里未保证不重复消费和按顺序消费,消费都是奇数,正确

    hread 1, num = 25
    thread 1, num = 47
    thread 1, num = 333
    
  3. ReentrantLock悲观锁

    class Demo{
    	public static void reentrantLockScene() {
            ReentrantLock reentrantLock = new ReentrantLock();
            Thread t1 = new Thread(() -> {
                while (true) {
                    reentrantLock.lock();
                    if (num % 2 == 1) {
                        try {
                            TimeUnit.MICROSECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("thread 1, num = " + num);
                    }
                    // ReentrantLock必须手动释放锁
                    reentrantLock.unlock();
                }
            });
    
            Thread t2 = new Thread(() -> {
                while (true) {
                    reentrantLock.lock();
                    try {
                        TimeUnit.MICROSECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num++;
                    reentrantLock.unlock();
                }
            });
            t1.start();
            t2.start();
        }
    }
    

    输出:

    thread 1, num = 79
    thread 1, num = 839
    thread 1, num = 1249
    
  4. AtomicInteger乐观锁

    class Demo{
    	public static void optimisticLockScene() {
            AtomicInteger number = new AtomicInteger();
            Thread t1 = new Thread(() -> {
                while (true) {
                    int origin = number.get();
                    if (origin % 2 == 1) {
                        try {
                            TimeUnit.MICROSECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (!number.compareAndSet(origin, origin)) {
                            System.out.println("the num has changed");
                        } else {
                            System.out.println("thread 1, num = " + origin);
                        }
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                while (true) {
                    try {
                        TimeUnit.MICROSECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    number.incrementAndGet();
                }
            });
            t1.start();
            t2.start();
        }
    }
    

    输出:

    thread 1, num = 1235
    the num has changed
    thread 1, num = 1241
    the num has changed
    thread 1, num = 1245
    

3.5.3 互斥锁与自旋锁、适应性自旋锁

3.5.3.1 概念

  • 互斥锁:当一个线程持有锁后,其他线程加锁就会失败,从而由操作系统控制进入睡眠状态,直到持有锁的线程释放锁后,其他线程再进行竞争加锁
  • 自旋锁:与互斥锁不同,线程加锁失败后不会睡眠,而是等待一段时间(CPU周期)后继续尝试加锁
  • 适应性自旋锁:自旋锁在Java1.6中改为默认开启,并引入了自适应的自旋锁。自适应意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态共同决定。

3.5.3.2 区别

  • 互斥锁加锁失败休眠后,会由用户态转为内核态,性能差;自旋锁只是在用户态进行等待,不会进行上下文切换,性能好。
  • 自旋锁由于在用户态进行等待,不会释放CPU资源,会导致CPU资源一直在白白浪费

3.5.3.3 常见示例

这两种锁都是最底层的锁实现,一般跟操作系统有关

  • 互斥锁:c语言中pthread_mutex_lock
  • 自旋锁:c语言中pthread_spin_lock

synchronized关键字在优化后,底层也会加自旋锁,且会适应性自旋,详见后面synchronized介绍。

利用CAS,实现自旋锁

public class SpinLock {

    private AtomicReference<Thread> cas = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        // 利用CAS加锁失败则自旋
        while (!cas.compareAndSet(null, thread)) {
            // 自旋,可以等待或者什么都不做
        }

    }

    public void unlock() {
        Thread thread = Thread.currentThread();
        cas.compareAndSet(thread, null);
    }
}

3.5.4 公平锁与非公平锁

3.5.4.1 概念

  • 公平锁:每个线程按调用加锁方法的顺序依次获取锁,排队获取
  • 非公平锁:每个线程抢占锁的顺序不定,有可能后加锁的先获取锁

3.5.4.2 区别

  • 公平锁实现多核情况下维护一个队列,代价更大,性能更低

3.5.4.3 常见示例

  • 公平锁:ReentrantLock可以控制传入参数指定为公平锁
  • 非公平锁synchronized
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
// 非公平锁,默认非公平锁
ReentrantLock lock = new ReentrantLock(false);

公平锁与非公平锁示例

public class FairLock {
    public static void fairLock() {
        // 公平锁
        ReentrantLock lock = new ReentrantLock(true);

        Runnable runnable = () -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " lock");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " unlock");
        };
        // 启动5个线程,按顺序依次获取锁
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(runnable);
            t.setName("thread-" + i);
            t.start();
        }
    }

    public static void noFairLock() {
        // 非公平锁
        ReentrantLock lock = new ReentrantLock(false);

        Runnable runnable = () -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " lock");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " unlock");
        };
        // 启动5个线程,随机获取锁
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(runnable);
            t.setName("thread-" + i);
            t.start();
        }
    }
}

3.5.5 可重入锁与不可重入锁

3.5.5.1 概念

  • 可重入锁:同一个线程可以多次获取同一个锁
  • 不可重入锁:同一个线程不能多次获取同一个锁,否则会产生死锁等

3.5.5.2 区别

  • 可重入锁降低了编程复杂性

3.5.5.3 常见示例

  • 可重入锁:Java中的大部分锁都是可重入锁,synchronizedReentrantLock

  • 不可重入锁:可以自己实现,具体为加锁一次后再次加锁就死锁

3.5.6 共享锁与排他锁(读锁与写锁)

3.5.6.1 概念

  • 共享锁:也称为读锁,多个线程可以持有这个锁
  • 排他锁:也称为写锁,只能有一个线程持有

3.5.6.2 区别

  • 读锁可以多个线程并发去读;
  • 只要成功加写锁后,读锁和写锁都无法再次加锁,直到写锁释放

3.5.6.3 常见示例

读写锁通常是一起用来提高并发读写性能,如ReentrantReadWriteLock

public class ReentrantReadWriteLockDemo {

    private static int num;

    public static void main(String[] args) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        Thread t1 = new Thread(() -> {
            while (true) {
                lock.readLock().lock();
                if (num % 2 == 1) {
                    try {
                        TimeUnit.MICROSECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread 1, num = " + num);
                }
                lock.readLock().unlock();
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                lock.writeLock().lock();
                try {
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                num++;
                lock.writeLock().unlock();
            }
        });
        t1.start();
        t2.start();
    }
}

3.5.7 可中断锁与不可中断锁

3.5.7.1 概念

  • 可中断锁:锁在执行时可被中断,如接收 interrupt 的通知,从而中断锁执行
  • 不可中断锁:锁在执行时不可被中断

3.5.7.2 区别

  • 当线程由于无法释放锁导致其他线程无法加锁时,可以通过中断让线程释放锁,这是可中断锁的优势

3.5.7.3 示例

public class InterruptLock {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                // 持有锁后保持10s
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    System.out.println("thread 1 is interrupted");
                }
            } catch (InterruptedException e) {
                // 线程被打断
                System.out.println("thread 1 is interrupted");
            } finally {
                // 必须释放锁,否则thread2 仍然无法获取锁
                lock.unlock();
                System.out.println("release lock in thread 1");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread 2 get lock");
            lock.unlock();
        });
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
    }
}

输出:

thread 1 is interrupted
release lock in thread 1
thread 2 get lock

3.6 synchronized、Reentrant详解

3.6.1 synchronized实现机制

synchronized锁的是对象的监视器,可以详见3.2。即java对象头的MarkWord中存有指向监视器monitor的指针

Java并发与多线程(3)——Java中的锁_第6张图片

3.6.1.1 WaitSet、EntryList、cxq

  • WaitSet:调用wait()方法等待的线程
  • EntryList:存放处于等待锁处于block中的线程,线程接下来就会去争夺锁
  • cxq:多个线程争抢锁,会先存入这个单向链表

Java并发与多线程(3)——Java中的锁_第7张图片

3.6.1.2 为什么说synchronized是非公平锁

由上可以看出,cxq队列中的线程有可能会比EntryList中的线程先抢到锁,所以是非公平锁。

3.6.1.3 无锁状态、偏向锁、轻量级锁、重量级锁

synchronized锁的状态有四种:无锁状态偏向锁轻量级锁重量级锁

  • 无锁状态:不加锁时的状态
  • 偏向锁:偏向锁是指一个线程获得锁后,消除这个线程锁重入(CAS)的开销。看起来让这个线程得到了偏袒。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令, 而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。 偏向锁则是在只有一个线程执行同步块时进一步提高性能
  • 轻量级锁:轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的。 轻量级锁是为了在线程交替执行同步块时提高性能 。
  • 重量级锁:Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又
    是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,导致锁性能差,显得很笨重。

3.6.1.4 锁升级(锁膨胀)

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

3.6.1.5 wait()、notify()/notifyAll()

synchronized加锁之后,可以调用wait()实现等待,调用notify()唤醒一个等待线程(WaitSet中的线程)或notifyAll()唤醒所有等待线程去抢占锁。

3.6.2 ReentrantLock的实现机制

ReentrantLock内部有两个同步器:FairSyncNonFairSync,依赖它们实现加锁。

Java并发与多线程(3)——Java中的锁_第8张图片

3.6.2.1 AQS(抽象队列同步器 )基本介绍

AQS定义了一套多线程访问共享资源的同步框架,在它内部维护了一个volatile int state(代表共享资源)和一个FIFO队列(多线程竞争共享资源时进入此队列)

public abstract class AbstractQueuedSynchronizer{
	// FIFO队列头
    private transient volatile Node head;
    // FIFO队列尾
    private transient volatile Node tail;
	// 共享资源
    private volatile int state;
}

state的访问方式有三种:

  • getstate
  • setstate
  • compareAndSet()

Java并发与多线程(3)——Java中的锁_第9张图片

3.6.2.2 AQS两种资源共享方式

  • 独占资源:ReentrantLock
    • isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
    • tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
    • tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
  • 共享资源:Semaphore/CountDownLatch
    • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败; 0 表示成功,但没有剩余
      可用资源;正数表示成功,且有剩余资源。
    • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回
      true,否则返回 false。

3.6.2.3 AQS实现核心

  • ReentrantLock: state 初始化为 0,表示未锁定状态。 A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前, A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
  • CountDownLatch:任务分为 N 个子线程去执行, state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的, 每个子线程执行完后 countDown()一次, state会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

3.6.2.4 为什么ReentrantLock是互斥的?

ReentrantLock一个线程加锁后,会调用AQS的tryAcquire(),加锁成功后其他线程再调用tryAcquire()时就会失败

3.6.2.5 为什么ReentrantLock是可重入的?

同一个线程可以重复调用AQS的tryAcquire(),每次调用state都会+1,每次unlock()都会-1,所以ReentrantLock加锁几次就要解锁几次,保证最终state=0

3.6.2.6 为什么ReentrantLock可以实现公平锁?

ReentrantLock内部有两个同步器FairSyncNonFairSync,可以分别实现公平锁和非公平锁

3.6.2.7 Condition的await()、signal()/signalAll()使用

Condition可以使ReentrantLock加锁的线程wait,主要是await()/signal()

3.6.3 synchronized与ReentrantLock总结

synchronized ReentrantLock
是否悲观锁
是否公平锁 非公平 默认非公平可实现公平
是否可重入锁
是否可中断锁 lockInterruptibly可以实现可中断
等待唤醒方式 Object的wait()、notify()/notifyAll Condition的await()、signal()和signalAll()

ReentrantLock的优势:

  • 可中断锁

  • 公平锁

  • 锁可以绑定多个条件,实现指定线程唤醒

    一个ReentrantLock对象可以同时绑定多个Condition对象,每一个Condition可以关联一个指定线程,实现指定线程唤醒。

3.6.4 Object与Condition等待的不同

ReentrantLock与Condition一起使用可以实现指定线程唤醒,而notify()是随机唤醒一个等待线程。

3.7 volatile关键字

3.7.1 volatile关键字作用

volatile关键字主要有两个作用

  • 保证可见性:保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的
    值对于其他线程是可以立即获取的。 不使用寄存器而是直接由内存保存变量值
  • 禁止指令重排序

3.7.2 volatile与synchronized区别

volatile只保证了可见性(每次从内存获取)与有序性(禁止指令重排序),不能保证原子性,所以无法取代synchronized。synchronized可以保证原子性。

3.8 CountDownLatch(线程计数器)

CountDownLatch作用类似于线程计数器,比如让某一个线程执行多少次后,另一个线程继续执行

主要方法:

// 初始化一个线程计数器,初始化后数量便无法改变,清零后也无法重新使用
public CountDownLatch(int count)
// 阻塞直到计数器清零
public void await() throws InterruptedException
// 计数器-1
public void countDown()

示例:t1等待t2执行5次后继续执行

public class CountDownLatchDemo {

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);

        Thread t1 = new Thread(() -> {
            try {
                System.out.println("线程阻塞...");
                latch.await();
                System.out.println("计数结束...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            while (true) {
                latch.countDown();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3.9 CyclicBarrier(回环栅栏)

CyclicBarrier的作用是让一组线程等待至某个状态之后再全部同时执行适用于保证一批线程同时结束

主要方法:

// 初始化需要一批次执行的线程数量
public CyclicBarrier(int parties) 
// 线程等待调用此方法
public int await() throws InterruptedException, BrokenBarrierException
// 用完后重置以重用
public void reset()

示例

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        // 并发开启5个线程处理业务, 等到业务处理完后,再处理结果
        for (int i = 0; i < 5; i++) {
            final int num = i;
            Thread thread = new Thread(() -> {
                System.out.println("线程" + num + "并发处理业务");
                try {
                    // 等待其余并发线程处理完成
                    cyclicBarrier.await();
                    System.out.println("线程" + num + "处理本线程的结果");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

输出:

线程0并发处理业务
线程1并发处理业务
线程2并发处理业务
线程3并发处理业务
线程4并发处理业务
线程4处理本线程的结果
线程0处理本线程的结果
线程3处理本线程的结果
线程2处理本线程的结果
线程1处理本线程的结果

CyclicBarrier与CountDownLatch的区别

  • CyclicBarrier可以重用,CountDownLatch 不可以。

  • CountDownLatch 在AQS队列中park的是主线程,而CyclicBarrier在AQS中park的是所有的子线程。

  • CountDownLatch 是放到AQS队列中,而CyclicBarrier是将子线程放到Condition队列中。

  • CountDownLatch 唤醒的是主线程,而CyclicBarrier 是通过singleAll函数,将所有的子线程移动到AQS队列中,然后再开始执行。

3.10 Semaphore

作用:Semaphore 可以控制同时访问的线程个数

主要方法:

// 初始化一个信号量
public Semaphore(int permits)
// 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可
public void acquire() throws InterruptedException
// 获取 permits 个许可
public void acquire(int permits)
// 释放许可。注意,在释放许可之前,必须先获获得许可。
public void release()
// 释放 permits 个许可
public void release(int permits)

示例

public class SemaphoreDemo {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5);
        // semaphore保证最多并发运行5个线程,模拟一个线程池
        for (int i = 0; i < 10; i++) {
            final int num = i;
            Thread thread = new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("线程池仍有空闲,线程" + num + "提交成功");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("线程" + num + "结束");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

输出:

线程池仍有空闲,线程0提交成功
线程池仍有空闲,线程2提交成功
线程池仍有空闲,线程1提交成功
线程池仍有空闲,线程3提交成功
线程池仍有空闲,线程4提交成功
线程0结束
线程3结束
线程1结束
线程池仍有空闲,线程6提交成功
线程2结束
线程池仍有空闲,线程5提交成功
线程4结束
线程池仍有空闲,线程8提交成功
线程池仍有空闲,线程9提交成功
线程池仍有空闲,线程7提交成功
线程8结束
线程6结束
线程5结束
线程9结束
线程7结束

你可能感兴趣的:(Java基础,java,后端)