并发编程专题 图灵

并发编程脑图

1.基础

进程与线程

什么是进程?
现代操作系统在运行一个程序时,会为其创建一个进程;例如,启动一个Java程序,操作系统就会创建一个Java进程。进程是OS(操作系统)资源分配的最小单位。
什么是线程?
线程是OS(操作系统)调度CPU的最小单元,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。CPU在这些线程上高速切换,让使用者感觉到这些线程在同时执行,即并发的概念,相似的概念还有并行!

2.深入理解Java内存模型(JMM)

JMM&volatile详解

什么是JMM模型

JMM是针对多线程的,JVM是针对GC的。
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。JMM是围绕原子性,有序性、可见性展开。

线程,工作内存,主内存工作交互图(基于JMM规范):
在这里插入图片描述
在这里插入图片描述

主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。
工作内存
工作内存线程独享的内存空间,里面的数据是从主内存考过来的副本。
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

并发编程的可见性,原子性与有序性问题

原子性:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
可见性:
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。

有序性:
有序性是指对于单线程的执行代码,代码的执行是按顺序依次执行的。

JMM如何解决原子性&可见性&有序性问题

原子性问题
除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
可见性问题
volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。如果不加volatile修改后别的线程也可以看到,但可能不会立即看到。

有序性问题
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述volatile关键字)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
Java内存模型:每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
下图为从源码到最终执行的指令序列示意图:

volatile

volatile内存语义
volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用
保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。

volatile的可见性

jmm缓存不一致性问题:
在这里插入图片描述
volatile可见性底层实现原理
在这里插入图片描述
关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中
示例:

public class VolatileVisibilitySample {
    volatile boolean initFlag = false;
    public void save(){
        this.initFlag = true;
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }
    public void load(){
        String threadname = Thread.currentThread().getName();
        while (!initFlag){
            //线程在此处空跑,等待initFlag状态改变
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
    }
    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.save();
        },"threadA");
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

volatile无法保证原子性

volatile可以被其他线程打断,无法保证原子性

//示例
public class VolatileVisibility {
    public static volatile int i =0;
    public static void increase(){
        i++;  //++底层是通过三条指令完成
    }
}

线程A在执行那i++(++底层是通过三条指令完成)操作时,假如此时执行到第二条指令,cpu资源被其他线程抢占,线程A暂停执行。此时线程B执行完++操作的三条指令,讲数据写回朱内存,此时系统硬件层面会通知线程A,主内存中i发生改变,立即从主内存中读取一次i值。线程A重新获取之内存中的i值后,此时获取cpu资源,继续指向++操作,但是发现i值发生可变化,就会抛弃本次++操作,从而导致少了一次++操作。数最终的值会小于等于预期的值。

也可以使用同步锁synchronized 或者 Lock保证++的原子性

volatile保证代码执行有序性

volatile禁止重排优化(指令重排是随机的根据和cpu指令集操作系统有关)
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。

非常典型的禁止重排优化的单例模式的例子,如下:

public class DoubleCheckLock {
    private volatile static DoubleCheckLock instance;
    private DoubleCheckLock(){}
    public static DoubleCheckLock getInstance(){
        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new  DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤2和步骤3间可能会重排序,如下:

memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象

在这里插入图片描述

3.JVM内置锁synchronized关键字

synchronized和ReentrantLock对比,jdk1.6后两者之间的性能差不多了
在这里插入图片描述

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。

  • 共享:资源可以由多个线程同时访问
  • 可变:资源可以在其生命周期内被修改
    引出的问题:

由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

如何解决线程并发安全问题?

实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

**Java 中,提供了两种方式来实现同步互斥访问:**synchronized Lock

同步器的本质就是加锁

加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)

不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。

synchronized原理详解

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁的方式:

1、同步实例方法,锁是当前实例对象

2、同步类方法,锁是当前类对象

3、同步代码块,锁是括号里面的对象

synchronized底层原理

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化, 如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。

Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

synchronized使用方法:

synchronized对象锁和全局锁

对象锁:只会锁住自己线程new的对象

锁普通方法
public synchronized void test() {}

锁当前对象
synchronized (this) {}

全局锁:锁的是类(所有线程new的对象)

锁静态方法
public static synchronized void test() {}

锁类
synchronized (A.class) {}
在这里插入图片描述

锁的膨胀升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。下图为锁的升级全过程:

锁升级等级:无锁态->偏向锁->轻量级锁->自旋锁->重量级锁

轻量级锁->自旋锁->重量级锁的锁升级:在锁状态为轻量级锁,另外一个线程在获取锁拿不到时,不会立即释放cpu资源而是通过一个while在哪里空循环,等待获取锁,如果在设定的循环次数内获取锁,就不会升级锁,如果获取不到才会去升级锁。因为锁竞争不激烈的时候,在那里自旋(不释放cp资源)等待获取锁,要比释放cpu资源等待唤醒的效率要高。

0

偏向锁(只有一个线程访问,同一个线程申请相同的锁)

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

默认开启偏向锁

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁(两线程访问及两个线程竞争同一个对象锁,但是交替执行(不同时执行及不会出现锁竞争)

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁(有竞争但不激烈:可以在自旋等待的次数内获取锁)

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。**这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,**因此自旋锁会假设在不久将来,当前的线程可以获得锁,**因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,**在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

逃逸分析(jvm对synchronized的优化)

new对象时,jvm通过逃逸分析,发现此对象的使用范围没有逃逸出本方法/只在一个线程中使用,就不会把创建的对象放在堆上,而是放在栈中,栈中对象随着方法退出而消除,可以减少推的GC。

使用逃逸分析,编译器可以对代码做如下优化:

一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

是不是所有的对象和数组都会在堆内存分配空间?

不一定

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析, -XX:+DoEscapeAnalysis : 表示开启逃逸分析 -XX:-DoEscapeAnalysis : 表示关闭逃逸分析。从jdk 1.7开始已经默认开启逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

锁消除

在这里插入图片描述

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。

锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析

:-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+EliminateLocks 表示开启锁消除。

**锁的粗化:**三个代码块都是对同一个对象加锁,如果代码连续调用三个方法,每次都要加锁再释放锁,编译器会通过锁的粗化,将三个锁的代码块放到一个代码块中

在这里插入图片描述

4.抽象队列同步器AQS应用Lock详解

Lock是AQS理论的落地应用

Java并发编程核心在于java.util.concurrent包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。

使用ReentrantLock进行同步
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
//业务逻辑
lock.unlock() //解锁

ReentrantLock实现四个核心原理

自旋+LocksSuport+CAS+队列

  • 自旋(自旋等待获取锁)
  • LocksSuport(自旋时释放cpu资源)
  • CAS(比较与交换,cas依赖硬件是原子操作,保证并发获取锁)
  • 队列(公平锁非公平锁)

ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized(AQS),所以这一个ReentrantLock同时具备公平与非公平特性。
上面主要涉及的设计模式:模板模式-子类根据需要做具体业务实现

除了Lock外,Java.util.concurrent当中同步器的实现如Latch,Barrier,BlockingQueue等,都是基于AQS框架实现

  • 一般通过定义内部类Sync继承AQS
  • 将同步器所有调用都映射到Sync对应的方法

AQS内部维护属性volatile int state (32位)

  • state表示资源的可用状态

AQS定义两种资源共享方式

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS定义两种队列

  • 同步等待队列
  • 条件等待队列

同步等待队列

AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
在这里插入图片描述
条件等待队列

Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁

在这里插入图片描述

5.抽象队列同步器AQS应用之阻塞队列BlockingQueue

概要

BlockingQueue底层基于CLH队列和条件队列实现。

**BlockingQueue(都是线程安全的队列),**是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以对队列进行入队或者出队操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的身影。

队列类型

  1. 无限队列 (unbounded queue ) - 几乎可以无限增长
  2. 有限队列 ( bounded queue ) - 定义了最大容量

队列数据结构

队列实质就是一种存储数据的结构

  • 通常用链表或者数组实现
  • 一般而言队列具备FIFO先进先出的特性,当然也有双端队列(Deque)优先级队列
  • 主要操作:入队(EnQueue)与出队(Dequeue)

在这里插入图片描述
常见的4种阻塞队列

  • ArrayBlockingQueue 由数组支持的有界队列
  • LinkedBlockingQueue 由链接节点支持的可选有界队列
  • PriorityBlockingQueue 由优先级堆支持的无界优先级队列
  • DelayQueue 由优先级堆支持的、基于时间的调度队列

ArrayBlockingQueue

队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好

队列创建:

BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>();

应用场景

在线程池中有比较多的应用,生产者消费者场景

工作原理

基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞

LinkedBlockingQueue

是一个基于链表的无界队列(理论上有界)

BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();

上面这段代码中,blockingQueue 的容量将设置为 Integer.MAX_VALUE 。

向无限队列添加元素的所有操作都将永远不会阻塞,[注意这里不是说不会加锁保证线程安全],因此它可以增长到非常大的容量。

使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。

DelayQueue

由优先级堆支持的、基于时间的调度队列,内部基于无界队列PriorityQueue实现,而无界队列基于数组的扩容实现。

队列创建:

BlockingQueue<String> blockingQueue = new DelayQueue();

要求

入队的对象必须要实现Delayed接口,而Delayed集成自Comparable接口

应用场景

电影票

工作原理:

队列内部会根据时间优先级进行排序。延迟类线程池周期执行。

BlockingQueue API

BlockingQueue 接口的所有方法可以分为两大类:负责向队列添加元素的方法和检索这些元素的方法。在队列满/空的情况下,来自这两个组的每个方法的行为都不同。

添加元素

方法说明
add()如果插入成功则返回 true,否则抛出 IllegalStateException 异常
put()将指定的元素插入队列,如果队列满了,那么会阻塞直到有空间插入
offer()如果插入成功则返回 true,否则返回 false
offer(E e, long timeout, TimeUnit unit)尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入

检索元素

方法说明
take()获取队列的头部元素并将其删除,如果队列为空,则阻塞并等待元素变为可用
poll(long timeout, TimeUnit unit)检索并删除队列的头部,如有必要,等待指定的等待时间以使元素可用,如果超时,则返回 null

在构建生产者 - 消费者程序时,这些方法是 BlockingQueue 接口中最重要的构建块。

6.并发编程之CountDownLatch&Semaphore应用与原理

线程间通信的桥/方式:

  • 阻塞队列BlockingQueue
  • CountDownLatch
  • Semaphore

Semaphore

  1. Semaphore 是什么?**

Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目,底层依赖AQS的状态State,是在生产当中比较常用的一个工具类。

2. 怎么使用 Semaphore?

2.1 构造方法

public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
  • permits 表示许可线程的数量
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

2.2 重要方法

public void acquire() throws InterruptedException
public void release()
tryAcquire(int args,long timeout, TimeUnit unit)
  • acquire() 表示阻塞并获取许可
  • release() 表示释放许可

2.3 基本使用

2.3.1 需求场景

资源访问,服务限流(Hystrix里限流就有基于信号量方式)。

**2.3.2 代码实现

public class SemaphoreRunner {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i=0;i<5;i++){
            new Thread(new Task(semaphore,"yangguo+"+i)).start();
        }
    }

    static class Task extends Thread{
        Semaphore semaphore;

        public Task(Semaphore semaphore,String tname){
            this.semaphore = semaphore;
            this.setName(tname);
        }

        public void run() {
            try {
                semaphore.acquire();               
                System.out.println(Thread.currentThread().getName()+":aquire() at time:"+System.currentTimeMillis());
                Thread.sleep(1000);
                semaphore.release();               
                System.out.println(Thread.currentThread().getName()+":aquire() at time:"+System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

打印结果:

Thread-3:aquire() at time:1563096128901

Thread-1:aquire() at time:1563096128901

Thread-1:aquire() at time:1563096129903

Thread-7:aquire() at time:1563096129903

Thread-5:aquire() at time:1563096129903

Thread-3:aquire() at time:1563096129903

Thread-7:aquire() at time:1563096130903

Thread-5:aquire() at time:1563096130903

Thread-9:aquire() at time:1563096130903

Thread-9:aquire() at time:1563096131903

从打印结果可以看出,一次只有两个线程执行 acquire(),只有线程进行 release() 方法后才会有别的线程执行 acquire()。

CountDownLatch

CountDownLatch使用及应用场景例子

CountDownLatch是什么?

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

使用场景:

Zookeeper分布式锁,Jmeter模拟高并发等

CountDownLatch如何工作?

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

API

CountDownLatch.countDown()
CountDownLatch.await();

CountDownLatch应用场景例子

比如陪媳妇去看病。

医院里边排队的人很多,如果一个人的话,要先看大夫,看完大夫再去排队交钱取药。

现在我们是双核,可以同时做这两个事(多线程)。

假设看大夫花3秒钟,排队交费取药花5秒钟。我们同时搞的话,5秒钟我们就能完成,然后一起回家(回到主线程)。

代码如下:

/**
 * 看大夫任务
 */
public class SeeDoctorTask implements Runnable {
    private CountDownLatch countDownLatch;

    public SeeDoctorTask(CountDownLatch countDownLatch){
        this.countDownLatch = countDownLatch;
    }

    public void run() {
        try {
            System.out.println("开始看医生");
            Thread.sleep(3000);
            System.out.println("看医生结束,准备离开病房");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (countDownLatch != null)
                countDownLatch.countDown();
        }
    }

}

/**
 * 排队的任务
 */
public class QueueTask implements Runnable {

    private CountDownLatch countDownLatch;

    public QueueTask(CountDownLatch countDownLatch){
        this.countDownLatch = countDownLatch;
    }
    public void run() {
        try {
            System.out.println("开始在医院药房排队买药....");
            Thread.sleep(5000);
            System.out.println("排队成功,可以开始缴费买药");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (countDownLatch != null)
                countDownLatch.countDown();
        }
    }
}

/**
 * 配媳妇去看病,轮到媳妇看大夫时
 * 我就开始去排队准备交钱了。
 */
public class CountDownLaunchRunner {

    public static void main(String[] args) throws InterruptedException {
        long now = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(new SeeDoctorTask(countDownLatch)).start();
        new Thread(new QueueTask(countDownLatch)).start();
        //等待线程池中的2个任务执行完毕,否则一直
        countDownLatch.await();
        System.out.println("over,回家 cost:"+(System.currentTimeMillis()-now));
    }
}

CyclicBarrier

运行的线程达到一定数量后,才可以执行。
与CountDownLaunch不同的是,可以多次使用:运行的线程达到一定数量后,A线程才可以执行,执行后CyclicBarrier的恢复,可以开始下一轮,反复使用。

栅栏屏障,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

API

​ cyclicBarrier.await();

应用场景

可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

示例代码:

7.Atomic类&Unsafe类

大部分场景Atomic类我们是用不到的。

原子操作

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。在多处理器上实现原子操作就变得有点复杂。本文让我们一起来聊一聊在Inter处理器和Java里是如何实现原子操作的。

Unsafe魔法类
在这里插入图片描述

AtomicInteger分析
    
do {
    oldvalue = this.getIntVolatile(var1, var2);//读AtomicInteger的value值
    ///valueOffset---value属性在对象内存当中的偏移量
} while(!this.compareAndSwapInt(AtomicInteger, valueOffset, oldvalue, oldvalue + 1));
return var5;

什么叫偏移量?
要用cas修改某个对象属性的值->,首先要知道属性在对象的内存空间的哪个位置,必须知道属性的偏移量

Dougli->Atomic原子包

atomic底层实现是基于无锁算法cas,里面的方法支持原子操作
在这里插入图片描述

Java当中如何实现原子操作**

在java中可以通过循环CAS的方式来实现原子操作。

JVM中的CAS操作正是利用了上文中提到的处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,具体的类可以参见juc下的atomic包内的原子类。

Atomic

在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。

**基本类:**AtomicInteger、AtomicLong、AtomicBoolean;

**引用类型:**AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference;

**数组类型:**AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

1、原子更新基本类型类

用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:

  • AtomicBoolean:原子更新布尔类型。
  • AtomicInteger:原子更新整型。
  • AtomicLong:原子更新长整型。

AtomicInteger的常用方法如下:

  • int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
  • boolean compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
  • void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的,Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。

**2、**原子更新数组类

通过原子的方式更新数组里的某个元素,Atomic包提供了以下三个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。

AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下

  • int addAndGet(int i, int delta):以原子方式将输入值与数组中索引i的元素相加。
  • boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

3、原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下三个类:

  • AtomicReference:原子更新引用类型。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

4、原子更新字段类

如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提供了以下三个类:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。

原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。

实现自定义类属性的原子操作

多线程可以下通过加锁方式改变类对象某个属相的值,但是太重,可以使用Atomic工具类无锁的cas方式实现。
在这里插入图片描述

对类对象的某个Intager属相原子修改

在这里插入图片描述
在这里插入图片描述
对类对象的某个非Intager属相原子修改
在这里插入图片描述

在这里插入图片描述

public class AtomicStudentAgeUpdater {
    private String name ;
    private volatile int age;

    public AtomicStudentAgeUpdater(String name,int age){
        this.name = name;
        this.age = age;
    }

    public int getAge(){
        return this.age;
    }

    public static void main(String[] args) {
        AtomicStudentAgeUpdater updater = new AtomicStudentAgeUpdater("杨过",18);

        System.out.println(ClassLayout.parseInstance(updater).toPrintable());

        updater.compareAndSwapAge(18,56);
        System.out.println("真实的杨过年龄---"+updater.getAge());
    }

    private static final Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicStudentAgeUpdater.class.getDeclaredField("age"));
            System.out.println("valueOffset:--->"+valueOffset);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void compareAndSwapAge(int old,int target){
        unsafe.compareAndSwapInt(this,valueOffset,old,target);
    }

}

对类对象的某个属相原子自增

public class AtomicIntegerFieldUpdateRunner {

    static AtomicIntegerFieldUpdater aifu = AtomicIntegerFieldUpdater.newUpdater(Student.class,"old");

    public static void main(String[] args) {
        Student stu = new Student("杨过",18);
        System.out.println(aifu.getAndIncrement(stu));
        System.out.println(aifu.getAndIncrement(stu));
        System.out.println(aifu.get(stu));
    }

    static class Student{
        private String name;
        public volatile int old;

        public Student(String name ,int old){
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }

}

CAS的ABA问题

t2号线程比t1号线程运行时间短,在他运行期间把原来的A改为B写会主内存,然后再从主内存取回B又改为A后又写回主内存。
t1号线程回来后,期望的和原来的一样,以为没有改变过,于是写回主内存。
但是中间有猫腻,2号线程已经把它改过了又改回去了。

aba问题,只是结果修改的过程问题(数值被A线程修改了,又改回了,B线程不知),没有线程不安全的问题,最终结果是正确的。
如果不在乎过程,只在乎最终结果可不用关心aba问题,直接使用Atomic的方法即可。

@Slf4j
public class AtomicAbaProblemRunner {
    static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        Thread main = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = atomicInteger.get();
                log.info("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
                if(isCasSuccess){
                    log.info("操作线程"+Thread.currentThread().getName()+"--Cas修改后操作数值:"+atomicInteger.get());
                }else{
                    log.info("CAS修改失败");
                }
            }
        },"主线程");

        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.incrementAndGet();// 1+1 = 2;
                log.info("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
                atomicInteger.decrementAndGet();// atomic-1 = 2-1;
                log.info("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
            }
        },"干扰线程");

        main.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        other.start();

    }
}

aba问题解决:(加一修改版本号,每修改一次加一次版本号)

//initialRef要改的初始值,initialStamp-初始版本号
AtomicStampedReference(V initialRef, int initialStamp)
public class AtomicStampedRerenceRunner {

    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);

    public static void main(String[] args){
        Thread main = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            System.out.println("操作线程" + Thread.currentThread()+ "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
            try {
                Thread.sleep(3000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+stamp + ",CAS操作结果: " + isCASSuccess);
        },"主操作线程");

        Thread other = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【increment】 ,值 a= "+ atomicStampedRef.getReference());
            stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
            System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 a= "+ atomicStampedRef.getReference());
        },"干扰线程");

        main.start();
        LockSupport.parkNanos(1000000);
        other.start();
    }
}

操作线程Thread[主操作线程,5,main]stamp=0,初始值 a = 1
操作线程Thread[干扰线程,5,main]stamp=1,【increment】 ,= 2
操作线程Thread[干扰线程,5,main]stamp=2,【decrement】 ,= 1
操作线程Thread[主操作线程,5,main]stamp=0,CAS操作结果: false

堆外内存

举个例子:文件上传,并发量也比较高;可以用unsafe申请堆外内存

堆外内存不属于GC管,注意用完一定要手动释放。否则内存泄露

public class AllocateMemoryAccess {

    public static void main(String[] args) {
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
        long oneHundred = 1193123491341341234L;
        byte size = 8;
        /*
         * 调用allocateMemory分配内存
         */
        long memoryAddress = unsafe.allocateMemory(size);
        System.out.println("address:->"+memoryAddress);
        /*
         * 将1写入到内存中
         */
        unsafe.putAddress(memoryAddress, oneHundred);
        /*
         * 内存中读取数据
         */
        long readValue = unsafe.getAddress(memoryAddress);

        System.out.println("value : " + readValue);

        //内存释放
        unsafe.freeMemory(memoryAddress);
    }

    //如果是在对象里申请内存可以在finalize中释放内存
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }
}

8.Collections之Map&List&Set详解

Map

HashMap

HashMap结构:
1.7-hashtable = 数组(基础) + 链表

(>=)1.8 = 数组 + 链表 + 红黑树

HashMap->数组的大小:

new HashMap();如果不写构造参数,默认大小16

如果说:写了初始容量:11 ?hashmap的容量就是11? 必须是2的幂次方。

/** * The default initial capacity - MUST be a power of two. */必须是2的指数幂?
如果设置size=11,
roundUpToPowerOf2(size),强型将非2的指数次幂的数值转化成2的指数次幂
怎么转化: 大于size且最接近size的2的指数次幂
1、必须最接近size,
2、必须大于=size,
3、是2的指数次幂

hashmap存储获取:

hashMap是懒加载,并不在new的时候创建对应的数组,而是在第一次put操作的时候。

hashmap的get,put操作时间复杂度O(1)

key.hashCode = 不确定 - 有符号的整型值

我们可以使用%(取模的方式定位要存储/获取数组对应的位置,key.hashCode % 16 = table.lenth = [0-15] = index = 3;)
但是hash并不是用取模计算index,而是用位运算(位运算是最接近机器语言的方式,其效率要大于取模)

array[index] = value;

hash碰:

数组所有的元素位是否能够100%被利用起来?

不一定,hash碰

引入链表结构解决hash冲突,采用头部插入链表法,链表时间复杂度O(n)

HashMap扩容:
当前hashmap存了多少element,size>=threshold
threshold扩容阈值 = capacity * 扩容阈值比率 0.75 = 16*0.75=12
扩容怎么扩?
数组扩容为原来的2倍, 16x2(满足2的幂次方)
在这里插入图片描述

16x0.75 =12,默认大于12,就会扩容。

在这里插入图片描述
数据迁移:

java7 数据+链表,多线程扩容可能出现链表成环,死锁问题

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) { 
                    e.hash = null == e.key ? 0 : hash(e.key);//再一次进行hash计算?
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

在这里插入图片描述
Jdk8-扩容
Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,,不用rehash采用高低位指针拆分转移方式,避免了出现链表成环,死锁问题。

假设链表在老的数组index(索引)位置为3, 老的数组容量为16

低位指针指向的链表,移到新的数组上的同样的index(3)位置

高位指针指向的链表,移到新的数组上的老的索引+老的数组容量(3+16)的位置

java8引入红黑树

链表转红黑树的条件:数组长度>=64且,链表长度>8

数组的长度<64, 而有一个链表成都大于8时,会先优先扩容,指导扩容到数组的长度大于等于64,才会转红黑树。

在这里插入图片描述

在这里插入图片描述

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {//hash函数,用于索引定位
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//存储数据Node没有初始化,此时初始化
    if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash用于定位,若为null,表明Node数组该位置没有Node对象,即没有碰撞
        tab[i] = newNode(hash, key, value, null);//对应位置添加Node对象
    else {//表明对应位置是有Node对象的,hash碰撞了
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//碰撞了,且桶中第一个节点就匹配
            e = p;//记录第一个节点
        else if (p instanceof TreeNode)//碰撞了,第一个节点没有匹配上,且桶为红黑树结构,调用红黑树结构方法添加映射
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//碰撞了 不为红黑树结构,那么是链表结构
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//如果到了链表尾端
                    p.next = newNode(hash, key, value, null);//链尾添加映射
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//链表长度大于TREEIFY_THRESHOLD值,转换为红黑树结构
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))//如果找到重复的key,判断该节点和要插入的元素key是否相等,如果相等,出循环
                    break;
                p = e;//为了遍历,和e = p.next结合来遍历
            }
        }
        if (e != null) { // existing mapping for key//key映射的节点不为空
            V oldValue = e.value;//取出节点值记录为老的节点值
            if (!onlyIfAbsent || oldValue == null)//如果onlyIfAbsent为false,或者老的节点值为null,赋予新的值
                e.value = value;
            afterNodeAccess(e);//访问后回调
            return oldValue;
        }
    }
    ++modCount;//结构性修改记录
    if (++size > threshold)//判断是否需要扩容
        resize();
    afterNodeInsertion(evict);//插入后回调
    return null;
}

put流程
在这里插入图片描述

Node
hash表中每个节点存储对象

static class Node<K,V> implements Map.Entry<K,V>{
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    //省略set、get等方法
}

concurrentHashMap

虽然在java8中对hashmap作了优化,避免了链表成环死锁的问题,但在多线程下还是建议concurrentHashMap
在这里插入图片描述

数据结构

ConcurrentHashMap的数据结构与HashMap基本类似
区别在于:
1、内部在数据写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作;
2、扩容时老数据的转移是并发执行的,这样扩容的效率更高。

ConcurrentHashMap也只是在put且发生hash碰撞时候才加同步锁,get的时候不加锁
java8加锁方式
在这里插入图片描述

并发安全控制

Java7 ConcurrentHashMap基于ReentrantLock实现分段锁,sements数据继承了ReentrantLock
锁的是一个hash表
在这里插入图片描述
在这里插入图片描述

Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现;
锁的仅是一个数组节点的链表,粒度更细
0
ConcurrentHashMap拥有出色的性能, 在真正掌握内部结构时, 先要掌握比较重要的成员:

  • LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh

  • TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。

  • UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值。

  • MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数。

  • MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容

  • TREEBIN, 置为-2, 代表此元素后接红黑树。

  • nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable上。

  • sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:

      • 0: table还没有被初始化
      • -1: table正在初始化
      • 小于-1: 实际值为resizeStamp(n)<
      • 大于0: 初始化完成后, 代表table最大存放元素的个数, 默认为0.75*n
  • transferIndex: table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标

  • ForwardingNode: 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就会帮着扩容。

扩容transfer
​ ConcurrentHashMap由三部分构成, table+链表+红黑树, 其中table是一个数组, 既然是数组, 必须要在使用时确定数组的大小, 当table存放的元素过多时, 就需要扩容, 以减少碰撞发生次数, 本文就讲解扩容的过程。扩容检查主要发生在插入元素(putVal())的过程:

扩容的整体步骤就是新建一个nextTab, size是之前的2倍, 将table上的非空元素迁移到nextTab上面去。

table扩容过程就是将table元素迁移到新的table上, 在元素迁移时, 可以并发完成, 加快了迁移速度, 同时不至于阻塞线程。所有元素迁移完成后, 旧的table直接丢失, 直接使用新的table。

主体步骤:

  • 一个线程插完元素后, 检查table使用率, 若超过阈值, 调用transfer进行扩容。
  • 一个线程插入数据时, 发现table对应元素的hash=MOVED, 那么调用helpTransfer()协助扩容。(线程2 put数据发现正在扩容,就会来协助扩容,如果上一个线程正在帮助扩容扩容部分为数组的(0-15), 则线程2就会帮忙扩容(16-31)

HashTable

也是用于并发,但hashTable锁的是整个数组,锁的粒度太粗,效率比较低
在这里插入图片描述

List

CopyOnWrite机制

如果使用读写锁(读读不互斥,读写互斥,写写互斥),在读写是也是同步的,影响效率。

CopyOnWriteArrayList

核心思想:读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。

划关键点:

1、CopyOnWrite适用于读多写少的情况,最大程度的提高读的效率;

2、CopyOnWrite是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;

3、如何使其他线程能够及时读到新的数据,需要使用volatile变量;

4、写的时候不能并发写,需要对写操作进行加锁;

缺点:
只能保证数据最终一致性
占用更多的内存资源(写时需要复制)

0

源码原理
写时复制

/*
 *   添加元素api
 */
public boolean add(E e) {
//复制副本往新的副本写时是枷锁的,不允许多个线程复制写。
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); //复制一个array副本
        newElements[len] = e; //往副本里写入
        setArray(newElements); //副本替换原本,成为新的原本
        return true;
    } finally {
        lock.unlock();
    }
}
//读的是原来的array,不是写锁里面的副本
public E get(int index) {
    return get(getArray(), index); //无锁
}

如果用在写多读少(就需要用读写锁实现了),不让当array特别大时,写时再拷贝,很容易造成频繁的GC
在这里插入图片描述

在这里插入图片描述

public class UseRwLock  implements GoodsService{

    private GoodsInfo goodsInfo;

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock getLock = lock.readLock();//读锁
    private final Lock setLock = lock.writeLock();//写锁

    public UseRwLock(GoodsInfo goodsInfo) {
        this.goodsInfo = goodsInfo;
    }

    @Override
    public GoodsInfo getNum() {
        getLock.lock();
        try{
            SleepTools.ms(5);
            return this.goodsInfo;
        }finally {
            getLock.unlock();
        }
    }

    @Override
    public void setNum(int number) {
        setLock.lock();
        try{
            SleepTools.ms(5);
            goodsInfo.changeNumber(number);
        }finally {
            setLock.unlock();
        }
    }
}

9.并发编程之Executor线程池原理与源码解读

线程

线程是调度CPU资源的最小单位,线程模型分为KLT模型与ULT模型,JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系,也就是说有一个java线程也会在操作系统里有一个对应的线程。Java线程有多种生命状态
NEW,新建

RUNNABLE,运行

BLOCKED,阻塞

WAITING,等待

TIMED_WAITING,超时等待

TERMINATED,终结

状态切换如下图所示:

等待状态和超时等待阻塞状态都是上下文切换。
在这里插入图片描述

线程的实现方式

Runnable,Thread,Callable
// 实现Runnable接口的类将被Thread执行,表示一个基本的任务
public interface Runnable {
// run方法就是它所有的内容,就是实际执行的任务
public abstract void run();
}
//Callable同样是任务,与Runnable接口的区别在于它接收泛型,同时它执行任务后带有返回内容
public interface Callable {
// 相对于run方法的带有返回值的call方法
V call() throws Exception;
}

线程池

“线程池”,顾名思义就是一个线程缓存,线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此Java中提供线程池对线程进行统一分配、调优和监控

线程池介绍

在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理。如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:

如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。

那么有没有一种办法使执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?

这就是线程池的目的了。线程池为线程生命周期的开销和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。

什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理的任务数量很大

线程池优势

  • 重用存在的线程,减少线程创建,消亡的开销,提高性能
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Executor框架

Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。

下图为它的继承与实现

0

从图中可以看出Executor下有一个重要子接口ExecutorService,其中定义了线程池的具体行为

1,execute(Runnable command):履行Ruannable类型的任务,

2,submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象

3,shutdown():在完成已提交的任务后封闭办事,不再接管新任务,

4,shutdownNow():停止所有正在履行的任务并封闭办事。

5,isTerminated():测试是否所有任务都履行完毕了。

6,isShutdown():测试是否该ExecutorService已被关闭。

线程池存在5种状态

RUNNING    = -1 << COUNT_BITS; //高3位为111
SHUTDOWN   =  0 << COUNT_BITS; //高3位为000
STOP       =  1 << COUNT_BITS; //高3位为001
TIDYING    =  2 << COUNT_BITS; //高3位为010
TERMINATED =  3 << COUNT_BITS; //高3位为011

1、RUNNING

(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

2、 SHUTDOWN

(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP

(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING

(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。

(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

进入TERMINATED的条件如下:

  • 线程池不是RUNNING状态;
  • 线程池状态不是TIDYING状态或TERMINATED状态;
  • 如果线程池状态是SHUTDOWN并且workerQueue为空;
  • workerCount为0;
  • 设置TIDYING状态成功。

0

线程池的具体实现

ThreadPoolExecutor 默认线程池

ScheduledThreadPoolExecutor 定时线程池

ThreadPoolExecutor

线程池的创建

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

在这里插入图片描述
最大线程数设置参考:

cpu密集型(cpu一直在计算,停顿很少 ) :cpu核数+1
IO密集型(cpu停顿等待比较多,读文件、网络请求都存在等待 ) :2cpu核数+1,或者线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) CPU数目 rocketmq,eureka,nacos 都是设置的2*cpu

最佳线程数 = cpu核数*[ 1+(io运算耗时 / cpu运算耗时)]

实际生产需要根据最终压测结果确定。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 5000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));

        for (int i=0;i<6;i++){
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("i m task :"+Thread.currentThread().getName());
                }
            },i);
        }

        threadPoolExecutor.shutdown();  //running->shutdown
        threadPoolExecutor.shutdownNow(); //running->stop

任务提交

1public void execute() //提交任务无返回值
2public Future<?> submit() //任务执行完成后有返回值

参数解释**

corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;

keepAliveTime

线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

unit

keepAliveTime的单位;

workQueue

用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:

  • 1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
  • 2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
  • 3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
  • 4、priorityBlockingQuene:具有优先级的无界阻塞队列;

threadFactory

它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

handler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

  • 1、AbortPolicy:直接抛出异常,默认策略;
  • 2、CallerRunsPolicy:用调用者所在的线程来执行任务(谁放进来的谁自己去执行);
  • 3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  • 4、DiscardPolicy:直接丢弃任务;

上面的4种策略都是ThreadPoolExecutor的内部类。

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

线程池监控

public long getTaskCount() //线程池已执行与未执行的任务总数
public long getCompletedTaskCount() //已完成的任务数
public int getPoolSize() //线程池当前的线程数
public int getActiveCount() //线程池中正在执行任务的线程数量

线程池原理

在和核心线程数未满时
在这里插入图片描述

execute方法执行流程如下:
0
在这里插入图片描述
源码分析

execute方法
在这里插入图片描述

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
/*
 * clt记录着runState和workerCount
 */
    int c = ctl.get();
/*
 * workerCountOf方法取出低29位的值,表示当前活动的线程数;
 * 如果当前活动线程数小于corePoolSize,则新建一个线程放入线程池中;
 * 并把任务添加到该线程中。
 */
    if (workerCountOf(c) < corePoolSize) {
/*
 * addWorker中的第二个参数表示限制添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断;
 * 如果为true,根据corePoolSize来判断;
 * 如果为false,则根据maximumPoolSize来判断
 */
        if (addWorker(command, true))
            return;
/*
 * 如果添加失败,则重新获取ctl值
 */
        c = ctl.get();
    }
/*
 * 如果当前线程池是运行状态并且任务添加到队列成功
 */
    if (isRunning(c) && workQueue.offer(command)) {
// 重新获取ctl值
        int recheck = ctl.get();
 // 再次判断线程池的运行状态,如果不是运行状态,由于之前已经把command添加到workQueue中了,
// 这时需要移除该command
// 执行过后通过handler使用拒绝策略对该任务进行处理,整个方法返回
        if (! isRunning(recheck) && remove(command))
            reject(command);
/*
 * 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法
 * 这里传入的参数表示:
 * 1. 第一个参数为null,表示在线程池中创建一个线程,但不去启动;
 * 2. 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断;
 * 如果判断workerCount大于0,则直接返回,在workQueue中新增的command会在将来的某个时刻被执行。
 */
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
/*
 * 如果执行到这里,有两种情况:
 * 1. 线程池已经不是RUNNING状态;
 * 2. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。
 * 这时,再次调用addWorker方法,但第二个参数传入为false,将线程池的有限线程数量的上限设置为maximumPoolSize;
 * 如果失败则拒绝该任务
 */
    else if (!addWorker(command, false))
        reject(command);
}

10.并发编程之定时任务&定时线程池

ScheduledThreadPoolExecutor
定时线程池类的类结构图

在这里插入图片描述
它用来处理延时任务或定时任务。
在这里插入图片描述
它接收SchduledFutureTask类型的任务,是线程池调度任务的最小单位,有三种提交任务的方式:

  1. schedule
    该方法是指任务在指定延迟时间到达后触发,只会执行一次。
    延迟delay执行一次,只执行一次
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit)
  1. scheduledAtFixedRate
    第一延迟initialDelay时间执行,之后周期 执行每隔period执行一次,
    如果有一次执行任务超过period时间,后面的应该周期执行的任务就会被堆积
   public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit)
  1. scheduledWithFixedDelay
    第一延迟initialDelay时间执行,之后在每次任务执行完成后延迟delay时间再执行下一次任务
   public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit)

**注意:**定时执行的任务一定要把任务中所有抛出的异常包住,否则定时任务就退出了再周期执行,定时线程池依然在,我们可以再往里面丢任务。

 private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

        scheduledExecutorService.scheduleAtFixedRate(
                () -> {
                    try {
                        execute();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }, configHelper.getInitialTime(), configHelper.getScheduledTime(), TimeUnit.SECONDS);
    }

java的Timer类(单线程的)主定时间或周期运行任务,但是如果任务抛出异常,timer定时线程也就直接挂掉了,因为定时任务线程是Timer类的一个成员属相,不可以在往里面丢任务。不推荐使用

    /**
     * The timer thread.
     */
    private final TimerThread thread = new TimerThread(queue);

11.14.编发编程之Future&ForkJoin框架原理分析

ForkJoin框架

ForkJoin 框架主要是针对cpu密集型。

ForkJoin 的核心思想是把一个大任务拆分成几个小任务,提高cpu的利用率。

Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork 就是把一个大任务切分为若干子任务并行的执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+…+10000,可以分割成 10 个子任务,每个子任务分别对 1000 个数进行求和,最终汇总这 10 个子任务的结果。如下图所示:

Fork/Jion特性:

  1. ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。(见 Java Tip: When to use ForkJoinPool vs ExecutorService )
  2. ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
  3. ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
    0

异步阻塞处理

ExecutorService executor = Executors.newFixedThreadPool(5);
@Slf4j
public class DoForkWork {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);

        Future<?> submit = executor.submit(() -> {
            log.info("this woker start do work");
            RestTemplate restTemplate = new RestTemplate();
            String forObject = restTemplate.getForObject("https://www.baidu.com/", String.class, new Object[]{});
            System.out.println(forObject);
            Thread.sleep(5000);
            return 1;
        });
        
        // 异步阻塞:处理其他业务逻辑

        try {
            System.out.println(submit.get(2,TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

        System.out.println("has got the result");
    }
}

1、异常处理**

ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的 getException 方法获取异常。示例如下

​ if(task.isCompletedAbnormally()){ System.out.println(task.getException()); }

getException 方法返回 Throwable 对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回 null。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值