目录
1.设计模式
单例模式
对应场景:有些时候我们希望有的对象,在整个程序中只有一个实例对象
在java中有两种模式可以达到这样的效果
饿汉模式:(迫切)程序启动,类加载之后,立即创建出实例
懒汉模式:(延时)在第一次使用实例的时候再常见,否则就不能创建
public class Main {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s1==s2);
}
}
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){};
}
true
带有static 类属性,由于每个类的类对象都是单例的,类对象的属性static也就是单例的
同时为了保险,要防止取new一个新对象,所以将构造方法写成private,这样就无法被new
但外面也不一定够无法调用,反射机制就可以在当前的单例模式中,创建出多个实例
懒汉模式
class Singleton1{
private static Singleton1 instance = null;
public static Singleton1 getInstance(){
if(instance==null){
instance=new Singleton1();
}
return instance;
}
private Singleton1(){};
}
这个的特点很容易就可以看出和上一个的区别,懒汉模式开始时并不创建新的对象,只有在用时才创建
那么这两个代码,谁才是线程安全的。
饿汉模式返回的都是同一个变量,没有问题。
懒汉模式的判断和创建并非是一个整体,如果两个线程同时调用一个为空的,就会修改两次,线程不安全。但通过枷锁可以让他线程安全。
private volatile static Singleton1 instance = null;
public static Singleton1 getInstance(){
if(instance==null){
synchronized (Singleton1.class){
if(instance==null)
{
instance=new Singleton1();
}
}
}
return instance;
}
因为加锁很耗费时间和时间,按照上面这样处理。并且就算加了锁第二个线程未必能读到第一个线程修改后的值,此时就需要个对象前面加一个volatile。同时还有一个用途,避免指令重排序。
指令重排序
是编译器优化的一种手段,保证原有逻辑不变的前提下,对diamagnetic执行顺序进行调整,使调整之后的执行效率提高,如果是单线程,那么这样没问题,但如果是多线程,那么这样就与可能会出错。
线程安全的单例模式的三个要点
加锁;
双重if;
volatile;
接下来就算第二个案例
2.阻塞队列
之前因该听过队列和优先级队列,阻塞队列就是一个具有阻塞功能的队列,
当队列满的时候继续入队列就会出现阻塞,阻塞到其他线程从队列中取走元素位置
当队列为空的时候,继续出队列,也会初见阻塞,阻塞到其他线程往队列中添加元素为止。
用处非常大
基于阻塞队列,实现生产者消费者模型——处理多线程的方式
模型优势:减少任务切换开销,减少锁竞争。
解耦合
就是降低模块之间的耦合
消峰填谷
主要起到一个缓冲的作用
Java标准库中提供了现成的阻塞队列
BlockingQueue
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue= new LinkedBlockingDeque<>();
queue.put("hello");
String elem=queue.take();//将里面的元素给取出来
System.out.println(elem);
elem=queue.take();
System.out.println(elem);
}
对于这个类来说,offer和poll不具有阻塞功能
put和take带有阻塞功能
现在要实现一下生产消费模型
import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
//阻塞队列
public class Main {
public static void main(String[] args) throws InterruptedException{
BlockingDeque<Integer> queue =new LinkedBlockingDeque<>();
//生产
Thread t1=new Thread(()->{
int count=1;
while(true){
try{
queue.put(count);
System.out.println("生产元素:"+count);
count++;
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
//消费
Thread t2=new Thread(()->{
while(true){
try{
Integer n=queue.take();
System.out.println("消费元素:"+n);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
生产元素:1
消费元素:1
生产元素:2
消费元素:2
生产元素:3
消费元素:3
生产元素:4
消费元素:4
下面初步实现一下阻塞队列
class MyBlockQueue{
private String[] items = new String[1000];
private int head=0;
//有效范围【head,tail); 当head和tail相等重合时,相当于空队列。
private int tail=0;
private int size=0;
public void put(String elem){
if(size>= items.length){
return;
}
items[tail]=elem;
tail++;
if(tail>=items.length)tail=0;
size++;
}
public String take(){
if(size==0)return null;
String elem=items[head];
head++;
if(head>=items.length) {
head = 0;
}
size--;
return elem;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException{
MyBlockQueue queue = new MyBlockQueue();
queue.put("aaa");
queue.put("bbb");
queue.put("ccc");
String elem=queue.take();
System.out.println(elem);
}
}
但这个队列线程不安全,如果有多个线程同时size++--就会出现问题
所以,先给put和take加锁,能够保证线程安全
class MyBlockQueue{
private String[] items = new String[1000];
volatile private int head=0;
//有效范围【head,tail); 当head和tail相等重合时,相当于空队列。
volatile private int tail=0;
volatile private int size=0;
public void put(String elem) throws InterruptedException{
synchronized (this) {
while(size >= items.length) {
this.wait();
}
items[tail] = elem;
tail++;
if (tail >= items.length) tail = 0;
size++;
this.notify();
}
}
public synchronized String take() throws InterruptedException{
while(size==0) {
this.wait();
}
String elem=items[head];
head++;
if(head>=items.length) {
head = 0;
}
size--;
this.notify();
return elem;
}
}
加上wait,notify,synchronized,volatile。保证线程安全和阻塞的效果。
只要使用wait建议搭配while来进行条件判定
3.定时器
这个也是日常开发中常见的组件,类似于闹钟
需要使用Timer类来实现
import java.util.*;
public class Main {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {//类似Runnable表示任务,它实现了RUnnable接口
@Override
public void run() {
System.out.println("hello");
}
},3000);//安排
System.out.println("程序开始运行");
System.out.println("world");
}
}
这个的使用还是比较简单的。
接下来自己尝试实现定时器。
import java.util.PriorityQueue;
import java.util.*;
//实现定时器
class MyTimerTask{
private long time;
//具体的任务是什么
private Runnable runnable;
public MyTimerTask(Runnable runnable,long delay){
//delay是一个相对的时间差,构造time时要根据当前的系统时间和delay进行构造。
time = System.currentTimeMillis()+delay;
this.runnable = runnable;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
}
//定时器类的本体
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
// 定时器的核心方法
public void schedule(Runnable runnable ,long delay){
MyTimerTask task = new MyTimerTask(runnable,delay);
queue.offer(task);
}
//还需要构造一个扫描线程,一方面取负责监控队首元素是否到点了,以及调用
public MyTimer (){
Thread t = new Thread(()->{
while(true){
try{
if(queue.isEmpty())continue;
MyTimerTask task=queue.peek();
long curtime =System.currentTimeMillis();
if(curtime>task.getTime()){
queue.poll();
task.getRunnable().run();
}else {
//让当前的扫描线程休眠一下
Thread.sleep(task.getTime()-curtime);
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
4.线程池
如果我们需要频繁的创建销毁线程,此时创建销毁线程的成本就不能忽视了,因此就可以使用线程池
前提是创建好一波线程,后续需要使用线程,就直接从池子里哪一个即可
当不再使用时,就放回到池子中
如果是从系统这里创建线程,就需要调用系统api,进一步的由操作系统内核完成线程的创建过程。——内核给所有的进程提供服务,非常繁忙不可控
如果是从线程池这里获取线程,上述的内核中进行的操作,都是提前做好了,现在的取线程的过程,纯粹的用户代码完成——可控
使用代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
//线程池对象和后面的创建线程的过程。最后面是工厂方法
//Executors 称为工厂类一般创建对象,都是通过new。但方法名字由缺陷,构造方法的名字固定就是类名。只能使用方法重载的方式来实现。
//里面的线程数量是固定的
//Executors.newCachedThreadPool();//创建一个动态调整线程数目的线程池。
//Executors.newSingleThreadExecutor();// 创建单个线程
//Executors.newScheduledThreadPool(100);
//类似于定时器的效果,添加一些任务,任务都在后续的某个时刻再执行,被执行的时候不是只有一个扫描线程来执行任务,可能是由多个线程共同执行所有的任务。
for(int i=0;i<10;i++)
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
使用时使用submit()来对线程安排任务。
面试题:Java标准库中线程池构造方法的参数和含义
int corePoolSize 核心线程数:ThreadPoolExecutor里面的线程个数,并非时固定不变的,会根据当前任务的情况动态发生变化,但就算没有任务,也至少得有这些线程。
int maximumPoolSize 最大线程数:无论有多少任务,线程数也不能比这个多
long keepAliveTime,TimeUnit unit:前者是允许摸鱼的最大时间,后面是时间单位。
Blocking Queue<Runnnable> workQueue:线程池内部有许多任务,可以使用阻塞队列来进行管理
ThreadFactory threadFactory:工厂模式。
RejectedExecutionHandler handler:线程池考察的重点,拒绝方式拒绝策略。线程池有一个阻塞对列,当阻塞队列满了之后,该如何应对
拒绝策略
ThreadPoolExecutor.AborPolicy :终止,直接抛出异常,线程池开摆了
ThreadPoolExcutor.CallerRunsPolicy:谁是添加这个新的任务的线程,谁就去执行这个任务
ThreadPoolExcutor.Discard0ldestPolicy:丢弃最早的任务,执行新的任务
ThreadPoolExcutor.DiscardPolicy:丢弃新的任务
上面谈到的线程池 Executors是封装过的
ThreadPoolExecutor是原生的。
那个都可以使用,具体选择看需求
自己实现一个线程池
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
//线程池
public class Main {
public static void main(String[] args) throws InterruptedException{
MyThreadPool myThreadPool = new MyThreadPool(4);
for(int i=0;i<1000;i++){
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"hello");
}
});
}
}
}
class MyThreadPool{
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<Runnable>();
public void submit(Runnable runnable) throws InterruptedException{
queue.put(runnable);
}
//表示线程池中有n个线程
public MyThreadPool(int n) throws InterruptedException{
for(int i=0;i<n;i++){
Thread t = new Thread(()->{
while(true){
try{
Runnable runnable = queue.take();
runnable.run();
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
}
创建线程池时,线程个数怎么来。
不同的项目中,cpu要做的工作是不同的
有的线程工作时,是cpu密集型,线程的工作全是运算,大部分工作都要在cpu上完成,所以cpu需要给他安排核心去工作才可以。所以安排N个线程即可,N为cpu的逻辑核心数。
有的线程工作时,是io密集型,读写文件,等待用户输入,网络通信。基本安排2*N个即可,因为大部分都在等待,不消耗cpu
但实际情况没有这么极端,两个都含有。这里最好的做法是,通过性能测试,选出最优的线程数目。