JUC简介

JUC

一. 概述


  1. JUC指的是JDK1.5中提供的一套并发包及其子包:
    • java.util.concurrent
    • java.util.concurrent.lock
    • java.util.cncurrent.atomic
  2. 主要内容有:阻塞式队列、并发映射、锁、执行器服务、原子性操作。

二. 原子性操作


原子性操作实际上是保证了属性的原子性,底层是基于CAS+volatile来实现的

Ⅰ. 关于CAS

👉CAS

Ⅱ.关于volatile

volatile是java中的关键字之一,是Java中提供的用于保证线程通信间的轻量级通信机制。

  1. 特性:
    1. 保证线程的可见性。一个线程对主内存的数据做了改变,其他线程能够立即感知到这个改变。
    2. 对单个读/写具有原子性,但是复合操作除外,例如i++不保证线程的原子性。原子性指线程的执行过程不可拆分,换言之,线程在执行过程中不会中断。加锁就是为了保证原子性。
  2. 内存语义:
    1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
    2. 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
  3. 实施机制
    1. 禁止指令重排。指令没有按照预定顺序调用执行,而是在底层产生了所谓的优化,导致顺序发生了改变。指令重排不能违背happen-before原则。
    2. 内存屏障

三. LOCK锁


Ⅰ. 锁一些概念

  1. 锁的公平和非公平原则:

    公平锁:锁的获取顺序应该符合请求的绝对时间顺序,也就是FIFO。

    非公平锁:只要CAS设置同步状态成功,则表示当前线程获取了锁

    1. 在资源有限的情况下,线程之间实际执行的次数并不均等,这种现象称之为非公平原则。在公平策略下,线程不能直接抢占资源,而是抢占入队顺序。此时线程之间实际执行次数大致相等,我们称之为公平策略。
    2. 相对而言,非公平的效率更高(不需要考虑调度问题)
  2. 锁的独占和共享

    独占锁:独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。ReentrantLocksynchronized 都是独占锁

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

    独享锁与共享锁都是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享ReentrantReadWriteLock中读锁是共享锁,写锁是独占锁。读锁的共享可以保证并发读是高效的,读写,写读,写写是互斥的。

  3. 锁的重入和非重入

    可重入锁:可重入锁也叫做递归锁,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层递归函数B,也需要获取该锁L的代码时,在不释放锁L的情况下,可以重复获取该锁L。

    非重入锁:非可重入锁也叫做自旋锁,对比上面,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层递归函数B时,仍然有获取该锁L的代码,必须要先释放进入函数A的锁L,才可以获取进入函数B的锁L。

  4. 锁的乐观和悲观

    乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将乐观锁的核心算法是CAS,比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。(不加锁就修改)

    悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。(加锁才修改)

  5. 读写锁

    读锁:当线程获取读锁时,允许其他线程的读操作,不允许写操作。

    写锁:当线程获取读写时,不允许其他线程的任何操作。

  6. 自旋

    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

Ⅱ. ReentrantLock

JDK1.5增加了LOCK锁,可以通过显示定义同步锁对像实现同步,是对共享资源进行访问的工具。相比synchronized,LOCK更加精细灵活。唯一实现类:ReentrantLock
【特点】

  1. 可重入。(Synchronized同)
  2. 如果不指定,默认非公平。(Synchronized同)
  3. 独占(Synchronized同)
  4. 悲观(Synchronized同)
  5. 底层采用AQS实现。

【案例】

import java.util.concurrent.locks.ReentrantLock;

/**
 * 银行账户类
 * 此类为可变类,亦是线程不安全类。
 * 若想变为线程安全类,需付出额外的方法
 *
 * 此例中,需要将修改balance的方法同步
 * 若使用Synchronized同步,则锁是this
 *
 * 此例也可显示定义锁对象,来同步方法
 * 注意要显示的释放锁
 */
public class Account{
    private String accountNo;
    private double balance;
    //定义锁对象
    private final ReentrantLock lock=new ReentrantLock();


    public void draw(double drawAmount){
        //加锁
        lock.lock();
        try{
            if(drawAmount<balance){
                System.out.println("目前余额:"+balance);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance-=drawAmount;
                System.out.println("余额:"+balance);
            }
            else {
                System.out.println("余额不足,提款失败");
            }
        }finally {
            //修改完成,释放锁
            lock.unlock();
        }
    }
}

Ⅲ. ReadWriteLock

ReadWriteLock:读写锁。在使用的时候先创建ReentrantReadWriteLock,通过这个对象获取读锁或者写锁,之后再加锁解锁或者解锁。

相比ReadWriteLock,ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

【关于StampedLock】

StampedLock是Java8新增的锁,在绝大多数场景下可以替代传统的读写锁。其在提供读写锁的同时,还支持优化读模式。优化读基于假设:大多数情况下读操作并不会和写操作冲突,所以可以先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入了,则尝试获取读锁。

Ⅳ.Condition

ConditionObject是同步器AbstractQueuedSynchronizer的内部类 ,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也会是比较合理的。

每个Condition对象都包含着一个队列(等待队列),是Condition对象实现等待/通知功能的关键。

  • 等待队列是一个FIFO队列,队列的每个节点都包含一个线程引用, 线程就是在Condition对象中等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造节点加入等待队列进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的经静态内部类AbstractQueuedSynchronizer.Node

    一个Condition包含一个等待队列,Condition拥有首节点(fristWaiter)和尾节点(lastWriter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列

  • 调用Condition的await()方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态,当从await()方法返回时,当前线程一定获取了Condition相关联的锁,

    如果从队列 (同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

  • 调用Condition的signal()方法,将唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。

【案例】👉线程通信

Ⅴ. synchronized 和 ReentrantLock的区别

  1. synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
  2. synchronized不需要显示的定义锁和释放锁。
  3. 既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
    1. ReentrantLock可以对获取锁的等待时间进行超时设置,这样就避免了死锁。
    2. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
    3. 可以实现公平策略
    4. ReentrantLock可以获取各种锁的信息。
    5. ReentrantLock可以灵活地实现多路通知

四. BlockingQueue - 阻塞式队列


Ⅰ. 特点

  1. 满足队列特点:FIFO(First In First Out)

  2. 阻塞:如果队列为空,则试图获取元素的线程会被阻塞;如果队列已满,则试图放入元素的线程会被阻塞。

  3. 不允许元素为null(LinkedList允许)

  4. 重要方法

    抛出异常 返回特殊值 永久阻塞 定时阻塞
    添加 add - IllegalStateException offer - false put offer
    获取 remove - NoSuchElementException poll - null take poll

Ⅱ. 常用的实现类

  1. ArrayBlockingQueue阻塞式顺序队列
    1. 底层基于数组存储数据
    2. 使用的时候需要指定容量,不能扩容
    3. 在多线程环境下不保证“公平性”
    4. 实现:ReentrantLock+Condition
  2. LinkedBlockingQueue阻塞式锁式队列
    1. 底层基于节点来存储数据
    2. 在使用的时候可以指定容量也可以不指定。如果指定容量,则容量不可变;如果不指定容量,则容量默认为Integer.MAX_VALUE = 231-1不可变。因为实际开发中,一般不会在队列中存储21亿个元素,所以一般认为此时的容量是无限的
  3. PriorityBlockingQueue具有优先级的阻塞式队列:
    1. 底层基于节点来存储数据
    2. 使用的时候可以指定容量也可以不指定。如果不指定则默认初始容量是11
    3. PriorityBlockingQueue会对放入的元素来进行排序,默认情况下元素采用自然顺序升序排序,要求元素对应的类实现Comparable接口,覆盖compareTo方法指定比较规则。
  4. SynchronousQueue 同步队列
    1. 在使用的时候不需要指定容量,默认容量为1且只能为1
    2. 应用:交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务

另:BlockingDeque阻塞式双端队列

  1. 允许从两端放入/获取元素。
  2. 遵循阻塞特点,在使用的时候需要指定容量。

五. 并发映射

六. 执行器服务

posted @ 2020-07-12 20:36  仰观云  阅读(1515)  评论(0编辑  收藏  举报
Live2D