目录
29.使用ThreadPoolExecutor创建线程池的原因
线程基础知识
1.线程和进程的区别
首先说进程,说白了我们打开的微信、QQ、Word就是一个进程,而这个进程的是由指令和数据组成,并且指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
说的在直白点,当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程分为多实例进程和单实例,如图所示
那么线程其实指的就是一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行,一个进程之内可以分为一到多个线程。说白了就是一个进程实例包含多个线程,这些线程去CPU里面执行最终线程才能运行。
二者的区别总结
1)进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
2)不同的进程使用不同的内存空间(说白了就是你打开IDEA和你打开文本文档占用的内存空间带大小不一样),并且在当前进程下的所有线程可以共享内存空间
3)线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2.并行和并发的区别
在开讲之前我们需要明确一个程序中能够同时运行多少个线程,取决于CPU的核心数,因为一个CPU核心在同一时刻只能运行一个线程,如果是4核心的CPU,那么就意味着能够同时运行4个线程。
并发:我们常常讲的并发数,说白了就是整个系统同时能够处理的请求数量。简单来说,并发的意义就是系统具有处理多个任务的能力。所以,平时我们说的,“系统的并发量”,就是指的系统同时处理的请求数,这个请求数实际对应的就是用户在访问这个网站时每一次操作向后端发起的请求。通常会使用TPS/QPS来表示,其中TPS表示每秒处理的事务数,QPS表示每秒处理的查询数。再说直白些,并发就是同一时间应对多件事情的能力。
并行:我们上面说到同一时刻能有几个线程同时运行取决于CPU的核心数量,假设现在我们的CPU是8核的,也就是说现在有8个线程同时执行任务,那么这就是并行,并行就是同一时间动手做多件事情的能力
3.线程创建方式
共有四种方式可以创建线程,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程
继承Thread类
package com.atguigu.auth.thread;
/**
* @author JiaWei
*/
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
实现runnable接口
package com.atguigu.auth.thread;
/**
* @author JiaWei
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable() ;
// 创建Thread对象
Thread t1 = new Thread(mr) ;
Thread t2 = new Thread(mr) ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
实现Callable接口
package com.atguigu.auth.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author JiaWei
*/
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}
线程池创建
package com.atguigu.auth.thread;
import java.util.concurrent.*;
/**
* @author JiaWei
*/
public class MyExecutors implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors());
// 关闭线程池
threadPool.shutdown();
}
}
4.runnable和callable的区别
1.分别继承run方法及call方法
2.runnable没有返回值,callable有返回值可以知道线程的执行结果,通过FutureTask调用get方法
3.runnable的run方法不能抛出异常不然会报错,而callable的call方法可以抛出异常。
5.线程的生命周期
创建一个线程分别会经历,新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待( WAITING )、时间等待(TIMED_WALTING)、终止(TERMINATED)
/**
* @author JiaWei
*/
public enum State {
/**
* 尚未启动的线程的线程状态
*/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自 * 操作系统的其他资源,例如处理器。
*/
RUNNABLE,
/**
* 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调 * 用Object.wait后重新进入同步块/方法。
*/
BLOCKED,
/**
* 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
* Object.wait没有超时
* 没有超时的Thread.join
* LockSupport.park
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify() * 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定 * 时等待状态:
* Thread.sleep
* Object.wait超时
* Thread.join超时
* LockSupport.parkNanos
* LockSupport.parkUntil
* </ul>
*/
TIMED_WAITING,
/**
* 已终止线程的线程状态。线程已完成执行
*/
TERMINATED;
}
6.线程状态的变化过程
1、一个线程创建出来之后他就是新建状态,当调用了start()方法之后,他就从新建状态转化成可运行状态,如果说当前线程抢到了CPU分配的时间片,就可以把线程的执行代码给执行完成,最后变成死亡状态。
2、可能还有一些其他情况,例如你加了锁,那么即使当前线程到了可运行状态,也会先进入阻塞状态,只有说获取到了锁才能去执行。
3、如果当前线程调用了wait()方法,那么当前线程就会进入等待状态,只有其他线程调用了natify()方法唤醒这个线程才能重新回到可执行状态。
4、如果当前线程调用了sleep()方法就会进入时间等待状态,时间到了这个线程就会重新回到可执行状态。
7.创建三个线程如何保证顺序执行
可以用线程类的join()方法,在当前线程当中调用上一个线程的join()方法就可以实现,上一个线程运行结束后当前线程才可以运行。
/**
* @author JiaWei
*/
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
8.notify()和notifyAll()的区别
我们都知道如果当前线程调用wait()方法就会进入等待状态,只有等其他线程调用natify()方法才能够被唤醒,那么notify()和natifyAll()的区别就是,如果多个线程都调用wait()方法,那么notify只能随机唤醒一个,而notifyAll()则全部都能唤醒。
/**
* @author JiaWei
*/
public class WaitNotify {
static boolean flag = false;
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
while (!flag){
System.out.println(Thread.currentThread().getName()+"...wating...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...flag is true");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock){
while (!flag){
System.out.println(Thread.currentThread().getName()+"...wating...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...flag is true");
}
});
Thread t3 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " hold lock");
lock.notifyAll();
flag = true;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
}
}
9.wait 和 sleep 方法的不同
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 方法归属不同:sleep(long) 是 Thread 的静态方法,而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有。
- 醒来时机不同:sleep(long)、wait(long)时间到了自然会被唤醒,wait()方法还可以被notify和notifyAll唤醒,如果不唤醒就会被一直阻塞下去。
- 锁特性不同:1.wait方法必须搭配synchronized锁使用,而sleep不需要搭配2.如果同一把锁两个方法使用,则当前wait方法执行完成后会释放锁,允许其他线程获取锁3.而sleep搭配synchronized并且两个方法共用一把锁,则当前sleep方法执行完成后不会释放锁
package com.atguigu.auth.thread;
/**
* @author JiaWei
*/
public class WaitSleepCase {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
sleeping();
}
private static void illegalWait() throws InterruptedException {
LOCK.wait();
}
private static void waiting() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
get("t").debug("waiting...");
LOCK.wait(5000L);
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
private static void sleeping() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
get("t").debug("sleeping...");
Thread.sleep(5000L);
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
}
线程安全知识
10.synchronized锁底层原理
首先回顾一下synchronized锁的使用方式及场景,此代码含义是20个线程抢10张票的逻辑,如果我们不加锁那么就会出现多个线程抢到一张票,或者出现抢票超过10张的情况。
/**
* @author JiaWei
*/
public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
ticketDemo.getTicket();
}).start();
}
}
}
synchronized是由JVM的内部对象Monitor(监视器)实现的,c++语言实现,
/**
* @author JiaWei
- monitorenter 上锁开始的地方
- monitorexit 解锁的地方
- 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
- 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放
*/
public class SyncTest {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
找到这个类的class文件,在class文件目录下执行`javap -v SyncTest.class`,反编译效果如下
在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁monitor主要就是跟这个对象产生关联,如下图
Monitor内部具体的存储结构:
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
执行流程就是假如现在有一个线程过来要执行代码,里面synchornized锁的代码块的Lock对象,他需要和Monitor进行关联,然后判断Monitor里面的Owner属性是否为null,如果为null则让当前线程直接占有,也就是说当前线程直接获取到了锁,并且Owner同时只能让一个线程占有,如果此时再来一个线程他就只能去EntryList中进行等待,并且等待的线程就处于阻塞状态,如果Owner中的线程执行完成释放了锁,EntryList中的线程就会去争抢Owner的拥有权,如果说当一个线程调用了Wait()方法之后就会处于等待状态,然后当前线程就会进入到WaitSet中。
11.Java内存模型JMM
Java内存模型(Java Memory Model)描述了Java程序中多线程下各种变量的访问规则,JMM把内存分为两块,一块是线程私有的工作区域(工作内存),一块是所有线程的共享区域(主内存),由于线程跟线程之间是相互隔离的如果想要交互并且共享数据,就需要通过主内存来完成。
12.CAS底层原理
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
我来解释CAS是如何保证在无锁情况下保证多线程操作共享数据的原子性,我们都知道Java的内存模型分为两部分,一块是线程私有的工作区域(工作内存),一块是所有线程的共享区域(主内存),由于线程跟线程之间是相互隔离的如果想要交互并且共享数据,就需要通过主内存来完成。假如现在主内存有一个变量V = 100,此时线程A将V同步到自己的工作内存并修改值为101,那么线程A在向主内存同步数据时,就需要将最开始从主内存同步到工作内存的V = 100去跟主内存的A作为比较,如果两个是一致的,那么就可以同步数据,但是此时线程B也要修改主内存中的V并且在最开始也将V = 100同步到工作内存当中,那么线程B将V修改成99他也需要将最开始从主内存中同步到工作内存的V=100拿去做比较,但是由于线程A将主内存的V修改成101,就跟线程B同步过来的V不一致了,他就无法修改主内存的V,那么此时他就会将主内存中最新修改后的V=101在次同步到自己的工作内存这个操作叫做自旋锁,然后此时线程B工作内存中的V和主内存的V是一致的,线程B就可以修改主内存的数据了,以下代码可供参考方便理解。
/**
* 自旋锁
* @author JiaWei
*/
public class CASDemo {
// 需要不断尝试
while(true){
int 旧值A = 共享变量V;
int 结果B = 旧值 + 1;
if (comparAndSwap(旧值,结果)){
// 成功,退出循环 }}
}
}
}
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现
13.乐观锁与悲观锁
乐观锁和悲观锁是在开发中用来控制多个人同时修改同一份数据时的冲突问题的方法。
乐观锁的方法是,在修改数据前先查看数据的版本号或者时间戳,再更新数据时比较版本号或者时间戳。如果版本号或时间戳相同,则表示此期间其他人没有修改过数据,可以自由修改。如果不同,则表示其他人已经修改过数据,就需重新查询数据,并重新检查是否可修改。
悲观锁的方法是在查询数据时加锁,以此控制其他人修改数据的权限。比如,一个人想修改数据,会先试图申请锁来获取数据的编辑权限,如果申请不到锁,则不能修改数据,需要等待其他人释放锁。如果申请到了锁,则可以自由修改,修改完后再释放锁给其他人使用。
乐观锁适用于多读少写的应用场景,比如社交媒体上的个人主页,数据变动不频繁。悲观锁适用于多写少读的应用场景,比如高并发的在线支付操作。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
14.volatile关键字底层原理
一个类的成员变量、静态成员变量被volatile修饰之后,那么就具备了两个功能,第一保证线程间的可见性,第二禁止进行指令重排序。
我们先说保证线程间的可见性,如下代码假设现在没有添加volatile关键字,线程一将stop改为true,线程二是可以读取到的,也就是说保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存,但是线程三做了死循环却是读取不到的,这是因为JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码将`stop`变量改变为了`false`也依然停止不了循环,那么为了也让线程三读到修改后的变量不让JIT做优化,我们只需要在静态变量加上volatile关键字就可以实现。
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
/**
* @author JiaWei
*/
public class VolatileDemo {
static volatile boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println(Thread.currentThread().getName() + ":modify stop to true...");
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + stop);
}, "t2").start();
new Thread(() -> {
int i = 0;
while (!stop) {
i++;
}
System.out.println("stopped... c:" + i);
}, "t3").start();
}
}
15.AQS底层原理
AQS是一个队列同步器,是一种锁机制,作为基础框架使用,比如Lock锁就是基于AQS实现的,下面我来说一下他的工作机制,如图所示,AQS中有一个属性state并且用volatile修饰保证了线程的可见性它里面两种状态0无锁1有锁,此时线程0获取到锁之后他会将状态修改成1有锁,此时如果有第一和第二个线程进来之后会被分配到一个队列中等待,其中线程一作为头线程二作为尾,此时线程0执行完之后释放锁,队列中的线程依次再去获取锁,这就是AQS的执行流程。
我们在衍生一下其他问题,如果多个线程共同去抢这个资源是如何保证原子性的呢?其实AQS是通过CAS保证原子性的,至于CAS上述有专题讲解。
AQS是公平锁吗,还是非公平锁?
直接上图,AQS两种锁都可以实现,非公平锁指的是,假如线程0执行完了释放锁,正常因该队列中的线程一去获取锁但是此时线程五过来了,他没有去队列排队,而是直接和线程一去争抢锁了,这就是非公平锁,而公平锁指的是加入现在线程0释放锁了,正常由队列中的线程一去获取锁,即使线程五过来了他也得去队列中排队。
16.ReentrantLock锁底层原理
先来回顾一下ReentrantLock锁的使用方式,我们创建一个锁之后调用lock()方法就相当于使用了锁,然后我们通常在finally代码块中释放锁,这里需要注意之所以在finally代码块中释放锁,是担心你的代码出现异常没人释放锁,就造成了死锁的情况。
import java.util.concurrent.locks.ReentrantLock;
/**
* @author JiaWei
*/
public class ReentrantLockDemo {
ReentrantLock lock = new ReentrantLock();
try{
//代码
//获取锁
lock.lock();
}finally{
//释放锁
lock.unlock();
}
}
ReentrantLock是支持公平锁和非公平锁,直接上源码,源码中分别是有参和无参,如果在创建时是无参创建,那么就是非公平锁,如果你传参了并且为true那么就是公平锁,由于ReentrantLock是基于AQS实现的具体公平锁和非公平锁的说明上述章节有所体现,需要注意的是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
NonfairSync继承Sync,而Sync继承AbstractQueuedSynchronizer ,所以ReentrantLock是基于AQS实现的。
abstract static class Sync extends AbstractQueuedSynchronizer {
}
说主题ReentrantLock的实现原理,ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,如图所示,此时线程一进来之后通过CAS原子性操作,修改State属性为1,然后Owner就会指向当前线程,也就是说明此线程占有锁成功,此时如果有线程二线程三也来获取锁就会获取失败然后进入到双向队列中等待,线程二为头线程三为尾,然后线程一执行完成释放锁之后,Owner就会唤醒双向队列中等待的线程,首先唤醒线程二其次是线程三,这是在公平锁的情况下,如果在创建ReentrantLock时指定为为非公平锁,那么此时有线程四过来的情况下就会和双向队列中的线程二进行争抢。
17.synchronized和Lock锁的区别?
1、synchronized是一个关键字通过c++ 语言实现,Lock是接口通过Java语言实现,并且继承AQS。
2、synchronized 不需要手动释放锁,而Lock需要手动调用unlock()方法释放锁,一般是在finally下释放,如果使用不当可能会造成死锁,也就是代码出现异常了然后不能释放锁,然后其他线程还获取不到。
3、synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
4、并且Lock 提供了synchronized 不具备的功能,例如公平锁,synchronized是非公平锁,但是lock锁在创建时可以指定为true也就是公平锁。
18.死锁产生条件的原因
死锁说白了就是指两个或多个线程无限期地等待对方释放资源的现象。通俗地说,就是这些进程互相等待对方先结束,结果谁都不肯先结束,导致大家都等待下去,无法继续执行。
如下代码的意思
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
控制台输出的结果,可以看到程序并没有停止运行,也就是产生了死锁。
/**
* @author JiaWei
*/
public class LockDemo {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
System.out.println("lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println("lock B");
System.out.println("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println("lock B");
try {
sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println("lock A");
System.out.println("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
步骤如下:
第一:查看运行的线程
第二:使用jstack查看线程运行的情况,下图是截图的关键信息
运行命令:`jstack -l 46032`
其他解决工具,可视化工具
-
jconsole
用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
-
VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
19.ConcurrentHashMap原理
我们都知道ConcurrentHashMap对比于HashMap是线程安全的,下面我来说一下他的原理,这里主要说JDK1.8数据结构跟HashMap数据结构一样数组+链表+红黑树,采用 CAS + Synchronized来保证并发安全进行实现,在多线程向数组添加数据时采用CAS自旋锁的方式,谁能首先添加成功,其他线程就进行同步保障数据的安全,假如现在添加的是链表或者红黑树,那就在添加数据的首节点用Synchronized锁住,这时候锁住的这个节点只能有一个线程进来,但是其他节点是可以进行操作的,这样就保证了线程安全。
20.ThreadLocal底层原理
ThreadLocal 实现了线程内的资源共享,在同一个线程内,在任意位置,都可以获得到ThreadLocal存储的值,并且多个线程之间是互相隔离的,他的实现原理是这样的,在ThreadLocal中有一个内部类叫做ThreadLocalMap他的实现类似于hashMap,在里面有一个Entry数组她负责来存储数据,其中Key就是当前的ThreadLocal对象value就是当前存储的数据。
ThreadLocal有三个方法分别是set存储数据,get获取数据,remove删除数据
1.调用set 方法,就是以 ThreadLoca 自己作为 key,资源对象作为value,放入当前线程的 ThreadLocalMap 集合中
2.调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
3.调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
set方法
get/remove方法
21.并发编程三大特性
其实并发编程的的三大特性,说的就是多线程编程下导致并发程序出现的三个问题以及如何解决的,分别是原子性、可见性、有序性
原子性
原子性是指并发操作要么全部完成,要么全部不完成,不会出现部分完成的情况。简单来说,就是多个线程同时对数据进行操作时,数据的状态不会出现混乱或者错误。我们看代码,20个线程去抢10张票,如果不加锁会就出现超卖,一张票被多个线程抢到,加上了锁就保证了原子性,解决原子性问题主要通过CAS自旋锁的方式实现,至于CAS上面有章节单独讲解。
/**
* @author JiaWei
*/
public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
ticketDemo.getTicket();
}).start();
}
}
}
可见性
可见性说的就是一个线程对共享变量的修改对另一个线程可见,看代码如果我们不加volatile此时线程一对静态变量flag的修改线程二是看不见的,加了就能够看见
/**
* @author JiaWei
*/
public class Thread {
public class VolatileDemo
private volatile static boolean flag = false,
public static void main(Stringl]args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
System.out.println("第一个线程执行完毕.");
}).start();
Thread.sleep(100) new Thread(() -> {
flag = true,
System.out.printn("第二线程执行完毕.");
}).start();
}
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行,不受并发操作的影响,在并发环境中,有一个指令重排的机制,处理器为了提高程序的运行效率,可能会对输入的代码进行优化,执行顺序未必是按照你编写代码的顺序执行的,有可能会打乱顺序执行,如下代码如果不加volatile执行顺序就乱了,加上了就好了,至于volatile上面有专门章节讲解。
/**
* @author JiaWei
*/
public class Thread {
volatile int x, int y;
@Actor
public void actor1() {
X = 1:
y = 1;
@Actor
public void actor2 (ll Result r){
r.r1 = y
r.r2 = X
}
线程池知识
22.为什么要用线程池?
我的理解线程池的本质是一种池化技术,而池化技术是一种资源复用的一个设计思想。而线程池里面复用的是线程资源。它的核心设计目的我认为有两个,第一个是它可以减少线程的频繁创建和销毁带来的性能开销,因为线程创建会涉及到 CPU 的上下文切换以及内存的分配这样一些工作。第二个是线程池本身会有一些参数来控制线程的创建数量,这样的话可以去避免无休止的创建线程带来的资源利用率过高的问题,所以它可以起到资源保护的一个作用。
然后上面提到的线程池里面的线程复用技术,因为线程本身并不是一个受控的技术,为啥这么说呢是因为线程的生命周期是由任务的运行状态来决定的,无法人为控制。所以为了实现线程的复用,线程池里面用到了阻塞队列,简单来说就是线程池里面工作的线程,它会去从阻塞队列里面去获取一个待执行的任务,一旦队列空了,那么这个工作线程就会被阻塞,直到下一次有新的任务进来。也就是说,工作线程是根据任务的情况来决定阻塞和唤醒,从而去达到线程复用的一个目的。
23.线程池七大核心参数
-
corePoolSize 核心线程数目
-
maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
-
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
-
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
-
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
-
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
-
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
24.线程池执行流程
1、当一个任务提交,首先判断核心线程数满没满,如果没满就创建一个线程去执行
2、如果核心线程数满了就判断阻塞队列满没满,如果没满就添加到阻塞队列中
3、如果阻塞队列满了那么就判断有没有达到最大线程数,如果没达到就由最大线程数创建线 程然后去执行。
4、如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有, 则使用非核心或者核心线程去执行任务
4、如果最大线程数满了,此时线程池没有能力去处 理新的任务就会进行拒绝策略。
25.四种拒绝策略
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程也就是主线程来执行任务
3.DiscardOldestPolicy:丢弃队列中最早进来的的任务,执行当前新的任务;
4.DiscardPolicy:直接丢弃任务
26.阻塞队列
阻塞队列我们知道就是线程池没有线程能够执行任务后,新进来的任务就会去阻塞队列中等待,那么阻塞队列一共有四种,这里我们主要说前两种常用的,第一个是ArrayBlockingQueue它是基于数组实现,并且必须指定队列大小,第二是LinkedBlockingQueue它是基于链表实现的,可可以指定队列大小也可以不指定如果不指定他的最大值就是Integer的最大值,其中他俩最大的区别是,在链表中头尾都上了锁也就是说一边可以入队一边可以出队,两个操作互不影响,效率相对来说是比较高的,数组是一把锁入队和出队都是这一把锁,相对于链表的两把锁来说效率是比较低的,所以开发中用的最多的也是LinkedBlockingQueue阻塞队列,但是千万要记得指定队列大小
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执 行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
27.如何确定核心线程数
在设置核心线程数之前,我们需要清楚一台电脑同时能够有几个线程执行取决于CPU是几核心,那么线程池执行任务的类型,主要分为两种
IO密集型任务
一般来说对于文件的读写、DB读写、网络请求等就是IO密集型任务,这种任务设置核心线程数我们通常采用公式计算,即核心线程数大小 = 2 * 计算机核心数 + 1;
CPU密集型任务
如果是计算型代码、JSON转换、就是CPU密集型任务,这种任务设置核心线程数采用,计算机CPU核心数 + 1,减少线程上下文切换带来的开销。
/**
* @author JiaWei
*/
public class Thread {
public static void main(String[] args) {
//查看机器的CPU核心数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
28.线程池的种类
newFixedThreadPool(适用于任务量已知,相对耗时的任务)
- 核心线程数与最大线程数一样,没有救急线程
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
/**
* @author JiaWei
*/
public class a {
public static void main(String[] args) {
public static ExecutorService newFixedThreadPool ( int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
}
}
newSingleThreadExecutor(适用于按照顺序执行的任务)
- 核心线程数和最大线程数都是1
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
/**
* @author JiaWei
*/
public class a {
public static void main(String[] args) {
public static ExecutorService newSingleThreadExecutor (){
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));}
}
}
newCachedThreadPool(可缓存线程池,适合任务数比较密集,但每个任务执行时间较短的情况)
- 核心线程数为0
- 最大线程数是Integer.MAX_VALUE
- 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
/**
* @author JiaWei
*/
public class a {
public static void main(String[] args) {
public static ExecutorService newCachedThreadPool () {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
}
newScheduledThreadPool(可以执行延迟任务的线程池,支持定时及周期性任务执行)
/**
* @author JiaWei
*/
public class a {
public static void main(String[] args) {
public ScheduledThreadPoolExecutor( int corePoolSize){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor( int corePoolSize, ThreadFactory threadFactory){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor( int corePoolSize, RejectedExecutionHandler handler){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor( int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);
}
}
}
29.使用ThreadPoolExecutor创建线程池的原因
阿里的Java开发手册是这样说的,使用ThreadPoolExecutor创建线程池能够根据业务及硬件的配置更加明确的设计线程池,反之其他线程如果不指定阻塞队列大小默认就是Integer.MAX的最大值,如果使用不当就会造成OOM也就是内存溢出
线程池使用场景
30.数据汇总
在日常开发中数据汇总是一个很常见的需求,比如在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息我们需要对其汇总返回给客用户;这三块信息都在不同的微服务,并且由于查询的数据量大小,和网络延迟的影响,对应的查询的时间也不一样,像这种情况我们就可以使用线程池异步执行,一个查询对应一个线程并行执行,将同步改为异步,查询时间取最长的那个,而不是之前查询的总和。下面有图和代码
线程池执行
@SneakyThrows
@GetMapping("/get/detail_new/{id}")
public Map<String, Object> getOrderDetailNew() {
long startTime = System.currentTimeMillis();
Future<Map<String, Object>> f1 = executorService.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);
return r;
});
Future<Map<String, Object>> f2 = executorService.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);
return r;
});
Future<Map<String, Object>> f3 = executorService.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);
return r;
});
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("order", f1.get());
resultMap.put("product", f2.get());
resultMap.put("logistics", f3.get());
long endTime = System.currentTimeMillis();
log.info("接口调用共耗时:{}毫秒",endTime-startTime);
return resultMap;
}
非线程池
@SneakyThrows
@GetMapping("/get/detail/{id}")
public Map<String, Object> getOrderDetail() {
long startTime = System.currentTimeMillis();
Map<String, Object> order = restTemplate.getForObject("http://localhost:9991/order/get/{id}", Map.class, 1);
Map<String, Object> product = restTemplate.getForObject("http://localhost:9991/product/get/{id}", Map.class, 1);
Map<String, Object> logistics = restTemplate.getForObject("http://localhost:9991/logistics/get/{id}", Map.class, 1);
long endTime = System.currentTimeMillis();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("order", order);
resultMap.put("product", product);
resultMap.put("logistics", logistics);
log.info("接口调用共耗时:{}毫秒",endTime-startTime);
return resultMap;
}
31.异步缓存
在开发中,我遇到过这样一个需求,大概是用户搜索完成后的记录,我们需要将他缓存至Redis作为搜素历史记录,这种接口我们通常会想这个简单在搜索接口拿到搜索关键词直接缓存就完事了这样确实可以,但是这是一个同步任务,如果我们缓存到Redis过程中网络出现问题比较耗时,那么这个接口就迟迟不能响应用户,那么我们就可以将搜索和保存使用线程池进行解构,如下所示。