前言

面试常考的四大并发实战案例一次性讲透!本文覆盖单例模式、阻塞队列、定时器、线程池四大核心模块,从理论原理到从零手写代码,串联线程安全、阻塞通信、任务调度、线程复用四大并发核心思想,零基础也能看懂,面试复习一站式收藏。

单例模式

一个非常经典的设计模式。

单例:单个实例(对象)。
代码中,有很多用于管理数据的对象,就应该是“单例”的。
此时,需要通过编码技巧,让编译器监督,强制要求,确保这个对象不会出现多个。

单例模式代码的核心要点

1 在类的内部,提供一个现成的实例。
2 把构造方法设置为 private,避免其它代码能够创建出实例。

通过上述方式,就强制了在使用这个类时不会创建出多个对象了。

饿汉模式—代码示例

//单例模式
//期望这个类能够有唯一一个实例
class Singleton{
	//static 成员 --- 在 Singleton 类被加载时,就会执行到这里的创建实例的操作
	private static Singleton instance = new Singleton();
	
	//通过这个方法来获取到刚才的实例
	//后续如果想使用这个类的实例,都通过 getInstance 方法来获取
	public static Singleton getInstance() {
		return instance;
	}
	
	//把构造方法设置为私有,此时类外面的其它代码,就无法 new 出这个类的对象了
	private Singleton(){
		
	}
}

public class Demo1 {
	public static void main(String[] args) {
		//Singleton 实例的使用
		Singleton s1=Singleton.getInstance();
		Singleton s2=Singleton.getInstance();
		//无论 getInstance 使用多少次,得到的对象都是同一个
		System.out.println(s1==s2);
	}
}

运行结果为:

true

注意:这里,Singleton 实例的创建时机,是在类加载的时候。— 饿汉模式(比较急切)。

与饿汉模式相对的是懒汉模式(在第一次使用时再创建实例)。

懒汉模式—代码示例

//懒汉模式
class SingletonLazy{
	private static SingletonLazy instance =null;
	
	//在首次调用 getInstance 时才会真正去创建实例,如果不调用,就不创建
	public static SingletonLazy getInstance() {
		if(instance == null) {
			instance = new SingletonLazy();
		}
		return instance;
	}
	
	private SingletonLazy() { }
}

饿汉模式 VS 懒汉模式

两种模式都是通过一个静态成员,把这个实例获取到,把构造对象设为private。
不同点:饿汉的创建时机比较早,在类加载时就创建出来了;懒汉的创建时机比较晚,在首次使用时才创建出来。

首先打个比方,文本编辑器,现在需要打开一个非常大的文件,有两种方式:

  1. 先把所有的内容都加载到内存中,然后再显示内容。— 饿汉模式
  2. 只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其他内容。— 懒汉模式

思考:刚刚写的饿汉模式和懒汉模式两种单例的写法,这两种写法是否是线程安全的?也就是说如果多个线程同时调用 getInstance,是否会出现问题?

  1. 饿汉模式线程安全。
    getInstance:只是读取,不会修改。
    若多个线程,同时读取同一个变量,不会有线程安全问题。
  2. 懒汉模式线程不安全。
    getInstance:既会读取,也会修改
    若多个线程,同时修改同一个变量,会有线程安全问题。

在这里插入图片描述
思考:如何保证懒汉模式是线程安全的呢?

加锁。

思考:锁要加到哪里?

锁,不是加了就线程安全,一定要确保锁加的位置正确,还要确保锁的对象也得是能够起到合理的锁竞争的效果。
bug 是由于if 判断语句和创建实例这两个操作被穿插形成的,因此加锁的关键就在于要确保这两个操作是一个整体。

class SingletonLazy{
	private static SingletonLazy instance =null;
	public static SingletonLazy getInstance() {
		synchronized (SingletonLazy.class) {
			if(instance == null) {
				instance = new SingletonLazy();
			}
		}
		return instance;
	}
	private SingletonLazy() { }
}

注意:一旦代码这么写,后续每次调用 getInstance,都需要先加锁了(加锁开销很大,并且可能引起阻塞等待),但是实际上,懒汉模式下的线程安全问题只是出现在最开始(对象还没 new)的时候,一旦对象 new 出来了,后续多线程调用 getInstance,就只有读操作,就不会线程不安全了。

一旦某个代码涉及到加锁,基本和“高性能”无缘了。

思考:是否有办法,既可以让代码线程安全,又不会对执行效率产生太多影响呢?

在加锁语句的外层,再引入一个 if 条件,判定一下,看看当前这里的锁,是否要加上。
如果对象已经有了,线程就安全了,此时就可以不加锁了。
如果对象还没有,存在线程不安全的风险,就需要加锁。

class SingletonLazy{
	private static SingletonLazy instance =null;
	
	public static SingletonLazy getInstance() {
		//这个if语句判断是否要加锁
		if(instance == null) {
			synchronized (SingletonLazy.class) {
				//这个if语句判断是否要new对象
				if(instance == null) {
					instance = new SingletonLazy();
				}
			}
		}
		return instance;
	}
	
	private SingletonLazy() { }
}

这两个 if 语句虽然判断条件相同,但是执行时机、执行结果、作用可能会差异很大。
但是指令重排序可能会对上述代码产生影响。

指令重排序

指令重排序,是在保证逻辑不变的前提下,编译器为了执行效率,可能会调整原有代码的执行顺序。

new操作,可能会触发指令重排序。

new操作分三步:

  1. 申请内存空间
  2. 在内存空间上构造对象
  3. 把内存的地址,赋值给 instance 引用

正常顺序:1 -> 2 -> 3
但是,对于编译器来说,步骤2和步骤3之间不存在数据依赖关系,就会发生指令重排序。
就会产生 1 -> 3 -> 2 的顺序。

在单线程下,无论是先执行2还是3,最终的对象都是完整的,结果是一样的,因此允许被重排序。

但是在多线程下,就可能有问题了,假设按照 1 -> 3 -> 2 的顺序来执行,现在有两个线程 t1和 t2 在执行上述代码,当 t1执行完1和3时,此时 instance 非空,指向的是一个未初始化的非法对象,这时 t2 执行,判定 instance == null 条件不成立,直接 return instance,进一步 t2 线程就可能会访问 instance 里面的属性和方法,就会出现bug。

思考:t1 执行到 new 的过程中,其实是先加了锁的,既然已经加了锁,t2 还能执行吗,还穿插进来吗?

t2 线程执行的第一个 if,没有涉及到任何加锁操作,这个 if 是完全可以执行的。锁的阻塞等待,一定是两个线程都加锁的时候才会触发。
由于 t2 执行第一个 if 时,条件不满足,没有进入 if 内部,此时加锁操作没有真正执行,未涉及到锁的阻塞等待。

针对上述问题,解决方法就是 volatile。

让 volatile 修饰 instance,此时就可以保证 instance 在修改的过程中不会出现指令重排序的现象了。

懒汉模式完整代码

class SingletonLazy{
	//使用volatile
	private static volatile SingletonLazy instance =null;
	
	public static SingletonLazy getInstance() {
		//两个if判断
		if(instance == null) {
			//正确加锁
			synchronized (SingletonLazy.class) {
				if(instance == null) {
					instance = new SingletonLazy();
				}
			}
		}
		return instance;
	}
	
	private SingletonLazy() { }
}

本节小结

单例核心依靠私有构造限制实例创建,双重检查锁搭配volatile,同时解决懒汉模式线程安全、指令重排序两大问题,是资源管理器标准落地写法。

阻塞队列

阻塞队列是多线程代码中常用的一种数据结构。
一个特殊的队列:

  1. 线程安全。
  2. 带有阻塞特性。
    (1)如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列中添加元素为止。
    (2)如果队列为满,继续入队列,就会发生阻塞,阻塞到其他线程从队列中取走元素为止。

阻塞队列,可以用来实现“生产者消费者模型”。

生产者消费者模型

生产者,把生产的内容,放到阻塞队列中。
消费者,就会从阻塞队列中获取内容。

解耦合

两个模块,联系越紧密,耦合就越高。
尤其是对于分布式系统来说,解耦合是非常有意义的。
在这里插入图片描述

削峰填谷

峰:短时间内,请求量比较多。
谷:短时间内,请求量比较少。

不同的服务器,访问量一样,跑的业务不同,单个访问,消耗的硬件资源不一样。

在生产者消费者模型中,就会解决该问题。
当服务器A收到了较大的请求量,就会把对应的请求写入到队列中,B仍然可以按照自己的节奏来处理请求。— 削峰。
上述的峰值情况只会短时间存在,过了峰值,A的请求量恢复正常,B就可以逐渐把积压的数据处理掉。— 填谷。

阻塞队列的实现方式与使用

两种实现:

  1. 基于数组—new ArrayBlockingQueue<>();
  2. 基于链表—new LinkedBlockingDeque<>();

使用:

  1. put:阻塞式入队列
  2. take:阻塞式出队列
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo3 {
	public static void main(String[] args) throws InterruptedException {
		BlockingQueue<String> queue=new LinkedBlockingDeque<>();
		queue.put("123");
		queue.put("234");
		queue.put("345");
		queue.put("456");
		String elem=queue.take();
		System.out.println(elem);
		elem=queue.take();
		System.out.println(elem);
		elem=queue.take();
		System.out.println(elem);
		elem=queue.take();
		System.out.println(elem);
		elem=queue.take();
		//在这里会阻塞,queue为空
		System.out.println(elem);
	}
}

实现阻塞队列

如何实现阻塞队列?

基于一个普通的队列,加上线程安全(加锁),加上阻塞(使用 wait + notify)。
普通队列可以基于数组,也可以基于链表。

如何基于数组实现一个队列?

基于数组实现的队列一般是环形队列。
在这里插入图片描述

  1. 初始情况下,队列为空,head 和 tail 是重合的。
  2. 当队列满的时候,head 和 tail 也是重合的。

针对上述情况,有两种解决方案:

  1. 浪费一个格子,让tail指向head的前一个位置,就算满了。
  2. 专门搞一个变量size,来表示元素个数,size为0,就是空,size为数组容量,就是满。

完整代码


//存字符串
class MyBlockingQueue{
	private String[] data = new String[1000];
	
	//加上 volatile,后续的代码中,有的要进行读(判定),有的要进行写(修改 )--- 要避免内存可见性问题
	private volatile int head = 0;//队列的起始位置
	private volatile int tail = 0;//队列的结束位置的下一个位置
	private volatile int size=0;//队列中有效元素的个数
	
	//入队列和出队列
	public void put(String elem) throws InterruptedException {
		//加锁,保证线程安全,锁对象一致就行
		synchronized (this) {
			//入队列之前先看看当前队列是不是满了
			while(size==data.length) {
				//如果队列满,继续插入元素就会阻塞
				this.wait();
				//注意:wait 除了 notify 唤醒之外,还可能因为interrupt方法,中断wait的状态
				//所以判断队列是否为满的条件应该为while循环---当wait返回时,进一步确认一下队列是否为满
				//使用 wait 时,往往都是使用while作为条件判定的方式,目的就是为了让wait被唤醒之后,还能再确认一下条件是否满足
			}
			//队列没满,添加元素
			data[tail++]=elem;
			//如果tail自增后到达了数组末尾,就需要让它回到开头(环形队列)
			if(tail==data.length) tail=0;
			size++;
			this.notify();//用来唤醒take中的wait
		}
		
	}
	//一个队列,put 和 take只有一边能阻塞。
	//如果 put 阻塞了,其他线程继续调用 put也都会阻塞,只能靠 take 唤醒
	//如果 take 阻塞了,其他线程继续调用 take也都会阻塞,只能靠 put 唤醒
	public String take() throws InterruptedException {
		//加锁,保证线程安全,锁对象一致就行
		synchronized (this) {
			//出队列之前先看看是否队列为空
			while(size==0) {
				//如果队列为空,取元素就会阻塞
				this.wait();
			}
			//队列不为空,把队首元素(head)删除掉,并返回
			String ret=data[head++];
			if(head==data.length) head=0;
			size--;
			this.notify();//用来唤醒put中的wait
			return ret;
		}
	}
}
//借助上述阻塞队列,实现一个简单的生产者消费者模型
public class Demo4 {
	public static void main(String[] args) {
		MyBlockingQueue queue=new MyBlockingQueue();
		//生产者、消费者,分别用线程表示
		//消费者
		Thread t1=new Thread(()->{
			while(true) {
				try {
					String elem=queue.take();
					System.out.println("消费元素:"+elem);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
			}
		});
		
		//生产者
		Thread t2=new Thread(()->{
			int num=1;
			while(true) {
				try {
					queue.put(num+"");
					System.out.println("生产元素:"+num);
					num++;
					Thread.sleep(500);
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
			}
		});
		
		t1.start();
		t2.start();
	}
}

本节小结

基于wait+notify实现阻塞环形队列,生产者消费者模型实现业务解耦、流量削峰两大工程核心价值,是并发通信基础容器。

定时器

定时器,软件开发中的一个重要组件。
约定一个时间,时间到了之后,执行某个代码逻辑。
尤其在网络通信时会用到定时器。比如客户端给服务器发送了一个请求,一直没有收到回应,但是客户端不能无限地等,所以设置一个最大等待时间,此时,就需要用到定时器。

定时器的使用

在标准库中,有现成的定时器的实现。

import java.util.Timer;
import java.util.TimerTask;

//定时器
public class Demo5 {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		Timer timer=new Timer();
		//给定时器安排了一个任务,指定在xxx时间执行
		timer.schedule(new TimerTask() {
			//TimerTask实现了Runnable接口
			//使用匿名内部类的写法,继承了TimerTask,并且创建了一个实例,目的是重写run方法,来描述任务的详细情况
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("3000");
			}
		}, 3000);//当前安排的任务在当前时间开始经过多长时间去执行
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("2000");
			}
		}, 2000);
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("1000");
			}
		}, 1000);
		System.out.println("程序启动!");
	}

}

1.主线程执行 schedule 方法时,就是把这个任务放到了 timer 对象中了。
2.与此同时,timer 里面包含一个扫描线程,一旦时间到了,扫描线程就会执行安排的任务。
3.Timer 内部的线程阻止了进程结束,当主线程结束时,timer 还没有结束。
4.Timer里可以安排多个任务。

定时器的实现

1.Timer 中需要有个扫描线程,扫描任务是否到时间去执行了。
2.需要有一个数据结构(优先级队列),把所有的任务都保存起来。
3.创建一个类,通过类的对象来描述一个任务(至少要包含任务内容和时间)。

思考:为什么保存任务的数据结构要用优先级队列呢?

  1. 如果使用数组 ArrayList,此时,扫描线程就需要不停地遍历数组中的每一个任务,判定每个任务是否都到达执行时间。
  2. 由于给 Timer 中添加的任务,都有一个时间属性,一定是时间小的先执行,此时使用优先级队列(可以使用O(1)的时间来获取时间最小的任务),就无需每次遍历所有任务了。
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask>{
	//要执行的任务
	private Runnable runnable;
	//要执行任务的时间,保存绝对时间更方便
	private long time;
	
	//这里的delay,相当于 schedule 方法中的相对时间
	public MyTimerTask(Runnable runnable,long delay) {
		this.runnable=runnable;
		this.time=System.currentTimeMillis()+delay;//绝对时间戳
	}

	@Override
	public int compareTo(MyTimerTask o) {
		// TODO 自动生成的方法存根
		return (int)(this.time-o.time);
	}
	
	public long getTime() {
		return this.time;
	}
	
	public Runnable getRunnable() {
		return this.runnable;
	}
}

class MyTimer{
	//使用优先级队列保存所有任务
	//对于优先级队列来说,要求里面的元素务必是可比较的(还有TreeSet/TreeMap)
	private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
	private Object locker=new Object();
	
	public void schedule(Runnable runnable,long delay) {
		synchronized (locker) {
			queue.offer(new MyTimerTask(runnable, delay));
			locker.notify();
		}
	}
	
	//实现扫描线程
	public MyTimer() {
		Thread t=new Thread(()->{
			//不停扫描队首元素,看是否到达时间		
			while(true) {
				try {
					synchronized (locker) {
						while(queue.isEmpty()) {
							//这里的wait在添加新任务时唤醒
							locker.wait();
						}
						MyTimerTask task=queue.peek();
						//比较当前队首元素是否可以执行了
						long curTime=System.currentTimeMillis();
						if(curTime>=task.getTime()) {
							task.getRunnable().run();
							queue.poll();
						}else {
							//时间没到时,wait阻塞,线程不会在CPU上调度
							//这里使用wait比sleep好,等待过程中,主线程可能会添加一个比此时更早的任务,就会使用notify唤醒
							locker.wait(task.getTime()-curTime);
						}
					}
				} catch (InterruptedException e) {
					// TODO 自动生成的 catch 块
					e.printStackTrace();
				}
			}
		});
		t.start();
	}
}


public class Demo6 {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		MyTimer timer=new MyTimer();
		//给定时器安排了一个任务,指定在xxx时间执行
		timer.schedule(new Runnable() {
			//TimerTask实现了Runnable接口
			//使用匿名内部类的写法,继承了TimerTask,并且创建了一个实例,目的是重写run方法,来描述任务的详细情况
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("3000");
			}
		}, 3000);//当前安排的任务在当前时间开始经过多长时间去执行
		timer.schedule(new Runnable() {
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("2000");
			}
		}, 2000);
		timer.schedule(new Runnable() {
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("1000");
			}
		}, 1000);
		System.out.println("程序启动!");
	}

}

本节小结

利用优先级队列实现定时任务调度,队首最小时间任务优先执行,搭配等待超时机制,兼顾性能与定时精准性。

线程池

线程池,用来优化频繁创建销毁线程的场景。

  1. 协程(轻量级线程),相比于线程,把系统调度的过程省略了。
  2. 线程池—也能使线程不是很慢。

线程池:

  1. 在使用第一个线程时,提前把后面的线程创建好,放在池子里。
  2. 后续如果想使用新的线程,就不必重新创建了,此时创建线程的开销就被降低了。

从池子取,这个动作,是纯粹用户态的动作;而创建新的线程,则是需要用户态+内核态相互配合完成。

线程池的使用

工厂模式

  1. 一种设计模式。
  2. 通常创建对象,使用new,new关键字就会触发类的构造方法,但是构造方法存在一定的局限性。因此,引入了工厂模式。
  3. 很多时候,构造一个对象,希望有多种构造方式,此时只能通过重载的方式区分,但重载要求参数类型/个数不同,因此存在局限性。
  4. 使用工厂设计模式就能解决这个问题。使用普通的方法代替构造方法完成初始化工作,普通方法就可以使用方法的名字来区分了,就不再收到重载的规则制约。
  5. 一般单独写一个类,给这个类写一些静态方法,由静态方法负责构造对象。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo7 {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		
		ExecutorService service=Executors.newFixedThreadPool(4);
		
		//添加任务,把执行逻辑添加到线程池里
		service.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO 自动生成的方法存根
				System.out.println("Hello");
			}
		});
		
		//线程池对象不是直接new,而是通过一个专门的方法,返回了一个线程池对象
		//此时构造出的线程池对象有一个特点:线程数目是能够动态适应的
		//cache:缓存,用过之后不着急释放,先留着以备后续使用
		//随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间
		//ExecutorService service=Executors.newCachedThreadPool();
		/*
		 * ExecutorService service=Executors.newFixedThreadPool(4);
		 * fixed:固定的,指定创建几个线程
		 * 
		 * ExecutorService service=Executors.newSingleThreadExecutor();
		 * 只有一个线程的线程池
		 * 
		 * ExecutorService service=Executors.newScheduledThreadPool(corePoolSize);
		 * 相当于定时器,不是一个扫描线程负责执行任务了,而是多个线程执行时间到的任务
		 * 
		 * 上述的几个工厂方法生成的线程池,本质上都是对一个类进行的封装,ThreadPoolExecutor
		 * 1. 构造---参数很多--经典面试题
		 * 2 添加任务
		 * */
	}

}

构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

  1. int corePoolSize(核心线程数), int maximumPoolSize(最大线程数):描述了线程池中线程的数目;这个线程池里的线程数目是可以动态变化的,变化范围就是[corePoolSize,maximumPoolSize]
  2. long keepAliveTime(规定线程池中非核心线程的最大空闲存活时间), TimeUnit unit(时间单位:ms,s,min…)
  3. BlockingQueue workQueue:阻塞队列,用来存放线程池中的任务,可以根据需要灵活设置这里的队列(比如需要优先级,PriorityBlockingQueue;不需要优先级,任务数目恒定,ArrayBlockingQueue;不需要优先级,任务数目变动较大,LinkedBlocking)
  4. ThreadFactory threadFactory:工厂模式的体现,此处使用ThreadFactory 作为工厂类,由这个类负责创建线程,主要是为了在创建过程中对线程的属性进行设置。
  5. RejectedExecutionHandler handler:线程池的拒绝策略,一个线程池能容纳的任务数量有上限。不同的拒绝策略有不同的效果(比如,直接抛出异常,或者新添加的任务由添加任务的线程负责执行,或者丢弃任务队列中最老的任务,或者丢弃当前新加的任务)

使用线程池,需要设置线程的数目,数目设置多少合适?

在接触到实际的项目代码之前,无法确定。
一个线程执行的代码,主要有两类:

  1. CPU密集型:代码里的主要逻辑是在进行算术运算/逻辑判断;
  2. IO密集型:代码里主要进行IO操作。

假设CPU逻辑核心数为N:

  1. 一个线程所有的代码都是CPU密集型,此时,线程池的数量就不应该超过N。更多的线程反而会增加调度的开销。
  2. 一个线程的所有代码都是IO密集型,此时不吃CPU,设置的线程数就可以是一个较大的值。

代码不同,线程池的设置数目就不同。
正确做法:使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目。

线程池的模拟实现


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class MyThreadPool{
	//任务队列
	private BlockingQueue<Runnable> queue=new ArrayBlockingQueue<>(1000);
	
	//通过这个方法把任务加到队列中
	public void submit(Runnable runnable) throws InterruptedException {
		//此时的拒绝策略是第五种,阻塞等待
		queue.put(runnable);
	}
	
	public MyThreadPool(int n) {
		//创建出n个线程,负责执行上述队列中的任务
		for(int i=0;i<n;i++) {
			Thread thread=new Thread(()->{
				//让这个线程从队列中消费任务并执行
				while (!Thread.currentThread().isInterrupted()) {
			        Runnable runnable;
			        try {
			            runnable = queue.take();
			            runnable.run();
			        } catch (InterruptedException e) {
			            System.out.println(Thread.currentThread().getName()+" 线程被中断,停止工作");
			            break; // 中断后跳出循环,安全结束线程
			        }
			    }
			},"池工作线程-"+i);
			thread.start();
		}
	}
}

public class Demo8 {

	public static void main(String[] args) throws InterruptedException {
		// TODO 自动生成的方法存根
		MyThreadPool myThreadPool=new MyThreadPool(4);
		for(int i=0;i<1000;i++) {
			int id=i;
			myThreadPool.submit(new Runnable() {
				@Override
				public void run() {
					//i 在匿名内部类中,捕获了外部类的变量,变量捕获只能捕获事实final,不能直接用i
					System.out.println("执行任务"+id);
				}
			});
		}
	}

}

本节小结

核心依靠循环常驻线程复用 + 阻塞队列缓冲任务,避免频繁创建销毁线程的内核开销,是高并发性能优化核心方案。

全文总结

本文一站式拆解 Java 并发四大面试核心实战组件,从底层原理到手写完整代码全覆盖:

  1. 单例模式:饿汉 / 懒汉两种实现,层层递进解决线程安全、指令重排问题,掌握volatile可见性与有序性落地;
  2. 阻塞队列:手写环形阻塞队列,理解阻塞等待机制,吃透生产者消费者模型在分布式系统解耦、削峰填谷的落地场景;
  3. Timer 定时器:原生 API 使用 + 自主实现优先级调度定时器,理解扫描线程、任务优先级调度底层逻辑;
  4. 线程池:原生线程池七大核心参数、拒绝策略、线程数调优思路,搭配极简手写线程池,吃透线程复用核心设计思想。

本文结合实战案例拆解 Java 并发高频核心考点,所有配套代码均可直接复制运行。后续会持续更新 Java 多线程进阶、锁机制、CAS、JUC 组件等并发干货,欢迎点赞收藏,一起交流技术、共同进步!

🔗 系列文章导航

本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:

🔹 上一篇:Java并发编程(四)|线程安全:如何让你的并发代码不“打架”
🔹 下一篇:Java 并发编程(六)|并发进阶高频:CAS、锁升级
👉 点击直达「Java并发编程」专栏合集

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐