多线程高并发原理笔记

@[多线程]

程序运行取决于CPU的执行
多线程的目的就是为了提高CPU的使用率

线程生命周期
在这里插入图片描述

1、new Thread()的方法新建一个线程,在线程创建完成之后,
	线程就进入了就绪(Runnable)状态,进入抢占CPU资源的状态
2、线程抢到了CPU的执行权之后,线程就进入了运行状态(Running)
3、该线程的任务执行完成之后(非常态的调用的stop()方法)后,线程就进入了死亡状态。
4、以下几种情况的时候,容易造成线程阻塞:
	1)线程主动调用了sleep()方法时,线程会进入则阻塞状态。
	2)线程主动调用了阻塞时的IO方法时,该方法有一个返回参数,当参数返回之前,线程会进入阻塞状态
	3)线程进入正在等待某个通知时,会进入阻塞状态

sleep()和wait()
在这里插入图片描述
调用对应的notify/signal方法(唤醒)
yield 让出一下CPU

java 线程模型

用户级线程 ULT
	不需要用户态、内核态切换,速度快;
	内核无感知,线程阻塞则进程阻塞
内核级线程 KLT
	有内核上维护了线程表,具有并行处理能力

java线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程, 内核线程于java-Thread是1:1的映射关系。
在这里插入图片描述

创建线程池?
----》减少线程创建、消亡所带来的开销,
且Java线程依赖于内核线程,创建线程需要进行操作系统状态切换
===》重用线程
--》线程池就是一个线程缓存,负责对线程进行统一分配、调优和监控。

什么时候用?
	单个任务处理时间比较短
	需要处理的任务数量很大

线程池优势?
	重用存在的线程,减少线程创建、消亡的开销,提高性能。
	提高响应速度,当任务到达时,任务可以无需等待线程创建就立即执行。
	提高线程的可管理性,可统一分配、调优和监控。

线程池的五种状态

在这里插入图片描述

运行(RUNNING)----》接受任务,执行队列中的任务
关闭(SHUTDOWN)------》不接受新任务,但执行队列中的任务,执行钩子函数onShutdown()
停止(STOP)------>不接受新任务,也不执行队列中任务,打断正在执行的work线程
TIDYING------》当所有的任务均中断,work数量为0了,线程将逐渐收紧为TIDYING,
				然后执行termnated()钩子函数
完全终止(termnated)----》当termnated钩子函数执行完毕,将转变状态为TERMnated

可以通过二进制的高3为判断是哪个状态。

Executor线程池超类接口。
线程池ThreadPoolExecutor

	概念:事先创建若干个可执行的线程放入一个池(容器) 中, 
		需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中, 
		从而减少创建和销毁线程对象的开销
		(降低资源消耗,提高响应速度,提高线程可管理性)

Executors工具类中常用线程池

(1)newSingleThreadExecutor    多任务串行执行场景
		创建一个单线程的线程池。
		核心线程数=最大线程数=1
		
(2)newFixedThreadPool     执行长期任务,性能较好
		创建固定大小的线程池,每提交一个任务创建一个线程,直到达到线程池最大
		核心线程数   最大线程数  --》自定义
		--》适用于负载较重的场景,对当前线程数量进行限制。
		(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
		
(3)newCachedThreadPool   适用于执行很多短期异步的小程序或负载量小的任务
		创建一个可缓存的线程池,大小依赖操作系统(JVM)能创建的最大线程数
		核心线程数=0     最大线程数  无界  -》根据需要创建
		--》适用于负载较轻的场景,执行短期异步任务。
		(时间短,结束快,不会cpu过度切换。)
		
(4)newScheduledThreadPool
		创建一个大小无限的线程池,支持定时/周期性执行任务需求。

内部实现均使用了ThreadPoolExecutor实现;
四个构造方法。

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    
}
	线程池参数:
		courePoolSize 
			指定线程池线程核数。默认情况下,线程池中的线程数为0,
			当有任务来之后,就会创建一个线程去执行任务。
			当线程池的线程数达到corePoolSize后,就会把达到的任务放到缓存队列当中。
		maximumPoolSize
			线程池中最大线程数量
			cpu密集型=cpu核数+1
			IO密集型=CPU核数 / (1 - 阻塞系数)       阻塞系数在 0.8 ~ 0.9之间
		keepAliveTime
			线程池中的线程没有任务执行时最多或保留多久时间会终止。
			默认情况下,只有当线程池中的线程数大于corePoolSize时,
			keepAliveTime才会起作用,即超过corePoolSize的空闲线程,
			在多长的时间内,会被销毁。
		unit 
			参数keepAliveTime的时间单位
		workQueue  推荐有界---无界一旦阻塞,占用内存资源。
			一个阻塞任务队列,用来存储等待执行的任务。3种选择:
			ArrayBlockingQueue;    //使用较少   ,遵循FIFO原则
			LinkedBlockingQueue;      //经常使用,遵循FIFO原则
			SynchronousQueue;     //经常使用
			PriorityBlockingQueue      优先级由任务的Comparator决定
		threadFactory
			线程工厂,主要用来创建线程,一般选择默认即可
		handler  
			拒绝策略(都作为静态内部类在ThreadPoolExcutor中进行实现),
				当任务太多时,如何拒绝任务,如下取值:
			ThreadPoolExecutor.AbortPolicy
				(默认)直接丢弃任务,抛出RejectedExecutionException异常,阻止系统工作
			ThreadPoolExecutor.DiscardPolicy
				丢弃任务,不予任何处理,不抛出异常
			ThreadPoolExecutor.DiscardOldestPolicy
				丢弃最老的一个任务,即队列最前面的任务,
				然后重新尝试执行任务,并重复此过程
			ThreadPoolExecutor.CallerRunsPolicy
				由调用线程处理该任务

为什么要加(阻塞)队列

1、因线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(非线程池缺点第 3 点)
2、创建线程池的消耗较高。(非线程池缺点第 1 点)
3、线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲

为什么先判断队列再最大线程数,而不是先最大再队列?

具体的源码实现有关。
当需要创建线程时,都会调用addWorker()方法,
会调用mainLock.lock()方法来
获取全局锁
,而获取锁就会造成一定的资源争抢
前者呢 ,第1步判断核心线程数时要获取全局锁,第2步判断最大线程数时,又要获取全局锁,
这样相比于先判断任务队列是否已满,再判断最大线程数,就可能会多出一次获取全局锁的过程。
==结论:为了尽可能的避免因为获取全局锁而造成资源的争抢

LinkedBlockingQueue的吞吐量比ArrayBlockingQueue的吞吐量要高。
前者是基于链表实现的,后者是基于数组实现的,正常情况下,不应该是数组的性能要高于链表吗?

两个阻塞队列的源码才发现,
前者读和写操作使用了两个锁,takeLock和putLock,读写操作不会造成资源的争抢,
后者读和写使用的是同一把锁,读写操作存在锁的竞争

线程池工作原理
在这里插入图片描述
线程池静态创建

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

线程池动态创建(阿里禁用Executors静态工厂构建线程池)
Executors是jdk提供的创建线程池的工厂类,默认4种,无需重构

ExecutorService executor = Executors.newCachedThreadPool();

(推荐)自定义调用ThreadPoolExecutor创建线程池

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

Executors的各个方法的弊端:

1)newFixedThreadPool和newSingleThreadExecutor:  (底层实现队列无界)
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM.

2)newCachedThreadPool和newScheduledThreadPool: (最大无界)
主要问题是线程数最多数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM.

线程池参数配置

1.任务的性质:CPU密集型任务(Ncpu+1个线程),IO密集型任务(2xNcpu)和混合型任务。
2.任务的优先级:高,中和低。
3.任务的执行时间:长,中和短。
4.任务的依赖性:是否依赖其他系统资源,如数据库连接。

countDownLatch

存在于java.util.cucurrent包下。
countDownLatch是在java1.5被引入,
一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。

**使一个线程等待其他线程各自执行完毕后再执行。**

是通过一个计数器来实现的,计数器的初始值是线程的数量。
每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,
表示所有线程都执行完毕,然后在闭锁上等待(阻塞队列)的线程就可以恢复工作

CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。 原子性的 。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);
	//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown(); //对计数器进行递减1操作,当计数器递减至0时,
	当前线程会去唤醒阻塞队列里的所有线程。

*CountDownLatch和CyclicBarrier区别:
	1.countDownLatch是一个计数器,线程完成一个记录一个,
		计数器递减,只能只用一次
	2.CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,
		然后继续执行,计数器递增,提供reset功能,可以多次使用

ThreadLocal

提供了线程本地变量,可保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,
每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。

核心机制:
	每个Thread线程内部都有一个Map   (ThreadLocalMap)
	Map里面存储线程本地对象(key)和线程的变量副本(value)
	内部类由由ThreadLocal维护的,负责向map获取和设置线程的变量值。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,
用独立的方式实现了Map的功能,其内部的Entry也独立实现。
**Key是弱引用类型的**,Value并非弱引用(弱引用,生命周期只能**存活到下次GC前**)
	==》就会有个问题:如果key被回收了,就存在一个null-value键值对,
	这个value既无法被访问到,同时如果线程生命周期很长(比如线程池里),
	那么这些null key的强引用关系:
	Thread --> ThreadLocalMap-->Entry-->null--Value导致Value不会回收,造成**内存泄漏。**
	
	===》解决:当调用set、get、remove方法的时候会去扫描key为null的Entry并清除(Entry=null)。
	但是这个并不是100%保证不出问题,如果这个Entry过期了,
	但是线程没有调用set、get或者remove,这个null key的Entry依然会存在,依然是内存泄漏了。
	所以还是要规范,不用了就调用remove清除。
	
Hash冲突怎么解决:
	非链表的方式,而是采用线性探测的方式。
		据初始key的hashcode值确定元素在table数组中的位置,
		如果发现这个位置上已经有其他key值的元素被占用,
		则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
	==》步长加1或减1,寻找下一个相邻的位置
		---》大量不同ThreadLocal对象放入map中时发生冲突或二次冲突--》低效率。

从ThreadLocal的set方法说起,set是用来设置想要在线程本地的数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 尝试获取当前线程内部的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // map不为空,就正常set值
    if (map != null)
        map.set(this, value);
    else
        // 否则就初始化Map
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    // 可以看出,ThreadLocalMap是存储在线程对象里的
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    // new个ThreadLocalMap,key和value分别为当前ThreadLocal对象已经传入的值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

阻塞

高并发

1.限流(接口)
**(1)漏桶算法**

在这里插入图片描述
短时间内有大量突发请求时,输出造成资源浪费。
(2)令牌桶算法
在这里插入图片描述
解决(1)的问题。允许平滑突发限流,平滑预热限流

2.分布式限流
	redis  计数器 限流

百万查询优化

1.请求合并:类似于转化为批量查询--RequestQueue存储每个请求的唯一id

在这里插入图片描述

并发协同工具:new CountDownLatch(THREAD_NUM)
作用:利用countDownLatch.await() 阻塞
eg. 有1000个用户(多线程)查询商品,countDownLatch.await() 阻塞,等待countDownLatch为0 代表所有线程都start ,再运行后续代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值