重点知识学习(8.3)--[JUC常用类 || Java中的14把锁 || 对象头 || Synchronized 与 ReentrantLock]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重点知识学习(8.3)--[JUC常用类 || Java中的14把锁 || 对象头 || Synchronized 与 ReentrantLock]相关的知识,希望对你有一定的参考价值。

文章目录


1. JUC常用类

首先回顾一下;之前学习的集合知识;HashMapArrayList 线程不安全,HashTableVector 线程安全,但是仅适用于低并发,在并发量过高时,由于锁的粒度过大,HashTable与Vector就显得有点吃力,效率不行.

可以看看构造,这个HashTable,Vector属于那种全面加锁的独占锁;安全强度没得说,确实安全;

ConcurrentHashMap

多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段”机制(jdk8 弃用了分段锁,使用 cas+synchronized)替代 Hashtable 的独占锁。进而提高性能。

放弃分段锁,转而采用Node锁的原因:

  • 加入多个分段锁浪费内存空间。
  • 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  • jdk8 放弃了分段锁而是用了 Node 锁,减低锁的粒度,提高性能,并使用 CAS操作来确保 Node 操作的原子性,取代了锁。
    • put 时首先通过 hash 找到对应链表,查看是否是第一个 Node,如果是,直接用 cas 原则插入,无需加锁。如果不是链表第一个 Node, 则直接用链表第一个 Node 加锁,此处为 synchronized锁。

简易测试

public class ConcurrentHashMapDemo 
    public static void main(String[] args) 
        ConcurrentHashMap<String,Integer> chm = new ConcurrentHashMap<>();
        //模拟多个线程;
        for (int i = 0; i <50; i++) 
            new Thread(()->
                chm.put(Thread.currentThread().getName(), new Random().nextInt(100));
                System.out.println(chm);
            ).start();

        

        System.out.println("main方法执行");
    



CopyOnWriteArrayList

ArraayList 是线程不安全的,在高并发情况下可能会出现问题,
Vector 虽说线程安全,但是高并发情况下,读操作可能会远远大于写操作
它的读操作和写操作都是加锁的.

读操作根本不会修改原有的数据,读操作是线程安全的,若在每次读取都进行加锁操作,从资源利用率来看是极为差劲的。可以考虑让多个线程同时访问 List 的内部数据 。

JDK 中提供了 CopyOnWriteArrayList 类,将读取的性能发挥到极致,在读取数据时不用加锁,并且写入时不会阻塞读取操作只是写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。(写写互斥)

CopyOnWriteArrayList 类的可变操作(add,set.....)都是通过创建底层数组的新副本来实现的
当 List 需要被修改时,不是直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。
写操作执行完之后,将修改完的副本替换成原来的数据,即可保证保证写操作不影响读操作。

CopyOnWriteArrayList 类在读取操作时,并未加锁,
在写操作时,内部单独加锁,而且里面是采用了副本数据操作.

简易测试

public class CopyOnWriteArrayListDemo 
    public static void main(String[] args) 
        CopyOnWriteArrayList<String> clist = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 6; i++) 
            new Thread(()->
                clist.add(Thread.currentThread().getName());
                System.out.println(clist);
            ).start();
        
    



CopyOnWriteArraySet

看到CopyOnWriteArraySet,可不要被他迷惑了,它的底层实现还是基于CopyOnWriteArrayList类;
特性是不能存储重复数据而已.



辅助类 CountDownLatch

CountDownLatch 类使一个线程等待其他线程各自执行完毕后再执行。

通过计数器来实现,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为 0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

  • 使当前线程 等待其他线程执行结束后再执行;
  • 相当于线程计数器,递减的计数器,
  • 指定线程数量,当有一个线程执行结束后就减1;直到递减为0; 关闭计数器;当前线程才能执行;

测试执行

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class CountDownLatchDemo 
    public static void main(String[] args) throws InterruptedException 
        CountDownLatch cdl = new CountDownLatch(6);
        for (int i = 0; i <= 10; i++) 
            new Thread(()->
                System.out.println(Thread.currentThread().getName());
                cdl.countDown();
                System.out.println("当前计数:"+cdl.getCount()+"--------------------->");
            ).start();
        
        //使得当前的main线程等待;
        cdl.await();
        System.out.println("main方法执行");
    

当然,若没有线程执行,计数器无法递减到0时,当前线程无法执行;



辅助类 CyclicBarrier

CyclicBarrier 作为同步辅助类,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门.

采用了递增的计数器,当线程数量到达指定数量时,才能执行当前线程;

简易测试

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class CyclicBarrierDemo 
    public static void main(String[] args) throws BrokenBarrierException, InterruptedException 
        CyclicBarrier cb = new CyclicBarrier(6,new Thread(()->
            System.out.println("main方法执行");
        ));

        for (int i = 0; i < 6; i++) 
            new Thread(()->
                System.out.println(Thread.currentThread().getName());
                System.out.println(cb.getNumberWaiting()+"------------->");
                try 
                    cb.await();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                 catch (BrokenBarrierException e) 
                    e.printStackTrace();
                
            ).start();
        
    



2. Java中的14把锁

这些锁并不全是真正的锁;有的指锁的特性,有的指锁的设计,有的指锁的状态;


乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

2.1乐观锁:

认为对于同一个数据的并发操作,不会发生修改的。更新数据的时,会采用尝试更新,不断重新的方式更新数据。
乐观的认为,不加锁的并发操作是没有事情的。

简单来说就是不加锁,比如 CAS机制就是一种没有锁的方式实现(乐观锁)


2.2悲观锁:

认为对于同一个数据的并发操作,一定发生修改,哪怕没有修改,也认为修改。
对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

  • 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁可提升性能。
  • 悲观锁在 Java 中的使用,就是利用各种锁。
  • 乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,比如原子类,通过 CAS 自旋实现原子操作的更新。

2.3可重入锁(递归锁)

指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

ReentrantLockSynchronized就是可重入锁

案例

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class ReentrantDemo 
    synchronized void setA() throws Exception
        System.out.println("方法A执行");
        setB();
    

    synchronized void setB() throws Exception
        System.out.println("方法B执行");
    

2.4读写锁(ReadWriteLock)

读写锁的特性

  • 多个读操作状况下;可以同时进行读

  • 写操作互斥 ;(只允许一个写者写,也不能读者写者同时进行)

  • 写操作优先级高于读操作(出现写者,后面的读者必须等待,唤醒等待线程时优先考虑排队中的写者)

  • 读写,写读 ,写写的过程是互斥的

简易案例

public class ReadWriteDemo 
    //使用的共享数据;
    private int data;
    private ReadWriteLock rwl = new ReentrantReadWriteLock();

    //写操作;
    public void write(int data) 
        //读取写锁;
        rwl.writeLock().lock();
        try 
            System.out.println(Thread.currentThread().getName() + "预备写入数据-->");
            this.data = data;
            System.out.println(Thread.currentThread().getName() + "写入了" + this.data);
         finally 
            //手动释放写锁;
            rwl.writeLock().unlock();
        
    

    //读操作;
    public void get() 
        //获取读锁;
        rwl.readLock().lock();
        try 
            System.out.println(Thread.currentThread().getName() + "预备读取数据-->");
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);
         finally 
            //手动释放读锁;
            rwl.readLock().unlock();
        
    


2.5分段锁

分段锁是的思想,将数据分段,并在每个分段上都会单独加锁,把锁进一步细粒度化,以提高并发效率.


2.6自旋锁(SpinLock)

自旋其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到锁了就继续,要是抢不到就阻塞线程。

  • 采用自旋(循环重试)的方式进行尝试获取执行权. 不会让线程进入到阻塞的状态,

  • 自旋锁过于消耗 CPU,由于自身要不断的循环重试,不会释放 CPU资源。

  • 加锁时间普遍较短时,适合使用自旋锁,可以极大提高锁的效率。


2.7共享锁

当前锁可被多个线程所持有,并发访问共享资源;

例如ReadWriteLock读写锁的读取锁就是共享锁


2.8独占锁(互斥锁)

当前锁一次只能被一个线程持有。
ReentrantLock,Synchronized是独占锁;ReadWriteLock 的写锁是独占锁;


AQS(AbstractQueuedSynchronizer)

抽象队列同步;该类位于 java.util.concurrent.locks 包下;

其中定义了多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,例如 ReentrantLock;

核心思想:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS是用 CLH 队列锁(CLH 同步队列是一个 FIFO 双向队列,AQS 依赖它来完成同步状态的管理)实现的,即将暂时获取不到锁的线程加入到队列中。

基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。


2.9公平锁

分配锁前,检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程


2.10非公平锁

在分配锁时;不考虑线程排队等待的情况,直接尝试获取锁,在获取不到时再排到队尾等待;
或者说,释放锁之后,谁抢占到就是谁的;

  • synchronized就是非公平锁;
  • ReentrantLock 默认是非公平锁,但是可通过AQS机制,进行改变,公平锁或非公平锁一念之间.

2.11 锁的状态: 无锁 , 偏向锁 , 轻量级锁 , 重量级锁

  • 锁状态通过对象监视器在对象头中的字段来表明
  • 随着竞争的情况逐渐升级状态,不可逆的过程,即不可降级
  • 不是Java 语言中的真正的锁,而是Jvm 为了提高锁的获取与释放效率而做的优化(使用 synchronized 时)。

无锁: 就是不加锁的状态;

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

轻量级锁: 当前锁状态为偏向锁时, 有另一个线程跑来访问这把锁,偏向锁就升级为轻量级锁,不用担心,其他线程会通过自旋的形式尝试获取锁(CAS自旋),不会发生阻塞,提高性能。

重量级锁 : 当锁状态为轻量级锁时,另一个线程虽然在自旋,但是自旋次数太多了,它还是无法获取到锁,或者说是突然出现大量线程访问当前锁; 那么当前的锁状态升级为重量级锁 ; 注意这时的状态会令其他线程进入阻塞状态.


3.对象头

Hotspot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java 对象头是实现 synchronized 的锁对象的基础,一般而言,synchronized 使用的锁对象是存储在 Java 对象头里。

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在 32 位虚拟机中,1 个机器码等于 4 字节,也就是 32bit)

对象头信息:


4.Synchronized 与 ReentrantLock

在之前学习线程的时候,学习过Synchronized 与 ReentrantLock的基本使用;

Synchronized 是隐式锁, 自动获取释放, 可以修饰方法及代码块; 可重入锁,非公平锁.

ReentrantLock 是显式锁,需要手动获取释放锁,仅可以修饰代码块;可重入锁,公平锁/非公平锁.

Synchronized

在抛出异常,或者调用了wait()方法时,或者代码执行完毕时,就会释放锁.

Java 提供的一种原子性性内置锁,Java 每个对象都可以把它当做是监视器锁,线程代码执行在进入 synchronized 代码块时候会自动获取内部锁,这个时候其他线程访问时候会被阻塞,直到进入synchronized 中的代码执行完毕或者抛出异常或者调用了 wait 方法,都会释放锁资源。

在进入 synchronized会从主内存把变量读取到自己工作内存,在退出的时候会把工作内存的值写入到主内存,保证了原子性。

synchronized 基于进入和退出监视器对象来实现方法同步和代码块同步。
同步方法使用ACC_SYNCHRONIZED 标记是否为同步方法,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,该标记表明线程进入该方法时,需要 monitorenter,退出该方法时需要 monitorexit。

Synchronized 实现加锁 释放锁 是指令级别的, 基于进入和退出监视器对象来实现同步;

  • 过在对象头设置标记,达到了获取锁和释放锁的目的。
  • 当线程进入监视器 +1操作 ;对象头锁标记被使用;
    退出监视器 -1 操作;对象头锁标记被改为无锁.
    当计数器到达0时,关闭;

例如对某个用了synchronized修饰方法的类进行反编译;
javap -verbose 类......

例如进入退出对象监视器的状态;

  • 代码块的同步是利用 monitorenter 和 monitorexit 字节码指令
  • 在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁。
  • 当前线程拥有了这个对象的锁,把锁的计数器+1;
  • 当执行 monitorexit 指令时将计数器-1;
  • 当计数器为 0 时,锁被释放


ReentrantLock

在内部可设置状态,以及实时监测状态;它采用了CAS思想,维护了一个AQS队列;

  • 在类的内部自己维护了一个锁的状态, 一旦有线程抢占到了,将状态改为1,其他线程进入到队列中等待锁的释放,
  • 锁一旦释放了,那么就唤醒头结点; 开始尝试去获得锁.

可看lock()方法;

若当前有三个线程去竞争锁,假设线程 A 的 CAS 操作成功了,获得了锁,将锁状态 state 改为 1,那么线程 B 和 C 则设置状态失败。

  • 由于线程 A 已经占用了锁,所以 B 和 C 失败,并且入等待队列。
  • 如果线程 A 拿着锁死死不放,那么 B 和 C 就会被挂起。B 和 C 相继入队尝试获取锁。
  • 若当前线程的节点的前驱节点是 head,就有资格尝试获取锁。

unlock()方法;

尝试释放锁,若释放成功,那么查看头结点的状态,如果是则唤醒头结点的下个节点关联的线程。


以上是关于重点知识学习(8.3)--[JUC常用类 || Java中的14把锁 || 对象头 || Synchronized 与 ReentrantLock]的主要内容,如果未能解决你的问题,请参考以下文章

JUC包(java.util.concurrent)下的常用子类

Java基础学习总结(193)—— JUC 常用并发工具类总结

juc学习一(volatile关键字及原子变量)

JUC 常用 4 大并发工具类

JUC 中 4 个常用的并发工具类

JUC学习记录