java工程师小结2

多线程  锁 :
记住多线程的状态转换图。。。。
http://www.importnew.com/21089.html

多线程:一个应用程序有多条执行路径
进程:正在执行的应用程序
线程:进程的执行单元,执行路径
单线程:一个应用程序只有一条执行路径
多线程:一个应用程序有多条执行路径

多进程的意义?
提高CPU的使用率
多线程的意义?
提高应用程序的使用率

什么是线程安全? 线程 程序执行流的最小单元 随着科技的发展  但现在可能不是了   理论上还是这么说的
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。
如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,
或者多个线程之间的切换不会导致该接口的执行结果存在二义性,
也就是说我们不用考虑同步的问题,那就是线程安全的。

进程与线程
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位


创建线程:
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方式来创建线程,如下所示:
1)继承Thread类创建线程   重写run方法
2)实现Runnable接口创建线程 实现run方法
3)使用Callable和Future创建线程 实现call方法
//源码
public interface Callable   { 
  V call() throws Exception;   

public class SomeCallable extends OtherClass implements Callable {
    @Override
    public V call() throws Exception {
        // TODO Auto-generated method stub
        return null;
    }
}:
Callable oneCallable = new SomeCallable();   
//由Callable创建一个FutureTask对象:   
FutureTask oneTask = new FutureTask(oneCallable);   
//注释:FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。 
  //由FutureTask创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   
//至此,一个线程就创建完成了。
------------------------使用Callable和Future创建线程---------------------
Callable接口提供了一个call方法作为线程执行体,call()方法比run()方法功能要强大。
call()方法可以有返回值 方法可以声明抛出异常


Java提供了Future接口来代表Callable接口里call()方法的返回值,
并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,
还实现了Runnable接口,因此可以作为Thread类的target。
在Future接口里定义了几个公共方法来控制它关联的Callable任务。
>boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
>V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
>V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,
经过指定时间没有返回抛出TimeoutException
>boolean isDone():若Callable任务完成,返回True
>boolean isCancelled():如果在Callable任务正常完成前被取消,返回True


创建并启动有返回值的线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例
从java8开始可以直接使用Lambda表达式创建Callable对象
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程  因为FutureTask实现了Runnable接口
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值


--------------------------------------三种创建线程方法对比--------------------------------------
实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,可以抛出异常
实现接口有好处:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
注:一般推荐采用实现接口的方式来创建多线程

Thread类最佳实践:
写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。
在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。


后台线程:
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,
又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。
当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。


Thread类提供一个isDaemon()方法,用来判断指定线程是否为后台线程。
主线程默认是前台线程,t线程默认也是前台线程。并不是所有的线程默认都是前台线程,
有些线程默认就是后台线程——前台线程创建的子线程默认就是前台线程,后台线程创建的子线程默认是后台线程。
注意:前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。
而且要将某个线程设置为后台线程,必须在该线程启动之前设置,
也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常


一些小点:
单线程环境下应该使用StringBuilder来保证较好的性能  当需保证多线程安全时,就应该使用StringBuffer
线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread
synchronized关键字是不能继承的


线程调度:
1.设置优先级:Thread类提供了setPriority(int newPriority)和getPriority()来设置
和返回指定线程的优先级.10最大,1最小,正常为5
////其实下面的应该算作控制  调度我们说有抢占式调度。分时调度
2.线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。
millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
3、线程等待:Object类中的wait()方法,导致当前的线程等待,
直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。
这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,
则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。


为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,
主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,
也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。
也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。


yield():暂停当前正在执行的线程对象,并执行其他线程。
        Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
        yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。
因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

sleep()和yield()的区别
        sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
        sleep 方法使当前运行中的线程睡眠一段时间,进入不可运行状态,这段时间的长短是由程序设定的,
yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。
实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,
如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。
所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
        另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()方法执行时,当前线程仍处在可运行状态,
所以,不可能让出较低优先级的线程些时获得 CPU 占有权。
在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,
那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。 

start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),
什么时候运行是由操作系统决定的。
但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。
关于interrupt:
其实这是一个如何停止线程的问题,要真正理解interrupt()方法,要先了解stop()方法。
在以前通过thread.stop()可以停止一个线程,注意stop()方法是可以由一个线程去停止另外一个线程,
这种方法太过暴力而且是不安全的,怎么说呢,线程A调用线程B的stop方法去停止线程B,
调用这个方法的时候线程A其实并不知道线程B执行的具体情况,这种突然间地停止会导致线程B的一些清理工作无法完成,
还有一个情况是执行stop方法后线程B会马上释放锁,这有可能会引发数据不同步问题。
基于以上这些问题,stop()方法被抛弃了。在这样的情况下,interrupt()方法出现了,
它与stop不同,它不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它它应该结束了(设置一个停止标志)。

真正符合安全的做法,就是让线程自己去结束自己,而不是让一个线程去结束另外一个线程。
通过interrupt()和.interrupted()方法两者的配合可以实现正常去停止一个线程,
线程A通过调用线程B的interrupt方法通知线程B让它结束线程,
在线程B的run方法内部,通过循环检查.interrupted()方法是否为真来接收线程A的信号,
如果为真就可以抛出一个异常,在catch中完成一些清理工作,然后结束线程。

Thread.interrupted()会清除标志位,并不是代表线程又恢复了,
可以理解为仅仅是代表它已经响应完了这个中断信号然后又重新置为可以再次接收信号的状态。
wait():
Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,
从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。
从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。
直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。
相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,
而是在相应的synchronized(){}语句块执行结束,自动释放锁后,
JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。
这样就提供了在线程间同步、唤醒的操作。


Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,
主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。



线程通信:
1》传统的线程通信,用this或者是被锁的对象来调用.
wait(),notify(), notifyAll()
2》使用Condition控制线程通信,调用Lock对象的newCondition()方法即可.
await(),signal(),signalAll()
3》使用阻塞队列(BlockingQueue)控制线程通信 管道
BlockingQueue是Queue的子接口,特征为:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,
则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞.
put(E e)
take(E e)
4>while轮询






死锁举例子:
package cn.itcast_02;
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}






线程池:
当程序中需要创建大量生存期很短暂的线程时,应该考虑使用线程池
线程池在系统启动时即创建大量空闲的线程,
程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行他们的run()或call方法,
当run()或call()方法执行结束后,该线程并不会立即死亡,而是再次返回线程池中成为空闲状态,
等待执行下一个Runnable对象的run()或call()方法.


步骤如下:
1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务.
3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例.
4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池.
package cn.itcast_08;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/*
 * 线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,
 而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
 * 
 * 如何实现线程的代码呢?
 * A:创建一个线程池对象,控制要创建几个线程对象。
 * public static ExecutorService newFixedThreadPool(int nThreads)
 * B:这种线程池的线程可以执行:
 * 可以执行Runnable对象或者Callable对象代表的线程
 * 做一个类实现Runnable接口。
 * C:调用如下方法即可
 * Future submit(Runnable task)
 * Future submit(Callable task)
 * D:我就要结束,可以吗?
 * 可以。
 */
public class ExecutorsDemo {
public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);


// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());


//结束线程池
pool.shutdown();
}
}


比较重要的线程类:
1.ThreadLocal类
用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。


实现:每个Thread都持有一个TreadLocalMap类型的变量
该类是一个轻量级的Map,功能与map一样。


2.原子类(AtomicInteger、AtomicBoolean……)  实现乐观锁
如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized


3.Lock类
lock: 在java.util.concurrent包内:
ReentrantLock
ReentrantReadWriteLock
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。


区别如下:
lock更灵活,可以自由定义多把锁的加锁解锁顺序(synchronized要按照先加的后解顺序)
提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的 带 超时时间版本。
本质上和监视器锁(即synchronized是一样的)
能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。
可以和Condition类的结合。
性能更高




多线程安全问题的原因(也是我们以后判断一个程序是否有线程安全问题的依据)
A:是否有多线程环境
B:是否有共享数据
C:是否有多条语句操作共享数据
同步解决线程安全问题
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 
用final域,有锁保护的域和volatile域可以避免非同步的问题。
A:同步代码块
synchronized(对象) {
需要被同步的代码;
}
这里的锁对象可以是任意对象。

B:同步方法
把同步加在方法上。
对象和类有内置锁
ReenTrantLock lock  unlock
局部变量线程同步 ThreadLocal
java.util.concurrent.LinkedBlockingQueue
LinkedBlockingQueue 类常用方法 
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue 
put(E e) : 在队尾添加一个元素,如果队列满则阻塞 
size() : 返回队列中的元素个数 
take() : 移除并返回队头元素,如果队列空则阻塞 
volatile 并不能实现同步
a.volatile关键字为域变量的访问提供了一种免锁机制, 
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 
volatile重要工作是避免线程脏读:当线程对volatile变量进行读操作时
会先将自己工作内存中的变量置为无效,之后再通过主内存拷贝新值到工作内存中使用。
volatile解决的是变量在多个线程之间的可见性,但不能完全保证数据的原子性。
现在JVM经过优化,已不会出现liveness failure 。所以没事别用volatile。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
回顾以前的线程安全的类
A:StringBuffer
B:Vector
C:Hashtable
D:如何把一个线程不安全的集合类变成一个线程安全的集合类
用Collections工具类的方法即可。synchronizedXxx()


多线程运行不能按照顺序执行过程中捕获异常的方式来处理异常,
异常会被直接抛出到控制台(由于线程的本质,使得你不能捕获从线程中逃逸的异常。
一旦异常逃逸出任务的run方法,它就会向外传播到控制台,除非你采用特殊的形式捕获这种异常。
这样会让你很头疼,无法捕捉到异常就无法处理异常而引发的问题。


1可以在run方法里面加try catch 
2给某个thread设置一个UncaughtExceptionHandler,
可以确保在该线程出现异常时能通过回调UncaughtExceptionHandler接口的
public void uncaughtException(Thread t, Throwable e) 方法来处理异常,
这样的好处或者说目的是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。
但是要特别明确的是:虽然是在回调方法中处理异常,但这个回调方法在执行时依然还在抛出异常的这个线程中!
另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时UncaughtExceptionHandler接口不一定会立即回调。
定义异常处理器
   要求,实现 Thread.UncaughtExceptionHandler的uncaughtException方法
/*
 * 第一步:定义符合线程异常处理器规范的“异常处理器”
 * 实现Thread.UncaughtExceptionHandler规范
 */
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    /*
     * Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught    "+e);
    }
}
定义使用该异常处理器的线程工厂
/*
 * 第二步:定义线程工厂
 * 线程工厂用来将任务附着给线程,并给该线程绑定一个异常处理器 
 */
class HanlderThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this+"creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created "+t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());//设定线程工厂的异常处理器
        System.out.println("eh="+t.getUncaughtExceptionHandler());
        return t;
    }
}

/*
 * 第三步:我们的任务可能会抛出异常
 * 显示的抛出一个exception
 */
class ExceptionThread implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("eh = "+t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}


/*
 * 第四步:使用线程工厂创建线程池,并调用其execute方法
 */
public class ThreadExceptionUncaughtExceptionHandler{
    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
        exec.execute(new ExceptionThread());
    }
}


如何获取线程对象的名称呢?
 * public final String getName():获取线程的名称。
 * 如何设置线程对象的名称呢?
 * public final void setName(String name):设置线程的名称
 * 
 * 针对不是Thread类的子类中如何获取线程对象名称呢?
 * public static Thread currentThread():返回当前正在执行的线程对象
 * Thread.currentThread().getName()






锁:


ReentrantLock
可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。
ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,
线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法.


使用方法是:


1.先new一个实例static ReentrantLock r=new ReentrantLock();
2.加锁r.lock()或r.lockInterruptibly();
此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,
那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。
(如果使用后者,必须throw interruptable exception 或catch)
3.释放锁r.unlock()
必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。
这里补充一个小知识点,finally是可以信任的:
经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。


ReentrantReadWriteLock可重入读写锁(读写锁的一个实现)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
  ReadLock r = lock.readLock();
  WriteLock w = lock.writeLock();
两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码


容器类 这里就讨论比较常用的两个:
BlockingQueue
ConcurrentHashMap  上面已经做出详细介绍
BlockingQueue阻塞队列
该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,
可以在队列头添加元素和在队尾删除或取出元素。类似于一个管道,特别适用于先进先出策略的一些应用场景。
普通的queue接口主要实现有PriorityQueue(优先队列)ArrayListBlockingQueue LinkedListBlockingQueue DelayQueue SynchronousQueue等


除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。
put会在队列满的时候阻塞,直到有空间时被唤醒;
take在队列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。


ThreadPoolExecutor
如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:


ExecutorService e = Executors.newCachedThreadPool();// 第一种是可变大小线程池,按照任务数来分配线程,
ExecutorService e = Executors.newSingleThreadExecutor();// 第二种是单线程池,相当于FixedThreadPool(1)
ExecutorService e = Executors.newFixedThreadPool(3); // 第三种是固定大小线程池。
e.execute(new MyRunnableImpl());// 然后运行
该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,
本质上,他们都是ThreadPoolExecutor类的各种实现版本。


ThreadPoolExecutor参数解释:
corePoolSize:池内线程初始值与最小值,就算是空闲状态,也会保持该数量线程。
maximumPoolSize:线程最大值,线程的增长始终不会超过该值。
keepAliveTime:当池内线程数高于corePoolSize时,经过多少时间多余的空闲线程才会被回收。回收前处于wait状态
unit:时间单位,可以使用TimeUnit的实例,如TimeUnit.MILLISECONDS
workQueue:待入任务(Runnable)的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死(starving)
threadFactory:线程工厂类,有默认实现,如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。


ReentrantLock和Condition的使用方式通常是这样的:
public static void main(String[] args) {
    final ReentrantLock reentrantLock = new ReentrantLock();
    final Condition condition = reentrantLock.newCondition();
    Thread thread = new Thread((Runnable) () -> {
            try {
                reentrantLock.lock();
                System.out.println("我要等一个新信号" + this);
                condition.await();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("拿到一个信号!!" + this);
            reentrantLock.unlock();
    }, "waitThread1");
 
    thread.start();
     
    Thread thread1 = new Thread((Runnable) () -> {
            reentrantLock.lock();
            System.out.println("我拿到锁了");
            try {
                Thread.sleep(3000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            System.out.println("我发了一个信号!!");
            reentrantLock.unlock();
    }, "signalThread");
     
    thread1.start();
}
Condition的执行方式,是当在线程1中调用await方法后,线程1将释放锁,并且将自己沉睡,等待唤醒,
线程2获取到锁后,开始做事,完毕后,调用Condition的signal方法,唤醒线程1,线程1恢复执行。
以上说明Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),
只有当该条件具备( signal 或者 signalAll方法被带调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。


自旋锁或自适应自旋锁:
因为线程阻塞后进入排队队列和唤醒都需要CPU从用户态转为核心态,尤其频繁的阻塞和唤醒对CPU来说是负荷很重的工作。
同时统计发现,很多对象锁的锁定状态只会持续很短的一段时间,例如一个线程切换周期,
这样的话在很短的时间内阻塞线程又很快唤醒线程显然不值得,所以引入了自旋锁概念。
所谓“自旋”,就monitor并不把线程阻塞放入排队队列,而是去执行一个无意义的循环,
就是后面的线程想看看前面的锁是否很快被释放
循环结束后看看是否锁已释放并直接进行竞争上岗步骤,如果竞争不到继续自旋循环,循环过程中线程的状态一直处于running状态。
明显自旋锁使得synchronized的对象锁方式在线程之间引入了不公平。但是这样可以保证大吞吐率和执行效率。
不过虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。
所以自旋的次数一般控制在一个范围内,例如10,50等,在超出这个范围后,线程就进入排队队列。
自适应自旋锁,就是自旋的次数是通过JVM在运行时收集的统计信息,动态调整自旋锁的自旋次数上界。


轻量锁和偏向锁
当多线程环境进入synchronized区域的线程没竞争时,JVM并不会马上创建对象锁,而是用轻量锁或偏向锁。
不过需要明确的是,轻量锁和偏向锁,都不能代替重量锁,只不过是在没有多线程竞争时,
没必要用重量锁而无畏的消耗资源。但是一旦出现了多线程竞争时,synchronized区域的轻量锁或偏向锁都会立即升级为重量锁。


轻量锁或偏向锁使用的条件是进入synchronized区域时没有其他任何其他线程在使用。
这时线程t访问对象的synchronized区域时,对象头的标志位Tag状态为01,
以及还有1位的偏向信息用于记录这个对象是否可用偏向锁。
然后t在对象上申请轻量锁时,若偏向信息为0,表明当前对象还未加锁,
或加过偏向锁(加过,注意是加过偏向锁的对象只能被同样的线程加锁,如果不同的线程想要获取锁,
需要先将偏向锁升级为轻量锁,稍后会讲到),在判断对当前对象确实没有被任何其他线程锁住后,即可以在该对象上加轻量锁。


加轻量锁的过程很简单:在当前线程的栈帧(stack frame)中生成一个锁记录(lock record),
这个锁记录比前面说的那个对象锁(管理线程队列的monitor)简单多了,它只是对象头的一个拷贝。
然后把对象头里的tag改成00,并把这个栈帧里的lock record地址放入对象头里。
若操作成功,那就完成了轻量锁操作。
如果不成功,说明有线程在竞争,则需要在当前对象上生成重量锁来进行多线程同步,
然后将Tag状态改为10,并生成Monitor对象(重量锁对象),对象头里也会放入Monitor对象的地址。最后将当前线程t排队队列中。


轻量锁的解锁过程也很简单就是把栈帧里刚才的那个lock record拷贝到对象头里,若替换成功,则解锁完成,
若替换不成功,表示在当前线程持有锁的这段时间内,其他线程也竞争过锁,并且发生了锁升级为重量锁,
这时需要去Monitor的等待队列中唤醒一个线程去重新竞争锁。


偏向锁是比轻量锁还轻量的锁机制。
当synchronized区域长期都由同一个线程加锁、解锁时,jvm就用偏向锁来做,它的加锁解锁比轻量锁操作起来指令更加简化。
不过一旦有其他线程使用synchronized区域,即使没有线程间竞争,也会把偏向锁升级为轻量锁,当然如果发生线程竞争就再升级为对象锁。


锁的公平与不公平:公平锁是指线程获得锁的顺序按照fifo的原则,先排队的先得。
非公平锁指每个线程都先要竞争锁,不管排队先后,所以后到的线程有可能无需进入等待队列直接竞争到锁。
非公平锁虽然可能导致某些线程饥饿,但是锁的吞吐率是公平锁好几倍,
synchronized是一个典型的非公平锁方案,而且没法做成公平锁。


线程饿死与死锁?
饥饿是什么?是进程无法得到资源,(cpu或者io资源或者别的什么资源),所以无法进行下去,称为饿死,
比较常见的就是在优先级调度中,不停的有高优先级的进程创建,导致的无法分配cpu,从而饥饿。
而按照《操作系统概念》当中介绍的,死锁必须具备以下四个条件 : 
1, 自己占有资源并且等待其他进程的资源,2 两个进程相互等待对方占有的资源 
3 资源不能被抢占 4资源不能共享。 
换句话说,死锁是两个或者多个进程,相互等待对方占有的资源,而又不能释放自己的资源,
所以这些进程都进入死锁状态了,进入死锁状态后,由于没有释放自己占有的资源,
所以新的进程在请求这些资源的时候可能不能得到资源,于是就饥饿了


饥饿就好比一直没钱花,你一直穷的没饭吃,从开始就不给你饭吃腻就没享受过,
而死锁就好比你有了一点甜头,想要更好的东西,但是好几个人和你抢,结果东西不够谁都不能给了,你得不到你想要的东西,
想要得到东西的话得东西够分才行


乐观锁:假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。
如果其他事务有更新的话,正在提交的事务会进行回滚
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,
因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
但如果直接简单这么做,还是有可能会遇到不可预期的结果,
例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

悲观锁:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;
还有会降低了并行性

线程间通信:由于多线程共享地址空间和数据空间,
所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。


进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。
以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。
这样进程间的通信就不局限于单台计算机了,实现了网络通信


# 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。
进程的亲缘关系通常是指父子进程关系。
# 有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
# 信号量(semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
# 消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
# 信号 (sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
# 共享内存(shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,
这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,
它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,
如信号两,配合使用,来实现进程间的同步和通信。
# 套接字(socket ) : 也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。




java中锁的优化 -- JVM对synchronized的优化  
1)锁消除  
    概念:JVM在JIT编译(即时编译)时,通过对运行上下文的扫描,去除掉那些不可能发生共享资源竞争的锁
,从而节省了线程请求这些锁的时间。  
    举例:  
        StringBuffer的append方法是一个同步方法,如果StringBuffer类型的变量是一个局部变量,
则该变量就不会被其它线程所使用,即对局部变量的操作是不会发生线程不安全的问题。  
        在这种情景下,JVM会在JIT编译时自动将append方法上的锁去掉。  
2)锁粗化  
    概念:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,即将加锁的粒度放大。  
    举例:在for循环里的加锁/解锁操作,一般需要放到for循环外。  
3)使用偏向锁和轻量级锁  
    说明:  
        1)java6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。  
        2)锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁、轻量级锁、重量级锁。  
        3)锁的状态会随着竞争情况逐渐升级,并且只可以升级而不能降级。  
          
    【偏向锁】  
        1)背景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,
为了让线程获得锁的代价更低而引入了偏向锁。  
          
        2)概念:核心思想就是锁会偏向第一个获取它的线程,
如果在接下来的执行过程中没有其它的线程获取该锁,则持有偏向锁的线程永远不需要同步。  
          
        3)目的:偏向锁实际上是一种优化锁,其目的是为了减少数据在无竞争情况下的性能损耗。  
    
        4)原理:  
            1>当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。  
            2>以后该线程在进入和退出同步块时就不需要进行CAS操作来加锁和解锁,
只需简单地判断一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。  
          
        5)偏向锁的获取:  
            1>访问Mark Word中偏向锁的标识位是否为1,如果是1,则确定为偏向锁。  
                说明:  
                    [1]如果偏向锁的标识位为0,说明此时是处于无锁状态,则当前线程通过CAS操作尝试获取偏向锁
,如果获取锁成功,则将Mark Word中的偏向线程ID设置为当前线程ID;并且将偏向标识位设为1。  
                    [2]如果偏向锁的标识位不为1,也不为0(此时偏向锁的标识位没有值),说明发生了竞争,
偏向锁已经膨胀为轻量级锁,这时使用CAS操作尝试获得锁。  

            2>如果是偏向锁,则判断Mark Word中的偏向线程ID是否指向当前线程,
如果偏向线程ID指向当前线程,则表明当前线程已经获取到了锁;  
              
            3>如果偏向线程ID并未指向当前线程,则通过CAS操作尝试获取偏向锁,
如果获取锁成功,则将Mark Word中的偏向线程ID设置为当前线程ID;  
              
            4>如果CAS获取偏向锁失败,则表示有竞争。
当到达全局安全点时(在这个时间点上没有正在执行的字节码),获得偏向锁的线程被挂起
,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。  
          
        6)偏向锁的释放:    
            1>当其它的线程尝试获取偏向锁时,持有偏向锁的线程才会释放偏向锁。    
            2>释放偏向锁需要等待全局安全点(在这个时间点上没有正在执行的字节码)。   
            3>过程:  
                首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,
如果线程不处于活动状态,则将对象头设置成无锁状态,  
                如果线程还活着,说明此时发生了竞争,则偏向锁升级为轻量级锁,
然后刚刚被暂停的线程会继续往下执行同步代码。  
          
        7)优点:加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距  
        8)缺点:如果线程间存在锁竞争,锁撤销会带来额外的消耗。
        9)说明:  
            1)偏向锁默认在应用程序启动几秒钟之后才激活。  
            2)可以通过设置 -XX:BiasedLockingStartupDelay=0 来关闭延迟。  
            3)可以通过设置 -XX:-UseBiasedLocking=false 来关闭偏向锁,程序默认会进入轻量级锁状态。
(如果应用程序里的锁大多情况下处于竞争状态,则应该将偏向锁关闭)  
  
    【轻量级锁】    
        1)原理:  
            1>当使用轻量级锁(锁标识位为00)时,线程在执行同步块之前,
JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,
并将对象头中的Mark Word复制到锁记录中(注:锁记录中的标识字段称为Displaced Mark Word)。  
              
            2>将对象头中的MarkWord复制到栈桢中的锁记录中之后,
虚拟机将尝试使用CAS将对象头中Mark Word替换为指向该线程虚拟机栈中锁记录的指针,
此时如果没有线程占有锁或者没有线程竞争锁,则当前线程成功获取到锁,然后执行同步块中的代码。
              
            3>如果在获取到锁的线程执行同步代码的过程中,另一个线程也完成了栈桢中锁记录的创建,
并且已经将对象头中的MarkWord复制到了自己的锁记录中,
然后尝试使用CAS将对象头中的MarkWord修改为指向自己的锁记录的指针
但是由于之前获取到锁的线程已经将对象头中的MarkWord修改过了
(并且现在还在执行同步体中的代码,即仍然持有着锁),
所以此时对象头中的MarkWord与当前线程锁记录中MarkWord的值不同,导致CAS操作失败
,然后该线程就会不停地循环使用CAS操作试图将对象头中的MarkWord替换为自己锁记录中MarkWord的值,
(当循环次数或循环时间达到上限时停止循环)
如果在循环结束之前CAS操作成功,那么该线程就可以成功获取到锁,
如果循环结束之后依然获取不到锁,则锁获取失败,对象头中的MarkWord会被修改为指向重量级锁的指针,
然后这个获取锁失败的线程就会被挂起,阻塞了。


            4>当持有锁的那个线程执行完同步体之后,
使用CAS操作将对象头中的MarkWord还原为最初的状态时
(将对象头中指向锁记录的指针替换为Displaced Mark Word ),
发现MarkWord已被修改为指向重量级锁的指针,因此CAS操作失败,该线程会释放锁并唤起阻塞等待的线程,
开始新一轮夺锁之争,而此时,轻量级锁已经膨胀为重量级锁,所有竞争失败的线程都会阻塞,而不是自旋。  
    
            自旋锁:    
                1)所谓自旋锁,就是让没有获得锁的进程自己运行一段时间自循环(默认开启),但是不挂起线程。 
                2)自旋的代价就是该线程会一直占用处理器如果锁占用的时间很短,自旋等待的效果很好,
反之,自旋锁会消耗大量处理器资源。    
                3)因此,自旋的等待时间必须有一定限度,超过限度还没有获得锁,就要挂起线程。    
  
        优点:在没有多线程竞争的前提下,减少传统的重量级锁带来的性能损耗。    
        缺点:竞争的线程如果始终得不到锁,自旋会消耗cpu。
        应用:追求响应时间,同步块执行速度非常快。   


    【重量级锁】    
        说明:  
            1)java6之前的synchronized属于重量级锁,效率低下,因为monitor是依赖操作系统的Mutex Lock(互斥量)来实现的。  
            2)操作系统实现线程之间的切换需要从用户态转换到核心态,
这个状态之间的转换需要相对较长的时间,时间成本相对较高。  
            3)在互斥状态下,没有得到锁的线程会被挂起阻塞,
而挂起线程和恢复线程的操作都需要从用户态转入内核态中完成。  
      
        优点:线程竞争不使用自旋,不会消耗cpu。  
        缺点:线程阻塞,响应时间缓慢。  
        应用:追求吞吐量,同步块执行速度较长  
     
锁优化 


   具体思路:减少锁持有时间,减小锁粒度,锁分离,锁粗化,锁消除。
  (1)减少锁持有时间  尽量少的加锁代码,例如用具体代码段代替方法加锁。 
  (2)减小锁粒度   把大对象尽量改成小对象,增加并行度减少锁竞争。同时有利于偏向锁,轻量级锁。例如ConcurrentHashMap
  (3)锁分离   读写分离,读读可重入,读写互斥,写写互斥。另一种分离,例如 LinkedBlockingQueue ,存数据和取数据从队列两端操作,两端各自加锁控制即可,两端的锁互不影响。
    (4)锁粗化 如果一段程序要多次请求锁,锁之间的代码执行时间比较少,就应该整合成一个锁,前提是不用同步的部分执行时间短。例如for循环里面申请锁,如果for循环时间不长,可以在for外面加锁。
    (5)锁消除 编译器级别的操作,如果jdk发现锁不可能被共享,会擦除这个锁。原理是逃逸分析,例如stringbuffer,本身操作是加锁的,如果只在局部使用不存在并发访问,那么会擦除锁,如果对象逃逸出去例如赋值给全局变量等,面临并发访问,就不会擦除锁。可以通过jvm参数来指定是否使用锁消除。

java8 新特性
(1)LongAdder  类似automicLong, 但是提供了“热点分离”。
过程如下:如果并发不激烈,则与automicLong 一样,cas赋值。
如果出现并发操作,则使用数组,数组的各元素之和为真实value,
让操作分散在数组各个元素上,把并发操作压力分散,一遇到并发就扩容数组,最后达到高效率。
一般cas如果遇到高并发,可能一直赋值失败导致不断循环,热点分离可以解决这个问题。
有点类似concurrenthashmap,分而治之。


 (2)completableFuture 对Future进行增强,支持函数式编程的流式调用。提供更多功能,压缩编码量。


 (3)stampedLock 改进读写锁,读不阻塞写。如果读的时候,发生了写,应该重新读,不是阻塞写。
 解决了一般读写锁读太多导致写一直阻塞的问题,读线程发现数据不一致时触发重新读操作。 
 原理是维护了一个stamp标记,在添加写锁的释放写锁的时候,stamp都会改变(比如++),
 代码在加读锁的时候,可以先得到stamp,读完数据释放读锁的时候,调用validate方法,
 检验刚才stamp和现在stamp是否相同,如果相同,说明读的过程中没有修改,读取成功,
 如果不相同,则说明读的时候发生了写,那么接下来两种策略,一个是继续用当前stamp为初试,
 继续读,读完比较stamp,是乐观的办法;另一种直接调用readlock(),升级为正常的读锁,是悲观办法。
 
 java 线程池调优
所有线程池的工作方式本质是一样的:有一个任务队列,一定数量的线程会从该任务队列获取任务然后执行。
任务的结果可以发回客户端,或保存到数据库,或保存到某个内部数据结构中,等等。
但是在执行完任务后,这个线程会返回任务队列,检索另一个任务并执行。

设置最大线程数
假设JVM有4个CPU可用,很明显最大线程数至少要设置为4。
的确,除了处理这些任务,JVM还有些线程要做其他的事,但是它们几乎从来不会占用一个完整的CPU,
至于这个数值是否要大于4,则需要进行大量充分的测试。

有以下两点需要注意:
一旦服务器成为瓶颈,向服务器增加负载时非常有害的;
对于CPU密集型或IO密集型的机器增加线程数实际会降低整体的吞吐量;

设置最小线程数
一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该保留几分钟,以处理任何负载飙升。
空闲时间应该以分钟计,而且至少在10分钟到30分钟之间,这样可以防止频繁创建线程。
将最小线程数设置为其他某个值(比如1),出发点是为了防止系统创建太多线程,以节省系统资源。
指定一个最小线程数的负面影响相当小

线程池任务大小
等待线程池来执行的任务会被保存到某个队列或列表中;当池中有线程可以执行任务时,就从队列中拉出一个。
这会导致不均衡:队列中任务的数量可能变得非常大。
如果队列太大,其中的任务就必须等待很长时间,直到前面的任务执行完毕。


对于任务队列,线程池通常会限制其大小。但是这个值应该如何调优,并没有一个通用的规则。
若要确定哪个值能带来我们需要的性能,测量我们的真实应用是唯一的途径。
不管是哪种情况,如果达到了队列限制,再添加任务就会失败。
ThreadPoolExecutor有一个rejectedExecution方法,用于处理这种情况,默认会抛出RejectedExecutionExecption。
应用服务器会向用户返回某个错误:或者是HTTP状态码500,或者是Web服务器捕获异常错误,
并向用户给出合理的解释消息—其中后者是最理想的。


设置ThreadPoolExecutor的大小
线程池的一般行为是这样的:创建时准备最小数目的线程,如果来了一个任务,
而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就会立即执行。
否则,任务被加入到等待队列,如果队列中已经无法加入新任务,则拒接之。


根据所选任务队列的类型,ThreadPoolExecutor会决定何时会启动一个新线程。
有以下三种可能:


SynchronousQueue
如果ThreadPoolExecutor搭配的是SynchronousQueue,则线程池的行为和我们预期的一样,
它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则会为新任务启动一个新线程。然而这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有的线程都在忙碌,则新的任务都会被拒绝,所以如果是管理少量的任务,这是个不错的选择,对于其他的情况就不适合了。


无界队列
如果ThreadPoolExecutor搭配的是无界队列,如LinkedBlockingQueue,则不会拒绝任何任务(因为队列大小没有限制)。
这种情况下,ThreadPoolExecutor最多仅会按照最小线程数创建线程,也就是说最大线程池大小被忽略了。
如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。


有界队列
搭配了有界队列,如ArrayBlockingQueue的ThreadPoolExecutor会采用一个非常负责的算法。
比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。
随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。
即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。


如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。
新线程会运行队列中的第一个任务,为新来的任务腾出空间。


这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。
这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;
这时第二个节流阀—最大线程数就起作用了。

你可能感兴趣的:(java工程师面试)