zoukankan      html  css  js  c++  java
  • Java多线程

    Java多线程

    进程和线程

    进程:进程是资源(CPU、内存等)分配的基本单位,他是程序执行时的一个实例。程序运行时系统会创建一个进程,并为它分配资源。然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为他分配CPU时间,程序开始真正的运行。
    线程:线程是程序执行时的最小单位,他是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

    进程是资源分配的最小单位,线程是程序执行的最小单位。

    实现多线程的三种方式

    1. 继承Thread类
    2. 实现Runnable接口
    3. 使用ExecutorService、Callable、Future实现有返回结果的多线程
      实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以说任务是通过线程驱动从而执行的。

    实现Runnable接口

    需要实现run()方法。
    通过Thread调用start()方法来启动线程。

    public class MyRunnable implements Runnable {
    	public void run () {
            //...
        }
        public static void main(String[] args) {
            MyRunnable instance = new MyRunnable();
            Thread thread = new Thread(instance);
            thread.start();
        }
    }
    

    实现Callable接口

    与Runnable相比,Callable可以返回值,返回值通过Future进行封装。

    public class MyCallable implements Callable<Integer> {
        public Integer call() {
            
        }
        public static void main(String[] args) {
            MyCallable mc = new MyCallable();
            FutureTask<Integer> ft = new FutureTask<>(mc);
            Thread thread = new Thread(ft);
            thread.start();
            System.out.println(ft.get());
        }
    }
    

    继承Thread类

    同样也是需要实现run()方法,并且最后也是调用start()方法来启动线程。

    public class MyThread extends Thread {
        public void run() {
            
        }
        public static void main(String[] args) {
            MyThread mt = new MyTread();
            mt.start();
        }
    }
    

    实现接口 VS 继承Thread

    实现接口会更好一些,因为:
    Java不支持多重继承,因此继承了Thread类就无法继承其他类,但是可以实现多个接口;类可能只要求可执行即可,继承整个类开销会过大。

    Thread和Runnable的区别和联系

    联系

    1. Thread类实现了Runnanle接口

    2. 都需要重写里面的Run方法。

    不同

    1. 实现Runnable的类更具有健壮性,避免了单继承的局限性
    2. Runnable更容易实现资源共享,能多个线程同时处理一个资源

    线程的生命周期

    yield方法:使用当前线程从执行状态变为就绪状态。

    sleep方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回可运行状态。不会放弃锁资源

    join方法:通常用于在main()主线程,不会放弃锁资源。

    deamon:守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程;main()属于非后台线程。

    使用setDaemon()方法将一个线程设置为后台线程

    线程的状态

    新建状态:
    使用new关键字和Thread类其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程。
    就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
    运行状态:
    如果就绪状态的线程获取CPU资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
    阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得资源后可以重新进入就绪状态。可以分为三种:
    等待阻塞:运行状态中的线程执行wait()方法,使线程进入到等待阻塞状态。
    同步阻塞:线程在获取synchronized同步锁失败(因为同步锁被其他线程占用)。
    其他阻塞:通过调用线程的sleep()和join()发出了I/O请求时,线程就会进入到阻塞状态。当sleep()状态超时,joind()等待线程终止或超时,或者I/O处理完毕,线程重新转入就绪状态。
    死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
    产生线程阻塞的原因:

    1. 调用Thread.sleep()方法进入休眠状态,不会放弃锁资源
    2. 通过wait()使线程挂起,直到线程得到notify()或notifyAll()消息或者java.util.concurrent类库中等价的signal()或signalAll()消息;Object中的方法会放弃锁资源
    3. 等待某个I/O完成
    4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。

    线程中断:

    使用中断机制即可终止阻塞的线程。

    使用interrput()方法来中断某个线程,他会设置的中断状态。Object.wait(),Thread.join()和Thread.sleep()三种方法在收到中断请求的时候会清除中断状态,并抛出InterruptedException。应当捕获这个InterrupatedException异常,从而做一些清理资源的操作。

    1. 不可中断的阻塞
      不能中断 I/O 阻塞和 synchronized 锁阻塞。
    2. Executor 的中断操作
      Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。
      如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。
    3. 检查中断
      通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。
      interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状态,从而判断一个线程是否已经被中断。
      interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
    4. 如何中断线程的执行?
      (1)设置退出标志(boolean类型的变量),使线程正常退出,也就是当run()方法完成后线程终止。
      (2)使用 interrupt() 方法中断线程。
      (3)使用 stop 方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
      如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,可能出现数据不一致的问题

    线程之间的协作

    同步:多个线程按一定顺序执行;
    通信:多个线程间的信息传递。

    线程同步

    给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。
    synchronized关键字

    1. Synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
      (1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用对象是调用这个代码块的对象;
      (2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
      (3)修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
      (4)修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    2. Synchronized的作用主要有三个:
      (1)确保线程互斥的访问同步代码
      (2)保证共享变量的修改能够及时可见
      (3)有效解决重排序问题

    3. Synchronized的原理
      在编译的字节码加入两条指令来进行代码的同步。
      monitorenter:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
      如果monitor的进入数为0,则该线程进入monitor,然后进入数设置为1,该线程即为monitor的所有者。
      如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
      如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor进入数为0,再重新尝试获取monitor的所有权。
      monitorexit:
      执行monitorexit:
      执行monitorexit的线程必须是objectref所对应的monitor的所有者。
      指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
      通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.illegalMonitorStateException的异常的原因。
      synchronized与,Lock的区别
      lock是一个接口,主要有以下几个方法;
      lock():获取锁,如果锁被暂用则一直等待;
      unlock():释放锁
      tryLock():注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true;
      tryLock(Long time,TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间

    private Lock lock;
    public int func(int value) {
    	try{
    		lock.lock();
    		//....
    	} finally {
    		lock.unlock();
    	}
    }
    

    (1)Lock的加锁和解锁都是由java代码实现的,而synchronized加锁和解锁的过程是由JVM管理的。

    (2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。

    (3)Lock能提高多个线程读操作的效率。

    (4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。底层主要靠volatile和CAS乐观锁操作实现的。

    线程通信

    wait()、notify()、notifyAll()
    他们都属于Object的一部分,而不属于Thread。而sleep()是Thread的静态方法;
    wait()会在等待时将线程挂起,而不是忙等待,并且只有在notify()或者notifyAll()到达时才唤醒。
    sleep()和yield()并没有释放锁,但是wait()会释放锁。
    实际上,只有在同步控制方法或同步控制块里才能调用wait()、notify()和notifyAll()。
    notify():
    该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁。
    notifyAll():
    使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁。

    线程安全

    线程安全:
    当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他协作操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

    线程安全的实现方法

    如何实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的租用。

    1. 互斥同步

      互斥同步(Mutual&Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。在Java中,最基本的互斥同步手段就是synchronized关键字。

      除了synchronized之外,我们还可以使用java.util.concurrent(下文称J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合 try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。不过,相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项;等待可中断、可实现公平锁,以及锁可以绑定多个条件。

      等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

      公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

      解绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这样做,只要多次调用newCondition()方法即可。

    2. 非阻塞同步 乐观锁实现
      互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。

    3. 无同步方案
      要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
      可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
      线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。比如Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式。
      Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++中 __declspec(thread)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。

    锁的类型和锁的优化

    在 java 中锁的实现主要有两类:内部锁 synchronized(对象内置的monitor锁)和显示锁java.util.concurrent.locks.Lock。
    在 java.util.concurrent.locks 包中有很多Lock的实现类,常用的有 ReentrantLock 和ReadWriteLock,其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer 类。

    锁的一些概念

    可重入锁:
    指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。
    自旋锁:
    所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会让出时间片,所以其他线程依旧有申请锁和释放锁的机会。自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。
    公平锁:
    按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利。
    读写锁:
    对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。
    独占锁:
    是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
    乐观锁:
    每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。
    偏向锁():
    大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的id会记录在对象的Mark Word中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
    轻量级锁(CAS):
    轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
    重量级锁:
    虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

    线程池

    为什么使用线程池

    1. 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度影响处理效率。(线程的复用)
    2. 线程并发数量过多,抢占系统资源从而导致阻塞(控制并发数量)
    3. 对线程进行一些简单的管理。(管理线程的生命周期)

    线程池的原理

    1. 线程复用:实现线程复用的原理应该要保持线程处于存活状态(就绪,运行或阻塞)
    2. 控制并发数量:(核心线程和最大线程数控制)
    3. 管理线程(设置线程的状态)

    任务被添加进线程池的执行策略

    1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
    2. 线程数量达到了corePoolSize,则将任务移入队列等待空闲线程将其取出执行(通过getTask()方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源,整个getTask操作在自旋下完成)
    3. 队列已满,新建线程(非核心线程)执行任务
    4. 队列已满,总线程数又达到了maximumPoolSize,就会执行任务拒绝策略

    常见四种线程

    (1)CachedThreadPool可缓存线程池

    根据源码可以看出:

    这种线程池内部没有核心线程,线程的数量是有限制的,最大是Integer最大值。

    在创建任务时,若有空闲的线程时则复用空闲的线程,若没有则新建线程。

    没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。

    适用:执行很多短期异步的小程序或者负载较轻的服务器。

    (2)FixedThreadPool定长线程池

    根据源码可以看出:

    该线程池最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。

    如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。

    适用:执行长期的任务,性能好很多。

    (3)SingleThreadPool单线程线程池

    根据源码可以看出:

    有且仅有一个工作线程执行任务

    所有任务按照指定顺序执行,即遵循队列的入队出队规则。

    适用:一个任务一个任务执行的场景。

    (4)ScheduledThreadPool周期线程池

    根据源码可以看出:

    DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。

    不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。

    这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。

    使用:周期性执行任务的场景(定期的同步数据)

    总结:除了new ScheduledThreadPool的内部实现特殊一点之外,其他线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。

    (5)ThreadPoolEexecutor类构造器语法形式:

    ThreadPoolExecutor(corePoolSize,maxPoolSize,KeepAliveTime,timeUnit,workQueue,threadFactory,handle);

    方法参数:

    corePoolSize:核心线程数(最小存活的工作线程数量)

    maxPoolSize:最大线程数

    keepAliveTime:线程存活时间(在corePoreSize<maxPoolSize情况下有用,线程的空闲时间超过了keepAliveTime就会销毁)

    timeUnit:存活时间的时间单位

    workQueue:阻塞队列,用来保存等待被执行的任务(①synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;②LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;③ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小)

    threadFactory:线程工厂,主要用来创建线程

    handler:表示当拒绝处理任务时的策略(①丢弃任务并抛出RejectedExecutionException异常;②丢弃任务,但是不抛出异常;③丢弃队列最前面的任务,然后重新尝试执行任务;④由调用线程处理该任务)

    (6)在ThreadPoolExecutor类中几个重要的方法

    1. l execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

    2. submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

    3. shutdown()不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。

    4. l shutdownNow()立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

    5. l isTerminated()方法

      调用ExecutorService.shutdown方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用shutdown方法后我们可以在一个死循环里面用isTerminated方法判断是否线程池中的所有线程已经执行完毕,如果子线程都结束了,我们就可以做关闭流等后续操作了。

    线程池中的最大线程数

    一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的个数)
    如果是CPU密集型应用,则线程池大小设置为N+1
    如果是IO密集型应用,则线程池大小设置为2N+1
    如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。
    但是,IO优化中,这样的估算公式可能更适合:
    最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
    因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
    创建线程的个数是还要考虑 内存资源是否足够装下相当的线程
    下面举个例子:
    比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。

  • 相关阅读:
    生成日期列表的函数.sql
    Archlinux下启用Thinkpad功能键
    使用临时表进行编号重排的处理示例.sql
    行值动态变化的交叉报表处理示例.sql
    工作日处理函数(标准节假日).sql
    字符串在编号查询中的应用示例及常见问题.sql
    分段更新函数.sql
    TypeMembersToIL.cs
    排序规则在拼音处理中的应用.sql
    text与image字段转换处理示例.sql
  • 原文地址:https://www.cnblogs.com/cqy1026/p/13606856.html
Copyright © 2011-2022 走看看