多线程知识总结
一、多线程
主要是为了解决单线程因阻塞而带来的效率问题,同时也充分利用多核CPU的优势。
1、拷贝主内存成员变量
线程工作内存是cpu寄存器和高速缓存的抽象描述,使用频率高的数据从主存拷贝到高速缓存中,每个线程在cpu高速缓存中对拷贝的数据进行读取、计算、赋值,再在合适的时候同步更新到主存的该数据。这样比一直去主内存访问要快的多。
2、产生安全问题
我们应该知道了在运行时数据内存区中虚拟机栈、pc寄存器、本地方法栈是每个线程都有的,很明显这些都是独立的不会发生线程不安全的问题,但是我们平时讨论的线程不安全、要加锁等等情况是怎么回事呢?
在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操作寄存器的速度要比操作计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操作CPU缓存的速度快于主存但慢于CPU寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称作RAM,所有的CPU都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。
当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先flush寄存器到CPU缓存,然后再在某些节点把缓存数据flush到主存。
想象一下我们的共享对象存储在主存,一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改,但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中。
3、线程的通信
线程之间的通信是依靠共享内存和线程方法的调用来实现。在多线程的体系下,Java的内存模型分为主内存和共享内存,通过内存之间的数据交换,依赖多线程的可见性,实现线程之间的通信;线程具有基本状态,主动调用线程的wait、notify方法也可以实现线程之间的通信。
4、三大特性
原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,直接赋值是原子性操作,而对于自增1是两个操作不是原子性操作。
可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行来说,可见性问题是不存在的。
有序性:在单线程中所有操作都是有序的,对于一个线程观察另一个线程,所有操作都是无序的。在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。虽然它们线程之间是互不影响的,但是它们对于数据的修改是对其他线程有很大影响。
二、线程的实现
1、不共享数据
不共享数据就是每个都是独立的线程,再去调自己的start方法就可以不共享数据了。不传对象参数,创建独立线程执行,每个线程都运行自己的run方法。NotShareData 继承Thread或者实现Runnable接口。
NotShareData nsd1=new NotShareData("1").start();
NotShareData nsd2=new NotShareData("2").start();
NotShareData nsd3=new NotShareData("3").start();
2、共享数据
需要有共享数据,则要在线程中传同一个对象,或者把该对象要共享的变量设为static就可以传不同对象,但是static修饰的变量是类变量,生命周期太长了,占用内存。
1.如果每个线程执行的代码相同,可以使用同一个Runnable对象。创建多个Thread,把共享对象(继承Thread或者实现Runnable接口,共享数据在共享对象里)传进去。
new Thread(对象).start();
new Thread(对象).start();
2.如果每个线程执行的代码不相同,就要用不同的runnable对象了。这种方式又有两种来实现这些runnable对象之间的数据共享
2.1 将共享数据封装在另一个对象中,然后将这个对象逐一传递给各个runnable对象中。每个线程共享数据的操作方法也分配到了这个对象身上去完成,这样容易实现针对该数据进行共享数据的互斥和通信
ShareData data = new ShareData();
new Thread(new MyRunnable1(data)).start();
new Thread(new MyRunnable2(data)).start();
2.2 将这些runnable对象作为某个类的内部类,共享数据作为这个外部类的成员变量,每个线程对共享数据的操作也分配到外部类,以便实现对共享数据进行的各个操作进行互斥和通信
new Thread(new Decrease()).start();
new Thread(new Increment()).start();
2.3 上面两种方式的结合:将共享数据封装到另一个对象中,各个线程对共享数据操作的方法也分配到那个对象上去完成,对象作为外部类的成员变量或方法的局部变量,每个runnable对象作为外部类中的成员内部类或局部内部类。
3、线程常用方法
1.wait()方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及CPU才能继续运行,又可能此时对象的状态已经发生了变化
2.调用obj.wait()后,线程就释放了obj的锁,当调用obj.notify/notifyAll的时候,但是仍无法获得obj锁。直到退出synchronized块,释放obj锁后
3.Thread还有一个sleep()静态方法,它也能使线程暂停一段时间。sleep与wait的不同点是:sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒
4.线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException
5.Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁,Thread.yield()方法,当前线程放弃CPU,但不会释放锁,调用该线程suspend()方法将该线程挂起,该线程不会释放锁
6.代码块执行结束,遇到break、return终止该代码块,出现了未处理Error和Exception,执行了线程对象的wait()方法都会释放线程锁
7.Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕,join方法是通过调用线程的wait方法来达到同步的目的的
8.Thread.yield( )方法,使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了
9.synchronized,主要用来给方法、代码块加锁。synchronized方法控制对类成员变量的访问,synchronized代码块所起到的作用和synchronized方法一样,只不过它使临界区变的尽可能短了,换句话说:它只把需要的共享数据保护起来。对于同步方法,锁是当前实例对象;对于同步方法块,锁是Synchonized括号里配置的对象;对于静态同步方法,锁是当前对象的Class对象。一个线程可以多次对同一个对象上锁
10.Volatile变量:当一个变量被声明为volatile时候,线程写入时候不会把值缓存在寄存器或者或者在其他地方(不能有私有拷贝变量,强迫线程将最新的值刷新到主内存),当线程读取的时候会从主内存重新获取最新值(都强迫从主内存中重读该变量的值),而不是使用当前线程的拷贝内存变量值。但是不能使用他来构建复合的原子性操作,也就是说当一个变量依赖其他变量或者更新变量值时候新值依赖当前老值时候不在适用,只会保证拿到值是最新的,但不能保证没有线程同时在修改该值。volatile原理是基于CPU内存屏障指令实现的。
三、线程安全实现方法
线程安全所做的任何操作其实都是围绕多线程的三个特性:原子性、可见性、有序性展开的
1、互斥同步(悲观锁)
互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。 同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。 而互斥是实现同步的一种手段,临界区(Critical Section)、 互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。 因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的
通过synchronized来实现
根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。对象锁是基于对堆内存内对象的头部加锁信息; 类锁是基于对类对应的 java.lang.Class对象加锁信息。同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,所以synchronized是Java语言中一个重量级的操作(对于锁的优化中就不是指重量级锁)。
底层是它依赖的是monitorenter和monitorexit指令,monitorenter锁定对象,必须通过monitorExit方法才能解锁。是可以重入的,也就是可以多次调用,然后通过多次调用monitorExit进行解锁;monitorExit解锁对象,前提是对象必须已经调用monitorEnter进行加锁。另外还要阻塞线程,park阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时);unpark释放被park创建的在一个线程上的阻塞。
2、非阻塞同步(乐观锁)
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步,有两种实现方法:
2.1、版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
2.2、CAS算法
CAS算法涉及到三个操作数,需要读写的内存值 V、进行比较的值A、拟写入的新值 B。当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。通过CAS实现了自旋锁,java里由compareAndSet通过Unsafe类实现CAS算法。
假如当前值为1,那么线程A和检查B同时执行到了各自的next都是2,current=1,假如线程A先执行了3,那么这个是原子性操作,会把档期值更新为2并且返回1,if判断true所以incrementAndGet返回2.这时候线程B执行3,因为current=1而当前变量实际值为2,所以if判断为false,继续循环,如果没有其他线程去自增变量的话,这次线程B就会更新变量为3然后退出。这里使用了无限循环使用CAS进行轮询检查,虽然一定程度浪费了cpu资源,但是相比锁来说避免的线程上下文切换和调度。
缺点:
1.ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效,我们可以使用锁或者利AtomicReference类把多个共享变量合并成一个共享变量来操作
CAS实现lock锁
lock锁有两个实现,分别是ReentrantLock(可重入锁)和ReentrantReadWriteLock锁。ReentrantReadWriteLock主要是读写锁分离,读读不互斥,读写和写读互斥。后面主要讲可重入锁,也是自旋锁。
ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue等安全队列集合它们线程安全的实现方式通过ReentrantLock来实现的,ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的,通过继承AbstractQueuedSynchronizer来实现,而底层正是CAS自旋。
ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别ReentrantLock表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),synchronized则表现为原生语法层面的互斥锁。ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、 可实现公平锁,以及锁可以绑定多个条件
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在有新晋获取锁的进程会有多次机会去抢占锁,如果获取失败就会被加入等待队列后则跟公平锁没有区别。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。为什么都默认是非公平的?因为使用公平锁的线程会进行了频繁切换,而频繁切换线程,性能必然会下降的厉害(新线程到队尾切换到队头的线程)
锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可
总结:简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
3、无同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。 同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,这里简单地介绍其中的两类:可重入代码和线程本地存储
3.1、可重入代码
这种代码也叫做纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。 相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的
3.2、线程本地存储
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。最重要的一个应用实例就是经典Web交互模型中的“一个请求对应一个服务器线程”的处理方式,这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题
如果一个变量要被多线程访问,可以使用volatile关键字声明它为“易变的”;如果一个变量要被某个线程独享,可以通过ThreadLocal类来实现线程本地存储的功能
四、线程锁的优化
锁优化的思路和方法有以下几种:减少锁持有时间、减小锁粒度、锁分离、锁粗化、锁消除。
减少锁持有时间:要减少其他线程等待的时间,所以,只需要在有线程安全要求的程序代码上加锁
减小锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。ConcurentHashmap 中使用分段锁提高 put() 操作的并发能力,默认情况下 ConcurentHashmap 有16个段,理想情况下,它可以同时接受16个线程同时插入
锁分离:根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥。即保证了线程安全,又提高了性能
锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁
锁消除:锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。因为有些JDK代码自带有锁如vector集合。
对synchronized锁做了如下锁优化,为了减少获得锁和释放锁所带来的性能消耗,提高性能,增加了从偏向锁到轻量级锁再到重量级锁的过度
1、偏向锁
有些时候,在整个同步周期内是没有竞争的,在这时,又只有一个线程在运行,这个时候没有其他线程在和它争抢cpu,那么这个时候进行的加锁,释放锁,重入锁等等操作都是多余且降低性能的。所以这个时候就进入了偏向锁的状态。
在这个过程中,线程可以忽略这些锁,不会进行锁的操作,就是好像在偏向这个线程一样,只有初始化的时候使用一次锁的操作,也就是它整个过程只有进入偏向锁这个状态使用CAS切换了下状态,其他时候任何的锁和CAS都不会做,偏向锁就是力取在无竞争的情况下将同步都去掉。如果此时再有其他线程竞争锁,那么偏向锁会膨胀为轻量级锁
2、轻量级锁
轻量级锁是相对于重量级锁而言的,轻量级锁也被称为非阻塞同步、乐观锁,当没有锁竞争,但是有多个线程在使用锁的时候,这个时候就是用CAS获得轻量级锁,注意!使用的是CAS操作,这是在没有锁竞争的情况下。这样做的好处就是避免了使用传统的重量级锁互斥量。主要有两种轻量级锁:自旋锁和自适应自旋锁。
2.1、自旋锁
自旋锁就是在竞争不到锁的时候,先不阻塞,进入自旋锁,在这个过程中,他就是做一些无意义的操作(如:几次空循环),然后在这个过程中会再次竞争锁,如果还没有竞争到,则会阻塞。自旋锁在锁持有时间长,锁竞争不激烈的情况下更能突出性能
2.2、自适应自旋锁
主要是为了解决自旋锁自旋时间不合理问题的,自适应自旋锁是根据上一个线程竞争这个锁所需要的时间来决定当前线程自旋的时间,如果上一个线程很快就获得了锁,那么就认为很快就能获得锁,所以就允许自旋时间长一点,比如100个循环;如果上一个线程很久才获得锁,那么就认为很难获得锁,就让自旋时间短一点或者直接阻塞,以免多余浪费cpu
3、重量级锁
重量级锁也称为阻塞同步、悲观锁,就是直接阻塞掉后来线程,owner没有释放锁,其他线程就只能等待owner线程释放锁。内部实现靠的是操作系统的互斥量mutex来实现的
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长
总结:
在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们。
偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;如果线程争用激烈,那么应该禁用偏向锁
五.线程池
无限制的新建线程,相互竞争,占用过多的系统资源,导致死锁以及OOM。而且这些线程缺乏统一的管理的功能,也缺乏定期执行,定时执行,线程中断的功能。
线程池类图:
1.线程池的作用
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2.线程池的创建
java通过Executors工厂类提供我们的线程池一共有4种:
fixedThreadPool() :启动固定线程数的线程池,在Android中,由于系统资源有限,比较常用。
CachedThreadPool():按需分配的线程池
ScheduledThreadPoolExecutor():定时,定期执行任务的线程池
ThreadPoolExecutor():指定线程数的线程池
前面三个可以通过Executors来创建线程池,但会出现堆积的请求处理队列可能会耗费非常大的内存,线程数最大数是Integer. M AX_VALUE,可能会创建数量非常多的线程等问题。所以建议使用ThreadPoolExecutor创建。而ThreadPoolExecutor不能通过Executors创建,下面主要讲ThreadPoolExecutor。
ExecutorService execut = Executors.newFixedThreadPool(3)
2.1、ThreadPoolExecutor来创建一个线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
corePoolSize :线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程。
当有任务过来的时候才会去创建创建线程执行任务。换个说法,线程池创建之后,线程池中的线程数为0,当任务过来就会创建一个线程去执行,直到线程数达到corePoolSize 之后,就会被到达的任务放在队列中。(注意是到达的任务)。换句更精炼的话:corePoolSize 表示允许线程池中允许同时运行的最大线程数。如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize :线程池允许的最大线程数,他表示最大能创建多少个线程。maximumPoolSize肯定是大于等于corePoolSize。
keepAliveTime :表示线程没有任务时最多保持多久然后停止。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用。换句话说,当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了keepAliveTime,那么就是shutdown。
Unit:keepAliveTime 的单位。
workQueue :一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能
threadFactory :线程工厂,用来创建线程。
handler :表示当拒绝处理任务时的策略。
2.2、任务缓存队列
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:
1)有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)*任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
2.3、拒绝策略
AbortPolicy:丢弃任务并抛出RejectedExecutionException
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
DiscardPolicy:丢弃任务,不做任何处理。
2.4、选择线程池数量
线程池的大小决定着系统的性能,过大或者过小的线程池数量都无法发挥最优的系统性能。当然线程池的大小也不需要做的太过于精确,只需要避免过大和过小的情况。一般来说,确定线程池的大小需要考虑CPU的数量,内存大小,任务是计算密集型还是IO密集型等因素。
NCPU = CPU的数量
UCPU = 期望对CPU的使用率 0 ≤ UCPU ≤ 1
W/C = 等待时间与计算时间的比率
如果希望处理器达到理想的使用率,那么线程池的最优大小为:
线程池大小=NCPU *UCPU(1+W/C)
2.5、线程池工厂
使用Executors中的DefaultThreadFactory,默认线程池工厂创建的线程都是非守护线程。使用自定义的线程工厂可以做很多事情,比如可以跟踪线程池在何时创建了多少线程,也可以自定义线程名称和优先级。如果将新建的线程都设置成守护线程,当主线程退出后,将会强制销毁线程池。
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
System.out.println("线程" + r.hashCode() + "创建");
// 线程命名
Thread th = new Thread(r, "threadPool" + r.hashCode());
return th;
}
}
2.6、扩展线程池
它提供了几个可以在子类中改写的方法:beforeExecute,afterExecute和terimated。在执行任务的线程中将调用beforeExecute和afterExecute,这些方法中还可以添加日志,计时,监视或统计收集的功能,还可以用来输出有用的调试信息,帮助系统诊断故障。
new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy()) {
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("准备执行:" + t.getName());
}
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完毕");
}
protected void terminated() {
System.out.println("线程池退出");
}
};
3.线程池的分析
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
线程池的处理流程如下:
1.首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
2.其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
3.最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务
上一篇: 史上最雷人的面试
下一篇: 搞笑不易,学渣辛苦了