jvm内存模型:
JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,
缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。
内存模型解决并发问题主要采用两种方式:
限制处理器优化
使用内存屏障
计算机内存模型,是解决多线程场景下并发问题的一个重要规范
可见性: Java 内存模型是通过在变量修改后将新值同步回主内存 在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的
Volatile 关键字提供了一个功能
有序性:可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性
Volatile 关键字会禁止指令重排 Synchronized 关键字保证同一时刻只允许一条线程操作。
Java 内存模型(JMM) 是一种规范 是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
Java 内存模型的作用 目的是保证并发编程场景中的原子性、可见性和有序性。
Java 中内存模型做了:Synchronized Volatile
伪缓存:
很少人会关注系统硬件及 JVM 底层相关的影响因素
高性能异步处理框架 Disruptor:最快的消息框架 其LMAX 架构能够在一个线程里每秒处理 6百万 订单
Collections:集合的包装类!提供静态方法辅助操作集合,动态给集合添加数据,重点:可以将一些线程不安全的类转换成线程安全的!
Collections.synchronizedMap(new HashMap());
-----------------------------------------------------------
1.UnSafe unsafe = Unsafe.getUnsafe(); //java封装操作内存的类
2.Runtime.getRuntime():
// java钩子:addShutdownHook()注册一个新的虚拟机关机挂钩。
// 一个关机钩子只是一个初始化但未启动的线程
// 当虚拟机开始关闭序列时,它将以一些未指定的顺序启动所有注册的关闭挂钩,并让它们同时运行
// 关机钩子在虚拟机的生命周期中的微妙时间运行,因此应该进行防御性编码。 特别是应该写成线程安全的,并尽可能避免死锁
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
//当虚拟机即将关闭的时候,会调用次线程
System.out.println(Thread.currentThread().getName());
}, "t2"));
System.out.println("asdf");
//这里写了一个无线循环,所以就算main线程已经销毁了,次线程还在执行,上面的虚拟机关闭钩子线程程序就不会执行!
new Thread(()->{while(true){}}).start();
}
// CPU的核心数
Runtime.getRuntime().availableProcessors();
----------------------------------------------
应用程序:可执行的软件 在一个应用程序中都会有进程
进程:
是正在运行的应用程序,是系统进行资源分配和调度的基本单位,是线程的集合,线程是独立运行的执行任务提高程序效率
进程是系统进行资源分配和调度的基本单位。他是程序的数据集合
进程是这些某个程序,一个操作系统有多个进程,一个进程有多个线程
进程中有多个不同的执行路径
在进程中,一定有一个主线程
多线程:
并发、大人物、定时任务、阻塞
多线程执行互补影响独立运行。线程就绪状态抢cpu的运行权
多线程代码就不再是顺序执行。而是各个线程交替执行
相当于开启一个线程,开启一个代码执行路径
同步:
代码(从上往下)顺序执行(单线程),我执行完了其他再执行,一次只执行一个线程
异步:
多线程同时执行,每个线程独立执行,不按主线程的顺序去等待执行
线程安全问题:
保证多个线程对共享数据操作的:原子性、可见性、有序性
volatile:
使用volatile关键修饰属性,修改属性的值,可以同步到其他线程。
不修饰其他线程修改了值不能同步,但是volatile不具备原子性。比如:当多个线程对一个数据进行a++的时候,有可能一个线程获取a的值,但是另一个线程a的值计算+完后,还没有一赋值。这就导致数据计算问题
注:线程在运行的时候,显示获取变量的副本值,然后再计算,这样当变量的值放生变化了,另外一个线程可能还是缓存里面的值。所有当线程都需要公用的属性需要加volatile,使更改了数据后。另外一个线程可以及时更新数据。
可见性:不具备原子性! 比如:一个线程在修改数据,一个线程读取数据,共享数据是volatile可见的,但还是保证不了值的原子性.
因为:因为我读取共享的数据有多条。而多条数据的【必须是一体的】光是可见性还不行。因为我读取了第一条值,然后读取了第二条值,这时候第二个值被修改了.那么数据就是问题数据
就算是可见性,那么也会读第二个数据的修改后的值。这样数据就没有原子性。出现的脏读!所有不保证数据的原子性
----------------------------
重点:
1.重排序:
为什么会发生重排序:编译器将代码翻译成汇编指令,给cpu执行,要在不改变程序执行结果的情况下,尽可能提高并行度,JMM同样遵守,所以在编译期间单线程数据操作,没有依赖关系,那么有可能会重排序(相当于是优化指令)
多线程之间的数据依赖不在之类,所以有可能会发生重排序.
>>举个例子:现在一个cup4核心8线程,那么指令要提高执行效率遵循并行原则。现在有两个线程A,B
int a=0;
boolean flag=false;
public void haha1(){
a=1;
flag=true;
}
public void haha2(){
if(flag){
int i=a*a;
System.out.println(i);
}
}
>> A线程执行haha1方法,B线程执行haha2方法,如果A线程先执行,在B线程执行,这样也使我们想要的结果。
但是cpu是多核心的,指令优化因为多个线程没有依赖关系,可能会发生重排序(提高程序并行度,提高cup使用效率),所有上面代码会有多重结果: 1.会输出1 2.上面都不会输出
所有有些共享数据,需要使用volatile关键字,使数据有依赖关系禁止重排序,同时也增加了可见性!
>>当代码中存在控制依赖行时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。
以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当操作3的条件判断为真的时候,就把该结算结果写入到变量i中
1. 并行度:数据并行执行的最大数目,同时执行多条指令称为指令并行
在计算机体系结构中,并行度是指指令或数据并行执行的最大数目。在指令流水中,同时执行多条指令称为指令并行
提高并行度的一个显著优点就是可以最大限度地利用计算资源或存储资源。
2. Busy spin: (忙循环)
线程不释放cpu的情况下等待事件技术
用于避免 使用wait等等让线程暂停后重新进入另外一个cpu核心执行,导致之前cup缓存数据丢失的技术
工作中如果要求延迟降到最低,并且线程是没有如何顺序的,这样就可以通过循环检测队列中的消息来代替sleep()或者wait(),它的唯一好处就是只需要等待很短的时间 如几微妙几纳秒
框架:LMAX(Lmax)分布式框架是一个高性能线程间通讯的库。
BusySpinWaitStrategy类:基于这个该类的实现类。使用busy spin 循环EventProcessors(时间处理器)等屏障
3.Thread Dump文件: 线程转储/线程导出
诊断Java应用问题的工具
用于下载线程的记录备份文件,比如服务器线程出现慢的问题,或者线程出现异常等问题,需要日志信息查看
每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力,虽然各个 Java虚拟机打印的thread dump略有不同,
但是大多都提供了当前活动线程的快照,及JVM中所有Java线程的堆栈跟踪信息,堆栈信息一般包含完整的类名及所执行的方法,如果可能的话还有源代码的行数。
Thread Dump特点:
1. 能在各种操作系统下使用
2. 能在各种Java应用服务器下使用
3. 可以在生产环境下使用而不影响系统的性能
4. 可以将问题直接定位到应用程序的代码行上
Thread Dump 能诊断的问题:
1. 查找内存泄露,常见的是程序里load大量的数据到缓存;
2. 发现死锁线程
何抓取Thread Dump:
一般当服务器挂起,崩溃或者性能底下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析. 在实际运行中,往往一次 dump的信息,
还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做threaddump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性
注:一个线程类只能.start()启动一次
每个线程都有一个用于识别目的的名称。 多个线程可能具有相同的名称
Thread.currentThread() 获取当前线程的信息
Thread.currentThread().getName()
synchronized 重点学习:
1:synchronized修饰实例方法称之为对象锁,修饰静态方法称之为类锁。
Thread 线程API:
静态方法:
Thread.currentThread(): 返回对当前正在执行的线程对象的引用。
Thread.activeCount(): 获得当前ThreadGroup(线程组)中活动线程的估计
//获取当前线程的线程组的所有线程对象,放到数组当中
Thread[] thread2= new Thread[10];
Thread.currentThread().getThreadGroup().enumerate(thread2);//将线程组的线程放到数组中
守护线程:
守护线程又称为任务线程
被守护的线程执行完毕销毁,那么他的守护线程同样销毁(不管有没有你执行完任务都销毁)
1.通过 thread对象的t8.setDaemon(true);方法设置我这个线程是当前执行线程的守护线程
2.t8.isDaemon() 判断此线程是否是守护线程
注意:设置为守护线程的时候,必须在线程初始化未开启(为使用start方法)的时候设置。如果线程已经运行start那么设置守护线程会抛异常
非守护线程:
线程分:进程的主线程、用户线程、守护线程
主线程和用户线程就是非守护线程
用户线程可以使用 setDaemon()方法设置为某个执行线程的守护线程
线程池:管理线程
消息中间件:
Mq 保证数据的一致性,并且可以保证线程的容错率 比如:批处理消息,某个线程对某个消息发送失败,要重试这种问题
线程安全:
1.线程不安全问题:当多个线程共享操作同一个全局变量,做写入或者修改的时候,会受到其他线程的干扰,导致数据问题,这个现象就叫做线程安全
2.线程安全:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况
同步synchronized: 注:同步的阻塞,线程是就绪状态。sleep当时间结束后线程变就绪状态,wiat当唤醒后变成就绪状态,join当被让的线程执行完销毁后边就绪状态
1.同步代码块 1.this(当前对象锁) 2.全局变量对象(对象锁) 3.静态的全局对象变量,就是该类的全部对象一个锁 4.类.class 当前类的class对象锁
2.同步方法 1.只有this当前对象锁 2.静态方法是当前类的Class对象锁
3.静态同步方法
注意:多个线程执行同一个对象的函数,函数里有同步方法和同步块:如果同步块使用的是this锁,那么同步块和同步方法是[同一把锁],如果同步块使用的是其他对象所,那么[不是一把锁]
同步方法:wait() notifyAll() 多个同步方法,多个线程来执行,如果对象是一样的,在A同步方法里使用notifyAll可以唤醒B方法的线程。
注:也就是说锁(同步监视器)的监视对象,在同步方法里面,任何线程执行了notifyAll,都是唤醒当前对象监视的所有线程
同理:当同一个对象,多个线程执行当前对象不一样的同步的方法,也只能是一个一个的执行,A方法有线程在执行了,要执行B方法的线程一样被锁住进入阻塞,等待其他线程调用notifyAll
多线程死锁现象:
一般出现在出现在嵌套锁的时候。当一个同步的方法,里面有同步块,同步块和同步方法不是一个锁。如果一个线程先拿到同步代码块的锁,而一个线程有得到了方法this锁。那么这样就死锁了。要注意!
多线程的三大特效:
原子性(保持代码的原子性)、
可见性(比如volatile关键字,让修改了数据可以及时让其他变量可见)、
有序性(比如说同步让线程有序,join、wait、notifyAll、synchronized、ReentrantLock类对象锁(手动锁))
java内存模型:
缓存空间 栈
主存 堆
// 创建一个锁的对象,实例变量:表示同一个实例一个锁;静态修饰:所有创建的对象公用一个锁
Lock reentrantLock = new ReentrantLock()
reentrantLock.lock();
reentrantLock.unlock();
线程中断:
interrupt() isInterrupted() interrupted()
1.interrupt() 设置线程为中断状态,值设置为true
2.判断中断线程:isInterrupted()为实例方法 、interrupted()为Thread的静态方法
3.线程就绪、执行状态为false,中止状态为false
4.线程中止和stop强行中止不同,他是用标记的方式让线程不再执行某些代码
5.线程[已经]被标记为中断(true)状态:1.该线程执行sleep、wait、join这三个阻塞方法将会[抛异常],同时将线程中断状态复原设为false
--注意:不管是synchronizid还是lock锁的线程通讯 wait\await,当在此状态,线程中断操作interrupt()都会唤醒此线程(想当于调用了notify/signal)方法,但注意:唤醒后线程抛出InterruptedException中断异常,
并且中断操作复原(线程就绪false,中断true,调用interrupt中断线程就是将中断值设置为true,线程异常复原后恢复false)
如果我们只用自己的 boolean类型变量来停止,那么线程在阻塞的时候是停不了线程的。使用interrupt可以唤醒线程并抛出异常,我们可以在catch里面去做处理,比如跳出循环
// -------------------------------------------------------
ThreadLocal:线程全局的私有变量
给每个线程设置【全局的私有变量】。每个线程都会有一个变量的副本,每个线程都保存对其线程局部变量副本的隐式引用;
线程消失后,线程本地实例的所有副本都将被垃圾收集(除非存在对这些副本的其他引用)。一般可以用来做每个线程的事物Id或者做用户id
// 使用ThreadLocal类给每个线程一个全局的私有变量
// .initialValue() 变量的初始值方法
注:initialValue方法可以不用重写。不重写默认值都是null
----------------------
----并发编程----- synchronized 包括ReentrantLock
-----------------------程序同步:是代码从上到下有顺序的执行
---------------------- 线程同步:保证多线程执行共享数据的原子性(执行修改数据的时候,所有修改的数据必须是一个整体,否则其他线程会出现脏读)、可见性、有序性
----------------------
java1.5带的并发包:java.util.concurrent.atomic包里面都是原子性的类,可以用于数学计算保证数据不出问题
java.util.concurrent.atomic:一个小型工具包,支持单个变量上的无锁线程安全编程。
1.AtomicInteger int原子更新的值
AtomicInteger atomicInteger = new AtomicInteger(int初始值,默认不传入冲0开始);
// .decrementAndGet() 原子减1后,返回当前值
// .getAndDecrement() 返回当前原子的值,然后减1
// .getAndIncrement() 返回当前原子的值,然后加1
// .incrementAndGet() 原子加1后,返回当前值
// .get() 原子当前值
UnSafe: 实现直接操作内存, sun.misc.UnSafe,该类有引导类的加载器加载,其他类使用不了,除非配置类的加载方式,然后通过反射调用
重要的作用:1.线程调度:线程挂起/恢复、获取/释放锁
2.系统相关:返回内存页大小,返回系统指针大小
3.内存相关:分配、拷贝、扩充、释放堆外内存,获取/设置给定内存地址中的值
4.CAS
5.Class相关:检测确保类的初始化、获取field(字段)的内存地址偏移量
线程相关:
// 阻塞线程
public native void park(boolean isAbsolute, long time);
// 取消阻塞线程
public native void unpark(Object thread);
// 获得对象锁(可重入锁)
public native void monitorEnter(Object o);
// 释放对象锁
public native void monitorExit(Object o);
// 尝试获取对象锁
public native boolean tryMonitorEnter(Object o);
//CAS算法锁
unsafe.compareAndSwapInt(this, stateOffset, expect, update)
1.ReentrantLock: 手动锁-可重入锁 ,用UnSafe类 park方法实现线程挂起,unpark实现中止一个挂起线程使其恢复正常
里面用的就是UnSafe类,实现线程挂起,将一个线程进行挂起是通过park方法实现的,调用 park后,
线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。
整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本park方法,但最终都调用了Unsafe.park()方法。
整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本park方法,但最终都调用了Unsafe.park()方法。
1.1:Condition: 可以有序的单个通知唤醒某个指定的线程,在一个Lock对象里可以创建多个condition实例,线程对象可以注册在指定的condition中从而选择性的进行线程通知,在调度线程上更加灵活
在使用notify()/notifuAll()方法进行通知时,被调度的线程却是由JVM随机选择的。但使用ReentrantLock结合condition类是可以实现上面讲的“选择性通知”,
这个功能是非常重要的,而且在condition类中默认提供的
synchronize就相当于整个Lock对象中只有一个单一的condition对象,所有的线程都注册在它一个对象上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权
2.synchronized:可重入锁
同步块执行原理:
每个对象都有同步监视器锁对象(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权
从synchronized修饰的地方开始:汇编指令开始monitorenter --到-- monitorexit结束,两个指令
注:1.每个对象有一个监视器锁(monitor)当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权
.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
.当前线程获取了monitor之后,会增加这个monitor的时间计数,来记录当前线程占用了monitor多长时间。
.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
2.执行monitorexit的线程,必须是objectref(将栈顶数值)所对应的monitor的所有者
.执行monitorexit指令时: monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权
同步方法:方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现)
1.执行同步方法时其常量池中多了ACC_SYNCHRONIZED标示符 JVM就是根据该标示符来实现方法的同步的
2.当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置
3.如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor
4.在方法执行期间,其他任何线程都无法再获得同一个monitor对象, 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成
Lock/synchronized区别:
相同点:都是使用同步监视器Monitor对象,当执行到同步块或者同步方法时,会通过monitorenter尝试获取monitor的所有权
区别:
1.实现机制:synchronized和Lock使用和底层实现方式不一样...
2.使用:synchronized调度不灵活,比如: 在使用notify()/notifuAll()方法进行通知时,被调度的线程却是由JVM随机选择的
Lock调度灵活, 比如:使用ReentrantLock结合Condition类是可以实现选择性通知:一个Lock对象里可以创建多个condition实例,线程对象可以注册在指定的condition中从而选择性的进行线程通知,在调度线程上更加灵活
//创建Lock监视器和对应的Condition通讯对象
final Lock lock = new ReentrantLock();
final Condition condition11 = lock.newCondition();
final Condition condition22 = lock.newCondition();
condition11.await 阻塞线程释放锁
注意:
condition11.signal() --唤醒相同监视器下当前使用condition11的单线程
condition22.signalAll() --唤醒相同监视器下当前使用condition11的所有线程
java.long.ThreadLock:用于线程
并发包:
java.util.concurrent.atomic //原子包 比如AtomicInteger 一个int类型的数据cas算法自增自减操作
1.AtomicReference //原子更新一个对象的引用
AtomicReference<Thread> aomicReference = new AtomicReference<Thread>();//创建一个AtomicReference对象
Student ss=new Student();
aomicReference.compareAndSet(null, ss);//比较更新:第一次aomicReference里面没有对象所有为null,那么于预期值一样,所有对象ss会更新到aomicReference中存着
aomicReference.get(); //获取到存放在aomicReference里面的对象
2.AtomicInteger //原子更新int的值,用于自增或者自减1,一个int类型的数据cas算法自增自减操作
java.util.concurrent.condition.locks //lock相关的都在并发包 比如:ReentrantLock类 Condition接口
1.java.util.concurrent.ConcurrentHashMap CAS+synchronized
1.CAS关键操:(用于节点的操作) 作获取table数组中索引为i的Node(节点)元素 ,CAS操作设置table数组中索引为i的元素
2.synchronized操作:用于对节点的map进行增加修改值
3.get方法查询先不加锁,当查询到null值,重新加锁再查询,因为有可能在查询的时候其他线程有删除操作,所以加锁查询(保证线程安全)
注意点:因为查询第一次未加锁,所以如果有这样的操作:比如,获取一个key的value,如果不存在(null),设置默认值(1),如果存在获取vlaue,在value的基础上操作(+1),然后将操作的值重新设置进入key
多线程下这样操作会出问题:因为多线程在第一次查询某个key的时候,会同时为null,后面线程没有在前一个线程的value上增加(+1),导致数据不准确。
所以:对应获取key的value然后再value的原基础上进行累计叠加操作,需要用Lock或者synchronized加锁!!!!
注:HashMap\Hashtable 底层实现一样,Hashtable所有方法都是synchronized,底层实现都是:【数组+链表+红黑树】通过Hash算法来计算数据的下标位置存放值
如果两个不同的key计算的hashcode一样,那么存放在同一个数组下标里面:查询的时候,通过key的hashcode获得对应的下标,获得对应位置的值,如果值只有一个返回值
如果值有多个,但是是链表的形式,那么通过循环去equals比较,返回相等的值!当数组一个下标里面的值超过了7(差不多6还是7)个转为红黑树存储(key是不能重复的,可以用数结构)
然后在红黑树结构去查询对应的建对应的值(红黑树数据结构是有序的,查询是二分算法非常快)
2.java.util.concurrent.CountDownLatch 用于:线程阻塞,等待其他线程计数(-1)到=0的时候自动唤醒执行,线程执行计数(-1),
指定一个数字,线程执行一次countDownLatch.countDown()就-1,主线程调用了countDownLatch.await()和join一样阻塞,当计数器>0,自动唤醒
注:CountDownLatch任何线程只要是使用同一个对象,就是在一起计数-1。也可以多个计数的线程使用await(),==0的时候都是自动唤醒
CountDownLatch countDownLatch = new CountDownLatch(2); 计数从2开始,使用一次countDownLatch.countDown()就-1,当到0的时候,唤醒通过当前对象使用await方法的所有线程
3.java.util.concurrent.CyclicBarrier 用于:允许一组线程全部等待彼此达到共同屏障点的同步辅助
多个线程等待然后在同一时刻执行。在多个线程全部执行await等待,执行一次await()计数器就自动+1,等计数器达到指定的值,那么所有wait的线程就全部自动唤醒
注:CyclicBarrier任何线程只要是使用同一个对象,就是在一起计数使用await()+1,达到自定义值的时候都是自动唤醒
// 创建一个 CyclicBarrier对象(计数同步)
CyclicBarrier cyclicBarrier = new CyclicBarrier(2); 计数器从0开始,调用一次await()就+1,到2之后,自动唤醒线程
4.java.util.concurrent.Semaphore 计数信号量(相当于线程调度的信号):[信号量通常用于限制线程数]二进制信号量具有属性(与许多Lock实现不同),“锁”可以由[除所有者之外]的线程释放(因为信号量没有所有权概念)。 这在某些专门的上下文中是有用的,例如死锁恢复。
//创建Semaphore信号量对象,设置最大的信号证书数。
private static Semaphore semaphore = new Semaphore(5);
// 获取信号认证
semaphore.acquire();
// 释放型号认证
semaphore.release();
集合接口Collection--AbstractCollection--ConcurrentLinkedDeque(是双向链表结构的无界并发队列) jdk1.7加入的并发队列
5.java.util.concurrent.ConcurrentLinkedDeque 是双向链表结构的无界并发队列,无界不阻塞线程安全的双队列,适用于并发场景下的队列
队列的头部是队列中最长的元素(先),队列的尾部是队列中最短时间的元素(后),新元素插入队列的尾部,队列检索操作获取队列头部的元素。先进先出原则。
注:双向链表结构的无界并发队列 ,一般用于消息处理或者说(只要是关于生产和消费的)
concurrentLinkedDeque.offer("编号" + i);// 向队列尾部插入数据
concurrentLinkedDeque.offerFirst("");//.offerFirst()前面插入指定的元素,2.offerLast(E e):在此deque的末尾插入指定的元素
addFirst(E e):在此deque前面插入指定的元素。
add(E e):在此deque的尾部插入指定的元素
ConcurrentLinkedDeque.addAll(Collection接口)尾部插入集合
getFirst():检索,但不删除,这个deque的第一个元素。
getLast():检索,但不删除,这个deque的最后一个元素。
poll():检索并删除由此deque表示的队列的头部(换句话说,该deque的第一个元素),如果此deque为空,则返回 null 。
pollFirst():检索并删除此deque的第一个元素,如果此deque为空,则返回 null 。
pollLast():检索并删除此deque的最后一个元素,如果此deque为空,则返回 null 。
一、ConcurrentLinkedDeque介绍
ConcurrentLinkedDeque 是双向链表结构的无界并发队列。从JDK 7开始加入到J.U.C的行列中。使用CAS实现并发安全,
ConcurrentLinkedDeque与ConcurrentLinkedQueue 的区别是该阻塞队列同时支持FIFO(先进先出)和FILO(栈.先进后出)两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。
适合“多生产,多消费”的场景。内存一致性遵循对 ConcurrentLinkedDeque 的插入操作先行发生于(happen-before)访问或移除操作。
相较于 ConcurrentLinkedQueue,ConcurrentLinkedDeque 由于是双端队列,所以在操作和概念上会更加复杂。
注意:size方法不是一个准确的操作
ConcurrentLinkedDeque的一些方法介绍
1、add(E e):在此deque的尾部插入指定的元素,返回值为Boolean。
2、addFirst(E e):在此deque前面插入指定的元素。
3、addLast(E e):在此deque的末尾插入指定的元素。
4、clear():从这个deque中删除所有的元素。
5、contains(Object o):返回 true如果这个deque包含至少一个元素 e ,返回值为Boolean。
6、descendingIterator():以相反的顺序返回此deque中的元素的迭代器,返回值为Iterator。
7、element():检索但不删除由此deque表示的队列的头部(换句话说,该deque的第一个元素)。
8、getFirst():检索,但不删除,这个deque的第一个元素。
9、getLast():检索,但不删除,这个deque的最后一个元素。
10、isEmpty():如果此集合不包含元素,则返回 true 。
11、iterator():以正确的顺序返回此deque中的元素的迭代器,返回值为Iterator 。
12、offer(E e):在此deque的尾部插入指定的元素,返回值为boolean。
13、offerFirst(E e):在此deque前面插入指定的元素,返回值为boolean。
14、offerLast(E e):在此deque的末尾插入指定的元素,返回值为boolean。
15、peek():检索但不删除由此deque表示的队列的头(换句话说,该deque的第一个元素),如果此deque为空,则返回 null 。
16、peekFirst():检索但不删除此deque的第一个元素,如果此deque为空,则返回 null 。
17、peekLast():检索但不删除此deque的最后一个元素,如果此deque为空,则返回 null 。
18、poll():检索并删除由此deque表示的队列的头部(换句话说,该deque的第一个元素),如果此deque为空,则返回 null 。
19、pollFirst():检索并删除此deque的第一个元素,如果此deque为空,则返回 null 。
20、pollLast():检索并删除此deque的最后一个元素,如果此deque为空,则返回 null 。
21、pop():从这个deque表示的堆栈中弹出一个元素。
22、push(E e):将元素推送到由此deque代表的堆栈(换句话说,在该deque的头部),如果可以立即执行,而不违反容量限制,则抛出 IllegalStateException如果当前没有可用空间)。
23、remove():检索并删除由此deque表示的队列的头(换句话说,该deque的第一个元素)。
24、remove(Object o):删除第一个元素 e ,使 o.equals(e) ,如果这样一个元素存在于这个deque,返回值为boolean。
25、removeFirst():检索并删除此deque的第一个元素。
26、removeFirstOccurrence(Object o):删除第一个元素 e ,使 o.equals(e) ,如果这样一个元素存在于这个deque,返回值为boolean。
27、removeLast():检索并删除此deque的最后一个元素。
28、size():返回此deque中的元素数。
集合接口Collection--BlockingQueue接口--
6.java.util.concurrent.BlockingQueue[接口] 阻塞有界队列
实现类一:java.util.concurrent.ArrayBlockingQueue --有界阻塞式队列数组(使用数组实现的队列)创建阻塞式的数组,这个队列排列元素FIFO(先进先出)
添加和查询:当这个数组的数据满了的时候阻塞,当满了被取数据后下标从0开始,(存数据是存最后一个,取数据是从第一个开始取)数据是先进先出的方式获取数据。循环获取
注意:数组列表的形式,此类没有默认的构造方法,【创建对象必须设置有界的最大值】当然如果要用它,希望是无限量的话可以使用int的最大值(Integer.MAX_VALUE)
ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<Integer>(5);// 创建一个ArrayBlockintQueue 有界阻塞数组列表队列
boolean bool = arrayBlockingQueue.offer(i, 2, TimeUnit.SECONDS);// ArrayBlockintQueue将数据添加到尾部,超时的时间,TimeUnit.SECONDS:超时时间为[秒] true如果添加成功 false为过了超时时间没添加
// 当队列数据已满,则等待设置的超时时间,在此时间类如果队列有空位置,则插入数据,否则此时间过了,没有空位置,则数据不添加
arrayBlockingQueue.offer(""); // 添加数据到尾部,如果队列已满返回true(数据不添加)
add(E e) //添加数据,如果队列数据已满返回true 并抛出异常
put(E e) //插入元素,如果队列已满,线程阻塞,等待队列有空位置,自动唤醒线程添加数据 注意:此方法可以非常简单实现生产者与消费者
arrayBlockingQueue.poll(超时时间,超时什么时间);//指定超时时间,列表没数据,在等待的时间结束后还是没数据返回null
arrayBlockingQueue.poll();//检索并删除此队列的头,如果此队列为空,则返回 null 。
arrayBlockingQueue.take();//获取第一个数据,当没有数据的时候阻塞,直到有数据
注意:因为生产者生产不过来,所有获得的是null,注.使用arrayBlockingQueue.take()方法获取数据,如果队列为null没有数据,线程阻塞,直到有数据!!!!
实现类二: LinkedBlockingQueue :【有界阻塞式】【双向队列链表】 注意:此对象有默认的构造方法,表示容量最大值为int的长度( Integer.MAX_VALUE )
-------------------------------------------------------------------------------------------------
线程池的核心:1.提高线程的响应速度、2.线程的重复利用、3.线程的管理
java.util.concurrent.Executor(线程池的顶级接口): 提供了四种创建线程池的方式
基本接口设计:
1.
一级Executor接口--- 二级ExecutorService接口 --- 三级.AbstractExecutorService抽象类 三级.ScheduledExecutorService接口 --- 四级.核心实现类ThreadPoolExecutor
四级.ForkJoinPool 分治线程池类(递归分治 并行执行 工作窃取) --五级.ScheduledThreadPoolExecutor定时线程任务的线程池类(继承了ThreadPoolExecutor)
注: 1.ThreadPoolExecutor类:线程池创建的主类!并发包提供的封装好的所有线程池,都是通过ThreadPoolExecutor类(构造方法)创建的,然后将次对象传入给对应的线程池构造方法
2.Executors类:封装好的创建线程池的辅助类,提供获得创建4中线程池的静态方法,方法都是调用ThreadPoolExecutor类(构造方法)创建的
比如此类实现创建线程池的实现:return newCachedThreadPool(new ThreadPoolExecutor());
4种线程池:
注:Executors为java为我们封装创建线程池的工厂类,通过他创建线程池
1.高速缓存的线程池(Cached):Executor.newCachedThreadPool();
2.可固定长读线程池(Fixed):Executor.newFixedThreadpool(3);
3.可定时的线程池: Executors.newScheduledThreadPool(3)
// 创建ScheduledExecutorService的延迟任务线程池
ScheduledExecutorService es = Executors.newScheduledThreadPool(3);
// 提交延迟线程任务:1.Runnable 2.延迟时间 3.延迟时间单位
es.schedule(new ThreadPoolExecutorText()::run, 10, TimeUnit.SECONDS);//10秒后执行次任务
4.单线程线程池: Executors.newSingLeThreadScheduleExecutor 始终只有一个线程在执行,所有提交的任务都是在等另一个任务执行完了,线程在执行后面的任务
线程池理解:
1.线程池都是通过ThreadPoolExecutor()构造方法创建,我们可以自定义,自定义遵循cup密集和io密集
2.线程池创建原理:1.核心线程池
2.最大线程池
3.当线程大于核心线程池是,线程需要结束生命周期等待任务的时间
4.等待的时间单位
5.BlockingQueue(有界阻塞队列)接口的实现类
线程池合理配置:遵循 cup密集和io密集
cpu密集:核心线程数配置和cpu核心相当 比如:个人电脑cup有4核的有8核心的,那么可以配置4个线程,这样可以同时执行4个线程,然后可以重复利用线程资源。根据cpu核心配置。
(如果超过了就没有必要了,因为线程执行是用cup核的,再多也只能同时执行核的几个线程)
cpu密集表示线程频繁调度,配置cpu的核心数,可以减少cpu一直频繁的切换线程频繁调度
IO密集:因为io的时候,在操作数据库等等,会用到锁,等等,那么多个线程之间肯定会出现一下阻塞、休眠的情况,那么为了提高效率:
让有线程在休眠、阻塞、释放锁等等情况有线程可以执行,配置最优的是:cup核心数*2
就是说cpu核心有4核,那么可以配置8个核心线程池
Runtime.getRuntime().availableProcessors(); // 获取CPU的核心数
-------------------------------------------------------------------------------------------------
ForkJoin框架:(ForkJoinPool分治线程池)继承AbstractExecutorService 多线程并发是分开执行任务的框架
框架通过(递归)把问题划分为子任务,然后并行的执行这些子任务,等所有的子任务都结束的时候,再合并最终结果的这种方式来支持并行计算编程。
通过递归算去写(也有归并算法的意思)拆分任务、形成消息队列,工作窃取模式
比如:有一个任务,单线程执行时间长,那么可以用ForkJoin框架创建多线程来分拆任务执行,提高程序效率,通过递归算法去拆分,一直把任务拆分到临界点(我们自定义的)不拆分。
当多个线程将任务执行完毕后进行汇总。
(注:线程拆分后的每层线程的上级线程都会join等待,等待子线程执行完拆分的任务,如果一个线程任务过多,而其他线程任务已经执行完了,
那么这个线程会去窃取其他线程的任务执行!这个就是工作窃取)
Fork 操作启动一个新的并行 Fork/Join 子任务
Join 操作一直等待直到所有的子任务都结束。
Fork/Join 算法,如同其他分治算法一样,总是会递归的、反复的划分子任务,直到这些子任务可以用足够简单的、短小的顺序方法来执行。
ForkJoin 【框架的核心】在于(1.轻量级调度机制),使用了 (2.工作窃取(Work-Stealing))所采用的基本调度策略。
1.每一个工作线程维护自己的调度队列中的可运行任务
2.队列以双端队列的形式被维护
3.对于一个给定的工作线程来说,任务所产生的子任务将会被放入到工作者自己的双端队列中
4.工作线程使用后进先出 LIFO(最新的元素优先)的顺序,通过弹出任务来处理队列中的任务
5.当一个工作线程的本地没有任务去运行的时候,它将使用先进先出 FIFO 的规则尝试随机的从别的工作线程中拿(窃取)一个任务去运行。
【每个工作线程】在【处理自己的】工作队列同时,会尝【试窃取一个任务】(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),
【窃取的任务】位于其他线程的【工作队列的队首】,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
简单说:自己工作的队列【先进后出】,在工作的同时还会去窃取其他线程的工作队列,窃取的时候是从其他线程队列首部窃取【先进线程】,这样就会减少线程竞争
6.当一个工作线程触及了 Join 操作,如果可能的话它将处理其他任务,直到目标任务被告知已经结束(通过 isDone() 方法)。所有的任务都会 无阻塞 的完成
7.当一个工作线程无法再从其他线程中获取任务和失败处理的时候,它就会退出并经过一段时间之后再度尝试直到所有的工作线程都被告知他们都处于空闲的状态
8.在这种情况下,他们都会阻塞直到其他的任务再度被上层调用
9.在既没有自己的任务,也没有可以窃取的任务时,进入休眠
10.ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池 Worker+Queue 的组合模式,而是维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度的减少碰撞。
简单说:使用了队列[]数组来维护自己的双向队列,不在像传统的线程池,使用最大线程(零时线程)+一个单向队列,这样在提交线程任务可以减少线程之间抢占cpu发生碰撞
递归设计的调优手段
注意:使用【后进先出 LIFO】 用来处理每个【工作线程的自己任务】,但是使用【先进先出 FIFO 】规则用于【获取别的任务】,这是一种(被广泛使用)的进行递归 Fork/Join 设计的一种调优手段。
体现了【递归分治算法】的大任务优先策略
工作窃取算法的优点: 利用了线程进行并行计算,减少了线程间的竞争
工作窃取算法的缺点: 如果双端队列中只有一个任务时,线程间会存在竞争。
窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列。
ForkJoinPool 最适合: 的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时
对比:ThreadPoolExecutor线程池和ForkJoinPool线程池对比
ThreadPoolExecutor:中每个任务都是由单个线程独立处理的,如果出现一个非常耗时的大任务(比如大数组排序),就可能出现线程池中只有一个线程在处理这个大任务,而其他线程却空闲着,这会导致 CPU 负载不均衡,空闲的处理器无法帮助工作繁忙的处理器。
ForkJoinPool : 可以用来解决这种问题,将一个大任务拆分成多个小任务后,使用 Fork 可以将小任务分发给其他线程同时处理,使用 Join 可以将多个线程处理的结果进行汇总。
1.ForkJoinPool:核心类 ---任务线程池的核心类
2.ForkJoinWorkerThread:执行任务的基类 ---这个线程是由ForkJoinPool管理的,用于执行 ForkJoinTask 这个类仅仅是为了添加功能而进行子类化 - 没有可以处理调度或执行的可覆盖的方法
但是,您可以覆盖主任务处理循环周围的初始化和终止方法。 如果您创建了这样一个子类,那么您还需要在ForkJoinPool中提供自定义ForkJoinPool.ForkJoinWorkerThreadFactory到ForkJoinPool 。
3.ForkJoinTask:执行任务的抽象类(相当于FutureTask) ---比传统的任务【更加轻量】,不再是 Runnable 的子类,提供Fork/Join方法用于【分割任务】以及【聚合结果】。
fork() 方法 : 做的工作只有一件事,既是把任务推入当前工作线程的工作队列里。
join() 方法 : 工作则复杂得多,也是它可以使得线程免于被阻塞的原因:
1.检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。
2.查看任务的完成状态,如果已经完成,直接返回结果
3.如果任务尚未完成,但处于自己的工作队列内,则完成它
4.如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式)执行,以期帮助它早日完成预 join 的任务
5.如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 Join 的任务时,则找到小偷的小偷,帮助它完成它的任务
6.递归地执行第 5 步
RecursiveTask<>: ForkJoinTask的子类,实现compute()方法,创建任务【注:继承此类的任务方法是有返回值的】 ---继承此类,实现compute()抽象方法,然后创建自己类的对象,使用父类继承来的invokeAll(自己类对象1,自己类对象2)方法执行任务,注:每次递归都会创建两个自定义任务对象提交
RecursiveAction<>: ForkJoinTask的子类,实现compute()方法,创建任务【注:继承此类的任务方法没有返回值】
RecursiveTask 和 是一样的,只不过RecursiveTask有返回值,RecursiveAction没有返回值
-------------------------------------------------------------------------------------------------
自旋锁: AtomicReference:原子更新对象引用
// 比较更新(预期值,更新值):atomicReferentce里面存放对象的引用是否为==null,如果==null,让存放的对象值更新为gengXing
boolean bool =atomicReferentce.compareAndSet(null, gengXing);// 比较更新(更新成功为true)
// 可以使用while(!bool){ bool =atomicReferentce.compareAndSet(atomicReferentce.get(), gengXing) } 去循环比较更新,这样就是第一个自旋锁
自旋锁大概的意思:就是说循环去获取锁,没有获得到锁,不阻塞,线程一直处于活跃状态!
重入锁:synchronized,ReentrantLock
非重入锁:
悲观锁:synchronized,ReentrantLock
乐观锁:
公平锁: Lock notFairLock = new ReentrantLock(true) //将参数设置为true:表示阻塞的线程先进先出,这就是公平锁了
非公平锁: synchronized, Lock notFairLock = new ReentrantLock(false) //ReentrantLock默认为false,所有不用填写
互斥锁:ReentrantReadWriteLock(读写锁、也是互斥锁)
读写锁:读取的时候就不能写,写的时候就不能读. 读取和写入在互斥
ReentrantReadWriteLock reentramtRedWriteLock = new ReentrantReadWriteLock();
ReadLock readLock = reentramtRedWriteLock.readLock();// 获取读取锁
WriteLock writeLock = reentramtRedWriteLock.writeLock();// 获取写入锁
分段锁:ConcurrentHashMap
-------------------------------------------------------------------------------------------------
rpc远程调用框架:
消息中间件:
同步接口: 同步交互,指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程
异步接口: 异步交互,指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待
区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
哪些情况建议使用同步交互呢?比如银行的转账系统,对数据库的保存操作等等,都会使用同步交互操作,其余情况都优先使用异步交互。
//通过Callable接口和FutureTask类 创建线程并且获取线程返回值
public static void main(String[] args) {
// 1.创建Callable接口的实现类
ThreadFangShi ts = new ThreadFangShi();
// 2.创建java.util.FutureTask类对象,将Callable接口的实现类传入
FutureTask<Integer> futureTask = new FutureTask<Integer>(ts);
try {
// 3.创建线程执行futureTask类(注:FutrueTask任务执行类已经实现了Runnable接口)
new Thread(futureTask, "t1").start();
// 4.futureTask.get()获取已经执行完,返回的结果
//注意: 当前线程执行到futureTask.get()方法时,会相当于调用过来 xx线程.join()方法进行阻塞,等待此线程执行完返回结果后,才继续执行
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
//通过线程池ExecutorService创建线程,执行Runnable接口和Callable接口的方法.
public static void main(String[] args) {
// 1. Executors(线程池工厂类)创建一个线程池,该线程池重用固定数量的从阻塞队列中运行的线程。
ExecutorService executorService = Executors.newFixedThreadPool(5);// 表示管理5个线程,重复利用
//2. execute(Runnable)方法,使用线程池执行Runnable实现类的放法
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//创建FutureTase任务执行对象,创建Callable任务接口实现类的对象
FutureTask<Integer> ft=new FutureTask(new Callable2<Integer>());
//ExecutorService线程池的.submit(Callable接口)执行Callable接口对应的任务
Future future=executorService.submit(ft);
try {
//获得线程执行完返回结果
int jieguo = ft.get();
System.out.println(jieguo);
} catch (Exception e) {
e.printStackTrace();
} System.out.println();
//注:线程池配置了5个线程,重复利用。所以下面6个线程任务,线程打印出来的名字有一个是重复的线程名字(因为线程重复利用)
//ExecutorService线程池.execute(Runnable接口)方法让线程执行Runnable接口的实现类
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//使用线程池执行多个线程
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//使用线程池执行多个线程
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//使用线程池执行多个线程
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//关闭线程池
executorService.shutdown();
}