Java并发编程与高并发---JUC

基本概念

——进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
——线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

Thread

线程的状态

public enum State {
	NEW,(新建)
	RUNNABLE,(准备就绪)
	BLOCKED,(阻塞)
	WAITING,(不见不散)
	TIMED_WAITING,(过时不候)
	TERMINATED;(终结)
}

守护线程

守护线程是为其他线程服务的
垃圾回收线程就是守护线程~
守护线程有⼀个特点:当别的⽤户线程执⾏完了,虚拟机就会退出,守护线程也就会被停⽌掉了。
也就是说:守护线程作为⼀个服务线程,没有服务对象就没有必要继续运⾏了
使⽤线程的时候要注意的地⽅

  1. 在线程启动前设置为守护线程,⽅法是 setDaemon(boolean on)
  2. 使⽤守护线程不要访问共享资源(数据库、⽂件等),因为它可能会在任何时候就挂掉了。
  3. 守护线程中产⽣的新线程也是守护线程
  4. 启动之后设置守护线程会抛出异常

优先级线程

线程优先级⾼仅仅表示线程获取的CPU时间⽚的⼏率⾼,但这不是⼀个确定的因素!
线程的优先级是⾼度依赖于操作系统的
java的线程优先级是分为1最低,5默认,10最高
如果存在线程组,则优先级不能大于线程组的优先级

线程的生命周期

调⽤sleep⽅法会进⼊计时等待状态,等时间到了,进⼊的是就绪状态⽽并⾮是运⾏状态!
调⽤yield⽅法会先让别的线程执⾏,但是不确保真正让出
调⽤join⽅法,会等待该线程执⾏完毕后才执⾏别的线程~
Java并发编程与高并发---JUC_第1张图片

interrupt⽅法

线程中断在之前的版本有stop⽅法,但是被设置过时了。现在已经没有强制线程终⽌的⽅法了!由于stop⽅法可以让⼀个线程A终⽌掉另⼀个线程B,被终⽌的线程B会⽴即释放锁,这可能会让对象处于不⼀致的状态。
线程A也不知道线程B什么时候能够被终⽌掉,万⼀线程B还处理运⾏计算阶段,线程A调⽤stop⽅法将线程B终⽌,那就很⽆辜了~
总⽽⾔之,Stop⽅法太暴⼒了,不安全,所以被设置过时了。
我们⼀般使⽤的是interrupt来请求终⽌线程~
要注意的是:interrupt不会真正停⽌⼀个线程,它仅仅是给这个线程发了⼀个信号告诉它,它应该要结束了(明⽩这⼀点⾮常重要!)
也就是说:Java设计者实际上是想线程⾃⼰来终⽌,通过上⾯的信号,就可以判断处理什么业务了。
具体到底中断还是继续运⾏,应该由被通知的线程⾃⼰处理
设置中断标志的目的是想由被通知的线程自己处理,而这些方式都阻塞掉了。
被阻塞掉的线程调用中断方法是不合理的(不允许中断已经阻塞的线程)[因为可能会造成中断无效]

interrupt线程中断还有另外两个⽅法(检查该线程是否被中断):
静态⽅法interrupted()–>会清除中断标志位
实例⽅法isInterrupted()–>不会清除中断标志位

wait/sleep 的区别

  1. sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都 能调用。
  2. sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
  3. 它们都可以被 interrupted 方法中断。

管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

为什么需要CPU cache : CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu -> cache -> memory ) .
CPU cache有什么意义:
1)时间局部性︰如果某个数据被访问,那么在不久的将来它很可能被再次访问
2)空间局部性︰如果某个数据被访问,那么与它相邻的数据很快也可能被访问;

缓存协议
Java并发编程与高并发---JUC_第2张图片
M表示被修改 ,E表示独享 ,S表示共享 ,I表示无效的
local read 本地读
local write 本地写
remote read 读取内存数据
remote write 写内存数据
CPU 多级缓存–乱序执行优化 ----》指令重排
处理器为提高运算速度而做出违背代码原有顺序的优化。

Java内存模型(JMM)

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。
Java并发编程与高并发---JUC_第3张图片
为什么会推导出JMM模型呢?

  • 因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题
  • Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM
    数据同步八大原子操作
    lock(锁定)∶作用于主内存的变量,把一个变量标识为一条线程独占状态
    unlock(解锁)︰作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    read(读取)︰作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
    load(载入)︰作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
    use(使用)∶作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
    assign(赋值)︰作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
    store (存储)︰作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
    write (写入)∶作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中
    Java并发编程与高并发---JUC_第4张图片

JVMM规范下,多线程对变量的读写过程

  • 我们定义的所有的共享变量都存储在物理主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须先在自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

并发模拟工具

Postman:Http请求模拟工具
Apache Bench:Apache附带工具,测试网站性能
JMeter: 压力测试工具
代码:Semaphore CountDownLatch,Cyclingbairrir

线程安全性

定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的
JMM可能带来可见性、原子性和有序性问题。

原子性

原子性指一个操作是不可中断的,即多线程坏境下,操作不能被其他线程干扰

可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JVMM规定了所有的变量都存储在主内存中

导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新
    volatile解决可见性问题
    通过加入内存屏障和禁止重排序优化来实现
    对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

有序性

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
解决方法volatile、synchronized、Lock
happens-before原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的tart()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则∶线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

atomic包

接下来我们会去介绍18罗汉以及LongAdder底层实现原理
(1).基本类型原子类(AtomicInteger、AtomicBoolean、AtomicLong)
(2). 数组类型原子类(AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray)
(3). 引用类型原子类
(AtomicReference、AtomicStampedReference、AtomicMarkableReference)
(4)对象的属性修改原子类 (AtomicIntegerFieldUp
dater、AtomicLongFieldUpdater、AtomicRefere nceFieldUpdater)
(5).原子操作增强类(DoubleAccumulator 、DoubleAdder、LongAccumulator 、LongAdder)
(6). 第17位罗汉:Striped64 第18位罗汉: Number

AtomicInteger内部维护了volatile int value和private static final Unsafe unsafe两个比较重要的参数。如果是JDK8,推荐使用LongAdder对象,比AtomicLong 性能更好(减少乐观锁重试次数)

CAS算法

CAS是指Compare And Swap,比较并交换,是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

public final int getAnddAddInt(Object var1,long var2,int var4){
    int var5;
    do{
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

这个方法的var1和var2,就是根据对象偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

CAS缺点:
CAS实际上是一种自旋锁,

  • 一直循环,开销比较大。
  • 只能保证一个变量的原子操作,多个变量依然要加锁。
  • 引出了ABA问题–时间差

AtomicStampedReference解决ABA问题,维护了一个“版本号”Stamp时间戳

synchorized

synchronized 是 Java 中的关键字,是一种同步锁,子类继承后默认情况下不同步,如调用父类方法则相当于同步
synchronized 修饰对象的不同

  • 修饰代码块:大括号括起来的代码,作用于调用的对象
  • 修饰方法:整个方法,作用于调用的对象
  • 修饰静态方法:整个静态方法,作用于所有对象
  • 修饰类:括号括起来的部分,作用于所有对象
    锁的八个问题–
    ----所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
    ----具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
    建议:高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
class Ticket {
 	//票数
 	private int number = 30;
	 //操作方法:卖票
	 public synchronized void sale() {
		 //判断:是否有票
		 if(number > 0) {
		 System.out.println(Thread.currentThread().getName()+" : 
		"+(number--)+" "+number);
		 }
	 }
}

获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时 JVM 会让线程自动释放锁。

Lock

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。

public interface Lock {
	void lock();//用来获取锁。如果锁已被其他线程获取,则进行等待
	//发生异常的时候不会自动释放锁,一般是配合try finally使用
	void unlock();
	void lockInterruptibly() throws InterruptedException;//
	boolean tryLock();
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

	Condition newCondition();
}

new Condition方法
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通
知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类
也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以
进行选择性通知, Condition 比较常用的两个方法:
• await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
• signal()用于唤醒一个等待的线程。
==注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前
Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

class Share {
    private int number = 0;

    //创建Lock
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1
    public void incr() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (number != 0) {
                condition.await();//线程进入阻塞状态
            }
            //干活
            number++;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            //通知
            condition.signalAll();//唤醒其他线程,进入就绪状态
        }finally {
            //解锁
            lock.unlock();
        }
    }

    //-1
    public void decr() throws InterruptedException {
        lock.lock();
        try {
            while(number != 1) {
                condition.await();//条件放行
            }
            number--;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

Lock 和 synchronized 有以下几点不同:

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断
  4. 通过Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。

volatile

volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性有序性,但是不能保证原子性。

volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。

在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。

happes-before先行发生原则

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before关系

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(可见性,有序性)
  • 两个操作之间存在happens-before关系,并不意外着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(可以指令重排)

happens-before之8条规则

①. 次序规则

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作(强调的是一个线程)
前一个操作的结果可以被后续的操作获取。将白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1

②. 锁定规则

(一个unlock操作先行发生于后面((这里的"后面"是指时间上的先后))对同一个锁的lock操作(上一个线程unlock了,下一个线程才能获取到锁,进行lock))

③. volatile变量规则

(对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间是的先后)

④. 传递规则

(如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出A先行发生于操作C)

⑤. 线程启动规则(Thread Start Rule)

(Thread对象的start( )方法先行发生于线程的每一个动作)

⑥. 线程中断规则(Thread Interruption Rule)

对线程interrupt( )方法的调用先发生于被中断线程的代码检测到中断事件的发生
可以通过Thread.interrupted( )检测到是否发生中断

⑦. 线程终止规则(Thread Termination Rule)

(线程中的所有操作都先行发生于对此线程的终止检测)

⑧. 对象终结规则(Finalizer Rule)

(对象没有完成初始化之前,是不能调用finalized( )方法的 )

安全发布对象

安全发布方法

在静态初始化函数中初始化一个对象引用
将对象的引用保存到volatile类型域或者AtomicReference对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中

不可变对象

final关键字∶类、方法、变量
修饰类∶不能被继承
修饰方法:1、锁定方法不被继承类修改;2、效率
修饰变量:基本数据类型变量、引用类型变量
Collections.unmodifiableXXX : Collection、List、Set、Map…
Guava : ImmutableXXX : Collection、List、Set、Map

final关键字的使用

线程安全手段

ThreadLocal线程封闭

ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)
api介绍

  • ①. protected T initialValue​():initialValue():返回此线程局部变量的当前线程的"初始值"(对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法)
  • ②. static ThreadLocal withInitial​(Supplier supplier):创建线程局部变量
  • ③. T get​():返回当前线程的此线程局部变量的副本中的值
  • ④. void set​(T value):将当前线程的此线程局部变量的副本设置为指定的值
  • ⑤. void remove​():删除此线程局部变量的当前线程的值

源码解析

  • Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals =null的变量,这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内ThreadLocalMap,ThreadLocalMap中有Entry数组
  • 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
  • t.threadLocals = new ThreadLocalMap(this, firstValue)如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本

线程不安全类与写法

StringBuilder -> StringBuffer
SimpleDateFormat -> DateTimeFormatter
ArrayList,HashSet, HashMap 等Collections

线程安全–同步容器

Vector

Vector 是矢量队列,Vector 继承了 AbstractList,实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。 Vector 实现了RandmoAccess 接口,即提供了随机访问功能。
在每一个方法上都加了synchronized,所以它是线程安全的

Collections工具类的同步容器

Collections.synchronizedXXX (List、Set、Map)都是线程安全的

CopyOnWriteArrayList

它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和
ArrayList 不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作(读多写少),需要在遍历期间防止线程间的冲突。
  2. 它是线程安全的。
  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  4. 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

独占锁效率低:采用读写分离思想解决
写线程获取到锁,其他写线程阻塞
复制思想: 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。
这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份–>动态数组与线程安全

public class NotSafeDemo {
	/**
	* 多个线程同时对集合进行修改
	* @param args
	*/
	public static void main(String[] args) {
		List list = new CopyOnWriteArrayList();
		for (int i = 0; i < 100; i++) {
			new Thread(() ->{
				list.add(UUID.randomUUID().toString());
				System.out.println(list);
			}, "线程" + i).start();
		}
	}
}

“动态数组”机制

  • 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
  • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList
    效率很低;但是单单只是进行遍历查找的话,效率比较高。

线程安全”机制

  • 通过 volatile 和互斥锁来实现的。
  • 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看 到其它线程对该 volatile变量最后的写入;就这样,通过 volatile 提供了“读 取到的数据总是最新的”这个机制的保证。
  • 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”, 再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥 锁”,就达到了保护数据的目的。

线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:ArrayList ----- Vector,HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低
Collections 构建的线程安全集合
java.util.concurrent 并发包下: CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全

AQS及其他J.U.C组件

AbstractQueuedSynchronizer - AQS

AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态(private volatile int state),通过CAS完成对status值的修改(0表示没有,1表示阻塞)
Java并发编程与高并发---JUC_第5张图片

CountDownLatch–减少计数

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

  • CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {
        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);
        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
                //计数  -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}

Semaphore–信号灯

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线
程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方
法获得许可证,release 方法释放许可

public class SemaphoreDemo {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();//允许许可数量的线程进入,-1

                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");

                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();//许可证返回+1
                }
            },String.valueOf(i)).start();
        }
    }
}

Cyclicbarrier–循环栅栏

CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

public class CyclicBarrierDemo {
    //创建固定值
    private static final int NUMBER = 7;
    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("*****集齐7颗龙珠就可以召唤神龙");//达到目标障碍数执行
                });
        //集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                    //等待
                    cyclicBarrier.await();//计数加一
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

ReentrantLock

ReentrantLock–可重入锁又叫递归锁,指的同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁的代码块。可重入锁可以避免死锁的问题
synchronized隐性锁—ReentrantLock显式锁

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenteri时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

ReadWriteLock接口

public interface ReadWriteLock {
	 /**
	 获取读锁
	 */ 
	 Lock readLock();
	 /**
	 获取写锁
	 */
	 Lock writeLock();
}

说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。
它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
线程进入读锁的前提条件:

  • 没有其他线程的写锁
  • 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。线程进入写锁的前提条件:
  • • 没有其他线程的读锁
  • • 没有其他线程的写锁

而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

class MyCache {
    //创建map集合
    private volatile Map<String,Object> map = new HashMap<>();

    //创建读写锁对象
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    //放数据
    public void put(String key,Object value) {
        //添加写锁
        rwLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+" 写完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            rwLock.writeLock().unlock();
        }
    }

    //取数据
    public Object get(String key) {
        //添加读锁
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 取完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        //创建线程放数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.put(num+"",num+"");
            },String.valueOf(i)).start();
        }

        TimeUnit.MICROSECONDS.sleep(300);

        //创建线程取数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.get(num+"");
            },String.valueOf(i)).start();
        }
    }
}
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
    原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

Callable&Future 接口

Callable

Callable接口的特点

  • 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于 Callable,需要实现在完成时返回结果的call()方法。
  • call()方法可以引发异常,而 run()则不能。
  • 为实现 Callable 而必须重写 call 方法
  • 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable
class MyThread2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		return 200;
	}
}

Future接口

当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future 对象。

将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable 返回)。Future 基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。

  • public boolean cancel(boolean mayInterrupt):用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为true时才会中断任务。
  • public Object get()抛出InterruptedException,ExecutionException:用于获取任务的结果。如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
  • public boolean isDone():如果任务完成,则返回 true,否则返回 false

Futuretask类

Java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建Thread 对象。因此,间接地使用 Callable 创建线程。

  • 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
  • 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
  • 一旦计算完成,就不能再重新开始或取消计算
  • get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
  • get 只计算一次,因此 get 方法放到最后
public class CallableDemo {
	 /**
	 * 实现 runnable 接口
	 */
	 static class MyThread1 implements Runnable{
		 /**
		 * run 方法
		 */
		 @Override
		 public void run() {
			 try { 
			 	System.out.println(Thread.currentThread().getName() + "线程进入了 run方法");
			 }catch (Exception e){
			 	e.printStackTrace();
			 }
		}
	}
	 /**
	 * 实现 callable 接口
	 */
	 static class MyThread2 implements Callable{
	 /**
	 * call 方法
	 * @return
	 * @throws Exception
	 */
	 @Override
	 public Long call() throws Exception {
	 	try {
			 System.out.println(Thread.currentThread().getName() + "线程进入了 call方法,开始准备睡觉");
			 Thread.sleep(1000);
			 System.out.println(Thread.currentThread().getName() + "睡醒了");
		}catch (Exception e){
			 e.printStackTrace();
 		}
		return System.currentTimeMillis();
 	}
 }
	 public static void main(String[] args) throws Exception{
		 //声明 runable
		 Runnable runable = new MyThread1();
		 //声明 callable
		 Callable callable = new MyThread2();
		 //future-callable
		 FutureTask<Long> futureTask2 = new FutureTask(callable);
		 //线程二
		 new Thread(futureTask2, "线程二").start();
		 for (int i = 0; i < 10; i++) {
			 Long result1 = futureTask2.get();
			 System.out.println(result1);
		 }
		 //线程一
		 new Thread(runable,"线程一").start();
	 }
}

Fork/Join框架

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:

  • Fork:把一个复杂任务进行分拆,大事化小
  • Join:把分拆任务的结果进行合并

CompletableFuture

  • 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法
  • 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作
  • 它实现了Future和CompletionStage接口
    Java并发编程与高并发---JUC_第6张图片
public class CompletableFutureTest2 {
    public static void main(String[] args)throws Exception {
        /**
         1.当一个线程依赖另一个线程时,可以使用thenApply()方法来把这两个线程串行化(第二个任务依赖第一个任务的结果)
         public  CompletableFuture thenApply(Function fn)
         2.它可以处理正常的计算结果,或者异常情况
         public CompletableFuture whenComplete(BiConsumer action)
         3.异常的处理操作
         public CompletableFuture exceptionally(Function fn)
         */
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(2);  } catch (InterruptedException e) {e.printStackTrace();}
            return 1;
        }).thenApply(result -> {
            return result+3;
        }).whenComplete((v,e)->{
            if(e==null){
                System.out.println(Thread.currentThread().getName()+"\t"+"result = " + v);
            }
        }).exceptionally(e->{
            e.printStackTrace();
            return null;
        });
        System.out.println(Thread.currentThread().getName()+"\t"+"over...");
        //主线程不要立即结束,否则CompletableFuture默认使用的线程池会立即关闭,暂停几秒
        try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
    }
}

四种创建方式
runAsync方法不支持返回值,有无指定线程池
supplyAsync可以支持返回值,有无指定线程池

Java并发编程与高并发---JUC_第7张图片

BlockingQueue–阻塞队列

阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素

当队列是空的,从队列中获取元素的操作将会被阻塞

当队列是满的,从队列中添加元素的操作将会被阻塞

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素

试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

常见队列有两种:FIFO,LIFO
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
Java并发编程与高并发---JUC_第8张图片
Java并发编程与高并发---JUC_第9张图片
常见的 BlockingQueue
ArrayBlockingQueue-- 由数组结构组成的有界阻塞队列

在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

LinkedBlockingQueue–>由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

DelayQueue—使用优先级队列实现的延迟无界阻塞队列

DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

PriorityBlockingQueue—支持优先级排序的无界阻塞队列

PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁

SynchronousQueue–不存储元素的阻塞队列,也即单个元素的队列
SynchronousQueue一种无缓冲的等待队列–直接消费
声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:

  • 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞 多余的生产者和消费者,从而体系整体的公平策略;
  • 非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平 锁,同时配合一个 LIFO队列来管理多余的生产者和消费者,而后一种模式, 如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

LinkedTransferQueue-- 由链表组成的无界阻塞队列
LinkedTransferQueue 采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为 null)入队

LinkedBlockingDeque—由链表组成的双向阻塞队列
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,
这里的阻塞一般有两种情况:

  • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时 再讲该元素插入该操作可以通过设置超时参数,超时后返回false 表示操作 失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException 异 常
  • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可 以通过设置超时参数

线程池

线程池简介

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
new Thread弊端:

  • 每次new Thread新建对象,性能差
  • 线程缺乏统一管理,可能无限制的刺连线性:可能占用过多系统资源导致死机或OOM
  • 缺少更多功能,如更多执行、定期执行、线程中断
    线程池的好处
  • 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
Java并发编程与高并发---JUC_第10张图片

ThreadPoolExecutor详解

线程池的状态:内部变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息。
其中高3位表示"线程池状态",低29位表示"线程池中的任冬数"

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

线程的状态:

  • RUNNING:线程池能够接受新任务,以及对新添加的任务进⾏处理。
  • SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进⾏处理。
  • STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
  • TIDYING:当所有的任务已终⽌,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执⾏钩⼦函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若⽤户想在线程池变为TIDYING时,进⾏相应的处理;可以通过重载terminated()函数来实现。
  • TERMINATED:线程池彻底终⽌的状态。
    Java并发编程与高并发---JUC_第11张图片
    Java并发编程与高并发---JUC_第12张图片
    线程池构造方法的参数:
  • corePoolSize:核心线程数量
  • maximumPoolSize:线程最大线程数
  • workQueue :阻塞队列,存储等待执行的任务,很重要
  • 线程池运行过程产生重大影响
  • keepAliveTime:线程没有任务执行时最多保持多久时间终止
  • unit : keepAliveTime的时间单位
  • threadFactory :线程工厂,用来创建线程
  • rejectHandler :当拒绝处理任务时的策略

线程数量要点:

  • 如果运⾏线程的数量少于核⼼线程数量,则创建新的线程处理请求
  • 如果运⾏线程的数量⼤于核⼼线程数量,⼩于最⼤线程数量,则当队列满的时候才创建新的线程
  • 如果核⼼线程数量等于最⼤线程数量,那么将创建固定⼤⼩的连接池
  • 如果设置了最⼤线程数量为⽆穷,那么允许线程池适合任意的并发数量

线程空闲时间要点:当前线程数⼤于核⼼线程数,如果空闲时间已经超过了,那该线程会销毁。
排队策略要点:

  • 同步移交:不会放到队列中,⽽是等待线程执⾏它。如果当前线程没有执⾏,很可能会新开⼀个线程执⾏。
  • ⽆界限策略:如果核⼼线程都在⼯作,该线程会放到队列中。所以线程数不会超过核⼼线程数
  • 有界限策略:可以避免资源耗尽,但是⼀定程度上减低了吞吐量

当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了:

拒绝策略(重点)

CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy: 直接丢弃,其他啥都没有
DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

线程池的种类与创建

Executors.newCachedThreadPool
⾮常有弹性的线程池,对于新的任务,如果此时线程池⾥没有空闲线程,线程池会毫不犹豫的创建⼀条新的线程去处理这个任务。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

线程池中数量没有固定,可达到最大值int,重复利用和回收,如没有,会重新创建线程

场景: 适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景

Executors.newFixedThreadPool
⼀个固定线程数的线程池,它将返回⼀个corePoolSize和maximumPoolSize相等的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景

Executors.newScheduledThreadPool

 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

(1)线程池中具有指定数量的线程,即便是空线程也将保留
(2)可定时或者延迟执行线程活动
场景: 适用于需要多个后台线程执行周期任务的场景

Executors.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景

Executors.newWorkStealingPool

jdk1.8 提供的线程池,底层使用的是 ForkJoinPool 实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 核数的线程来并行执行任务
场景: 适用于大耗时,可并行执行的场景

线程池底层工作原理

  1. 在创建了线程池后,线程池中的线程数为零
  2. 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
    2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
    4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

实际际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池(线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式—因为会堆积大量请求)

死锁

在Java中使⽤多线程,就会有可能导致死锁问题。死锁会让程序⼀直卡住,不再程序往下执⾏。我们只能通过中⽌并重启的⽅式来让程序重新执⾏。
造成死锁的原因可以概括成三句话:当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃⾃⼰拥有的资源
四个必要条件

  • 互斥条件
  • 请求和保持条件
  • 不剥夺条件
  • 环路等待条件

发⽣死锁的原因主要由于:
线程之间交错执⾏,解决:以固定的顺序加锁。
执⾏某⽅法时就需要持有锁,且不释放,解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
永久等待,解决:使⽤ tryLock() 定时锁,超过时限则返回错误信息

死锁编码和定位
主要是两个命令配合起来使用,定位死锁。
jps指令:jps -l可以查看运行的Java进程。
jstack指令:jstack pid可以查看某个Java进程的堆栈信息,同时分析出死锁。

多线程并发最佳实践

  • 使用本地变量
  • 使用不可变类
  • 最小化锁的作用域范围:S=1/(1-a+a/n)
  • 使用线程池的Executor,而不是直接new Thread执行
  • 宁可使用同步也不要使用线程的wait和notify
  • 使用BlockingQueue实现生产-消费模式
  • 使用并发集合而不是加了锁的同步集合
  • 使用Semaphore创建有界的访问
  • 宁可使用同步代码块,也不使用同步的方法
  • 避免使用静态变量

Spring与线程安全

Spring bean : singleton.prototype
无状态对象

多线程中的虚假唤醒

出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了

	if(num != 0){
		this.wait();
	}
	改为	
	while(num != 0){
		this.wait();
	}

你可能感兴趣的:(笔记,java)