目录
既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
一、JAVA线程
线程状态:
1.1 开启线程的方式
JAVA开启线程的方式:继承Thread、实现Runnable、实现callable
注意:相比于runnable , callable可以有返回值,也可以抛出异常这点很关键.
某个页面需要三块信息,A-2秒;B-1秒;C-1秒 这里就4秒。通过callable就能2秒返回。
补充为什么run方法,一般会写while(true),这只是为了让线程一致执行,方便调试
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadTest test1 = new ThreadTest();
test1.setName("test1");
test1.start();
}
}
public class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableTest run1 = new RunnableTest();
Thread thread1= new Thread(run1);
thread1.setName("run1");
thread1.start();
}
}
public class CallableTest implements Callable {
@Override
public Object call() throws Exception {
return "1234";
}
public static void main(String[] args) {
FutureTask futureTask1 = new FutureTask(new CallableTest());
Thread thread1 = new Thread(futureTask1);
thread1.start();
try {
System.out.println(futureTask1.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
1.2 FutureTask
FutureTask包含了取消与启动计算的方法,查询计算是否完成以及检索计算结果的方法。只有在计算完成才能检索到结果,调用get()
方法时如果任务还没有完成将会阻塞调用线程至到任务完成。一旦计算完成就不能重新开始与取消计算,但可以调用runAndReset()
重置状态后再重新计算。
当FutureTask处于未启动或者已启动的状态时,调用FutureTask对象的get方法会将导致调用线程阻塞。当FutureTask处于已完成的状态时,调用FutureTask的get方法会立即放回调用结果或者抛出异常。
当FutureTask处于未启动状态时,调用FutureTask对象的cancel方法将导致线程永远不会被执行;当FutureTask处于已启动状态时,调用FutureTask对象cancel(true)方法将以中断执行此任务的线程的方式来试图停止此任务;当FutureTask处于已启动状态时,调用FutureTask对象cancel(false)方法将不会对正在进行的任务产生任何影响;当FutureTask处于已完成状态时,调用FutureTask对象cancel方法将返回false;
1.3 join()
其实就是挂起的用线程wait,等待被调用线程执行完
public class ThreadJoinTest extends Thread{
@Override
public void run() {
System.out.println( "run start: "+LocalDateTime.now());
try {
Thread.sleep(5000); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( "run finish: "+LocalDateTime.now());
}
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+":"+LocalDateTime.now());
ThreadJoinTest threadJoinTest = new ThreadJoinTest();
threadJoinTest.start();
threadJoinTest.join(); //有没有这行结果不一样
System.out.println( "结束! "+LocalDateTime.now());
}
}
也可以保证线程执行顺序
/**
* @author 10450
* @description t1 t2 t3 顺序执行
* @date 2022/10/13 21:27
*/
public class OrderThread {
public static void main(String[] args) {
final Thread t3 = new Thread(){
@Override
public void run() {
System.out.println(1);
}
};
final Thread t2 = new Thread(){
@Override
public void run() {
System.out.println(2);
try {
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
final Thread t1 = new Thread(){
@Override
public void run() {
System.out.println(3);
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t3.start();
t2.start();
t1.start();
}
}
1.4 yield
yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
二、多线程问题
一个变量在内存中,被多个线程使用,这个变量叫:共享变量。
2.1 JVM在运行时候的内存分配过程
线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这也是JVM为了提供性能而做的优化。
2.2 缓存不一致
每个线程有自己的内存栈,将内存中变量拷贝到自己的内存栈中。这就有可能造成缓存不一致。
从计算机层面层面上解决缓存不一致问题:
1、总线锁 LOCK :有可能造成CPU阻塞
2、缓存一致性协议 MESI :保证缓存中使用的变量副本是一致的
2.3 并发多线程中三特性
原子性、可见性、有序性
原子性:要么全部成功,要么全部失败
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性:即程序执行的顺序按照代码的先后顺序执行
2.4 指令重排
为了程序的运行效率,会对代码进行优化,不保证执行的顺序与代码的顺序一样,但是保证保证程序最终执行结果和代码顺序执行的结果是一致的。
注意:指令重排只能保证单线程的最终执行结果和代码顺序执行结果一直。多线程无法保证!
三、JAVA内存模型
JMM(Java Memory Model) JAVA内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信。JMM 控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存
和从内存中取出变量
这样底层细节。
PS:这里讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
3.1 原子性
int i = 10; //原子操作
int j = i ; // 3步, 读取i 赋值j j写入内存
i++;// 等同于i=i+1; 3步, 读取i 赋值i+1 i写入内存
3.2 可见性-volatile
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
PS:通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.3 有序性
java内存模型中,由于JVM优化性能,会造成编译器或者处理器对指令进行重排。同样,指令重排能保证单线程执行结果与顺序执行结果一直。
volatile保证一定的“有序性”。
PS:另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
四、volatile关键字
volatile修饰的共享变量,有两个特点:可见性和有序性。
即1、每次修改变量是,能被其他线程读取到。2、防止指令重排
参考:volatile和Cache一致性协议之MESI_jjavaboy的专栏-CSDN博客_volatile和缓存一致性协议
4.1 volatile不保证原子性
下面代码结果不一定是5000,因为i++不是原子性。
public class AutomaticTest {
public volatile int i = 0 ;
public void increaseI(){
i++;
}
public static void main(String[] args) throws InterruptedException {
final AutomaticTest test = new AutomaticTest();
for (int p=0;p<5;p++) {
new Thread() {
public void run() {
for (int k = 0; k < 1000; k++) {
test.increaseI();
}
}
}.start();
}
Thread.sleep(2000);
System.out.println(test.i);
}
}
可以用Lock或者Synchronized进行完善。
public synchronized void increaseI(){
i++;
}
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
4.2 volatile原理
编译成汇编指令的时候,volatile修饰的变量前面会有Lock修饰。
lock前缀会形成“内存屏障”,防止指令重排、缓存修改后立即写入内存、当有内存写入后,其他读取该变量会失效。如下图。
4.3 场景
volatile性能比synchronized好,但是不保证原子性,所以可用于以下情况:
1、标记状态
2、double lock
public class MySingleton {
private volatile static MySingleton instance= null;
private MySingleton(){
}
public static MySingleton getInstance(){
if(instance==null){
synchronized (MySingleton.class) {
if(instance==null)
instance = new MySingleton();
}
}
return instance;
}
}
4.5 MESI
MESI协议:缓存一致性协议。每个Cache line有4个状态
Modified 修改、exclusive 独占、share 共享、invalid 失效
既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法
五、线程池
5.1 线程池的使用
1、降低资源消耗。减少了创建和销毁线程的次数,每次工作线程都可以被重复利用,可执行多个任务。
2、提高响应速度。当任务到达时,省去了创建新线程的时间
3、提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下,每个线程大约需要1MB内存,线程数量越多,消耗的内存也越大,最后可能导致死机。
5.2 JAVA线程池场景
1)任务数多但资源占用不大
2)任务数不多但资源占用大
举例:ABS项目资产导入校验,Excel导入后解析可能上万条数据。每行数据需要校验。
举例:产品系统,更新完产品数据需要统计,这个产品分类
3)极端场景情况
场景解读:如遇任务资源较大、任务数较多同时处理效率不高的场景下,首先需要考虑任务的产生发起需要限流,理论上讲为保障系统的可用性及稳定运行,任务的发起能力应当略小于任务处理能力,其次对于类似场景可以采用以时间换取空间的思想,充分利用系统计算资源,当遇到任务处理能力不足的情况下,任务发起方的作业将被阻塞,从而充分保护系统的资源开销边界,但可能会导致CPU核心态的使用率高,如
BlockingQueue queue = newSynchronousQueue<>();
ThreadPoolExecutor executor = newThreadPoolExecutor(64, 64, 0, TimeUnit.SECONDS, queue);
1) BlockingQueue即阻塞队列
什么是阻塞队列
1)支持阻塞的插入put方法:意思是当队列满时,队列会阻塞插入元素的线程,
直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队
列变为非空。
可以很好的解决生产与消费者的问题
常用阻塞队列
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
最重要的参数:ReentrantLock重入锁
5.3 线程池的处理流程
第一步: 首先判断核心线程池是否已满:
如果没有满,创建新线程并执行任务
如果已满,进入第二步
第二步: 判断当前工作队列是否已满:
如果没有满,放入工作队列中
如果已满,进入第三步
第三步: 判断当前线程数是否超过最大线程数:
如果没有超过,新建线程并执行任务
如果超过,进入第四步
第四步:交给饱和策略来处理无法执行的任务。
PS: 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
注意:
1.执行第一步和第三步需要获取全局锁。
2.核心池还没满的时候是还在预热过程。
3.工作队列会循环的从工作队列中获取任务来执行。
这样设计可以在执行execute()方法的时候,尽量避免获取全局锁。因为当线程池预热之后(线程数大于等于核心线程数corePoolSize),基本上所有方法都会执行步骤二,而步骤二不需要获取全局锁。
5.4 ThreadPoolExecutor
ThreadPoolExecutor的完整构造方法的签名是:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
.
corePoolSize - 池中所保存的线程数,包括空闲线程。核心线程会一直存活,即使没有任务需要执行的空闲线程、当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
maxiPoolSize-池中允许的最大线程数。
keepAliveTime - 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
allowCoreThreadTimeout:允许核心线程超时
queueCapacity:任务队列容量(阻塞队列)
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory - 执行程序创建新线程时使用的工厂。
rejectedExecutionHandler- 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
ThreadPoolExecutor是Executors类的底层实现。
在JDK帮助文档中,有如此一段话:
“强烈建议程序员使用较为方便的Executors
工厂方法Executors.newCachedThreadPool()
(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)
(固定大小线程池)Executors.newSingleThreadExecutor()
(单个后台线程)
它们均为大多数使用场景预定义了设置。
5.5 Executors工厂类
四种线程池
1、newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程。(线程最大并发数不可控制)
CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
2、newFixedThreadPool:创建一个固定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool : 创建一个定时线程池,支持定时及周期性任务执行。
ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
4、newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
两种提交任务的方法
ExecutorService 提供了两种提交任务的方法:
- execute():提交不需要返回值的任务
- submit():提交需要返回值的任务
七、高并发下库存加减问题
背景:互联网的秒杀场景,高并发下分布式集群服务。JVM中的Lock、Synchronized就不适合了。
思路:将多线程编程单线程。要么加分布式锁、要么用消息队列。
1、并发量不高的情况下,添加分布式锁。
2、为了提高性能,可以redis内存数据库增加性能,在通过MQ将同步到数据库中。
3、以上两种单线程无法满足并发量高的场景。并行异步减库存。
库存协调器就需要业务功能去实现。
补充:前端
面对高并发的抢购活动,前端【扩容】【静态化】【限流】
(1)扩容:,通过增加前端池的整体承载量来抗峰值。
(2)静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
(3)限流:一般都会采用IP级别的限流,即针对某一个IP,限制单位时间内发起请求数量。或者活动入口的时候增加游戏或者问题环节进行消峰操作。
(4)有损服务:最后一招,在接近前端池承载能力的水位上限的时候,随机拒绝部分请求来保护活动整体的可用性。
面试题
1、在 java 中守护线程和本地线程区别?
2、线程与进程的区别?
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务,线程是操作系统能够进行运算调度的最小单位
3、如何在Java中实现线程?如何选择?
继承Thread、实现Runnable、实现callable
JAVA是继承只能单继承场景,但是可以有实现多个接口
而 Runnable和Callable区别在在于Callable call() 方法可以返回值和抛出异常
4、
3、什么是多线程中的上下文切换?
4、死锁与活锁的区别,死锁与饥饿的区别?
5、Java 中用到的线程调度算法是什么?
6、什么是线程组,为什么在 Java 中不推荐使用?
7、为什么使用 Executor 框架?
8、在 Java 中 Executor 和 Executors 的区别?
9、什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
10、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
11、什么是 Executors 框架?
12、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
13、什么是 Callable 和 Future?
14、什么是 FutureTask?使用 ExecutorService 启动任务。
15、什么是并发容器的实现?
16、多线程同步和互斥有几种实现方法,都是什么?
17、什么是竞争条件?你怎样发现和解决竞争?
18、你将如何使用 thread dump?你将如何分析 Thread dump?
19、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?
20、Java 中你怎样唤醒一个阻塞的线程?
21、什么是可重入锁(ReentrantLock)?
22、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?