目录案例一单例模式1.饿汉模式2.懒汉模式2.1单线程背景下的懒汉模式2.2多线程背景下的懒汉模式案例二阻塞队列1.阻塞队列说明2.阻塞队列语法3.阻塞队列实战案例三定时器1.定时器说明2.定时器语法3.定时器实战案例四线程池1.线程池说明2.线程池语法3.线程池实战案例一单例模式说明如其名单例模式就是一个程序只允许某个类型创建一个对象只允许有一个实例。可以想象如果一个类背后实现了非常多的数据当程序员创建了这个类的对象时需要消耗很多时间占用很多资源才能把这个对象创建好如果有人失误在代码中多new了这种对象不敢想象要浪费多少资源。单例模式是一种设计模式。单例模式的具体实现方式有很多最常见的就是“懒汉”和“饿汉”两种。1.饿汉模式public class HungryMode { private static HungryMode instance new HungryMode(); public static HungryMode getInstance(){ return instance; } //重要步骤使用private修饰构造方法禁止创建实例 private HungryMode(){ } } class Main{ public static void main(String[] args) { HungryMode instance HungryMode.getInstance(); //报错 // HungryMode instance2 new HungryMode(); } }说明正所谓饿汉他一定很饥饿看见食物就立马吃掉就像上述代码在创建成员变量instance时直接给成员变量赋值。2.懒汉模式由于懒汉的实现方式在不同背景下代码书写方式不同2.1单线程背景下的懒汉模式public class LazyMode { private static LazyMode instance null; public static LazyMode getInstance(){ return instance new LazyMode(); } //重要一步没有这一步就不是单例模式了 private LazyMode(){ } } class Main2{ public static void main(String[] args) { LazyMode lazyMode LazyMode.getInstance(); //报错 // LazyMode lazyMode1 new LazyMode(); } }说明之所以叫懒汉就是当正真要获取对象时才实例对象就像上述getInstance()打个比方生活中两口子吃完饭把全部的4个碗丢到洗碗槽等到下次需要多少个盘子就洗几个盘子出来用这样效率其实就高很多因为后面我可能只会用到2个盘子3个盘子永远不用到4个盘子如果一吃完就洗完盘子就多余洗那几个浪费时间浪费资源。注意在计算机中懒是褒义词。2.2多线程背景下的懒汉模式上述单线程下的懒汉模式代码在多线程背景下使用会出现实例多个对象的现象。public class LazyMode { private static LazyMode instance null; public static LazyMode getInstance(){ return instance new LazyMode(); } //重要一步没有这一步就不是单例模式了 private LazyMode(){ } } class Main2{ public static void main(String[] args) { Thread t1 new Thread(()-{ LazyMode lazyMode LazyMode.getInstance(); //打印哈希值 System.out.println(lazyMode); }); Thread t2 new Thread(()-{ LazyMode lazyMode LazyMode.getInstance(); System.out.println(lazyMode); }); t1.start(); t2.start(); } }每个线程getInstance()方法都会实例一个新对象这就不是单例模式了因此要设计一个在多线程背景下能正常运行的懒汉模式。public class MultithreadingLazyMode { private static MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ synchronized(lock){ //如果instance还未被赋值 if(instancenull){ return instance new MultithreadingLazyMode(); } return instance; } } } class Main3{ public static void main(String[] args) { Thread t1 new Thread(()-{ MultithreadingLazyMode instance MultithreadingLazyMode.getInstance(); System.out.println(instance); }); Thread t2 new Thread(()-{ MultithreadingLazyMode instance MultithreadingLazyMode.getInstance(); System.out.println(instance); }); t1.start(); t2.start(); } }大家觉得这个代码有问题吗有的话问题出在哪问题这样的代码使用起来效率太低了每次在getInstance()获取对象时都会阻塞一下所以需要改进。修改后public class MultithreadingLazyMode { private static MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ //第一个if是判断是否需要加锁不需要则返回对象 if(instancenull){ synchronized(lock){ //第二个if是判断是否需要给引用new对象。这个if也是很关键的如果没有第二层if,其他线程进入外层if代码块 // 刚好另个线程给引用创建好实例了后面其他线程又会给引用进行实例对象 if(instancenull){ return instance new MultithreadingLazyMode(); } } } return instance; } }上述两个if的作用是不同的相信大家仔细想想还是很轻松就能理解的。我们再看看修改后的代码效率的问题解决了那安全问题是否存在呢“线程安全问题”。仔细一看天塌了。instance new MultithreadingLazyMode() 语句存在指令重排序问题new MultithreadingLazyMode() 在操作系统中正常执行顺序分为三步操作申请内存在空间上构造对象内存空间的首地址赋值给引用正常来说执行顺序是123但也有可能出现132的情况。在132的基础上可能发生图中的情况。注意引用是被static修饰的成员变量也就是类变量了为了避免发生上面的“指令重排序问题”使用volatile修饰引用最终多线程下的懒汉模式代码public class MultithreadingLazyMode { private static volatile MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ //第一个if是判断是否需要加锁不需要则返回对象 if(instancenull){ synchronized(lock){ //第二个if是判断是否需要给引用new对象。这个if也是很关键的如果没有第二层if,其他线程进入外层if代码块 // 刚好另个线程给引用创建好实例了后面其他线程又会给引用进行实例对象 if(instancenull){ return instance new MultithreadingLazyMode(); } } } return instance; } }案例二阻塞队列1.阻塞队列说明说明阻塞队列是一种特殊队列也满足“先进先出”原则。阻塞队列是一种线程安全的数据结构它具有以下特点当队列满时再向队列添加元素就会产生阻塞等待直到其他线程从队列拿走元素。当队列为空再向队列取元素会产生阻塞等待直到其他线程向队列中添加元素。阻塞队列的一个典型应用场景就是“生产者消费者模型”。生产者消费者模型很好理解。比如家中三个人包饺子我擀皮另两个人包我生产皮给另两个人我就是生产者他们就是消费者。我擀的比较快他们包的慢就会产生阻塞等待这时候我就等他们把堆起来的皮先使用掉我才能继续擀我擀的慢则反之。下划线部分就是阻塞队列作用生产者消费者模型应用在企业中如客户端与服务器的生产消费关系。客户端浏览器等发送请求给服务器服务器处理完请求然后返回响应给客户端你以为发送的请求立马就被处理了其实并不是。发送的请求被一个专门接收请求的服务器接收后再把请求交给另一个正真对请求做处理的服务器如图注意服务器处理请求会消耗硬件资源包括不限于CPU、内存、硬盘、网络带宽资源。注意阻塞队列太重要了甚至会把队列做成单独的服务器。消息队列中可以有N个队列。如图的服务器搭配会有严重安全隐患。上游服务器A激增海量请求时入口服务器A干的活简单处理单个请求消耗的资源少可能不会挂但是下游服务器B处理请求的逻辑复杂就算服务器B配置再高在面临大量请求时资源消耗殆尽你也得挂所以一般好的企业会在服务器A、服务器B间放入“消息队列”这样就解决大量请求把下游服务器搞挂的问题。如图当服务器A传的请求过多消息队列会阻塞等到队列中的元素被服务器B拿走处理掉一些服务器A才能往消息队列中添加元素请求相反 消息队列中没有元素也会阻塞服务B等到服务器A往里添加元素请求服务器B才能获取请求并处理。2.阻塞队列语法BlockingQueue(阻塞队列)是一个接口实现它的类有ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。BlockingQueue有put、take、offer、poll和peek等方法但带有阻塞效果的只有put和take方法。put方法用于阻塞式入队列如果队列空间满了则阻塞take方法用于阻塞式出队列如队列为空则阻塞。3.阻塞队列实战package BlockingQueue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class TestBlockingQueue { public static void main(String[] args) { //ArrayBlockingQueue的put和take具有阻塞等待特点重写后的offer和poll也有阻塞特点 BlockingQueueString queue new ArrayBlockingQueue(10);//capacity(容量)10个元素 //生产者消费者模型 //生产者 Thread t1 new Thread(()-{ //生产 for (int i 0; i 1000; i) { try { queue.put(i); System.out.println(生产任务i); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); //消费者 Thread t2 new Thread(()-{ while(true){ try { //消费 System.out.println(消费任务queue.take()); Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t1.start(); t2.start(); } }案例三定时器1.定时器说明说明定时器类似于闹钟到某个时间就自动执行一些代码操作如网络通信中对方500ms内没返回数据则断开尝试重连。还比如一个数据3秒后删除。2.定时器语法Timer(定时器)类它的核心方法是scheduleschedule方法的参数类型为TimerTask和long。TimerTask是一个抽象类并实现了Runnable接口如图上述是使用较多的两个schedule方法第一个schedule意为在现在多少毫秒后执行任务第二个schedule意为某个时间点执行任务。3.定时器实战package Timer; import java.util.Timer; import java.util.TimerTask; public class TestTimer { public static void main(String[] args) { //定时器的使用 Timer timer new Timer(); timer.schedule(new TimerTask() { Override public void run() { System.out.println(完成任务); } },3000); } }上述代码schedule方法第一个参数实参是匿名内部类第二个实参为3000。运行程序3秒后打印“完成任务”。除了这种“单线程定时器”还有线程池实现了定时器不了解线程池的先看下面的线程池讲解package Timer; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class TestTimer2 { public static void main(String[] args) { //有一种线程池实现了定时器 ScheduledExecutorService scheduledExecutorService new ScheduledThreadPoolExecutor(4);//线程数目 scheduledExecutorService.schedule(()-{ System.out.println(定时线程池); },1000, TimeUnit.MILLISECONDS); //关闭所有线程 scheduledExecutorService.shutdown(); } }上述代码schedule的第三个实参是时间单位TimeUnit是一个枚举类可以枚举毫秒纳秒等时间单位。案例四线程池1.线程池说明说明线程池中有不少线程可指定数目需要时直接拿来用就行了提高开发效率。在以前每有一个客户端连接服务器就得创建一个线程用完后又销毁线程这样不断创建销毁造成的开销太大于是人们干脆先创建一批线程让这些线程不断执行发来的任务因此线程池就诞生了。线程池小总结线程池最大的优点就是节约了频发创造线程、销毁线程的资源消耗。2.线程池语法Executors(线程池)类标准库中的线程池Executors.newFixedThreadPool(10)能创建出包含10个固定线程数目的线程池。Executors.newCachedThreadPool()创建会根据需要增加线程数目的线程池。这两个方法的放回类型为ExecutorService线程池通过ExecutorService esExecutors.newFixedThreadPool(10)es.submit提交任务到线程池。Executors除了上面两种创建线程池的方法还有许多创建只有一个线程的线程池newSingleThreadExecutor适合执行有顺序要求的任务串行执行。还可以创建能延迟执行任务的线程池newScheduledThreadPool这个方法放回的类和案例三最后“线程池实现定时器”的类是一样的都是ScheduledExecutorService 。3.线程池实战package package1; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo38 { public static void main(String[] args) { //可固定线程数量的线程池 // ExecutorService threadPool Executors.newFixedThreadPool(5); //根据需要自动增加线程数的线程池 ExecutorService threadPool Executors.newCachedThreadPool(); for (int i 0; i 1000; i) { int idi; threadPool.submit(()-{ System.out.println(hello idThread.currentThread().getName()); }); } //shutdown能关闭所有线程池中的线程但并不能保证线程池内的任务一定能执行完 //awaitTermination方法能等到线程池中的任务执行完再关闭所有。 threadPool.shutdown(); } }
JavaEE初阶:多线程案例
目录案例一单例模式1.饿汉模式2.懒汉模式2.1单线程背景下的懒汉模式2.2多线程背景下的懒汉模式案例二阻塞队列1.阻塞队列说明2.阻塞队列语法3.阻塞队列实战案例三定时器1.定时器说明2.定时器语法3.定时器实战案例四线程池1.线程池说明2.线程池语法3.线程池实战案例一单例模式说明如其名单例模式就是一个程序只允许某个类型创建一个对象只允许有一个实例。可以想象如果一个类背后实现了非常多的数据当程序员创建了这个类的对象时需要消耗很多时间占用很多资源才能把这个对象创建好如果有人失误在代码中多new了这种对象不敢想象要浪费多少资源。单例模式是一种设计模式。单例模式的具体实现方式有很多最常见的就是“懒汉”和“饿汉”两种。1.饿汉模式public class HungryMode { private static HungryMode instance new HungryMode(); public static HungryMode getInstance(){ return instance; } //重要步骤使用private修饰构造方法禁止创建实例 private HungryMode(){ } } class Main{ public static void main(String[] args) { HungryMode instance HungryMode.getInstance(); //报错 // HungryMode instance2 new HungryMode(); } }说明正所谓饿汉他一定很饥饿看见食物就立马吃掉就像上述代码在创建成员变量instance时直接给成员变量赋值。2.懒汉模式由于懒汉的实现方式在不同背景下代码书写方式不同2.1单线程背景下的懒汉模式public class LazyMode { private static LazyMode instance null; public static LazyMode getInstance(){ return instance new LazyMode(); } //重要一步没有这一步就不是单例模式了 private LazyMode(){ } } class Main2{ public static void main(String[] args) { LazyMode lazyMode LazyMode.getInstance(); //报错 // LazyMode lazyMode1 new LazyMode(); } }说明之所以叫懒汉就是当正真要获取对象时才实例对象就像上述getInstance()打个比方生活中两口子吃完饭把全部的4个碗丢到洗碗槽等到下次需要多少个盘子就洗几个盘子出来用这样效率其实就高很多因为后面我可能只会用到2个盘子3个盘子永远不用到4个盘子如果一吃完就洗完盘子就多余洗那几个浪费时间浪费资源。注意在计算机中懒是褒义词。2.2多线程背景下的懒汉模式上述单线程下的懒汉模式代码在多线程背景下使用会出现实例多个对象的现象。public class LazyMode { private static LazyMode instance null; public static LazyMode getInstance(){ return instance new LazyMode(); } //重要一步没有这一步就不是单例模式了 private LazyMode(){ } } class Main2{ public static void main(String[] args) { Thread t1 new Thread(()-{ LazyMode lazyMode LazyMode.getInstance(); //打印哈希值 System.out.println(lazyMode); }); Thread t2 new Thread(()-{ LazyMode lazyMode LazyMode.getInstance(); System.out.println(lazyMode); }); t1.start(); t2.start(); } }每个线程getInstance()方法都会实例一个新对象这就不是单例模式了因此要设计一个在多线程背景下能正常运行的懒汉模式。public class MultithreadingLazyMode { private static MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ synchronized(lock){ //如果instance还未被赋值 if(instancenull){ return instance new MultithreadingLazyMode(); } return instance; } } } class Main3{ public static void main(String[] args) { Thread t1 new Thread(()-{ MultithreadingLazyMode instance MultithreadingLazyMode.getInstance(); System.out.println(instance); }); Thread t2 new Thread(()-{ MultithreadingLazyMode instance MultithreadingLazyMode.getInstance(); System.out.println(instance); }); t1.start(); t2.start(); } }大家觉得这个代码有问题吗有的话问题出在哪问题这样的代码使用起来效率太低了每次在getInstance()获取对象时都会阻塞一下所以需要改进。修改后public class MultithreadingLazyMode { private static MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ //第一个if是判断是否需要加锁不需要则返回对象 if(instancenull){ synchronized(lock){ //第二个if是判断是否需要给引用new对象。这个if也是很关键的如果没有第二层if,其他线程进入外层if代码块 // 刚好另个线程给引用创建好实例了后面其他线程又会给引用进行实例对象 if(instancenull){ return instance new MultithreadingLazyMode(); } } } return instance; } }上述两个if的作用是不同的相信大家仔细想想还是很轻松就能理解的。我们再看看修改后的代码效率的问题解决了那安全问题是否存在呢“线程安全问题”。仔细一看天塌了。instance new MultithreadingLazyMode() 语句存在指令重排序问题new MultithreadingLazyMode() 在操作系统中正常执行顺序分为三步操作申请内存在空间上构造对象内存空间的首地址赋值给引用正常来说执行顺序是123但也有可能出现132的情况。在132的基础上可能发生图中的情况。注意引用是被static修饰的成员变量也就是类变量了为了避免发生上面的“指令重排序问题”使用volatile修饰引用最终多线程下的懒汉模式代码public class MultithreadingLazyMode { private static volatile MultithreadingLazyMode instance null; public static Object lock new Object(); public static MultithreadingLazyMode getInstance(){ //第一个if是判断是否需要加锁不需要则返回对象 if(instancenull){ synchronized(lock){ //第二个if是判断是否需要给引用new对象。这个if也是很关键的如果没有第二层if,其他线程进入外层if代码块 // 刚好另个线程给引用创建好实例了后面其他线程又会给引用进行实例对象 if(instancenull){ return instance new MultithreadingLazyMode(); } } } return instance; } }案例二阻塞队列1.阻塞队列说明说明阻塞队列是一种特殊队列也满足“先进先出”原则。阻塞队列是一种线程安全的数据结构它具有以下特点当队列满时再向队列添加元素就会产生阻塞等待直到其他线程从队列拿走元素。当队列为空再向队列取元素会产生阻塞等待直到其他线程向队列中添加元素。阻塞队列的一个典型应用场景就是“生产者消费者模型”。生产者消费者模型很好理解。比如家中三个人包饺子我擀皮另两个人包我生产皮给另两个人我就是生产者他们就是消费者。我擀的比较快他们包的慢就会产生阻塞等待这时候我就等他们把堆起来的皮先使用掉我才能继续擀我擀的慢则反之。下划线部分就是阻塞队列作用生产者消费者模型应用在企业中如客户端与服务器的生产消费关系。客户端浏览器等发送请求给服务器服务器处理完请求然后返回响应给客户端你以为发送的请求立马就被处理了其实并不是。发送的请求被一个专门接收请求的服务器接收后再把请求交给另一个正真对请求做处理的服务器如图注意服务器处理请求会消耗硬件资源包括不限于CPU、内存、硬盘、网络带宽资源。注意阻塞队列太重要了甚至会把队列做成单独的服务器。消息队列中可以有N个队列。如图的服务器搭配会有严重安全隐患。上游服务器A激增海量请求时入口服务器A干的活简单处理单个请求消耗的资源少可能不会挂但是下游服务器B处理请求的逻辑复杂就算服务器B配置再高在面临大量请求时资源消耗殆尽你也得挂所以一般好的企业会在服务器A、服务器B间放入“消息队列”这样就解决大量请求把下游服务器搞挂的问题。如图当服务器A传的请求过多消息队列会阻塞等到队列中的元素被服务器B拿走处理掉一些服务器A才能往消息队列中添加元素请求相反 消息队列中没有元素也会阻塞服务B等到服务器A往里添加元素请求服务器B才能获取请求并处理。2.阻塞队列语法BlockingQueue(阻塞队列)是一个接口实现它的类有ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。BlockingQueue有put、take、offer、poll和peek等方法但带有阻塞效果的只有put和take方法。put方法用于阻塞式入队列如果队列空间满了则阻塞take方法用于阻塞式出队列如队列为空则阻塞。3.阻塞队列实战package BlockingQueue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class TestBlockingQueue { public static void main(String[] args) { //ArrayBlockingQueue的put和take具有阻塞等待特点重写后的offer和poll也有阻塞特点 BlockingQueueString queue new ArrayBlockingQueue(10);//capacity(容量)10个元素 //生产者消费者模型 //生产者 Thread t1 new Thread(()-{ //生产 for (int i 0; i 1000; i) { try { queue.put(i); System.out.println(生产任务i); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); //消费者 Thread t2 new Thread(()-{ while(true){ try { //消费 System.out.println(消费任务queue.take()); Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t1.start(); t2.start(); } }案例三定时器1.定时器说明说明定时器类似于闹钟到某个时间就自动执行一些代码操作如网络通信中对方500ms内没返回数据则断开尝试重连。还比如一个数据3秒后删除。2.定时器语法Timer(定时器)类它的核心方法是scheduleschedule方法的参数类型为TimerTask和long。TimerTask是一个抽象类并实现了Runnable接口如图上述是使用较多的两个schedule方法第一个schedule意为在现在多少毫秒后执行任务第二个schedule意为某个时间点执行任务。3.定时器实战package Timer; import java.util.Timer; import java.util.TimerTask; public class TestTimer { public static void main(String[] args) { //定时器的使用 Timer timer new Timer(); timer.schedule(new TimerTask() { Override public void run() { System.out.println(完成任务); } },3000); } }上述代码schedule方法第一个参数实参是匿名内部类第二个实参为3000。运行程序3秒后打印“完成任务”。除了这种“单线程定时器”还有线程池实现了定时器不了解线程池的先看下面的线程池讲解package Timer; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class TestTimer2 { public static void main(String[] args) { //有一种线程池实现了定时器 ScheduledExecutorService scheduledExecutorService new ScheduledThreadPoolExecutor(4);//线程数目 scheduledExecutorService.schedule(()-{ System.out.println(定时线程池); },1000, TimeUnit.MILLISECONDS); //关闭所有线程 scheduledExecutorService.shutdown(); } }上述代码schedule的第三个实参是时间单位TimeUnit是一个枚举类可以枚举毫秒纳秒等时间单位。案例四线程池1.线程池说明说明线程池中有不少线程可指定数目需要时直接拿来用就行了提高开发效率。在以前每有一个客户端连接服务器就得创建一个线程用完后又销毁线程这样不断创建销毁造成的开销太大于是人们干脆先创建一批线程让这些线程不断执行发来的任务因此线程池就诞生了。线程池小总结线程池最大的优点就是节约了频发创造线程、销毁线程的资源消耗。2.线程池语法Executors(线程池)类标准库中的线程池Executors.newFixedThreadPool(10)能创建出包含10个固定线程数目的线程池。Executors.newCachedThreadPool()创建会根据需要增加线程数目的线程池。这两个方法的放回类型为ExecutorService线程池通过ExecutorService esExecutors.newFixedThreadPool(10)es.submit提交任务到线程池。Executors除了上面两种创建线程池的方法还有许多创建只有一个线程的线程池newSingleThreadExecutor适合执行有顺序要求的任务串行执行。还可以创建能延迟执行任务的线程池newScheduledThreadPool这个方法放回的类和案例三最后“线程池实现定时器”的类是一样的都是ScheduledExecutorService 。3.线程池实战package package1; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo38 { public static void main(String[] args) { //可固定线程数量的线程池 // ExecutorService threadPool Executors.newFixedThreadPool(5); //根据需要自动增加线程数的线程池 ExecutorService threadPool Executors.newCachedThreadPool(); for (int i 0; i 1000; i) { int idi; threadPool.submit(()-{ System.out.println(hello idThread.currentThread().getName()); }); } //shutdown能关闭所有线程池中的线程但并不能保证线程池内的任务一定能执行完 //awaitTermination方法能等到线程池中的任务执行完再关闭所有。 threadPool.shutdown(); } }