Java-线程安全的四个经典案例和线程池

单例模式

有些对象,在一个程序中应该只有唯一 一个实例(光靠人保证不靠谱 借助语法来保证) 就可以使用单例模式

在单例模式下 对象的实例化被限制了 只能创建一个 多了的也创建不了

单例模式分为两种:饿汉模式和懒汉模式

饿汉模式:

饿急眼了,不吃(创建)不行了,就是在类定义时就创建一个实例

Class Singleton{
private static singleton instance= new singleton();
public static Singleton getInstance(){
return instance;
}
Private Singleton(){
}(防止new实例)
};

static 静态 实际效果和字面含义没啥关系 实际的含义是 类属性/类方法(对应实例属性实例方法)类对象只能创建一个

效果是无论get多少个实例都是同一个实例 且无法new实例

懒汉模式:

ε=(´ο`*))) 啥时候用啥时候再创建吧

class Singletonlazy {
         private static Singgletonlazy instance = null;
         public static Singletonlazy getInstance(){
         If(instance == null){
            Instance = new Singletonlazy();
}
Return instance;
}
}

 那么回到多线程,饿汉模式和懒汉模式是线程安全的吗

饿汉模式get只是多线程读 没问题

懒汉模式 第一次调用get有的地方在读有的地方在写 实例创建好之后就安全了(一个线程在读 一个线程在创建 还没创建完 就读了 就又创建了)

所以我们可以选择对它加锁

synchronized(SingletonLazy.class){
 If(instance == null){
            Instance = new Singletonlazy();
}}

但是如果这么写的话,每次的判断操作也被锁了,多线程中还是不可以同时运行(一个判断另一个就会阻塞)所以我们可以

if(instance==null){
synchronized(SingletonLazy.class){
 If(instance == null){
            Instance = new Singletonlazy();
}}}

在单线程中我们连续使用两个相同的if没有意义 但是隔了一个synchronized就是另一说了 

饿汉模式和懒汉模式是一种设计思想 不是固定的代码 我们再举一个例子

1.饿汉 把10g都读取到内存中 读取完毕之后再允许用户进行查看和修改

2.懒汉 一次只读一点 随着用户翻页 继续再读后续内容

生产者消费者模型

先来了解一下 阻塞队列

阻塞队列和正常队列的区别是啥子呢 就是当对列为空 尝试出队列 就会阻塞。队列为满时 尝试入队列也会阻塞

生产者消费者模型就是通过阻塞队列实现的

我们用包饺子来举例 把包出饺子作为最终目标(没有煮饺子 吃饺子的环节)

一个人擀饺子皮 其他人包(因为擀面杖数量有限 所以效率会远高于 每个人自己擀饺子皮自己包)

生产者消费者模型(必须得有盖帘才算生产者消费者模型)

生产者:负责擀饺子皮

盖帘: 阻塞队列

消费者:负责包饺子的人

生产者消费者模型的好处是什么

1.可以做到更好的解耦合(高内聚就是写一个功能的时候尽量让这个功能的代码集中放置 不要东一块西一块)

如果生产者和消费者直接交互 耦合性很高 b寄了可能给a也干碎了

如果 生产者->阻塞队列->消费者 双方只想着和阻塞队列交互就可以了 他俩谁寄了都不影响对方

2.削峰填谷(就跟三峡大坝似得 把雨季的水给旱季) 提高整个系统的抗风险能力

大规模用户同时访问a a再给b同步 b如果没有太强抗压能力可能就寄了

生产者消费者模型

在a和b之间放一个阻塞队列  即使a的压力很大 b仍然按照既定的频率来取数据

代码实现

 public static void main(String[] args) {
            BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
            Thread productor = new Thread(()->{
            	int n = 0;
            	while(true) {
            		try {
						System.out.println("生产元素"+n);
						queue.put(n);
						n++;
						Thread.sleep(500);//这行代码,没有实际意义 是为了让运行结果更方便观察而已
					} catch (Exception e) {
						e.printStackTrace();
					}
            	}
            });
            productor.start();
            Thread customer = new Thread(()->{
            	while(true) {
            		try {
						System.out.println("消费元素"+queue.take());				
						Thread.sleep(500);
					} catch (Exception e) {
						e.printStackTrace();
					}
            	}
            });
            customer.start();
	    }

模拟实现一个阻塞队列

class myqueue{
	int[] queue = new int[100];
	int size = 0;
	int head = 0;
	int tail = 0;
	
	public void put(int a) throws InterruptedException{
		synchronized (this) {
			if(size==queue.length) {
				this.wait();
			}
			
			queue[tail] = a;
			tail++;
			if(tail==queue.length) {
				tail = 0;
			}
			size++;
			this.notify();
		}
	}
	public Integer take() throws InterruptedException{
		int ret = 0;
		synchronized (this) {
			if(size==0) {
				this.wait();
			}
			ret = queue[head];
			head++;
			if(head==queue.length) {
				head=0;
			}
			size--;
			this.notify();
			return ret;
		}
	}
}

不要担心这个notify唤醒的是当前功能的wait 因为如果有wait就做不到这一步

定时器 

使用方法是这样的

先实例化一个Timer对象

然后调用它的schedule方法

schedule有两个参数 第一个参数是Timertask对象 重写里面的run 就是业务代码

第二个参数是时间 一个整形 单位ms

public static void main(String[] args) {
            Timer timer = new Timer();
            timer.schedule(new TimerTask() {			
				@Override
				public void run() {
					System.out.println("任务");
								}
			}, 1000);
	    }

你可能会问那我直接用sleep多好

sleep把线程阻塞了 什么都干不了 但是定时器等待的过程中还可以干别的

command表示任务 after表示暂停多久

这个schedule(时间表) 这是个表啊 也就是说可以放入多个元素

模拟实现

Timer 内部要组织很多任务

还需要有一个线程 通过这个线程来扫描定时器内部 看看哪个时间到了该执行了

使用优先级队列来组织这些任务 使用优先级队列可以排序时间

System.currentTimeMillis()系统当前时间戳\

PriorityQueue<MyTask> queue

这个队列会被多个线程同时访问

schedule可能在多线程中被调用 每次调用都要往队列里添加元素

内部还需要有专门的线程执行任务

涉及到出入 所以不安全

PriorityBlockingQueue用阻塞队列

public void schedule()

进入构造就创建一个线程 循环不断尝试获取队首元素并且判定元素是否可以就绪(大量的产生循环 空转 没有实际性的任务)

所以 在判断结束(不可就绪)后 就让他等一会(sleep 不行 即使可以就绪了sleep还在休息 可以用wait wait可以提前唤醒)(每次插入任务都唤醒判断一下)

都统一的时间戳 如果规定时间比当前时间大(没到时间呢)就塞回去 

如果到了 就执行

优先级队列需要定义优先原则 实现Comparable接口 并重写compareTo 所以要在被比较元素的类里面写

这个return 谁减谁 可以写代码试( 如果第一个参数小于第二个参数,就返回一个负数,如果等于就返回0,如果大于就返回一个正数。而且要求返回int 还要强转)

private Object locker = new Object();

wait 和 notify 都是java.util里面的文件 创建一个object类就可以

原则上来讲 只要判断队首元素就可以(等待时间最少)   但是要防止新加入的元素比队首时间还小 (所以加了notify 所以说用sleep不行) 每次加入新元素都唤醒线程一下 如果这个新元素的时间比之前的时间要早 那就变成按照新的队首元素判断 如果比之前的首元素时间要晚也无所谓 接着等呗

 一个误区

这里这个synchronized 用的是locker作为对象 但是queue.put没有使用locker的资源 是不是就锁不住了? 并不是 它既然锁住了locker 那么其他线程在运行代码块的时候获取不到locker(已经被锁住) 还是会阻塞 所以达成了目的

线程池

进程太重量了 创建和销毁成本较高

线程 就是针对上述问题的优化(公用一组系统资源)

但是更频繁的话 线程也扛不住了

进一步优化

1.线程池

2.协程(纤程) 轻量级线程 

八线程创建好 放在池里 需要使用直接从线程池里面取 用完还回去 就不需要创建和销毁

 相当于十个线程去运行这个一百个任务

模拟实现

这个线程是持续运行的 只要submit有东西输入进去了 就会把它运行

猜你喜欢

转载自blog.csdn.net/chara9885/article/details/130739038