1、多线程的案例
其设计模式有多种,我们重点学习两种:单例模式和工厂模式。
2、案例一:单例模式
单例:指的是单个实例(或单个对象)。比如,在之前数据库的学习中,接触到的JDBC中,有个DataSource数据源,它就可作为一个单例, 这种需求就是在一个数据库的操作中,有一份数据源就足够了。
单例模式:就是借助编程语言语法的特性,强行限制某个类不能创建多个特性。
如何实现=》强行限制某个类:借助static。用static修饰的成员或者属性都是类成员/类属性。属性变为类属性,此时就是“单个实例”。
(1)饿汉模式
下面是单例模式的一种实现=》称:饿汉模式。
其中,用static保证该实例为单个实例;
其次,类外如果访问该实例,只能通过一个get方法得到;
最后,为了防止在类外可以new一个该类对象 ,将构造方法设置为私有的。
=》饿汉模式的创建时间:类加载的阶段。创建时机早。
=》但懒汉模式,创建的时机更迟。
(2)懒汉模式
=》如果整个代码后序,没有调用getInstance方法,这样就节省构造实例的过程;
=》如果后序调用的比较晚,可以与其他操作岔开。【还有个好处:一般程序刚启动,初始化的东西会很多,系统资源紧张;这样的话也会缓解】
(3)、两个单例模式是否线程安全?
要看线程是否安全,即就是看多个线程同时调用getInstance()方法,是否会出现问题?
(1)饿汉模式=》线程安全
(2)懒汉模式=》线程不安全
a)如何解决懒汉线程不安全?
方法:加锁,把多个操作打包成一个原子操作。
但是如下,加锁的位置,仍不能保证原子性操作。
将锁的范围扩大:
t2的load在t1完成结束后进行。
但是又出现新的问题:
上述懒汉模式的线程不安全问题,只是在创建实例之前(即首次调用的时候)才会触发。一旦线程创建好了,就线程安全了。
而我们加锁,则是一直都在加锁,即使线程安全了也一样。
=》加锁的开销大【可能会涉及到用户态与内核态的之间切换,这样的切换成本也大】
优化: 在加锁的外层,再加上一层判定条件
又一问题:
优化: 针对指令重排序问题,使用volatile
一个完整的,线程安全的懒汉模式:
3、案例二:阻塞队列
阻塞队列是一种特殊的队列。
=》阻塞队列是线程安全的
=》带有阻塞功能:当队列满,继续入队列,入队列的操作就会阻塞;直至队列不满,入队列才能完成。 当队列空,继续出队列,出队列操作就会阻塞,直至队列不空,出队列才能完成。
=》应用场景:生产者消费者模型【描述多线程协同工作的一种方式】。
=》其他用处【优势】:
->:使用阻塞队列,有利于代码“解耦合”
->:削峰减谷 :阻塞队列相当于一个缓冲区,平衡了生产者与消费者之间的处理能力。
比如,突发事件容易引起流量暴增。如果使用阻塞队列,此时流量骤增时,A队列承受了压力,B、C还是按照原来的节奏消费数据,对它两的冲击不大。
(1)标准库中的阻塞队列
=》其中,BlockingQueue只是一个接口,其有多种实现方式。
=》阻塞式的入队、出队,对应方法---->put ; take
=》其他方法,如offer,poll,peek等,不带阻塞特性。
(2)自己实现阻塞队列
step1:实现一个普通的循环队列
step2:上面出队,入队需要进行大量的修改操作,多个线程工作时,线程不安全;因此,需要进行加锁操作,同时,为了保证内存可见性问题,对变量加上volatile关键字。
step3:实现阻塞操作
原则:当入队时,如果队列满了,就阻塞;[当有出队列操作成功时,就notify通知]
当出队时,如果队列空了,就阻塞;
唤醒之后,需要再次判断一下,看是否真的满足条件了
因此此处使用的是while.
对阻塞队列=》生产者消费者模型的一个举例:
4、案例三:定时器
(1)标准库中的定时器
效果:
我们可以看出,执行完上述任务后,进程并没有退出。
Timer内部需要一组线程来执行注册任务。而这里的线程是 前台线程,会影响进程退出。
(2)自己实现定时器
step1: 安排的任务【描述这个任务】,任务包含两方面的信息,一个是要执行啥工作,一个是啥时候执行;
step2:看下如何让MyTimer管理多个任务
=》采用优先级队列,不是线程安全的,使用BlockingQueue来实现。
step3:任务已经被安排到优先级阻塞队列了,然后需要从队列取元素。
=》创建一个单独的扫描线程,让这个线程一直检查队首元素,看时间是否到了;如果时间到了,则执行该任务。
step4:运行上述代码,出错了。比较的对象mytask,是不对的。所以必须自己实现一个比较的规则。
实现Comparable接口,重写compareTo方法。
最终的结果展示:
step5: 上述会出现忙等的情况
假设现在12点,定2:30的闹钟。此时CPU并没有被空闲出来,这里就会一直忙等。
=》假设sleep两个小时,这样也是不行的。假设在1点时插入个新任务,这样就无法完成。
->使用wait解决上述忙等问题。
但目前还有问题,take操作和后续的判断,put,wait不是原子的。=》解决:加锁。
修改之后,这里的notify就不会出现在take和wait之间了。
结果: