多线程核心知识
Posted wlwl
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程核心知识相关的知识,希望对你有一定的参考价值。
线程生命周期(线程状态)
Java中的线程的生命周期大体可分为5种状态。
新建:创建完线程、还没调用start方法。
就绪:已经调用start方法,等待CPU分配时间片。
运行:run方法正在运行中。
阻塞:wait、sleep、yield、join 使线程阻塞住。
死亡:run方法运行完毕。
多线程通信
yield
yield()让出线程时间片,增大线程切换的几率。由于它是native修饰的,所以是通过其他语言直接操作机器实现的。 public static native void yield();
sleep
sleep(long millis)会让线程阻塞住millis秒,在这个时间段内不再参与到CPU竞争。sleep()是通过millis参数来设置一个定时器实现的,时间一结束,若没有其他线程正在执行,则会同其他线程一起抢占cpu资源。 public static native void sleep(long millis) throws InterruptedException;
join
我们现在有两个线程的话,在第二个线程里面调用第一个线程的join方法,程序会立即切换到第一个线程去执行。点进去thread.join()源码发现,join主要是下面几行代码实现的。 public final void join() throws InterruptedException { // 最多等待millis毫秒,此线程才能死;millis=0意味着永远等待。 join(0); } // 可以看到调用join(long millis)方法,实际还是通过synchronized实现的。 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) {//如果线程还没执行完 wait(0);//释放对象锁,程序停止执行。 } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } 可以看到join的底层还是对方法加锁,然后通过wait()使自己阻塞,从而切换到其他线程执行;join()方法的java文档中写到:a thread terminates the this.notifyAll method is invoked.也就是说,其他线程run()方法运行完了以后,会调用this.notifyAll,释放锁。这样线程获得锁以后就可以继续往下执行了。
wait
wait()会让线程休眠,释放锁。 (1)为什么 wait/notify/notifyAll一定要放在同步代码块里,我们可以看下面这个案例。 boolean flag = false; // A线程代码 while(!flag){ wait(); } // B线程代码 if(!flag){ condition = true; // nofity随机唤醒一个等待的线程;notifyAll唤醒等待该锁的所有线程 notify(); } 1. 如果线程A刚执行完while(!flag),准备执行wait(),此时线程A的时间片已经耗尽 2. B线程执行完 condition = true; notify();的操作,由于A并没有wait,所以B的notify不会起任何效果 3. 此时A又获得了时间片,继续执行wait(),此时由于没有notify()唤醒她,那么她会一直沉睡下去。 所以相当于:锁对象里面维护了一个队列,线程A执行lock的wait方法,把线程A保存到list中,线程B中执行lock的notify方法,从等待队列中取出线程A继续执行。 (2)为什么 wait要放在while中判断而不是if中
如果采用if判断,当线程从wait中唤醒时,判断条件已经不满足处理业务逻辑的条件了,从而出现错误的结果。所以我们业务需要像下面一样在判断一次。而循环则是对上述写法的简化 synchronized (monitor) { // 判断条件谓词是否得到满足 if(!locked) { // 等待唤醒 monitor.wait(); if(locked) { // 处理其他的业务逻辑 } else { // 跳转到monitor.wait(); } } }
为什么要使用线程池
诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。风险与机遇:用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
总结起来就三点:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用。
线程池原理剖析
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
下面4种是比比较常用的线程池创建方式,第一个用的比较多。
public class ExecutorDemo { public static void main(String[] args) { // 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5); for (int i = 0; i < 15; i++) { final int temp = i; newFixedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); /** * 控制台输出 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-1 * pool-1-thread-2 * pool-1-thread-3 * pool-1-thread-4 * pool-1-thread-5 * * 总结: * 因为线程池大小为5,超出的请求需要排队等待线程的分配。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() */ } } }
public class ExecutorDemo { public static void main(String[] args) { // 创建一个可缓存线程池,如果线程池无可用线程,则回收空闲线程、否则新建线程。 ExecutorService executorService = Executors.newCachedThreadPool(); // executorService.submit(() -> { // System.out.println(Thread.currentThread().getName()); // }); for (int i = 0; i < 15; i++) { final int temp = i; executorService.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } /** * 控制台输出 * pool-1-thread-1 * pool-1-thread-2 * pool-1-thread-2 * pool-1-thread-3 * pool-1-thread-1 * pool-1-thread-7 * pool-1-thread-6 * pool-1-thread-5 * pool-1-thread-4 * pool-1-thread-8 * pool-1-thread-9 * pool-1-thread-10 * pool-1-thread-11 * pool-1-thread-12 * pool-1-thread-13 * * 总结: * 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 * */ } }
public class ExecutorDemo { public static void main(String[] args) { // 创建一个定长线程池,支持定时及周期性任务执行。 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); System.out.println(LocalTime.now()); for (int i = 0; i < 10; i++) { scheduledExecutorService.schedule(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+" 现在时间:"+ LocalTime.now()); } }, 3, TimeUnit.SECONDS); } /** * 控制台输出 * 19:06:29.623 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 * pool-1-thread-1 现在时间:19:06:32.626 */ } }
public class ExecutorDemo { public static void main(String[] args) { // 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 ExecutorService executorService3 = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; executorService3.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); } } }
threadPoolExecutor实现原理
/** * 自定义线程池 */ public class ExecutorDemo { public static void main(String[] args) { /** * 上面4种线程池的创建其实都是对于ThreadPoolExecutor的封装,点进去看发现就是: * new ThreadPoolExecutor(int corePoolSize, * int maximumPoolSize, * long keepAliveTime, * TimeUnit unit, * new LinkedBlockingQueue<Runnable>()) */ /** * @param corePoolSize 核心线程数(空闲也不会被回收) * @param maximumPoolSize 最大线程数 * @param keepAliveTime 线程空闲时的超时时间 * @param TimeUnit 超时时间单位 * @param BlockingQueue<Runnable> 阻塞队列,线程排队时就装在这个队列里面 */ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,2,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(3)); /** * 上面已经创建好线程池了,核心线程为1,也就是初始线程数为1;最大线程2;队列最多容纳3个线程等待。下面我们有6个任务要执行 * (1) 第一个任务开始,直接就交给了核心线程去执行 * (2) 后面的线程进来,由于没有线程了就会放到队列里面去等待。如果之前的线程空闲了就会继续复用;否则放入队列等待新的线程创建 * (3) 假使线程都在使用中,因为最大线程数为2,那么最多同时运行2个线程,由于队列也只能排队3个线程,那么这个线程池最多就只能容纳5个线程 * (4) 如果线程池已经被占满,此时还有第6个任务要进来,那么线程池就会溢出报错。 */ for (int i = 1; i <= 6; i++) { final int temp = i; threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "任务" + temp); } }); } /** * 控制台输出 * pool-1-thread-1任务1 * pool-1-thread-1任务2 * pool-1-thread-1任务3 * pool-1-thread-1任务4 * pool-1-thread-2任务5 * java.util.concurrent.RejectedExecutionException: Task com.Thread.A_newThread.pool.ExecutorDemo$1@6d6f6e28 rejected from java.util.concurrent.ThreadPoolExecutor@135fbaa4[Running, pool size = 2, active threads = 2, queued tasks = 3, completed tasks = 0] * at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047) * at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823) * at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369) * at com.Thread.A_newThread.pool.ExecutorDemo.main(ExecutorDemo.java:35) */ } }
如何合理分配线程池
一个线程池究竟设置多大才合适这个并没有一个固定值,而是根据不同任务而定的。与任务的CPU密集度和任务的IO密集度有关。
CPU密集型
指的就是任务需要大量的运算(即run方法中代码业务很多),而没有阻塞,cpu一直全速运行。cpu密集的任务只有在多核cpu上通过多线程加速,在单核cpu是不会有任何速度提升的。
IO密集型
是指任务需要大量的io(即大量的阻塞),也是只有在多核cpu上通过多线程加速,在单核cpu是不会有任何速度提升。
所以想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
1. 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
2. 任务的优先级:高、中、低。
3. 任务的执行时间:长、中、短。
4. 任务的依赖性:是否依赖其他系统资源,如数据库连接等。
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务;IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
最佳线程数目 = (线程等待时间/线程CPU时间+1)* CPU数目。比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:(1.5/0.5+1)*8=32。
以上是关于多线程核心知识的主要内容,如果未能解决你的问题,请参考以下文章