单例模式能保证某个类在程序中只存在唯一 一份实例, 而不会创建出多个实例.单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
1)static 保证这个实例唯一
2)static 保证这个实例在一定时机中被创建出来
//此处保证Singleton这个类只能创建出一个实例
class Singleton{
//在此处,先把这个实例给创建出来
private static Singleton instance = new Singleton();
//如果需要使用这个唯一实例,统一通过Singleton.getInstance()方式来获取
public static Singleton getInstance(){
return instance;
}
//为了避免Singleton类不小心被复制出多份
//把构造方法设为private.在类外面,就无法通过new的方式来创建这个Singleton实例
private Singleton(){
}
}
public class Thread20 {
public static void main(String[] args) {
Singleton s = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s == s2);
}
}
结果:true
上述写的饿汉模式和懒汉模式,在多线程环境下调用getInstance.可以发现饿汉模式是线程安全的,而懒汉模式是不安全的
饿汉模式多线程调用,只涉及到"读操作",线程是安全的
但是导致每次getInstance 都需要加锁
一旦对象new完,后续调用getInstance,此时instance的值一定是非空的,因此就会直接触发return
基于上述讨论,就可以给上面的代码加上一个判定,如果对象还没创建,才加锁
如果对象已经创建过了,就不加锁
代码如下:
上述的代码仍存在问题
instance=new Singleton()
拆成三个步骤
- 申请内存空间
- 调用构造方法,把这个内存空间初始化成一个合理的对象
- 把内存空间的地址赋值给instance引用
正常情况下,是按照123这个顺序来执行的
编译器还有优化操作,指令重排序
为了提高程序效率,调整代码执行顺序
123这个顺序就可能变成132(如果是单线程,123和132没有本质区别)
假设
t1是按照132的步骤执行的
t1执行的13之后,执行2之前,被切出cpu
t2来执行(当t1执行完3之后,t2看起来,此处的引用就非空了)
此时此刻,t2就相当于直接返回了instance引用
并且可能会尝试使用引用中的属性
但是由于t1中的2(装饭)操作还没执行完,t2拿到的是非法的对象,还没构造完成的不完整对象
这就需要volatile
1.解决内存可见性
2.禁止指令重排序
完整代码如下:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Thread21 {
public static void main(String[] args) {
SingletonLazy s = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s == s2);
}
阻塞队列是一个队列,先进先出,不过,是特殊的队列,带有特殊的功能阻塞
1.如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里添加元素(队列不空为止)
2.如果队列满了,执行入队操作,同样会阻塞,阻塞到另一线程从队列取走元素(队列不满)
阻塞队列的一个典型应用场景就是"生产者消费者模型"
上述场景中,A和B之间的耦合是比较高
A要调用B,A务必要知道B的存在,如果B挂了,很容易引起A的bug,此外,如果在加一个C服务器,A也要修改不少代码
此时,A和B之间的耦合度就降低很多
A不知道B的存在,A只知道队列(A的代码没有任何一行代码和B相关)
B不知道A的存在,B只知道队列(B的代码没有任何一行代码和A相关)
如果B挂了,对于A没有任何影响,因为队列还好着,A仍然可以给队列插入元素,如果队列满,就会阻塞
同理,A挂了,B也没影响,B仍可以从队列获取元素,等到队列为空,就先阻塞.
生产者消费者模式,带来的好处:
1.实现了发送方和接收方之间的"解耦"
2.可以做到"削峰填谷",保证系统的稳定性
1.入队列,put
2.出队列,take
都带有阻塞功能
相关代码
public class Thread23 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
//创建两个线程,来作为生产者和消费者
Thread customer = new Thread(()->{
while (true) {
try {
Integer result = blockingQueue.take();
System.out.println("消费元素: " + result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while (true){
try {
blockingQueue.put(count);
System.out.println("生产元素 : "+ count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
class MyBlockingQueue{
private int[] items = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
public void put(int value){
if(size == items.length){
return;
}
items[tail] = value;
tail++;
//1)
// tail = tail % items.length;
//2)
if(tail >= items.length){
tail = 0;
}
size++;
}
public Integer take(){
if(size == 0){
return null;
}
int result = items[head];
head++;
if(head >= items.length){
head = 0;
}
size --;
return result;
}
}
判断队列是空还是满
1)浪费一个元素
2)引入一个size,记录个数
class MyBlockingQueue{
private int[] items = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
// 入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length){
//队列满了,此时要产生阻塞
this.wait();
}
items[tail] = value;
tail++;
//tail的处理
//tail = tail % items.length;
if(tail >= items.length){
tail = 0;
}
size++;
// 这个notify唤醒take中的wait
this.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
int result = 0;
synchronized (this) {
while (size == 0){
//队列空,也应该阻塞
this.wait();
}
result = items[head];
head++;
if(head >= items.length){
head = 0;
}
size--;
//唤醒put中的wait
this.notify();
}
return result;
}
}
两个线程中的wait不可能同时触发
take的wait同理
类似于一个"闹钟",达到一个设定的时间之后就执行某个指定好的代码
当网络编程的时候,可以使用定时器,来进行"止损"
标注库中提供了一个Timer类.Timer类的核心方法为schedule
schedule 方法 包含两个参数.第一参数时间到要执行的任务代码,第二个参数指定多长时间之后执行
具体代码
public class Thread25 {
public static void main(String[] args) {
System.out.println("程序启动!");
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务1");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务3");
}
},1000);
}
}
结果