Java多线程四个案例
目录
一,单例模式
单例模式也就是一个进程中只允许有一个实例,是我们平时在开发中比较重要常见的一种模式,我们单例模式一般分为两种,一种是饿汉模式,一种是懒汉模式
饿汉模式:
//饿汉模式
class Singleton{
//static保证了成员或者方法是属于类的,不属于实例,所以你不能通过new实例来调用它,刚刚好保证单例,因为new实例会创建多个对象
private static Singleton instance=new Singleton();
//单例模式最关键的
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
在饿汉模式里面,我们调用的开始就直接创建了唯一实例,所以线程安全,因为没有判断等其他操作,只有读的操作
懒汉模式(原始):
class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
在get方法里面判断了是否创建过实例之后再来操作,但是这样的懒汉模式线程不安全,当我们两个线程同时判定是否为空的时候,可能就会出现创建了两个instance实例,单例模式就失败了,并且new操作会因为指令重排序导致出现问题,指令重排序:编译器的一种底层优化,调整指令执行顺序,使代码效率更高。
(1)就比如new这个操作可以简化理解为三个操作:1:分配内存空间,2:针对空间进行初始化,3:内存空间首地址,赋值到引用变量中。按正常顺序,我们应该是123,但是经过指令重排序会导致变成132,就会传回去一个没有数据的值。
(2)并且外层还有if判断,加上new操作,就会使这个创建操作不是原子的,也会因为线程的随机调度导致线程安全问题,所以我们要在instance变量前面加上volatile修饰,防止指令重排序,并且在if和new的这个操作外层套上一把锁,将两个操作打包为一个整体。
(3)但是我们还需要思考一个问题,当我们第一次创建好实例之后,就不需要每次都使用锁然后判断了,这样会浪费时间和内存空间,所以我们外层需要再使用一层if判断
懒汉模式(改进):
class Singleton2 {
private static volatile Singleton2 instance;
private Singleton2() {
}
public static Singleton2 getInstance() {//双重检查
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
总结一下单例模式注意线程安全需要注意的点1:正确的加锁,2:双重if判断,3:volatile
dataSource实现单例:
//dataSource实现单例模式
class DataSource {
private static volatile DataSource dataSource;
private DataSource() {
}
public static DataSource getInstance() {
if (dataSource == null) {
synchronized (DataSource.class) {
if (dataSource == null) {
DataSource ds=new DataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/java117?characterEncoding=utf8&useSSL=false&serverTimezone=UTC");
dataSource.setUser("root"); // 你的mysql账号
dataSource.setPassword("你的密码"); // 你的mysql密码
dataSource=ds;
}
}
}
return dataSource;
}
}
这里有个注意点,就是我们用来一个ds这样的零时变量,最后再赋值回去,这样的作用是什么:因为有指令重排序这样的问题,所以如果没有这个零时变量,其他线程可能最坏情况下拿到还没初始化,并且还没有set的database对象,因为即使有锁,外线程依旧可以拿到里面的对象,因为DataSource是static修饰的,所有线程是共用的这一个
二,阻塞队列
队列特点:先进先出
阻塞队列的特点:1:线程安全,2:阻塞功能(阻塞队列为空的时候尝试出的时候,队列满尝试入的情况)
阻塞功能可以帮助实现生产者-消费者模型:1:降低资源的竞争,2:解决耦合(耦合就是代码功能的关联性,我们追求低耦合,关联性越低越好),3:削峰填谷(可以使请求高峰以及请求低谷的时候,服务器依旧可以按照自己的节奏来执行)
生产者-消费者的不足:1:单个请求的响应时间会受影响,2:使服务器结构更加复杂,3:这个模型更适合异步

阻塞队列的相关方法展示:
方便分为峰值和谷值来展示
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
//阻塞队列
public class demo8 {
//生产快,但是消费慢,因为消费要有sleep,所以队列就产生了阻塞,每次保证队列只有两个元素
//如果我不设置队列大小,不用阻塞队列,生产者就会疯狂的生成,然后占满内存,抛出异常
public static void main1(String[] args) {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
//生产者
Thread t1=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
blockingQueue.put(i);
System.out.println("生产了"+i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
Thread.sleep(500);
int take=blockingQueue.take();
System.out.println("消费了"+take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
//消费快,生成慢
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
//生产者
Thread t1=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
Thread.sleep(500);
blockingQueue.put(i);
System.out.println("生产了"+i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
int take=blockingQueue.take();
System.out.println("消费了"+take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
对于代码的解释:如果我们不使用阻塞队列进行阻塞的话,会出现
1:瞬时流量洪峰:生产者疯狂生成任务,消费处理不过来 → 队列满,生产者 put 阻塞(防止 OOM)。
什么是OOM:JVM 堆内存已经全部用光,还想继续创建新对象,JVM 直接抛OutOfMemoryError异常,程序直接崩溃。
2:流量低谷:没有新任务产生,队列全部消费完 → 队列空,消费者 take 阻塞,CPU 不轮询空转
什么是空转:线程一直在 CPU 循环做无效判断,啥业务活不干,但是一直占用 CPU,白白消耗 CPU 资源。
阻塞队列就很好地解决了以上的问题
自己实现简单的阻塞队列:
//自己创建的阻塞队列
class MyBlockingQueue{
Object locker=new Object();
private String[] array = new String[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
public MyBlockingQueue(int capacity){
array=new String[capacity];
}
public void put(String elm) throws InterruptedException {
synchronized (locker) {
//队列是满的
while (size == array.length) {
locker.wait();
}
//队列不满开始放东西
array[tail] = elm;
tail++;
if (tail >= array.length) {
tail = 0;
}
size++;
locker.notify();
}
}
public String take() throws InterruptedException {
synchronized (locker) {
//当队列为空的时候
while (size==0){
locker.wait();
}
//当队列不为空的时候
String elm = array[head];
head++;
if (head >= array.length) {
head = 0;
}
size--;
locker.notify();
return elm;
}
}
}
public class demo3 {
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue=new MyBlockingQueue(2);
//生产者
Thread t1=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
Thread.sleep(500);
myBlockingQueue.put(i+"");
System.out.println("生产了"+i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2=new Thread(()->{
for (int i = 0; i < 500; i++) {
try {
String take=myBlockingQueue.take();
System.out.println("消费了"+take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
阻塞我们用到的是wait和notify的组合,因为我们需要达成某种条件才会阻塞,并且达到某种条件才会唤醒,不能单纯的sleep以及join,并且我们这里用到的循环是while,因为while可以有再次判断的机制
三,线程池
当我们刚引入线程的时候,我们会发现线程比进程好太多了,但是接下来我们还是会发现线程的使用开销也大了一些,所以我们再引入了线程池,也就是把线程提前创建好放在里面,这样就比操作系统创建的效率来的更高

这是一台电脑的基本组成,当我们调用start来创建线程的时候,我们会有从用户态到内核态再到用户态的切换,这样会消耗一些时间和内存,但是线程池是纯用户态的操作,会更加节省开销
线程池的构造

我们来介绍一下这里的参数
(1)corePoolSize(核心线程数),maximumPoolSize(最大线程数)
这里涉及到两类线程1:核心线程(在创建线程池当初就已经有了)2:非核心线程(因为任务太多额外创建的),两者相加就是最大线程数
(2)keepAliveTime (数值),unit(单位)
这里描述了非核心现场释放的时机
(3)workQueue(任务队列)
这里放要执行的任务,并且可以保证线程安全,可以让调用者手动指定队列
(4)threadFactory(线程工厂)
工厂设计模式:有一个工厂类,可以不依赖构造方法
可以自己提供工厂类,当然也可以用系统提供的工厂类(里面有两种)
(5)handler(拒绝策略)
任务队列到达上限,就会触发拒绝策略,主要有四种1:抛出异常,
2:调用submit这个线程自己负责来执行,![]()
3:把任务队列中,最老的任务丢弃,把空的留给新的,
![]()
4:把任务队列中,最近添加的任务丢弃,把空的留给更加新的
![]()
线程池的创建
MyThreadFactory myThreadFactory=new MyThreadFactory("消息业务",false);
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(
2,
4,
2,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
myThreadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
package thread;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class MyThreadFactory implements ThreadFactory {
//获取旧的编号,并且进行自增
private final AtomicInteger threadIndex = new AtomicInteger(1);
private final String threadName;
private final boolean isDaemon;
public MyThreadFactory(String threadName, boolean isDaemon) {
this.threadName = threadName;
this.isDaemon = isDaemon;
}
@Override
public Thread newThread(Runnable r) {
//先自定义名字
String threadname= this.threadName + threadIndex.getAndIncrement();
//创建线程
Thread thread = new Thread(r,threadname);
//设置守护线程
thread.setDaemon(isDaemon);
//设置未捕获异常处理器:线程运行抛出异常会进入这里
thread.setUncaughtExceptionHandler((t, e) -> {
System.err.println("【线程异常】线程名:" + t.getName() + ",异常信息:" + e.getMessage());
e.printStackTrace();
});
return thread; // 返回给线程池使用
}
}
这是我们自己创建了一个线程工厂然后传入进去,当然我们也可以用操作系统自带的
不过操作系统还提供了一个更加简单的构造方法,使用Excutor这个工厂类来创建线程池

前面两个是普通线程池,可以自动扩容,三四是指定了线程池的大小,六七十控制在未来的某个时间点执行,其他的可以自行去了解一下
并且我们往线程池中添加任务是使用submit关键字
自己实现固定数量的线程池
//自己创建线程池
class MyThreadPool{
private BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(10);
public void submit(Runnable runnable) throws InterruptedException {
blockingQueue.put(runnable);
}
//添加构造方法
public MyThreadPool(int size){
for (int i = 0; i < size; i++) {
Thread t=new Thread(()->{
try {
while (true) {
Runnable runnable = blockingQueue.take();
runnable.run();}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
t.setDaemon(true);
}
}
}
四,定时器
定时器就和闹钟一样,在规定时间后进行执行
定时器的使用
public static void main(String[] args) throws InterruptedException {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务执行了");
}
},1000);
Thread.sleep(2000);
timer.cancel();
}
自己实现简易定时器
自己实现定时器的主要要点:(1)因为编译器的定时器需要我们传入任务然后时间,所以我们也需要创建一个任务类,里面定义了runnable以及时间(有时间间隔delay,也有任务时间time)
(2)我们用到优先级队列来存储任务,并且在任务类里面添加比较方法,让优先级队列知道是通过时间来进行排序的
(3)通过锁来使操作原子化,并且锁的位置也很重要
(4)通过wait/notify来进行阻塞等待以及唤醒,第一个wait是等待队列不为空,第二个等待是为了等待到达任务的执行时间,两个等待同时因为schedule方法里面的notify来唤醒,因为第一个等待本身就是等待不为空,schedule方法里面添加了方法所以就唤醒,第二个是因为可能新添加的任务时间更早,可能需要重新排序,但是在我们add方法执行后的瞬间就会重新进行排序了,所以同时在这里唤醒就是合理的
import java.util.PriorityQueue;
//自己实现定时器
//先得有一个任务类,定时器中的任务在这里编写以及初始化
class TimerTask implements Comparable<TimerTask>{
private Runnable runnable;
//目的是判定是否到达执行时间
private long time;
//delay是时间间隔
public TimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
//在MyTimer里面通过run方法进行执行
public void run(){
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(TimerTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer {
Object locker =new Object();
private PriorityQueue<TimerTask> queue = new PriorityQueue<>();
public void schedule(Runnable runnable, long delay) {
synchronized (locker){
TimerTask timerTask = new TimerTask(runnable, delay);
queue.add(timerTask);
locker.notify();
}
}
//去执行任务
public MyTimer() {
Thread thread = new Thread(() -> {
while (true) {
synchronized (locker) {
TimerTask timerTask = queue.peek();
try {
while (timerTask == null) {//队列为空
locker.wait();
timerTask = queue.peek();
}
long currentTime = System.currentTimeMillis();
if (currentTime >= timerTask.getTime()) {//到达执行时间
queue.poll().run();
} else {
locker.wait(timerTask.getTime() - currentTime);
}
}catch (InterruptedException e){
throw new RuntimeException("wait被终止");
}
}
}
});
thread.setDaemon(true);
thread.start();
}
}
public class demo12 {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(() ->
System.out.println("定时任务执行了1000"), 1000);
myTimer.schedule(() ->
System.out.println("定时任务执行了2000"), 2000);
myTimer.schedule(() ->
System.out.println("定时任务执行了3000"), 3000);
Thread.sleep(5000);
}
}
更多推荐


所有评论(0)