❣博主主页: 33的博客❣
▶️文章专栏分类:JavaEE◀️
🚚我的代码仓库: 33的代码仓库🚚
🫵🫵🫵关注我带你了解更多线程知识
目录
- 1.前言
- 2.单例模式
- 2.1饿汉方式
- 2.2饿汉方式
- 3.阻塞队列
- 3.1概念
- 3.2实现
- 4.定时器
- 4.1概念
- 4.2实现
- 5.线程池
- 5.1概念
- 5.2实现
- 6.总结
1.前言
在开发过程中,我们会遇到很多经典的场景,针对这些经典场景,大佬们就提出了一些解决方案,我们只需要按照这个解决方案来进行代码的编写,这样就不会写得很差。
2.单例模式
我们先了解什么是设计模式,在开发过程中,我们会遇到很多经典的场景,针对这些经典场景,大佬们就提出了一些解决方案,按照这个方式进行编程,代码就不会很差,就例如下期中的棋谱,如果按照棋谱下棋也是不会下很烂的。
所谓单例就是单个实例(对象),那么怎么保证一个类只有一个对象呢?我们需要通过一些编程技巧来达成这样的效果。
在一个类的内部提供一个实例,把构造方法设置为private避免再构造出新的实例
2.1饿汉方式
饿汉方式:
class Singleton{
private static Singleton instance=new Singleton();
public static Singleton getsingleton(){
return instance;
}
private Singleton(){
}
}
public class Demo17 {
public static void main(String[] args) {
Singleton s1=Singleton.getsingleton();
//Singleton s2=new Singleton();
}
}
上述代码中,创建一个实例的时候在类加载的时候,太早就创建了,那可不可以晚一点呢?
2.2饿汉方式
懒汉方式:
class Singleton2{
private static Singleton2 instance2=null;
public static Singleton2 getInstance2(){
if(instance2==null){
instance2=new Singleton2();
}
return instance2;
}
private Singleton2(){};
}
但懒汉模式是不安全的,如下:
这样就创建了两个实例,所以我们需要对读取和修改进行加锁操作
class Singleton3{
private static Singleton3 instance3=null;
Object lock=new Object();
public static Singleton3 getInstance2(){
synchronized (lock){
if(instance3==null){
instance3=new Singleton3();
}
}
return instance3;
}
}
这样就只有在调用getInstance2方法才会去创建对象,但是又引出了新的问题,其实是否创建对象,只需要在第一次调用这个方法的时候判断,一旦创建好,以后都不用在再去判断了,可这样写,每次调用这个方法都会去判断,这样就消耗不少资源。我们进行优化:
class Singleton3{
private static Singleton3 instance3=null;
Object lock=new Object();
public static Singleton3 getInstance2(){
if(instance3==null){
synchronized (lock){
if(instance3==null){
instance3=new Singleton3();
}
}
}
return instance3;
}
private Singleton3(){};
}
大家以为到这儿,代码完美了吗?其实并不是,在new的时候可能会引起指令重排序问题,那么什么是指令重排序问题呢?指令重排序也是编译器为了提高执行效率,做出的优化,在保持逻辑不变的前提下,可能对编译器做出优化.
例如我们要去一个水果超市买香蕉、苹果、火龙果、猕猴桃四种水果但它们在不同的展区。
优化前:
优化后
在通常情况下,在单线程中,指令重排序,就能够保证逻辑不变的情况下,把程序的效率提高,但在多线程中就不一定了,可能会误判。
new操作是可能触发指令重排序
new可以分为3步:
1.申请内存空间
2.在内存空间上构造对象(构造方法)
3.把内存地址复制给instance引用。
如果内存进程优化:
1.申请内存空间
2.把内存地址复制给instance引用。
3.在内存空间上构造对象(构造方法)
那么该怎么解决这个问题呢?可以使用volatile让其修饰instanse就可以保证,在修改instanse的时候就不会出现指令重排序问题。
class singleton{
private static volatile singleton instance=null;
public static singleton getinstance(){
if (instance==null){
synchronized (singleton.class){
if (instance==null){
instance=new singleton();
}
}
}
return instance;
}
private singleton(){}
}
public class Demo21 {
public static void main(String[] args) {
singleton s1=singleton.getinstance();
}
}
这个时候才算真正的完成一个单例模式。
3.阻塞队列
3.1概念
阻塞队列也是多线程编程中比较常见的一种数据结构,它是一种特殊的队列,它具有线程安全的特点,并且带有阻塞特性。
阻塞队列最大的意义就是用来实现“生产者消费者模型”
例如:一家人在一起包饺子,但是擀面杖只有一个,那么就指定一定人擀饺子皮就称它为生产者,每擀一张皮,就放入圆盘中,其余人都包饺子称为消费者,如果圆盘中没有饺子皮了,消费者就要等待生产,如果圆盘中放满了就要等待生产者就要等待消费。
那么为啥要引入“生产者消费者模型”呢?队我们又有什么好处呢?
1.解耦合:就是降低两个代码块的紧密程度
2.削峰填谷
那么在Java中,怎么实现阻塞队列呢?在标准库里,以及提供了线程的
public static void main(String[] args) throws InterruptedException {
BlockingDeque<Integer> deque=new LinkedBlockingDeque<>();
deque.put(1);
deque.put(2);
deque.put(3);
System.out.println(deque.take());
System.out.println(deque.take());
System.out.println(deque.take());
System.out.println(deque.take());
}
最后一次出队列,队列已经空了,所以就会阻塞:
3.2实现
既然我们已经会使用阻塞队列了,那我们能不能自己实现一下呢?我们底层可以采用循环数组来实现。
public class MyBlockingqueue {
String[] arr=new String[20];
private volatile int head=0;//后续中既会读又会改,为了避免内存可见性+volatile
private volatile int end=0;
private volatile int size=0;
public void put(String elem) throws InterruptedException {
synchronized (this){
if(end==arr.length){
this.wait();
return;
}
arr[end]=elem;
end++;
size++;
this.notify();//唤醒因为队列空导致的阻塞
if(end==arr.length){
end=0;
}
}
}
public String tack() throws InterruptedException {
synchronized (this){
if (size==0){
this.wait();
}
String ret=arr[head];
head++;
size--;
this.notify();//唤醒因为队列满导致的阻塞
if(head==arr.length){
head=0;
}
return ret;
}
}
}
4.定时器
4.1概念
定时器也是日常开发中常见的组件,约定一个时间,时间到达后就会执行某个逻辑。在Java标准库中,有一个线程的标准库。
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("2000");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("1000");
}
},1000);
System.out.println("定时器打开");
}
主线程执行schdule方法时,此时就把任务放到了timer中,此时timer中也也包含一个线程,叫做扫描线程,一旦时间到达就会给改线程安排任务。那么我们能不能自己实现呢?
4.2实现
1.需要定义一个类来描述一个任务,这个任务需要包含时间,和实际任务。
2.需要有一个数据结构,把任务全部存到数据库中
3.Timer中需要一个线程来描述任务是否到达时间
一个任务类:
lass MyTimerTask implements Comparable<MyTimerTask>{
private Runnable runnable;
private long time;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public long gettime(){
return time;
}
public Runnable getrun(){
return runnable;
}
@Override
public int compareTo(MyTimerTask o) {
return (int) (this.time-o.time);
}
}
一个Timer类:
public class MyTimer {
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
Object locker=new Object();
public void schedule(Runnable runnable,long time ){
synchronized (locker){
queue.offer(new MyTimerTask(runnable,time));
locker.notify();
}
}
public MyTimer(){
Thread t=new Thread(()->{
while (true){
try {
synchronized (locker){
while (queue.isEmpty()){
locker.wait();
}
MyTimerTask task= queue.peek();
long curenttime=System.currentTimeMillis();
if (curenttime>=task.gettime()){
task.getrun().run();
queue.poll();
}else {
locker.wait(task.gettime()-curenttime);
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
测试类:
class M{
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2000");
}
},2000);
System.out.println("开始");
}
}
5.线程池
5.1概念
池,这个词,是计算机中一种比较重要的思想方法,很多地方都涉及到,比如内存池,进程池,连接池等等。
线程池,就是指在使用第一个线程的时候就把其他线程线程一并创建好,后续如果想要使用这个其他线程,就不必再重新创建新的线程,直接从线程池中回去即可。
那么为啥线程创建好放在池子里后续再从池子中取,比新建线程的效率更高呢?
从池子中取是纯用户态的操作,而创建线程是用户态+内核态相互配合完成的。如果一段程序是在系统内核中执行的就是叫内核态,否则为用户态。当创建线程时,就需要调用系统api,进入内核进入一系列操作,但操作系统内核不仅仅是给该线程提供服务,也要给其他线程提供服务,那么这个效率就是非常低的了。
在Java标准库中,提供了写好的线程池,直接用即可。
public static void main(String[] args) {
ExecutorService service= Executors.newCachedThreadPool();
}
线程池对象并不是直接new的,而是调用一个方法返回线程池对象Executors.newCachedThreadPool()称为工厂模式。
通常情况下创建一个对象需要用new关键字,new关键字会触发类的构造方法,但构造方法具有局限性,例如:在一个类中,我即能用笛卡尔坐标系来表示一个点,又能有极坐标的方法表示一个:
class point{
//笛卡尔坐标
public point(double x,double y){}
//极坐标
public point(double a,double b){}
}
但如果要在一个类中实现多个构造方法,那么就要保证构造方法的参数不同,或者是类型不同。为了解决构造方法的局限性,我们就使用工厂设计模式。
工厂设计模式就是指,用一个单独的类,再使用静态普通方法代替构造方法做的事情。
class PointFactory{
public static Point MackXY(){
Point p=new Point();
.......
return p;
}
public static Point MackAB(){
Point p=new Point();
.......
return p;
}
}
在构造线程池中也有多种方法:
public static void main(String[] args) {
//线程池是动态的,cache缓存用了之后不立即释放
ExecutorService service= Executors.newCachedThreadPool();
//固定创建几个线程
ExecutorService service1=Executors.newFixedThreadPool(3);
//相当于定时器,但不是一个扫描线程进程操作而是多个线程了
ExecutorService service2=Executors.newScheduledThreadPool(4);
//固定只有一个线程
ExecutorService service3=Executors.newSingleThreadExecutor();
}
上述多种方法都是对ThreadExecutor进行的封装,这个类非常丰富,提供了很多参数,标准库中上述多种方法实际给这个类填写了不同的参数来构造线程。
具体看最后一种构造方法,因为包含了前面三种
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
int corePoolSize:核心线程数
int maximumPoolSize:最大线程数
long keepAliveTime:非核心线程在终止之前等待新任务的最长时间
TimeUnit unit:时间单位
BlockingQueue workQueue:阻塞队列,存放线程池的任务
ThreadFactory threadFactory:用于创建新线程的工厂。
RejectedExecutionHandler handler:线程拒绝策略
RejectedExecutionHandler handler:线程拒绝策略
一个线程池中,能容纳的线程数目已经达到最大上限,继续再添加将有不同的效果:有以下4种效果
1.ThreadPoolExecutor.AbortPolicy:线程池直接抛出异常
2ThreadPoolExecutor.CallerRunsPolicy:新添加的任务由添加任务的线程自己执行
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最老的任务
4.ThreadPoolExecutor.DiscardPolicy:丢弃当前新加的任务
5.2实现
public class MyThreadPool {
//设置任务队列
BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();
//任务放到队列
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//线程执行
public MyThreadPool(int n) throws InterruptedException {
for (int i=0;i<n;i++){
Thread t=new Thread(()->{
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
}
class M{
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool=new MyThreadPool(4);
for (int i=0;i<100;i++){
int id=i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("i="+id);
}
});
}
}
}
6.总结
单例模式,阻塞队列,定时器,线程池,是一些常用的多线程代码,希望同学们能够熟练掌握它们得使用方法,感兴趣的同学也可以自己实现一下。
下期预告:多线程进阶