java多线程(初阶)
1.单例模式
通过静态方法来new一个实例化对象,且构造方法被private修饰,是java中的一种设计模式,分为懒汉模式,饿汉模式。
(1)饿汉模式:
class Singleton {private static Singleton instance = new Singleton();private static Singleton getInstance() {return instance;}private Singleton() {}
}
在类加载的时候对象就已经被new出来了,只需要通过静态方法来获取就行了。
(2)懒汉模式:
class SingletonLazy {private static SingletonLazy instance = null;private static SingletonLazy getInstance() {if(instance == null) {instance = new SingletonLazy();}return instance;}private SingletonLazy() {}
}
类加载的时候对象并没有创建出来,调用静态方法以后才能够new出对象,并且返回。
(3)注意事项:
懒汉模式(多个线程修改同一个变量)比饿汉模式(多个线程读取同一个变量)更不安全。原因:
上述情况导致对象new了两次,违背了单例的要求。创建对象不一定是轻量的,背后可能是重量的。故对上述懒汉模式中的代码进行加锁。
此时创建对象的这个操作就是原子的,当有一个线程获取到这把锁,在它没有释放锁之前,其他线程只能等待这把锁被释放,从而导致阻塞。这个等待时间不知道有多长,可能会阻塞很久,所以又做了如下优化:
第一个if作用:用来判断是否要进行加锁,第二个if作用:用来判断是否要进行new对象。如果一个对象已经被new好了,另一个线程调用该方法的时候,在第一个if就进不去,就不会进行加锁,直接返回对象。
但在这个过程中可能会出现一个问题:指令重排序。
new对象的这个操作分三步:
a.内存分配地址
b.在内存空间上构造对象
c.将地址赋予instance引用
正常来说应该abc的执行顺序,而现在有可能的执行顺序时acb,如果按照这个执行顺序,有可能线程t1在还ac执行完过后,instance是一个不为空的非法对象,此时线程t2来调用这个方法了,直接就返回了instance对象(非法的),所以为了避免上述的情况,就让volatile修饰变量。
2.阻塞队列
是一种特殊的队列,线程安全且具有阻塞特性。
阻塞特性:
a.在队列满的时候,往队列中插入元素会阻塞等待;
b.在队列为空的时候,删除元素也会阻塞等待;
(1)生产者消费者模型:(典型的使用阻塞队列完成的模型)
生产者把生产出来的内容放到阻塞队列中,消费者从队列中获取内容。
(2)优点:
a.解耦合
添加了阻塞队列:
通过上述方式耦合程度大大降低。
b.削峰填谷
(3)JAVA标准库中的阻塞队列(BlockingQueue)
是一个接口,有三种实现方式:基于链表(适合:数据不要求比较,且数据量大),基于顺序表,基于优先级队列(适合:数据要求比较)
ps:阻塞队列是继承于Queue(队列)的,尽量不要使用Queue里面的方法,因为可能会造成线程不安全且没有阻塞特性。
3.定时器
客户端发起请求之后,正常情况下来说,服务器是会接收并且处理请求,然后返回响应。但是也有可能服务器挂了,客户端等待了很久都没有接收到响应。
对于客户端来说不能无限的等待下去,到达这个最大期限以后会再次发送请求,如果还是没有接收到响应,就会直接断开于服务器的连接或者做其他的事。
上述这个最大期限就是定时器所需要完成的事情。
(1)JAVA标准库中的定时器(Timer)
在java.util包下。
eg:
TimeTask是一个抽象类,实现了Runnable接口;delay(延时)单位:ms
启动程序观察,程序并没有停止。
原因;Timer中的其他线程没有执行完毕。(Timer中不仅有扫描线程还有一些其他的线程,这些线程没有结束是导致进程没有结束的主要原因)扫描线程就是Timer中执行TimeTask任务的一个线程。
(2)自定义定时器
a.Timer中需要一个线程,来扫描是否要执行任务。
b.需要一个数据结构把任务保存起来
c.还需要一个类,来描述当前的任务(任务内容和时间)
MyTimerTask类:
public class MyTimerTask{private Runnable runnable;private long time;public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;}public Runnable getRunnable() {return this.runnable;}public long getTime() {return this.time;}
}
MyTimer类:
public class MyTimer {//有一个数据结构来存储任务,其中任务执行顺序是按照时间来执行的private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(new Comparator<MyTimerTask>() {@Overridepublic int compare(MyTimerTask o1, MyTimerTask o2) {return (int)(o1.getTime()-o2.getTime());}});//创建锁对象private Object locker = new Object();//schedule方法用来放任务public void schedule(Runnable runnable, long time) {synchronized (locker) {queue.offer(new MyTimerTask(runnable,time));locker.notify();}}public MyTimer() {//扫描线程,不停地进行扫描当前的任务是否要执行。Thread t = new Thread(()-> {while (true) {try {synchronized (locker) {while(queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();if(System.currentTimeMillis() >= task.getTime()) {//执行任务task.getRunnable().run();queue.poll();}else {locker.wait(task.getTime() - System.currentTimeMillis());}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();}
}
test类:
public class test {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}}, 1000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}},3000);System.out.println("程序启动!!");}
}
程序启动:
也不会结束,由于while循环(模拟了Timer里面程序的结束结果)。
ps:notify:
4.线程池
线程是轻量级线程,但是频繁地创建线程这个开销也是不容忽视的。
有两种解决方式:
a.协程:比线程还轻量,但是在JAVA不常用
b.线程池:在创建线程1的时候,就把其他线程2,3,4,5.....给创建好了,要用的时候直接去池子中取
从池子中取要比直接创建的速率要快?
是的。因为从池子中取这个操作是一个纯用户态的操作,这个过程是可控的,而通过new来创建对像,是内核态+用户态的操作,这个过程是不可控的。(创建线程会调用相关api,并到系统内核中去执行)
(1)ThreadPoolExecutor(线程池)
java中提供了一些线程池,但本质都是对ThreadPoolExecutor的封装
a.构造方法
主要理解最后一个:
分别为核心线程数和最大线程数。
线程池中线程的数目是不确定的,在核心线程数和最大线程数范围之间。
举个例子来理解:
一个公司中有正式员工和实习生,所有的正式员工就相当于核心线程数,公司所有正式员工+实习生就是最大线程数。实习生不允许摸鱼,而正式员工允许,在任务较多的,做不过来就会多找几个实习生来帮助完成任务,当任务不多的时候,这时候就看哪个实习生在摸鱼的,就将他开除。
线程池中类似这样做来满足效率的需求和避免过多的系统开销。
线程活跃时间,就类似于上述允许实习生摸鱼的时间。
阻塞队列(消息队列),用来存放线程池中的任务。
这个一个工厂类,由该类来负责创建对象(通过该类中的静态方法来创建并返回),并且返回。(工厂模式的体现)
拒绝策略:当线程池中任务已经放满了过后,此时再来一个新的任务,对于不同的拒绝策略,有着不同的效果:
关于线程池中线程的数目:
是一个不确定的值(如果答出某个确切的值都是错误的)
原因:线程中的代码一般分两种:cpu密集型和io密集型
cpu密集型:代码主要进行算术运算和逻辑运算
io密集型:代码主要涉及io操作
假设一个cpu上最多允许跑的线程数目是N个:
假设一个代码中所有都为cpu密集型,线程池中的线程数目不应该超过N个,超过的话就会把cpu吃满,影响cpu的执行速率,反而更多的线程还会增加调度的开销。
假设一个代码中所有都为io密集型,由于是对硬盘进行操作,不吃cpu,所以线程池中的线程数目可以超过N个。
所以线程池中的数目是不确定的。不同的代码,线程池中的线程数目是不同的(无法知道一个代码中是cpu密集型还是io密集型)。
(2)自定义线程池:
a.搞一个数据结构来存储任务。
b.ThreadPoolExecutor中需要创建出n个线程来执行任务(构造方法中实现)
c.需要一个submit方法来提交任务。
MyThreadPoolExecutor类:
public class MyThreadPollExecution {//阻塞队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue(100);//提交任务public void submit(Runnable runnable) throws InterruptedException {//此处应该有拒绝策略,这里省略synchronized (locket) {queue.put(runnable);locket.notify();}}private Object locket = new Object();public MyThreadPollExecution(int n) {//创建n个线程来执行任务for(int i = 0; i < n; i++) {Thread t = new Thread(()-> {try {synchronized (locket) {while (queue.isEmpty()) {locket.wait();}Runnable runnable = queue.take();runnable.run();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}}
}
test类:
public class test {public static void main(String[] args) throws InterruptedException {MyThreadPollExecution myThreadPollExecution = new MyThreadPollExecution(4);for(int i = 0; i < 1000; i++) {int id = i;myThreadPollExecution.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务" + id);}});}}}
执行结果: