1.单例模式
简介:保证创建的类中只有一个实例
1.1饿汉模式
在类加载的时候就创立实例
优点:写法简单,类加载就完成实例化,天然线程安全。
缺点:类加载就创建对象,如果从来不用这个对象,会造成内存浪费,没有懒加载。
1.2懒汉模式
用到的时候才创建对象(懒加载),多线程会创建多个实例
优点:类加载的时候不会创建对象,只有调用getInstance()的时候才去创建实例。如果全程不使用这个单例,对象就不会被创建,节省内存。
缺点:线程不安全
1.3解决懒汉模式线程不安全方法(双层if)
代码示例:
1)外层第一个 if:if(instance == null)
作用:避免每次都进入锁,提升性能
如果没有外层 if,每次调用 getInstance 都要抢锁,退化成性能很差的同步方法懒汉。
2)内层第二个 if:if(instance == null)
作用:防止多个线程等待锁之后重复创建对象
举例:
线程 A、B 同时通过外层if(instance==null),都判定为 null。
A 抢到锁,进入同步块;B 卡在锁外面等待。
A 创建完对象,释放锁。
B 拿到锁,如果没有内层 if,B 直接继续 new,又创建一个新对象!破坏单例。
内层第二次判空,就是为了解决这个场景: B 拿到锁之后,再检查一次 instance 是否为 null,发现已经被 A 创建好了,就不再 new。
3)instance = new SingletonLazy()三步
首先分配内存
构造方法初始化对象
instance 引用指向内存地址
这里必须给instance加volatile,禁止指令重排序。 如果不加 volatile,重排序会变成 1‑3‑2,线程拿到半初始化对象。
2.堵塞队列
2.1概念
阻塞队列,是队列 + 线程阻塞等待。
当队列满,往队列放元素的线程会阻塞等待; 当队列空,从队列取元素的线程会阻塞等待。
Java标准库提供了堵塞队列
BlockingQueue是一个接口,真正实现的类是LinkedBlockingQueue.
put()方法用于堵塞队列的入队列,take()方法用于堵塞队列的出队列
BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.
自己实现的堵塞队列(环形队列):
2.2生产者消费模型
生产者线程:负责生产数据,把数据放入换成区(堵塞队列)
消费者线程:负责从缓冲区取出数据
作用
1)解耦合
生产者和消费者代码、线程互相不直接调用,中间依靠缓冲区交互。一方代码修改,不会直接影响另一方,降低耦合。
2)削峰填谷
生产者、消费者处理速度不一样。生产者生产快,消费者处理慢:数据先存到缓冲区,生产者不会被迫停下来等消费者。
突发大量任务过来,缓冲区可以 “缓存流量”,避免瞬间请求压垮消费者。
类似水库:洪水来了先存水库,下游慢慢放水,抵御流量高峰。当枯水期时,存放的水可慢慢排放
代码示例:
输出:
3.线程池
为什么要使用线程池?
时间大量创建、销毁进程,系统开销很高->引进了多线程,创建、切换、销毁开销,比进程小很多。
新问题:服务器请求量越来越大如果每来一个请求就new Thread(),用完就销毁。
频繁创建销毁线程,依然会带来不小开销。
解决方法就是引入线程池
提前把一批线程创建好,放在池子里保持
有任务了直接从池子里拿现成的线程干活
任务执行完不会销毁,归还到池子里复用
省去反复向操作系统申请创建线程开销,速度更快
3.1核心类ThreadPoolExecutor
参数理解:
1)corePoolSize 核心线程数:只要不关闭线程池,这些线程一直存活。
核心线程(线程池一开始就已经创立的),非核心线程(后续线程池发现任务太多而额外创立的)
2)maximumPoolSize 最大线程数:核心不够 + 队列满了,才新建非核心线程,最多到这个数。
最大线程数 = 核心线程 + 非核心线程
3)keepAliveTime:非核心线程空闲多久就被销毁。
4)workQueue 阻塞队列:任务来了,核心线程忙,任务就放到队列排队。
5)threadFactory:创建线程的⼯⼚,参与具体的创建线程⼯作.通过不同线程⼯⼚创建出的线程相当于 对⼀些属性进⾏了不同的初始化设置
6)handler 拒绝策略:队列满、线程数到达最大值,新任务来了怎么处理。
四种拒绝策略
3.2实现线程池
测试
输出结果
可以看到创建的四条线程在反复执行着1000个任务
4.定时器
概念:定时器就相当于是闹钟,时间一到就执行设定好的任务
Java提供了Timer类,核心方法为schedule
以下是自己实现的定时器