Java 多线程(三)
文章目录
- 多线程的代码案例
- 定时器
- 实现一个定时器
- 线程池
- Java标准库中的线程池使用
- 模拟实现一个简单的线程池
多线程的代码案例
定时器
- 在标准库中也是有现成的定时器实现的
- 给定时器安排某个任务,让它在某个时间去执行
- 定时器让这个程序稍等一会才会执行
import java.util.Timer;
import java.util.TimerTask;public class Demo24 {public static void main(String[] args) {Timer timer = new Timer();// 给定时器安排某个任务,让它在某个时间去执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器的任务!");}},2000);System.out.println("程序启动!");}
}
4. Timer里可以安排多个任务
import java.util.Timer;
import java.util.TimerTask;public class Demo24 {public static void main(String[] args) {Timer timer = new Timer();// 给定时器安排某个任务,让它在某个时间去执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000!");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000!");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000!");}},1000);System.out.println("程序启动!");}
}
实现一个定时器
- 实现步骤:
- 使用哪个数据结构更好呢?
可以使用优先级队列,根据时间小的任务优先执行
一个线程扫描任务
一个数据结构存储任务
一个类描述任务内容和时间
- 使用time保存一个绝对的时间戳,可以更好地进行后续的比较,而相对时间戳需要换算
Runnable 是一个接口(Interface),它代表了一段可以被线程执行的代码任务
实现定时器的代码:
import java.util.PriorityQueue;
import java.util.TimerTask;// 通过这个类,描述这个任务
class MyTimeTask implements Comparable<MyTimeTask> {// 描述一个任务private Runnable runnable;// 执行任务的时间private long time;// 构造方法,此处的delay就是schedule传入的相对时间public MyTimeTask(Runnable runnable,long delay){this.runnable = runnable;this.time = System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTimeTask o) {return (int) (this.time - o.time);}public long getTime(){return time;}public Runnable getRunnable(){return runnable;}
}// 搞一个定时器的类
class MyTimer{// 使用一个数据结构保存所有的任务PriorityQueue<MyTimeTask> queue = new PriorityQueue<>();// 使用这个对象作为锁对象private Object locker = new Object();// 主线程对队列进行修改,添加了新的元素public void schedule(Runnable runnable,long delay){synchronized (locker) {queue.offer(new MyTimeTask(runnable, delay));locker.notify();}}// 搞一个线程扫描队列public MyTimer(){Thread t1 = new Thread(()->{// 扫描线程对队列进行了修改// 扫描线程,看一个线程的队首元素是否,看是否是到达了时间while (true) {try {synchronized (locker) {// 使用while,在后续线程被唤醒时,再次确认一下条件while (queue.isEmpty()) {// 阻塞等待,用wait进行等待// 这里的wait应该由其他线程唤醒// 添加了任务就应该唤醒locker.wait();}MyTimeTask task = queue.peek();// 比较一下当前的对首元素是否可以执行了long curTime = System.currentTimeMillis();if(curTime >= task.getTime()){// 当前时间达到了任务时间可以进行任务了task.getRunnable().run();// 完成任务,把任务从队列中删除掉queue.poll();}else{// 当前时间还没有到任务时间不执行任务// 什么都不干,等待下一轮的时间判定// 等待这个时间差locker.wait(task.getTime() - curTime);}// 还可以写一个如果开始的delay时间是负数,那么本身现在时间// 大于任务时间,就提示错误}} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();}
}
public class Demo25 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000!");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000!");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000!");}},1000);System.out.println("程序启动!");}
}
两个线程对同一个队列进行操作,线程是不安全的,需要加锁
3. 调试的问题:
4. 这里还有一个问题:忙等
如何解决这个忙等呢?
需要有一个超时时间的wait等待,等待到下一个任务时间的开始,超时时间为下次任务时间减现在的系统时间
线程池
-
虽然线程对比进程是更高效了,但是线程频繁的创建和销毁,这样的开销也是不容忽视的
-
有两种办法可以进一步提高线程的效率:
协程(轻量级线程):相比于线程,把系统调度的步骤给省略了,需要我们自己调度。协程是当下比较流行的并发编程的手段,但是java中不流行线程池也是一种手段
-
线程池的优点:在使用第一个线程的时候,就把2,3,4,5线程创建好,后续想要使用新的线程就不必重新创建了,直接拿过来就能用(此时创建线程的开销就被降低了)
-
为什么从池子里面取的效率比创建线程的效率更高?
因为用户态只需要给自己去做,效率更高,而内核态是cpu要给所有线程提供服务,效率自然就低了,从池子里取只有用户态,创建线程有用户态 + 内核态
Java标准库中的线程池使用
- 线程池不是直接new,而是通过一种专门的方法,返回一个线程池对象
import java.util.TimerTask;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo27 {public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(4);// 给线程池添加任务service.submit(new Runnable() {@Overridepublic void run() {System.out.println("world");}});}
}
- 线程池的创建又涉及到了工厂模式
- 通常我们创建对象用new,new关键字会触发构造方法,但是构造方法存在局限性,工厂模式就是给构造方法填坑的
填什么坑呢?
举个例子:
4. 四种类型的线程池:(前两个比较常用)
第一种cached是动态适应的线程池数量
第二种fixed是申请固定数量的线程池
newSingleThreadExecutor(),只有一个线程的线程池(用的不多)
newScheduledThreadPool(int corePoolSize),相当于定时器,不是一个线程扫描执行任务了,而是多个线程执行时间到的任务了
5. ThreadPoolEexcutor
的主要方法有两个:
构造
注册任务(添加任务)
ThreadPoolEexcutor的构造方法的参数有很多(重点),经典面试题
- java.util.concurrent:关于并发编程的(在java中主要是多线程)
- 对参数的解释:
正式员工用完也不会被销毁,实习生用完并且不用你了就会被销毁
描述线程数目:
拒绝策略:
不同的拒绝策略:
拒绝策略的例子:
8. 线程数目和拒绝策略(面试必考)
- 线程池的数量如何设置最合适?
正确的做法应该是使用实验的方式,对程序进行性能测试,实验中使用不同的线程池个数进行测试,看哪种情况下最符合你的需求
模拟实现一个简单的线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPool{// 任务队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 添加任务public void submit(Runnable runnable) throws InterruptedException {// 此处相当于拒绝策略了,就是第五种策略,阻塞等待了queue.put(runnable);}public MyThreadPool(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 Demo28 {public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(4);for(int i = 0;i < 1000;i++){int id = i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {// 在匿名内部类中涉及了变量捕获问题,id就是System.out.println("执行任务 + " + id);}});}}
}
此时这里的id每次循环都是一个新的id,并且每次循环都会赋值上一个初值,所以说每次的id是一个不变的值(final)