一、线程安全的单例模式
单例模式:是一种设计模式,使用单例模式能保证一个类在程序中只存在唯一的一个实例。
单例模式可以分为饿汉式和懒汉式。实现单例模式的类的构造方法一般为私有的,防止在类外创建出多个实例。
1、饿汉模式
饿汉模式就是在类加载的时候就创建了实例。不管用不用得到实例,实例都会首先创建好,所以饿汉模式是线程安全的 。
饿汉模式的代码实现:
public class Singleton1 { /** * 模拟实现饿汉模式 */ private static Singleton1 instance=new Singleton1(); private Singleton1(){ } public static Singleton1 getInstance() { return instance; } }
2、懒汉模式
懒汉模式相较于饿汉模式比较优化,不会在开始就创建出实例而是在用的时候才进行创建。
代码实现:
public class Singleton2 { /** * 懒汉模式的模拟实现 */ private static Singleton2 instance; private Singleton2(){ } public Singleton2 getInstance(){ if(instance==null){ instance=new Singleton2(); } return instance; } }
在获取到实例时候进行的操作先进行读然后再进行创建,这个操作不是原子性的,就在多线程抢占式执行的过程中存在线程安全问题,于是就对该操作进行加锁,锁对象采用该类的类对象,因为类对象只有一份这样可以保证多个线程在调用getInstance方法时使用的锁对象都是同一个:
public Singleton2 getInstance(){ synchronized(Singleton2.class){ if(instance==null){ instance=new Singleton2(); } } return instance; }
这样修改之后,在每次获取实例的时候都需要进行加锁,即使实例已经创建好了,这样就有可能引起锁的竞争十分影响效率,于是在加锁之前就先进行if判断实例是否已经创建好,避免盲目加锁:
public Singleton2 getInstance(){ if(instance==null){ synchronized(Singleton2.class){ if(instance==null){ instance=new Singleton2(); } } } return instance; }
但是此处会出现指令重排序问题,因为在创建单例对象时需要进行以下三个步骤:
- 创建内存;
- 对内存进行初始化;
- 将内存的地址赋值给引用。
但是在实际开发的过程就有可能出现1->3->2的情况,那么其他线程在进行访问的时候就有可能出现非null的引用,但是实际访问的时候会出现一个非法的对象,所以在定义对象时需要加上volatile来防止指令重排序问题。
private static volatile Singleton2 instance;
所以,最终多线程环境下的懒汉模式为:
public class Singleton2 { /** * 懒汉模式的模拟实现 */ private static volatile Singleton2 instance; private Singleton2(){ } public Singleton2 getInstance(){ if(instance==null){ synchronized(Singleton2.class){ if(instance==null){ instance=new Singleton2(); } } } return instance; } }
二、阻塞队列
阻塞队列是一种特殊的队列,但同样遵守先进先出的规则 。
阻塞队列的特性:
队列为空时,继续出队列就会阻塞,直到有线程向该队列中添加元素。
队列满时,继续入队列时就会阻塞,直到有线程从该队列中取走元素。
Java的标准库也存在阻塞队列:BlockingQueue,但它只是一个接口,真正实现类是LinkedQueue,其常用方法有put()(入队列)、take()(出队列),还有offer()、pull()、peek()等方法,但这些方法并不带阻塞性。
生产者——消费者模型
生产者负责生产资源,而消费者负责消费资源,如果没有阻塞队列则消费者和消费者之间有着较高的耦合性,两者都需要为彼此提供一些接口方法,使用了阻塞队列之后可以让两者充分地解耦合。
还有如果不使用阻塞队列,消费者需求暴涨就会出现生产者所需的资源突然增多,如果硬件条件不足,系统就会崩溃,但是使用了阻塞队列之后,需求增多只会影响阻塞队列的数据增多而对于生产者并没有太大的影响,如果消费者需求突减,也只是会让阻塞队列的数据减少所以阻塞队列还可以对请求实现“削峰填谷”。
利用数组模拟实现阻塞队列:
public class MyBlockingQueue<T> { private T[] data= (T[]) new Object[1000]; private int size; private int tail; private int head; //添加元素 public void put(T t) throws InterruptedException { synchronized (this){ if(size==data.length){ //队列已满,继续添加元素就会阻塞 this.wait(); } data[tail]=t; tail++; if(tail>= data.length){ tail=0; } size++; //队列添加元素成功,就会唤醒因队列为空造成阻塞的线程 this.notify(); } } //取出元素 public T take() throws InterruptedException { synchronized (this){ if(size==0){ //队列为空,继续取出元素就出阻塞 this.wait(); } T t=data[head]; head++; if(head>=data.length){ head=0; } size--; //队列取出元素成功,就会唤醒因队列已满而阻塞的线程 this.notify(); return t; } } }
模拟实现生产者——消费者模型:
public static void main(String[] args) { MyBlockingQueue<Integer> mq=new MyBlockingQueue<>(); Thread producer=new Thread(()->{ try { int num=1; while(true){ mq.put(num); System.out.println("生产第"+num+"个"); num++; Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } }); producer.start(); Thread consumer=new Thread(()->{ try { int num=0; while(true){ num=mq.take(); System.out.println("消费第"+num+"个"); Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } }); consumer.start(); }
运行结果:
由于两者设置的 休眠时间一致,所以生产者生产出一个资源就会立即被消费者消费。
三、定时器
定时器就相当于一个闹钟,到达设定的时间之后,就会执行某段设定好的代码。
定时器是一个常用的组件,就比如在加载网页的时候,一般当加载一段时间后未加载出来,就会显示检查网络设置。
1、标准库中定时器的使用用法
在标准库java.util.Timer中,使用Timer类定义一个实例对象,然后再使用其核心方法是schedule,此方法的两个参数分别是执行的任务是什么,还有在多久之后执行。
使用演示:
public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("测试标准库中的定时器方法"); } },3500); System.out.println("main方法"); }
运行结果:
先执行main方法,在3500ms之后执行TimerTask中的任务。
2、模拟实现定时器
a、首先需要创建出一个专门的类来表示schedule中的任务(TimerTask)
在该类中需要定义一个Runnable接口来定义任务,再定义一个时间来表示任务在多久之后执行,还需要定义一个run方法用于描述任务。在定义构造方法时应该注意参数中的时间是延迟时间而不是执行任务的时间,需要用系统当前的时间加上延迟时间。
class MyTask{ private Runnable runnable; private long time; public MyTask(Runnable runnable,long delay){ this.runnable=runnable; this.time=System.currentTimeMillis()+delay; } public long getTime(){ return time; } public void run(){ runnable.run(); } }
b、使用合适的数据结构组织任务
在定时器中需要不断比较多个任务的时间,来决定任务的执行顺序,可以使用PriorityQueue优先级队列来实现,但是在不断地取任务的过程中涉及线程安全问题,就采用PriorityBlockingQueue来实现。
public PriorityBlockingQueue<MyTask> pbq=new PriorityBlockingQueue<>();
c、实现schedule方法注册到队列中去
public void schedule(Runnable runnable,long delay){ MyTask mytask=new MyTask(runnable, delay); pbq.put(mytask); }
d、执行到达时间的任务
在Mytimer类的构造方法创建出一个线程用于从队列中取出一个任务,如果任务时间小于当前时间就将任务重新加入到队列中去,否则就执行该任务。
public MyTimer() { Thread t=new Thread(()->{ while(true){ try { MyTask task=pbq.take(); long currentTime= System.currentTimeMillis(); if(currentTime< task.getTime()){ pbq.put(task); }else{ task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); }
但是这样模拟实现后,在main方法中测试是运行结果显示有问题:
显示需要在MyTask类中实现Comparable接口,因为是优先级队列需要进行排序就要实现Comparable接口的comparaTo()方法。
@Override public int compareTo(Object o) { MyTask task=(MyTask) o; return (int)(this.getTime()-((MyTask) o).getTime()); }
但是此时的代码还存在一个问题:就是队列中的任务的执行时间还没到,执行线程就会一直进行时间判断,处于忙等的状态,于是就可以利用wait和notify,wait等待任务执行时间减去当前时间,当在队列中加入任务时,就需要进行唤醒,需要查看新加入的任务是否需要执行。这样就可以使执行变得更加有效率。
public class MyTimer{ public PriorityBlockingQueue<MyTask> pbq=new PriorityBlockingQueue<>(); Object locker=new Object(); public void schedule(Runnable runnable,long delay){ MyTask mytask=new MyTask(runnable, delay); pbq.put(mytask); synchronized (locker){ locker.notify(); } } public MyTimer() { Thread t=new Thread(()->{ while(true){ try { MyTask task=pbq.take(); long currentTime= System.currentTimeMillis(); if(currentTime< task.getTime()){ pbq.put(task); synchronized(locker){ locker.wait(task.getTime()-System.currentTimeMillis()); } }else{ task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); } }
四、线程池
每次创建和销毁线程时,都需要在内核态支持运行,这样的效率十分低,所以就在反复创建线程时就使用线程池直接在用户态运行,这样就可以极大地减少每次创建线程和销毁线程的损耗。
标准库中的线程池叫做ThreadPoolExecutor,在java.util.concurrent包中,也有简化版本的线程池Executor。
使用标准库中的Executors类的newFixedThreadPool(int n)创建出n个线程的线程池,然后再使用该对象.submit()注册任务到线程池中。
public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(10); for(int i=0;i<1000;i++){ int a=i+1; pool.submit(new Runnable() { @Override public void run() { System.out.println("任务:"+a); } }); } }
判定线程池中恰当的线程数目:
需要进行性能测试。
例如写一个服务器程序,服务器程序里面通过线程池使用多线程来处理用户请求,此时就可以对服务器进行性能测试:构造出一些请求发送给服务器,利用服务器不同的线程数对比程序处理的速度和程序持有的CPU占用率来找到一个合适的平衡点得到合适的线程池中的数目。
创建线程池的几种方式:
- newFixedThreadPool:创建固定线程数的线程池;
- newCachedThreadPool:创建线程数目动态增长的线程池;
- newSingleThreadExecutor:创建只包含单个线程的线程池;
- newScheduledThreadPool:创建延迟时间后执行命令或者定期执行命令,是进阶版的定时器。
模拟实现线程池:
public class MyThreadPool { //1.用Runnable来实现描述一个任务 //2.使用阻塞队列来存放任务 private LinkedBlockingDeque<Runnable> queue=new LinkedBlockingDeque<>(); //3.描述一个工作线程就是从队列中取任务并执行 static class Worker extends Thread{ private LinkedBlockingDeque<Runnable> queue=null; public Worker(LinkedBlockingDeque<Runnable> 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<Thread> list=new ArrayList<>(); public MyThreadPool(int n){ for(int i=0;i<n;i++){ Worker worker=new Worker(queue); worker.start(); list.add(worker); } } //5.实现添加任务方法 public void submit(Runnable runnable) throws InterruptedException { queue.put(runnable); } }