static修饰的成员叫“类成员”-》“类属性/类方法”
不加static修饰的成员,叫做“实例成员”-》“实例属性/实例方法”
一个Java程序中,一个类对象只存在一份(JVM保证的),进一步的也就保证了类的static成员也是只有一份的
类对象:就是.class文件,被JVM加载到内存后,表现出的模样。类对象里就有.class文件中的一切信息,包括类名是啥,类里有哪些属性,每个属性叫啥名字,每个属性是啥类型,属性是public、private……
类:相当于实例的模板,基于模板可以创建出很多的对象来
对象:就是实例
单例模式的两种实现:
饿汉模式:比较着急的去进行创建实例
懒汉模式:不太着急的去创建实例,只是在用的时候,才真正创建
关于多线程的案例:
案例一、实现一个线程安全的单例模式:要求代码中的某个类,只能有一个实例,不能有多个
通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例
(1)饿汉模式:
//实现单例模式-饿汉模式 class Singleton{ //1、使用static创建一个实例,并且立即进行实例化 //这个instance对应的实例,就是该类的唯一实例 private static Singleton instance=new Singleton();//立即初始化实例,所以叫饿汉模式 //被static修饰的类成员只有一份 //2、为了防止程序员在其他地方不小心new这个Singleton,就可以把构造方法设为private private Singleton(){} //3、提供一个方法,让外面能够拿到唯一实例 public static Singleton getInstance(){ return instance; } } public class Demo3 { public static void main(String[] args) { Singleton singleton=Singleton.getInstance(); } }
饿汉模式中getInstance仅仅是读取了变量的内容,如果多个线程只是读同一个变量,不修改,此时仍然是线程安全的~
(2)懒汉模式:
//实现单例模式-懒汉模式 class Singleton2{ //1、就不是立即就初始化实例 private static Singleton2 instance=null; //2、把构造方法设为peivate private Singleton2(){} //3、提供一个方法来获取到上述单例的实例 //只有当真正需要用到这个实例的时候,才会真正去创建这个实例-》懒汉模式 public static Singleton2 getInstance(){ if(instance==null){ instance=new Singleton2(); } return instance; } } public class Demo4 { public static void main(String[] args) { Singleton2 instance=Singleton2.getInstance(); } }
懒汉模式中,既包含了读,又包含了修改,而且这里的读和修改还是分成两个步骤的(不是原子的),存在线程安全问题~
改进方案:进行加锁
但是事实上,线程不安全问题只发生在instance初始化之前,也就是说初始化之后,已经线程安全了,可是按照上述的加锁方式,每次调用getInstance都会进行加锁,会存在已经线程安全了,但是还因为加锁存在大量的锁竞争(存在的问题)
改进方案:让getInstance初始化之前进行加锁,初始化之后就不再加锁(在加锁之前加上一层判断条件)
但是还是会有问题,当刚开始就有很多线程都去调用getInstance时,就会造成大量的读instance内存的操作,可能会让编译器把这个读内存操作优化成读寄存器操作,也就是出现内存可见性问题,可能会引起第一个if判定失效,但是对于第二个if判定影响不大(因为synchronized可以保证内存可见性),由于可能会引起第一个if判定失效,会导致不该加锁的给加锁了,但是不会引起第二层if的误判(不至于说创建多个实例)
改进方案:给instance加上volatile
因此要想利用懒汉模式创建一个线程安全的单例模式需要注意:
1、正确的位置加锁:为了保证线程安全
2、双重if判定:第一层if是为了避免不必要的加锁,初始化之后就不用加锁了
3、volatile:为了避免第一层if由于内存可见性问题失效
案例二、阻塞队列
阻塞队列同样也是一个符合先进先出规则的队列,相比于普通对列,阻塞队列又有一些其他方面的功能
阻塞队列的特性:(就是在普通队列中加了这两个特性)
1、线程安全
2、产生阻塞效果
(1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止
(2)如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止
基于上述特性,就可以实现“生产者消费者模型”
阻塞队列就可以作为生产者消费者模型中的交易场所
假设:A、B、C三个人一起来擀饺子皮包饺子,A负责擀饺子皮,B、C负责包饺子,此时A就是饺子皮的生产者,要不断生产一些新的饺子皮,B、C就是饺子皮的消费者,要不断的使用饺子皮,对于包饺子来说,用来放饺子皮的“盖帘”就是”交易场所“
假设:有两个服务器A、B,A作为入口服务器直接接受用户的网络请求,B作为应用服务器,来给A提供一些数据
如果不使用生产者消费者模型:
此时A和B的耦合性是比较强的,在开发A代码的时候就得充分了解B提供的一些接口,在开发B的时候也得充分了解到A是怎么调用的,一旦想把B换成C,此时A的代码就需要较大的改动,而且如果B挂了,也可能直接导致A也顺带挂了~
对于请求:A是生产者,B是消费者;
对于响应:A是消费者,B是生产者;阻塞队列就是交易场所
此时,A只需要关注如何和队列交互,不需要认识B,B也只需要关注如何和队列交互,也不需要认识A,如果B挂了,对A没有影响,如果把B换成了C,A也完全感知不到,能够做到让多个服务程序之间更充分的解耦合
如果不使用生产者消费者模型且当请求量突然暴涨时:
此时A的请求量暴涨,就会直接导致B暴涨~
A作为入口服务器,计算量很轻,如果请求量暴涨,问题也不是很大,但是B作为应用服务器,计算量可能很大,需要的系统资源也更多,如果请求更多了,需要的资源进一步增加,如果主机的硬件不够,可能程序就挂了
如果使用生产者消费者模型且当请求量突然暴涨时:
此时A的请求量暴涨,就会导致阻塞队列的请求暴涨,而不会直接导致B暴涨(“削峰”)~
由于阻塞队列没啥计算量,只是单纯的存个数据,可以抗住更大的压力
此时B这边仍然按照原来的速度来消费数据(“填谷”-》此时就算请求没没那么多了,B还是会按照原来的速度来从阻塞队列中拿已经存好的数据消费),不会因为A的暴涨而引起暴涨,B会被保护的很好,不会因为这种请求的波动而引起崩溃
生产者消费者模型的优点:
(1)能够让多个服务程序之间更充分的解耦合
(2)能够对于请求进行“削峰填谷”
Java标准库中阻塞队列的用法:
BlockingDeque
queue=new LinkedBlockingDeque<>(); queue.put("hello");//往阻塞队列里放元素 String s=queue.take();//从阻塞队列里拿元素 自己实现一个阻塞队列:
class MyBlokingQueue{//利用循环数组来创建阻塞队列 private int[]data=new int[1000]; private int size=0;//有效元素个数 private int head=0;//队首下标 private int tail=0;//队尾下标 private Object locker=new Object();//专门的锁对象 public void put(int value) throws InterruptedException {//入队 synchronized (locker){//由于put的每一行都在操作公共变量,所以直接给整个方法加锁 if(size==data.length){ locker.wait();//对于put来说,阻塞条件就算队列为满 //针对哪个对象加锁,就使用哪个对象wait } data[tail]=value;//在队尾添加元素 tail++; if(tail>=data.length){//当tail到达数组的末尾,此时该将tail循环到数组的0下标位置 tail=0; } size++; //如果入队列成功,则队列非空,就唤醒take中的阻塞等待 locker.notify(); } } public Integer take() throws InterruptedException {//出队列 synchronized (locker){//由于take的每一行都在操作公共变量,所以直接给整个方法加锁 if(size==0){ locker.wait();//对于take来说,阻塞条件就算队列为空 } int ret=data[head];//从队头出元素 head++; if(head>=data.length){ head=0; } size--; //take 成功之后,就唤醒put中的等待 locker.notify(); return ret; } } }
通过阻塞队列实现一个生产者消费者模型:
private static MyBlokingQueue queue=new MyBlokingQueue(); public static void main(String[] args) {//实现一个生产者消费者模型 Thread producer=new Thread(()->{ int num=0; while(true){ try { System.out.println("生产了:"+num); queue.put(num); num++; } catch (InterruptedException e) { e.printStackTrace(); } } }); producer.start(); Thread customer=new Thread(()->{ while(true){ try { int num=queue.take(); System.out.println("消费了:"+num); } catch (InterruptedException e) { e.printStackTrace(); } } }); customer.start(); }
当生产比较慢时,因为消费者进程循环很快,一下就把队列清空了,当消费者队列为空,就会阻塞等待,直到生产者生产了新的数据
此时生产者生产的快了,瞬间就会生产好1000个元素 (队列长度为1000),之后队列满了,再生产就会阻塞,此时每次消费一个元素才能生产一个元素
案例三:定时器-》用于进行计时,在一定时间之后,被唤醒并执行某个之前设定好的任务
Java标准库中定时器的用法:
java.uiil.Timer
方法:schedule(任务TimerTask是啥,多长时间后执行)
import java.util.Timer; import java.util.TimerTask; public class Demo4 { public static void main(String[] args) { Timer timer=new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello timer"); } },1000);//shedule的参数有两个,一个是任务TimerTask,一个是时间1000 System.out.println("main "); } }
自己实现一个定时器:
Timer的主要任务:
1、描述任务:任务的具体内容、任务的执行时间
创建一个专门的类来表示一个定时器中的任务(就像TimerTask)
2、组织任务(通过数据结构堆PriorityBlockingQueue)
定时器内部要能够存放多个任务 ,虽然安排任务的时候,是无序的,但是执行任务的时候,要按照时间先后来执行,保证每次执行的都是最小的时间间隔后的任务
比如说现在给安排了3个任务:分别是1个小时后去做作业、3个小时后去上课、10分钟之后去休息,此时就先执行休息,再执行做作业,最后执行去上课
3、执行时间到了的任务
需要先执行时间最靠前的任务,此时就需要一个线程,不停的去检查当前优先队列的队首元素,判断当前最靠前的这个任务是不是时间到了
自己实现一个定时器的总代码:
//创建一个类,表示一个任务 //1、描述一个任务:runnable+time class MyTask implements Comparable
{ private Runnable runnable;//任务具体要要干啥 private long time;//任务具体啥时候干,保存任务要执行的毫秒级时间戳 //after是一个时间间隔,不是绝对的时间戳的值 public MyTask(Runnable runnable, long delay) { this.runnable = runnable; this.time = System.currentTimeMillis() + delay; } public void run(){ runnable.run();//通过调用run方法来执行任务具体要执行的工作 } public long getTime(){ return time; } @Override public int compareTo(MyTask o) { return (int) (this.time-o.time);//时间小的在前,时间长的在后 } } class MyTimer{//定时器内部要能够存放多个任务 //2、利用优先级队列来组织若干个任务 private PriorityBlockingQueue queue=new PriorityBlockingQueue<>(); //3、实现schedule方法来注册任务到队列中 public void schedule(Runnable runnable,long delay){ MyTask task=new MyTask(runnable,delay); queue.put(task);//把任务都加到定时器里 synchronized (locker){ locker.notify();//每次任务插入成功之后,都唤醒一下扫描线程,让线程重新检查一下队首的任务看是否时间到了要执行 } } private Object locker=new Object(); //4、创建一个扫描线程,这个扫描线程不停的获取到队首元素,并且判定时间是否到达 public MyTimer(){ Thread t=new Thread(()->{ while(true){ try { MyTask task=queue.take();//先取出队首元素 long curTime=System.currentTimeMillis();//获取下当前时间 //再比较下看看当前这个任务时间到了没 if(curTime 案例四:线程池
在线程池里创建线程比直接从系统中创建线程更高效,因为直接从系统里创建线程需要经过内核态,而直接把线程从线程池里取是用户态操作,而用户态更高效,因为用户态是可控的,而内核态由于不知道内核都需要做哪些操作,因此内核态是不可控的,可能有时高效,有时低效
例子:把一个线程池想象成一个公司,公司里面有很多员工在干活,把员工分成两类,正式员工、临时工
Java标准库中线程池ThreadPoolExecutor的使用:
java.uiil.concurrent
构造方法:
corePoolSize:核心线程数 (相当于正式员工的数量)
maximumPoolSize:最大线程数 (相当于正式员工数+临时工数)
long keepAliveTime:(相当于允许临时工摸鱼的时间)
unit:时间的单位
workQueue:任务队列,线程池会提供一个submit方法,让程序员把任务注册到线程池中,加到这个任务队列中
threadFactory:线程工厂,线程是怎么创建出来的
hander:拒绝策略,当任务满了怎么做
问题:有一个程序,这个程序要并发的/多线程的来完成一些任务,如果使用线程池的话,这里的线程数设为多少合适??
答:不同类型的程序,由于单个任务里面CPU上计算的时间和阻塞的时间是分布不相同的 ,要通过性能测试的方式找到合适的值,例如,写一个服务器程序,服务器里通过线程池,多线程的处理用户请求,就可以对这个服务器进行性能测试,比如构造有一些请求发送给服务器,要测试想能,这里的请求就需要构造很多,比如每秒发送500/1000/……根据实际的业务场景,构造一个合适的值,根据这里不同线程池的线程数来观察,程序处理任务的速度,程序持有CPU的占用率。当线程数多了,整体的速度是会变快,但是CPU占用率也会高;当线程数少了,整体的速度是会变慢,但是CPU占用率也会下降 ,需要找到一个让程序速度能接受,并且CPU占用也合理这样的平衡点
Java标准库中简化版本线程池Executors的使用:
(本质上是对ThreadPoolExecutor进行了封装,提供了一些默认参数)
//创建一个固定线程数目的线程池,参数指定了线程个数 ExecutorService pool= Executors.newFixedThreadPool(10); //创建一个自动扩容的线程池,会根据任务量来自动进行扩容 ExecutorService pool=Executors.newCachedThreadPool(); //创建一个只有一个线程的线程池 ExecutorService pool=Executors.newSingleThreadExecutor(); //创建一个带有定时器功能的线程池,类似于Timer ExecutorService pool=Executors.newScheduledThreadPool();
自己实现一个线程池:
1、先能够描述任务(直接使用Runnable)
2、需要组织任务(直接使用BlockingQueue)
3、能够描述工作线程
4、还需要组织这些线程
5、需要实现往线程里面添加任务
class MyThreadPool { //1、描述一个任务,直接使用Runnable,不需要额外的创建类了 //2、使用一个数据结构来组织若杠任务 private BlockingQueue
queue = new LinkedBlockingDeque<>(); //3、描述一个线程,工作线程的功能就是从任务队列中取任务并执行 static class Worker extends Thread { //当前线程池中有若干个Worker线程~这些线程内部都持有了上述的任务队列 private BlockingQueue queue = null; public Worker(BlockingQueue queue) { this.queue = queue; } @Override public void run() { //就需要拿上面的队列 while (true) { try { //循环的去获取任务队列中的任务 //这里如果队列为空,就直接阻塞,如果队列非空,就获取到里面的内容 Runnable runnable = queue.take(); //获取到之后,就执行任务 runnable.run(); } catch (InterruptedException e) { e.printStackTrace(); } } } } //4、创建一个数据结构来组织若干个线程 private List workers=new ArrayList<>(); public MyThreadPool(int n){ //在构造方法中,创建出若干个线程,放到上述的数组中 for (int i = 0; i < n; i++) { Worker worker=new Worker(queue); worker.start(); workers.add(worker); } } //5、创建一个方法,能够允许程序员来放任务到线程池中 public void submit(Runnable runnable){ try { queue.put(runnable); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Demo2 { public static void main(String[] args) { MyThreadPool pool=new MyThreadPool(10); for(int i=0;i<100;i++){ pool.submit(new Runnable() { @Override public void run() { System.out.println("hello threadpool"); } }); } } }