——进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
——线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
public enum State {
NEW,(新建)
RUNNABLE,(准备就绪)
BLOCKED,(阻塞)
WAITING,(不见不散)
TIMED_WAITING,(过时不候)
TERMINATED;(终结)
}
守护线程是为其他线程服务的
垃圾回收线程就是守护线程~
守护线程有⼀个特点:当别的⽤户线程执⾏完了,虚拟机就会退出,守护线程也就会被停⽌掉了。
也就是说:守护线程作为⼀个服务线程,没有服务对象就没有必要继续运⾏了
使⽤线程的时候要注意的地⽅
线程优先级⾼仅仅表示线程获取的CPU时间⽚的⼏率⾼,但这不是⼀个确定的因素!
线程的优先级是⾼度依赖于操作系统的
java的线程优先级是分为1最低,5默认,10最高
如果存在线程组,则优先级不能大于线程组的优先级
调⽤sleep⽅法会进⼊计时等待状态,等时间到了,进⼊的是就绪状态⽽并⾮是运⾏状态!
调⽤yield⽅法会先让别的线程执⾏,但是不确保真正让出
调⽤join⽅法,会等待该线程执⾏完毕后才执⾏别的线程~
线程中断在之前的版本有stop⽅法,但是被设置过时了。现在已经没有强制线程终⽌的⽅法了!由于stop⽅法可以让⼀个线程A终⽌掉另⼀个线程B,被终⽌的线程B会⽴即释放锁,这可能会让对象处于不⼀致的状态。
线程A也不知道线程B什么时候能够被终⽌掉,万⼀线程B还处理运⾏计算阶段,线程A调⽤stop⽅法将线程B终⽌,那就很⽆辜了~
总⽽⾔之,Stop⽅法太暴⼒了,不安全,所以被设置过时了。
我们⼀般使⽤的是interrupt来请求终⽌线程~
要注意的是:interrupt不会真正停⽌⼀个线程,它仅仅是给这个线程发了⼀个信号告诉它,它应该要结束了(明⽩这⼀点⾮常重要!)
也就是说:Java设计者实际上是想线程⾃⼰来终⽌,通过上⾯的信号,就可以判断处理什么业务了。
具体到底中断还是继续运⾏,应该由被通知的线程⾃⼰处理
设置中断标志的目的是想由被通知的线程自己处理,而这些方式都阻塞掉了。
被阻塞掉的线程调用中断方法是不合理的(不允许中断已经阻塞的线程)[因为可能会造成中断无效]
interrupt线程中断还有另外两个⽅法(检查该线程是否被中断):
静态⽅法interrupted()–>会清除中断标志位
实例⽅法isInterrupted()–>不会清除中断标志位
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
为什么需要CPU cache : CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu -> cache -> memory ) .
CPU cache有什么意义:
1)时间局部性︰如果某个数据被访问,那么在不久的将来它很可能被再次访问
2)空间局部性︰如果某个数据被访问,那么与它相邻的数据很快也可能被访问;
缓存协议
M表示被修改 ,E表示独享 ,S表示共享 ,I表示无效的
local read 本地读
local write 本地写
remote read 读取内存数据
remote write 写内存数据
CPU 多级缓存–乱序执行优化 ----》指令重排
处理器为提高运算速度而做出违背代码原有顺序的优化。
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。
为什么会推导出JMM模型呢?
Postman:Http请求模拟工具
Apache Bench:Apache附带工具,测试网站性能
JMeter: 压力测试工具
代码:Semaphore CountDownLatch,Cyclingbairrir
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的
JMM可能带来可见性、原子性和有序性问题。
原子性指一个操作是不可中断的,即多线程坏境下,操作不能被其他线程干扰
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JVMM规定了所有的变量都存储在主内存中
导致共享变量在线程间不可见的原因
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
解决方法volatile、synchronized、Lock
happens-before原则
接下来我们会去介绍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是指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实际上是一种自旋锁,
AtomicStampedReference解决ABA问题,维护了一个“版本号”Stamp时间戳
synchronized 是 Java 中的关键字,是一种同步锁,子类继承后默认情况下不同步,如调用父类方法则相当于同步
synchronized 修饰对象的不同:
class Ticket {
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale() {
//判断:是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" :
"+(number--)+" "+number);
}
}
}
获取锁的线程释放锁只会有两种情况:
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 有以下几点不同:
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。
volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性。
volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。
在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在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
ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)
api介绍
源码解析
StringBuilder -> StringBuffer
SimpleDateFormat -> DateTimeFormatter
ArrayList,HashSet, HashMap 等Collections
Vector 是矢量队列,Vector 继承了 AbstractList,实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。 Vector 实现了RandmoAccess 接口,即提供了随机访问功能。
在每一个方法上都加了synchronized,所以它是线程安全的
Collections.synchronizedXXX (List、Set、Map)都是线程安全的
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和
ArrayList 不同的时,它具有以下特性:
独占锁效率低:采用读写分离思想解决
写线程获取到锁,其他写线程阻塞
复制思想: 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 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();
}
}
}
“动态数组”机制
线程安全”机制
线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:ArrayList ----- Vector,HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低
Collections 构建的线程安全集合
java.util.concurrent 并发包下: CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全
AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态(private volatile int state),通过CAS完成对status值的修改(0表示没有,1表示阻塞)
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 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 的构造方法中传入的第一个参数是最大信号量(可以看成最大线
程池),每个信号量初始化为一个最多只能分发一个许可证。使用 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.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–可重入锁又叫递归锁,指的同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁的代码块。可重入锁可以避免死锁的问题
synchronized隐性锁—ReentrantLock显式锁
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenteri时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
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接口的特点
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future 对象。
将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable 返回)。Future 基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。
Java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建Thread 对象。因此,间接地使用 Callable 创建线程。
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 框架要完成两件事情:
public class CompletableFutureTest2 {
public static void main(String[] args)throws Exception {
/**
1.当一个线程依赖另一个线程时,可以使用thenApply()方法来把这两个线程串行化(第二个任务依赖第一个任务的结果)
public CompletableFuture thenApply(Function super T,? extends U> fn)
2.它可以处理正常的计算结果,或者异常情况
public CompletableFuture whenComplete(BiConsumer super T,? super Throwable> 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可以支持返回值,有无指定线程池
阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
常见队列有两种:FIFO,LIFO
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
常见的 BlockingQueue
ArrayBlockingQueue-- 由数组结构组成的有界阻塞队列
在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
LinkedBlockingQueue–>由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
DelayQueue—使用优先级队列实现的延迟无界阻塞队列
DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
PriorityBlockingQueue—支持优先级排序的无界阻塞队列
PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁。
SynchronousQueue–不存储元素的阻塞队列,也即单个元素的队列
SynchronousQueue一种无缓冲的等待队列–直接消费
声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
LinkedTransferQueue-- 由链表组成的无界阻塞队列
LinkedTransferQueue 采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为 null)入队
LinkedBlockingDeque—由链表组成的双向阻塞队列
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,
这里的阻塞一般有两种情况:
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
new Thread弊端:
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,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;
线程的状态:
线程数量要点:
线程空闲时间要点:当前线程数⼤于核⼼线程数,如果空闲时间已经超过了,那该线程会销毁。
排队策略要点:
当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了:
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 核数的线程来并行执行任务
场景: 适用于大耗时,可并行执行的场景
实际际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池(线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式—因为会堆积大量请求)
在Java中使⽤多线程,就会有可能导致死锁问题。死锁会让程序⼀直卡住,不再程序往下执⾏。我们只能通过中⽌并重启的⽅式来让程序重新执⾏。
造成死锁的原因可以概括成三句话:当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃⾃⼰拥有的资源
四个必要条件
发⽣死锁的原因主要由于:
线程之间交错执⾏,解决:以固定的顺序加锁。
执⾏某⽅法时就需要持有锁,且不释放,解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
永久等待,解决:使⽤ tryLock() 定时锁,超过时限则返回错误信息
死锁编码和定位
主要是两个命令配合起来使用,定位死锁。
jps指令:jps -l可以查看运行的Java进程。
jstack指令:jstack pid可以查看某个Java进程的堆栈信息,同时分析出死锁。
Spring bean : singleton.prototype
无状态对象
出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了
if(num != 0){
this.wait();
}
改为
while(num != 0){
this.wait();
}