文章目录
一.单例模式
单例模式是非常经典的设计模式,在我们写代码的时候,在单例模式下只能有一个对象,但是在日常开发中,我们很有可能忘记这个事情,所以这时候需要编译器来监督我们完成此工作,确保对象不会出现多个。在之前的代码过程中,也遇到过需要类似需要编译器来进行强制监督的关键词final、interferce、@Overide 、 throws等。
1.1 饿汉模式
在定义类的时候就已经创建了对象。
class Singleton{//实例 饿汉模式 这里在类加载就创建
//线程安全,因为该类在多线程中无论怎样都只是new了一次
private static Singleton instance=new Singleton();
//通过此方法获取到实例,后续如果使用这个类的实例,都通过getInstance方法进行获取
public static Singleton getInstance(){
return instance;
}
//防止重复new出instance,将其设为private,则无法new出
private Singleton(){}
}
public class Test {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
}
}
1.2 懒汉模式
在定义类的时候没有创建对象,而是在方法调用的时候初次对对象进行创建
class SingleLazy{//懒汉模式
private static SingleLazy instance= null;
//在首次调用instance的时候才开始创建实例,不调用不创建实例
//而饿汉模式因为在getinstanfe中有进行判断条件,当在多线程的环境下,线程都是一起执行的,这里的判断都生效,则new了多个对象,则是不安全
//如果想要解决该线程不安全问题需要给该类的方法中加锁
public static SingleLazy getInstance() {
//保证条件是整体才是线程安全,需要将整体的每个部分都包含,锁的对象,需要起到合理的锁竞争的效果。
if (instance == null) {
synchronized (instance) {
if (instance == null) instance = new SingleLazy();
}
}
return instance;
}
private SingleLazy(){}
}
public class Test {
public static void main(String[] args) {
SingleLazy s1=SingleLazy.getInstance();
SingleLazy s2=SingleLazy.getInstance();
SingleLazy s3=SingleLazy.getInstance();
SingleLazy s4=SingleLazy.getInstance();
}
}
上述两个类都属于单例模式,而不一样的是创建对象的时机不一样。
在我们在多线程开发的时候,如果在多线程中调用饿汉单例模式,因为类在开始时就创建了对象(只能创建一个),这是没有问题的。
在懒汉模式中,我们将类成员定义为null,初次创建对象是在条件中进行的,而当我们在条件中时,因为多线程是同时执行,我们这时候可能会因为多个条件成立而进入从而new出多个对象,这样会导致线程不安全。
如何避免避免这个情况发生?
这时候我们就可以通过加锁的形式,将这个对象进行上锁(synchronized),注意,上锁是一个整体,不是只锁一部分,这里的if也需要锁,如果不锁if则依然先进入if语句等待,这时候条件还是成立!
这里还需要考虑一件事情,就是如果加锁之后,每次都需要进行加锁,这样会导致我们线程的速度很慢,这里我们需要在附加一个条件
举例:如果线程1和线程2在执行,线程1和线程2是否为空,都为空,进入,但是注意这里第一个if条件进去是上锁的,这时候我们的线程2就需要进行等待线程1释放,当释放完成后,线程2进入if条件,这时候instance已经不为空了,这时候直接释放返回结果。
1.3 指令重排序
指令重排序是在逻辑不变的情况下,编译器为了优化执行的效率,则需要对指令重排序。
创建一个对象的时候需要的步骤
1.首先申请开辟一块内存
2.在内存中构造对象
3.把内存的地址赋值给成员而上述代码中,如果线程1和线程2在执行的过程中,线程1可能会在编译器中为了优化从而将创建对象的步骤打乱,由原来的123,可能修改为132步骤,直接将将一个地址给到instance,这时候给到t2的是一个非空的非法对象,这时候因为不为空,t2直接返回该结果,这时候t2可以访问到属性方法,就会导致出现bug。
1.4 解决方法volatile
通过volatile来修饰取消因为JVM引发的高效率指令重排序,这时候就可以解决此问题。
class SingleLazy{//懒汉模式
private static volatile SingleLazy instance= null;
//在首次调用instance的时候才开始创建实例,不调用不创建实例
//而饿汉模式因为在getinstanfe中有进行判断条件,当在多线程的环境下,线程都是一起执行的,这里的判断都生效,则new了多个对象,则是不安全
//如果想要解决该线程不安全问题需要给该类的方法中加锁
public static SingleLazy getInstance() {
//保证条件是整体才是线程安全,需要将整体的每个部分都包含,锁的对象,需要起到合理的锁竞争的效果。
if (instance == null) {
synchronized (instance) {
if (instance == null) instance = new SingleLazy();
}
}
return instance;
}
private SingleLazy(){}
}
public class Test {
public static void main(String[] args) {
SingleLazy s1=SingleLazy.getInstance();
SingleLazy s2=SingleLazy.getInstance();
SingleLazy s3=SingleLazy.getInstance();
SingleLazy s4=SingleLazy.getInstance();
}
}
1.如果是懒汉模式单例,首先需要对成员进行修饰volatile
2.双重条件判断
3.进行上锁 synchronized
二.阻塞队列
特殊对队列
- 线程安全
- 带有阻塞特性
-
- 如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往列里添加元素为止
-
- 如果队列为满,继续入队列,也会发生阻塞,阻塞到其他线程从队列中取走元素为止。
这里我们的客户端发起请求,服务器进行接收,而大多数的服务器不只有一个,以分布式的服务器进行接收,但是其他的服务器在发起请求时另一个服务器接收到了,但是另一个服务器出现bug,这时候将接收到的响应返回给服务器,造成影响,耦合过高。
也有可能服务器A分布给其他服务器因为其他服务器接收到的每秒钟的请求量太大,单个访问消耗的硬件资源不同,高并发量太大就出现bug。
这时候我们就引出了阻塞队列来进行规范这一系列的问题(将阻塞队列封装成单独的服务程序,部署到特定的机器上,成为消息队列)。
如果通过消息队列来接收获取数据,这时候就会降低耦合,来减少两者的联系。
如果请求量高的情况下,可以通过在队列积压来进行阻塞,发送给服务器B的请求量能够处理,不出现bug,当请求量逐渐达到正常标准,服务器B就可以将积压的数据慢慢处理完成。
在Java的标准库中,有专门的阻塞队列来供开发者进行使用,但是它是由接口来实现的,所以在我们new一个对象时,需要通过Blocking数组或者Blocking链表来实现队列。
public class Test {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> blockingDeque= new LinkedBlockingDeque<>();
blockingDeque.put("111");
blockingDeque.put("222");
blockingDeque.put("333");
blockingDeque.put("444");
String elem=blockingDeque.take();
System.out.println(elem);
elem=blockingDeque.take();
System.out.println(elem);
elem=blockingDeque.take();
System.out.println(elem);
elem=blockingDeque.take();
System.out.println(elem);
elem=blockingDeque.take();
//阻塞队列第五次因空则进行阻塞
System.out.println(elem);
}
}
2.1 模拟实现阻塞队列(生产者消费者模型)
我们可以基于普通的循环队列,数组或者是链表的形式来实现阻塞队列。
阻塞队列,是当我们在put或者是take的时候,我们需要让两者进行上锁,使其中一个线程执行结束释放锁,另一个线程在进行执行。而在上锁之后,基于我们对阻塞特性,队列为空或者队列为满,需要一方对待,直到添加元素或者拿去元素之后被唤醒。(如果作为消费者,如果生产者没有生产出来这是不现实的或者生产者生产的足够多,没有人购买也是不现实的)
当在生活中我们购买产品的时候,都是在生产者生产出来我们在进行购买,我们基于这个进行这个逻辑进行实现,将生产者进行睡眠,使生产者的生产时间变慢。
class MyBlockingQueue {
public String[] arr;
//为了避免内存可见性,系统会进行读和写,加上volatile来进行避免
public volatile int head;
public volatile int tail;
public volatile int size;
private final Object locker=new Object();
public MyBlockingQue() {
this.arr = new String[5];
}
public void put(String elem) throws InterruptedException {
synchronized (locker) {
//当wait被唤醒时,还需要在检查一下是否size是满的
while(size == arr.length){
//队列满就会阻塞
locker.wait();
}
arr[tail] = elem;
tail++;
if (tail == arr.length) tail = 0;
size++;
//唤醒take的wait阻塞状态
locker.notify();
}
}
//put 和take是相互联系的,要么空要么满,如果一方为空或者是满,则需要一方进行唤醒。
public String take() throws InterruptedException {
synchronized(locker){
while(size==0) {
//队列拿取为空就会阻塞
locker.wait();
}
String ret=arr[head];
head++;
if(head==arr.length)head=0;
size--;
//唤醒put的wait阻塞状态
locker.notify();
return ret;
}
}
}
public class Test {
public static void main(String[] args) {
//创建消费者和生产这模型
MyBlockingQue myBlockingQue=new MyBlockingQue();
//生产者
Thread t1=new Thread(()->{
int num = 1;
while(true) {
try {
myBlockingQue.put(num + "");
System.out.println("生产者产出:" + num);
num++;
//生产者每间隔500ms生成一次
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2=new Thread(()->{
while ((true)) {
try {
System.out.println("消费者购买:" + myBlockingQue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
三.定时器(日常开发常见的组建)
约定一个时间,时间到达之后,执行某一部分的代码逻辑。
在java的标准库的中就有实现定时器的方法,我们通过创建一个Timer类型的对象,然后通过schedule方法来进行定时器的推迟运行时间。
这里的定时器的方法schedule可以看到在java的标准库中包含两个参数,第一个参数就是我们需要执行的任务,第二个是我们要推迟的时间。
这里Timer是一个线程,并且我们可以同时进行多个任务。
import java.util.Timer;
import java.util.TimerTask;
public class Test {
public static void main(String[] args) {
//这里的Timer中也会创建一个线程。
Timer timer=new Timer();
//new出timerTask的方法
timer.schedule(new TimerTask() {
//重写run方法
@Override
public void run() {
System.out.println("5000");
}
//推迟5000ms后启动
},5000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("2000");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
System.out.println("程序启动");
}
}
3.1 创建并模拟定时器实现
1.首先我们需要创建一个Timer线程,来查询推迟程序运行的时间是否到达,这时候就可以开始执行程序。
2.我们需要有一个数据结构,用来存储所有的任务。
这里我们可以使用一个优先级队列,来进行存储。
3.还需要创建一个类,通过类的对象来描述一个任务(需要任务内容和时间)。
package Demo21;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;
class MyTimerTask implements Comparable<MyTimerTask>{
//这里来创建Runnable和long类型的对象
//通过runnable中的run方法来执行代码
private Runnable runnable;
//创建实例所需要的时间
private long time;
public MyTimerTask(Runnable runnable, long time) {
this.runnable = runnable;
//补充一个绝对时间,通过系统调用当前的时刻,并与自定义的推迟时间结合,算出一个推迟多久的执行时间
this.time = time+System.currentTimeMillis();
}
//通过runnable来调用run方法
public Runnable runnable(){
return runnable;
}
//获取时间
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
//优先级队列比较大小,this-参数为最小值放在优先级队列队首
return (int)(this.time-o.time);
}
}
class MyTimer{
// 在构造方法中使用线程进行扫描,主线程和t线程“同时执行”。
public MyTimer(){
Thread t=new Thread(()->{
while(true){
//这里上锁是需要让线程进行等待
synchronized (locker){
//这里如果为空,则需要进行等待
while(queue.isEmpty()){
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//到了这里则队列不为空,就需要去释放队首,这里需要的条件是时间是否满足释放的要求
long time=System.currentTimeMillis();//这里获取最新的时间
MyTimerTask task=queue.peek(); //这里获取队首的元素
//比较当前时间和任务时间,当前时间到达任务时间,则释放队首
if(time>=task.getTime()){
//运行代码
task.runnable().run();
//释放队首元素
queue.poll();
}else{
//这里没有到达时间的情况下,就需要进行等待,但是等待也是需要有参数的,不能一直等待下去
//这里设置的等待时间应该是我们队首(最小的)时间
//这里每次poll出去重新获取到的时间,就是队首需要等待的时间
try {
locker.wait(task.getTime()-time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
t.start();
}
//创建一个优先级队列
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
//创建一把锁
private final Object locker=new Object();
//通过schedule创建一个优先级队列,然后通过线程扫描来进行是否添加数据和时间
public void schedule(Runnable runnable,long delay){
synchronized (locker){
queue.offer(new MyTimerTask(runnable,delay));
//这里是用来唤醒两个wait的阻塞等待
locker.notify();
}
}
}
public class Test {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行时间:1000");
}
}, 1000);
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行时间:5000");
}
}, 5000);
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行时间:3000");
}
}, 3000);
myTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行时间:2000");
}
}, 2000);
}
}
四.线程池
首先,线程的诞生是因为进程的创建/销毁过慢。
而为了进一步提升线程的创建/销毁频率,引出线程轻量级线程(将系统调用的过程省略,由人工手动调度)。
在JAVA中标准库内没有协程,可以引用第三方库来实现此功能,但是通过第三方库无法保证百分百的高效率执行,这时候我们就需要运用到线程池。
package Demo23;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//工厂模式
ExecutorService service= Executors.newCachedThreadPool();
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
上述通过线程进行操作则是通过调用API来去创建线程需要从内核态中操作(内核态对系统所有的操作进行服务)不可预期,不可控制
线程池优化频繁创建/销毁的场景。
线程池优化是通过用户态操作,用户态操作是可以预期的,可以控制的。
在Java中的标准库中有线程池的方法供我们使用。
newCachedThreadPool
对线程池进行缓存,此构造方法构造出来的对象是具有自适应能力(可以随着添加任务,线程中的线程会根据需要自动创建出来,创建出来之后不会着急销毁,会在线程池中保留一定时间)。
newSingleThreadExecutor
创建单个线程
newFixedThreadPool
固定创建带有参数的线程的线程池,不具有自适应能力
newScheduledThreadPool
和定时器,不是一个扫描线程负责执行任务,而是多个线程进行执行时间的任务
上述的线程池都是对一个类进行的封装ThreadPoolExecutor,通过这些方法来对这个类填写不同的参数进行构造线程池。
4.1线程池的构造方法的使用
core size核心线程数量
参数1为核心线程数量,参数类型int
核心线程就是主要进行工作的线程。
maximum size最大线程数量
参数2为最大线程数量,参数类型int
最大线程是核心线程与其他线程,其他线程用来辅助核心线程完成工作,提升效率,又避免了过多的系统开销。
keepAliveTime 保持存活的时间
参数3类型:long
参数3为其他线程在不进行辅助工作时,所存在的时间。
unit 单位
参数4类型:TimeUnit
参数4为存活时间的绝对时间,以ms、s、min来换算。
workQueue
参数5类型:BlockingQueue(阻塞队列)
这里的队列数据结构可以根据需求进行更改,灵活设置
参数5用来存放线程的中的任务。
threadFactory
参数5类型:ThreadFactory类
参数5为工厂模式,通过此模式来创建线程。(通过进行类方法static修饰,构成相同参数的工厂类方法)。
handler
参数6类型:RejectedExecutionHandler类(线程池的拒绝策略)
参数6表示一个线程池中容纳的任务数量又上限,如果超出上限,则会出现多种效果。
方法 | 效果 |
---|---|
AbortPolicy | 如果池中任务数量已经满了,再次添加即抛出异常 |
CallerRunsPolicy | 新添加的任务,又调用者进行执行 |
DiscardOldestPolicy | 丢弃任务中最老的任务 |
DiscardPolicy | 丢弃当前的任务 |
4.2 模拟实现线程池
package Demo24;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPoll {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
private final Object locker = new Object();
public void submit(Runnable runnable) throws InterruptedException {
//第五种策略阻塞
queue.put(runnable);
}
public MyThreadPoll(int n) {
//创建出n个线程,负责执行上述队列的任务
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
//让这个线程,从队列中消费任务,并进行执行
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThreadPoll myThreadPoll=new MyThreadPoll(10);
for(int i=0;i<500;i++){
//
int num=i;
myThreadPoll.submit(new Runnable() {
@Override
public void run() {
//变量捕获无法被进行修改,可以通过一个临时变量来捕获
System.out.println("执行任务:"+num);
}
});
}
}
}
五.锁策略(特点)
5.1悲观锁和乐观锁
对后续锁冲突是否激烈来进行预测。
悲观锁:如果预测下来锁的冲突概率大,需要的工作增加。
乐观锁:如果预测下来锁的冲突概率小,需要的工作减少。
5.2重量级锁和轻量级锁
轻量级锁:锁的开销比较小。
重量级锁:锁的开销比较大。
5.3自旋锁(Spin lock)和挂起等待锁
自旋锁:用户态实现,是一种轻量级锁的典型实现(通过while循环进行)。
挂起等待锁:通过API来进行实现,通过内核操作,属于重量级锁的典型实现。
5.4读写锁
读写锁是将加锁操作,分成读锁和谢锁。
两个线程加锁过程中
读加锁和读锁之间,不会产生竞争。
读锁和写锁之间,有竞争。
谢锁和写锁之间也有竞争。
5.5可重入锁和不可重入锁
一个线程针对同一把锁,连续的加锁两次,不会死锁,就是可重入锁,出现死锁,则是不可重入锁。
5.6公平锁和不公平锁
标签:第三篇,Java,SingleLazy,void,队列,线程,new,多线程,public From: https://blog.csdn.net/weixin_60489641/article/details/144562596当很多个线程同时获取一把锁时,一个线程能够拿到锁,而其他的锁需要等待拿到锁的线程,当线程释放掉锁时,按着“先来后到”顺序进行分配,此时是公平锁。
非公平锁则是剩下的线程以“均等”的概率,重写竞争锁。
操作系统的API默认情况为非公平锁。