案例一:单例模式
单例模式(一种设计模式)单个实例/对象 => 某个类,在一个进程中,只创建出一个实例。
使用单例模式可以对代码进行更严格的校验和检查。
static =>静态的“类属性”,instance就是Singleton类对象里持有的属性。
其他代码想使用这个类的实例,就需要通过getInstance()方法来获取,不能在代码中重新new该对象。
Singleton.class(从.class文件加载到内存中,表示这个类的一个数据结构)
每个类的类对象只存在一个。类对象中的static属性,自然也只有一个。所以这个instance指向的对象是唯一的一个对象。
1)“饿汉模式”
单例模式中一种简单的写法。
“饿” => “非常迫切” 实例在类加载时就已经创建好了,创建时机非常早。
线程安全 => 对于饿汉来说,getInstance直接返回Instance实例,本质是“读操作”
2) “懒汉模式”
代码一:
创建实例的时机更晚,只有第一次使用的时候才会创建实例。
首次调用getInstance,则instance为null,就会进去if,从而创建实例;再次调用,就不进入if,直接返回首次创建好的。这样设定,仍可以保证,该类的实例是唯一的一个,并且创建实例的时机不是程序驱动时,而是第一次调用getInstance时。
导致线程被new了两次,
代码二:将synchronized套在if外
多线程的代码是非常复杂的,代码稍微变换一点,结论就截然不同。
具体代码具体分析!!!
但是上述代码仍然存在问题:
如果Instance已经创建过了,此时后续在调用getInstance都是直接返回Instance实例。但针对这个已没有线程安全问题的代码,仍然每次调用都先加锁,再解锁 =>效率低
代码三:双重if
第一个if判定的是是否要加锁,第二个if判定的是是否要创建对象
3)指令重排序引起的线程问题
编译器优化的一种方式
调整原有的代码的执行顺序,保证逻辑不变的前提下,提高程序效率。
instance = new SingletonLazy();=>可以拆分成三个大的步骤(不是三个指令)
1.申请一段内存空间
2.在这个内存上调用构造方法,创建出这个实例
3.把这个内存地址赋值给instance引用变量
t2没有进入if触发加锁,直接return。
t2在return后,若使用instance里面的属性/方法就可能会出现错误,所以此时instance的属性是未初始化的“0”。
解决方法:核心思想volatile
1.保证内存可见性,每次访问变量必须重新读取内存,而不会优化到寄存器/缓存中。
2.禁止指令重排序,被volatile修饰的变量的读写操作相关指令,不能被重排序。
优化代码:
案例二:阻塞队列
阻塞队列 =>基于普通队列做出的扩展
1.线程是安全的
2.具有阻塞特征
a)如果针对一个已经满了的队列进行入队列,此时入队列操作会阻塞,直到队列不满之后。(其他线程出队列)
b)如果针对一个已经空了的队列进行出队列,此时出队列操作会阻塞,直到队列不空之后。(其他线程入队列)
生产者消费者模型
1.引入生产者消费者模型,可以更好地做到“解耦合”(把代码的耦合程度,从高降低)
实际开发中,经常会涉及到“分布式系统” => 服务器整个功能不是由一个服务器完成的,而是每个服务器负责一部分功能,通过服务器之间的网络通信,最终完成整个功能。
代价:需要加机器引入更多的硬件资源
1)阻塞队列,并非是简单的数据结构,而是基于这个数据结构实现的服务器程序,又被部署到单独的主机上了
2)整个系统的结构更复杂了,要维护的机器更多了
3)效率,引入中间商,有差价,请求从A发出到B收到,这个过程经历队列的转发,有一定的开销。
优点:
1.解耦合 2.削峰填谷(三峡大坝)所谓的峰,谷不是长时间持续的,是短时间出现的
服务器处理每个请求,都是需要消耗硬件资源的(cpu,内存,硬盘,网络宽带...)
即使一个请求消耗的资源比较少,但是有很多请求加在一起,总的消耗的资源就多了!!!
上述任何一种硬件资源达到瓶颈,服务器就挂了(发送请求,不会返回响应)
入口服务器A:做的工作一般比较简单,每个请求消耗的资源少,
用户服务器B,C:完成的工作更复杂,每个请求消耗的资源多。(从数据库中找到对应的用户信息)
一旦外界的请求出现突发峰值,B和C就挂了。
阻塞队列:数据结合(队列没啥业务逻辑,只是存储数据,抗压能力比较强)
消息队列:基于阻塞队列实现服务器程序。
引入后,即使外界的请求出先峰值,也由队列承担请求峰值,B,C仍按原速度请求。
Java标准库中提供了现成的阻塞队列数据结构。
ArrayBlockingQueue
BlockingQueue => LinkedBlockingQueue
(interface接口) PriorityBlockingQueue
queue.put("aaa"):使用put和offer一样都是入队列
put带有“阻塞功能”,offer没有阻塞功能(队列满了会返回结果)
queue.take(),也带有阻塞功能出队列
阻塞队列中没有提供带有阻塞功能的获取队首元素的方法。
基于数组实现阻塞队列(环形队列)
1)先实现普通队列
2)再加上线程安全
3)再加上阻塞功能
判断队列空满:1)浪费一个格子,tail最多走到head前一个位置。 2)引入size变量
可能出现交叉唤醒,put1为线程1执行到wait,put2为线程2执行到wait,take1为线程3执行到notify唤醒put1,put1执行到notify唤醒put2
if只判定一次。一旦程序阻塞后在被唤醒,这中间间隔的时间很长,这个过程会有很多变数。难以保证条件,条件是否仍然满足,入队列的条件是否具备。
改成while后,意味着wait被唤醒后再判定一次条件 =>多了一步确认操作。如果再次确认,发现队列还是满的,就继续等待。
生产者消费者模型
实际开发中,生产者消费者模型,往往是有多个生产者,消费者。
生产者,消费者不仅仅是一个线程,也可能是一个独立的服务器程序,甚至一组服务器相连。
生产者消费者模型 => 最核心的部分是阻塞队列,使用synchronized和wait/notify达到线程安全,线程阻塞。