1. 进程与线程的概念
进程:操作系统中一个程序的执行周期
线程:一个程序同时执行多个任务。通常来讲,每一个任务就称为一个线程。
进程与多线程比较:
- 与进程相比,线程更加的“轻量级”,创建、撤销一个线程比启动、撤销一个进程开销要小的多。一个进程中的所有线程共享此进程的所有资源。
- 没有进程就没有线程,进程一旦终止,其内的线程将不复存在。
- 进程是操作系统资源调度的基本单位,进程可以独享资源;线程需要依托进程提供的资源,无法独立申请操作系统资源,是OS任务执行的基本单位。
高并发:访问的线程量非常非常高
高并发带来的问题:服务器内存不够用,无法处理新的请求。
主方法是JVM虚拟机里的一个主线程。
2.Java 的多线程实现
2.1 继承Thread 类实现多线程
java.long.Thread 是线程操作的核心类。新建一个线程最简单的方法就是直接继承Thread,而后覆写run()方法(相当于主线程的main()方法)。
无论哪种方式实现多线程,线程启动一定调用 Thread 类提供的 start() 方法。
线程start()方法只能调用一次,多次调用 java.long.IllegalThreadStateException,抛出线程非法的状态异常。
start() -> start0(JVM) -> 进行资源调度,系统分配(JVM)-> run(Java方法)执行线程的具体操作任务。
package revase.demo;
class MyThread extends Thread {
private String title;
MyThread(String title) {
this.title = title;
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.title + ", i = " + i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread m1 = new MyThread("myThread1");
MyThread m2 = new MyThread("myThread2");
MyThread m3 = new MyThread("myThread3");
m1.start();
m2.start();
m3.start();
}
}
2.2 实现Runnable接口来实现多线程,覆写run()方法
public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
}).start();
}
}
2.3 继承Thread类与实现 Runnable 接口的关系,覆写 run() 方法
- Thread类与自定义线程类(实现了Runnable接口),是一个典型的代理设计模式。Thread类负责辅助真实业务操作(资源调度,创建线程并启动)。自定义线程类负责真实业务的实现(run方法具体要做啥事)。
- 使用Runnable接口实现的多线程程序类可以更好的描述共享的概念。
多个线程使用同一个Runnable对象
2.4 实现 Callable 接口实现多线程-当线程有返回值时只能实现Callable实现多线程,覆写call() 方法
juc-JDK1.5新增的并发程序编程包
java.util.concurrent.Callable<V>
实现Callable接口而后覆写call()方法,有返回值:V call() throws Exception;
Future<V> 接口:
V get() throws InterruptedException,ExecutionException;
取得Callable接口的返回值。
3. 多线程常用操作方法
3.1 线程命名与取得
- 通过构造方法在创建线程时设置线程名称
public Thread(String name)
public Thread(Runnable target, String name)
- 取得线程名称
public final String getName()
- 设置进程名称
public final synchronized void setName(String name)
3.2 线程休眠方法sleep():运行态->阻塞态
public static native void sleep(long millis) throws InterruptedException
线程休眠:让当前线程暂缓执行,等到了预计时间后再恢复执行。
线程休眠会交出cpu,但是不会释放锁。
3.3 线程让步(yield()方法):运行态-就绪态
暂停执行当前的线程对象,并执行其他线程。
yield()方法会让当前线程交出CPU,同样不会释放锁。但是yield()方法无法控制具体交出CPU的时间,并且 yield() 方法只能让拥有相同优先级的线程有获取CPU的机会。
3.4 等待其他线程终止(join()方法)
会释放对象锁。
join() 方法只是对 Object 提供的 wait() 方法做了一层包装而已。
等待该线程终止。如果在主线程中调用该方法,会让主线程休眠,让调用该方法的线程先执行完毕后再恢复执行主线程。
3.5 线程停止
3.5.1 设置标记位停止线程(推荐)
3.5.2 调用Thread类的stop方法强制停止线程,该方法不安全已经被Deprecated。(不安全)
线程执行一半强行终止,数据可能会被废弃
3.5.3 调用Thread类的interrupt()方法。(难理解)
- interrupt() 方法只是将线程状态设置为中断状态而已,它不会中断一个正在运行的线程。此方法只是给线程传递一个中断信号,isInterrrupt = true,程序可以根据此信号来判断是否需要终止。
- 当线程中使用了wait、sleep、join导致此线程阻塞,则interrupt()会在线程中抛出InterruptException,在catch块中捕获该异常,并且将线程的中断状态由true置为false。
3.6 线程优先级
线程优先级是指优先级越高越有可能先执行,但仅仅是有可能而已。
设置优先级:
public final void setPriority(int newPriority)
取得优先级:
public final int getPriority()
MAX_PRIORITY=10;
NORM_PRIORITY=5;
MIN_PRIORITY=1;
主线程只是一个普通优先级而已。
线程具有继承性:只是继承优先级而已。
从A线程启动B线程,则B和A的线程优先级是一样的。
3.7 守护线程
守护线程是一种特殊的线程,又称为陪伴线程。Java中一共两种线程:用户线程与守护线程。
Thread类提供== isDemon() ==区别两种线程,返回false表示该线程为用户线程;否则为守护线程。典型守护线程就是垃圾回收线程。
只要当前JVM进程中存在任何一个用户线程没有结束,守护线程就在工作;只要当最后一个线程结束时;守护线程才会随着JVM一同停止工作。
Thread提供 setDemon() 将用户线程设置为守护线程。
4. 同步问题
4.1 synchronized实现同步处理(加锁操作)(内建锁)
同步代码块:在方法中使用synchronized(对象),一般可以锁定当前对象this。表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。
同步方法:在方法声明上加synchronized,表示此时只有一个线程能够进入同步方法。
4.2 synchronized对象锁概念
synchronized(this)以及普通synchronized方法,只能防止多个线程同时执行同一个对象的同步段。
synchronized锁的是括号中的对象而非代码段。
全局锁(类名 . class):锁代码段同步方法
实现:
- 使用类的静态synchronized与stataic一起使用,此时锁的是当前使用的类而非对象。
- 在代码块中锁当前Class对象 synchronized(类名称 . class) { }
4.3 synchronized底层实现
4.3.1 同步代码块底层实现:
执行同步代码块后首先要执行 monitorenter 指令,退出时执行 monitorexit 指令。
使用synchronized 实现同步,关键点就是要获取对象的监视器 monitor 对象。当线程获取 monitor 对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的 monitor监视器。
通常一个monitorenter 指令会同时包含多个monitorexit 指令。因为JVM要确保所获取的锁无论在正常执行路径或异常执行路径都能正确解锁。
4.3.2 同步方法底层实现:
当使用synchronized标记方法时,字节码会出现访问标ACC_SYNCHRONIZED。该标记表示在进入该方法时,JVM需要进行 monitorenter 操作,在退出该方法时,无论是否正常返回,JVM均需要进行 monitorexit 操作。
当JVM执行 monitorenter 时,如果目标对象 monitor 的计数器为0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将 monitor 计数器+1。
在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁);否则需要等待,直到持有线程释放该锁。
当执行 monitorexit 时,JVM须将锁对象的计数器 -1。当计数器减为0时,代表该锁已被释放掉,唤醒所有正在等待的线程去竞争该锁。
4.3.3 对象锁(monitor)
对象锁机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁。它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。
Lock(JDK1.5(juc))- Java语言层锁
4.4 synchronized 优化
4.4.1 CAS(Compare and Swep)
悲观锁:线程获取锁(JDK1.6之前)是一种悲观锁的概念,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。
乐观锁:CAS操作(又称为无锁操作)是一种乐观锁策略。假设所有线程访问共享资源时不会出现冲突。由于不会出现冲突自然不会阻塞其他进程。因此进程就不会出现阻塞停顿的状态。出现冲突时,无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,出现冲突重试当前操作直到没有冲突为止。
4.4.2 CAS比较交换:
CAS可以理解为CAS(V,O,N)
- V:当前冲突地址实际存放的值
- O:期望值(旧值)
- N:更新的新值
当V=O时,期望值与实际值相等,该值没有被其它线程修改过,即值O就是目前最新的值,因此可以将新值赋值给V。
反之,如果V=!O,表面该值已经被其他线程修改了,因此O值不是当前最新值,返回v,无法修改。
当多个线程使用操作时,只有一个线程会成功,其余线程均失败,失败的线程会重新尝试(自旋)或挂起线程(阻塞)。
内建锁在老版本最大的问题在于:
在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,这是一种互斥同步(阻塞同步)。
而CAS不是将线程挂起。当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起,也叫做非阻塞同步。
4.4.3 CAS问题
4.4.4 ABA问题
解决:使用 atomic: AtomicStampedReference 类来解决
添加版本号:1A->2B->3A(把当前的版本更新为最新)
4.4.5 自旋会浪费大量的CPU资源
自旋浪费CPU----解决方法:自适应自旋(重量级锁的优化)
与线程阻塞相比,自旋会浪费大量的处理器资源。因为当前线程仍处于运行状态,只不过跑的是无用指令。它 期望在运行无用指令的过程中,锁能够被释放出来。
解决:自适应自旋;根据以往的自旋是能否获取到锁,来动态调整自选的时间(循环次数),如果在自旋时获取到锁,则会稍微增加下一次自旋的时长,否则就稍微减少下一次自旋的时长。
4.4.6 公平性
内建锁无法实现公平机制,而Lock体系可以实习公平锁。
4.5 Java 对象头
Java 对象头Mark Word字段存放内容:
对象的Hashcode
分代年龄
锁标记位
JDK1.6之后一共四种锁的状态
- 无锁 0 01
- 偏向锁 1 01
- 轻量级锁 00
- 重量级锁 10
- GC标记 11
根据竞争状态的激烈程度,锁会自动进行升级,锁不能降级(为了提高获取与释放的效率)
4.6 偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获取锁的开销降低引入偏向锁。
偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求一把锁。
偏向锁的获取:
当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段存储锁偏向的线程ID,以后的线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入。
当线程访问同步块失败时,使用CAS竞争锁,并将偏向锁升级为轻量级锁。
偏向锁的释放:开销较大
偏向锁使用了一种等到竞争出现才释放锁的机制,当其他进程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将偏向锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)。如果持有线程已经终止,则将锁对象的对象头设置为无锁状态。
偏向锁头部Epoch字段值:
表示此对象偏向锁撤销次数,默认撤销40次以上,表示此对象不再使用于偏向锁,在下次线程挨次获取次对象时,直接变为轻量级锁。
只有一次CAS过程,出现在第一次枷锁时。
JDK6之后偏向锁默认开启
4.7 轻量级锁:
多个线程在不同时刻请求同一把锁,也就是不存在锁竞争的情况。针对这种情况,JVM采用轻量级锁来避免线程的阻塞和唤醒。
4.8 重量级锁
多个线程在同一时刻竞争同一把锁。
锁只能升级不能降级:效率考虑。
4.9 三种锁特点:
- 偏向锁只会在第一次请求锁时采用CAS操作并将锁对象的标记字段记录下当前线程地址。在此后的运行过程中,所以偏向锁的线程无需加锁操作。针对的是锁只会被同一线程持有的情况。
- 轻量级锁采用CAS操作,并将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。针对的是多个线程在不同时间段申请同一把锁的情况。
- 重量级锁会阻塞、唤醒和请求加锁的过程,针对的是多个线程同时竞争同一把锁的情况。JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。
4.10 其他优化
锁粗化
将多次裂解在一起的加锁与解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁。
锁消除
删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中堆上的数据不会逃逸出当前线程,则认为此代码是线程安全的,无需加锁。
5. 生产消费者模型
用于同步代码块或同步方法并且是内建锁。
5.1 wait()----痴汉方法(运行态->阻塞态)
wait()就是使方法停止运行,会释放对象锁。
wait()方法会使当前线程调用该方法后进行等待,并且将该线程置入锁对象的等待队列中,直到接到通知或被中断为止。
wait()方法只能在同步方法或同步代码块中调用,如果调用wait()时没有适当的锁,会跑出异常。
wait()方法执行后,当前线程释放锁,其他线程可以竞争该锁。
wait()方法从运行态回阻塞态。
wait()之后的线程继续执行有两种方法:
调用notify()方法唤醒等待线程;
线程等待是调用interrupt()中断线程 ;
wait(long time):如果到了预计时间还未被唤醒,线程将继续执行。
5.2 notify() - (阻塞态-> 运行态)
任意一个对象的monitor有两个队列,一个称为同步队列(排队竞争对象锁),还有一个等待队列(存放所有调用对象wait等待的线程)。
- notify()方法也必须在同步方法或同步代码块中调用,用来唤醒等待在该对象的线程。如果有多个线程等待,则任意挑选一个线程唤醒。
- notify()方法执行后,唤醒闲扯呢过不会立即释放对象锁,要等待唤醒线程全部执行完毕后才释放锁。
5.3 notifyAll()
唤醒所有在该线程上等待的线程。
5.4 线程阻塞
- 调用sleep()方法,周董放弃占有的CPU,不会释放对象锁。
- 调用阻塞式IO方法(read(), write()),在该方法返回前,线程阻塞。
- 线程视图获取一个monitor,但该monitor被其他线程所持有导致阻塞。
- 线程等待某个通知,既调用wait()。
5.5 monitor的两个队列
每个monitor都有两个队列,一个称为同步队列,另一个称为等待队列。
同步队列中存放了因为竞争monitor失败而导致阻塞的线程,这些线程等待CPU调度再次竞争锁。
等待队列存放因为调用wait()导致线程等待的线程,唤醒后进入同步队列竞争锁。
6. java.util.lock
6.1 lock简介
lock lock = new ReentrantLock();
try{
lock.lock();
//以下代码只有一个线程可以运行
...
}
finally{
lock.unlock();
}
8.2 lock 常用API
lock体系拥有可中断的获取锁以及超时获取锁等内建锁不具备的特性。
void lock(); // 获取锁
void lockInerruptibly() thows InterruptedException(); //相应中断锁
boolean tryLock(); //获取锁返回true,反之返回false
boolean tryLock(long time, TimeUnit unit); //超时获取锁,在规定时间未获取到锁返回false
Condition newCondition();//获取与lock绑定的等待通知组件
void unlock();//解锁
lock中所有的方法其实都是调用了器静态内部类中的Sync中的方法,而Sync继承了AbstractQueuedSynchronizer(AQS-简称同步器)。
lock–面向使用者,定义了使用者与锁交互的接口。
AQS–面向锁的实现者,屏蔽了面向装态的观管理、线程排队、线程等待与唤醒等底层操作。
6.3 AQS-同步器
同步器是用来构建锁以及其他同步组件的基础框架,它的实现主要是依赖一个int状态变量以及通过一个FIFO队列共同构成同步队列。
子类必须重写AQS的用protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队与阻塞机制。int状态的更新使用了getState(), setState() 以及compareAndSetState()。
子类推荐使用静态内部类来继承AQS实现自己的同步语义。同步器既支持独占锁,也支持共享锁。
6.4 AQS的模板模式
AQS使用模板方法模式。将一些与状态相关的核心方法开放该子类重写,而后AQS会使用子类重写的关于状态的方法进行线程的排队、阻塞以及唤醒等操作。
锁与AQS的关系:
锁面对使用者,定义了使用者与锁交互的接口。
同步器面对锁的实现,简化了锁的实现方式,屏蔽同步状态管理,屏蔽了面向装态的观管理、线程排队、线程等待与唤醒等底层操作。
8.5 AQS详解
在同步组件中,QAS是最核心的部分,同步组件的实现依赖AQS提供的模板方法来实现同步组件语义。
AQS实现了对同步状态的管理,以及对阻塞线程进行排队、等待通知等等底层实现。
AQS核心组成:
同步队列,独占锁的获取与释放共享锁的获取与释放以及可中断锁,超时等待锁获取这些特性的实现。
独占锁:
void acquire(int arg):独占式获取同步状态,如果获取失败插入同步队列进行等待。
void acquireInteruptibly(int arg):在1的基础上,此方法可以在同步队列中响应中断。
boolean tryAcquireNanos(int arg, long nanosTimeOut):在2的基础上增加了超时等待功能,到了预计时间还未获得锁直接返回。
boolean try Acquire(int arg):获取锁成功返回true,否则返回false。
boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点。
共享式锁:
void acquireShared(int arg) : 共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态。
void acquireSharedInterruptibly(int arg) : 增加了响应中断的功能
boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) : 在2的基础上增加了超时等待功能
boolean releaseShared(int arg) : 共享锁释放同步状态。
在AQS内部有一个静态内类Node,这是同步队列中每个具体的节点。
节点有如下属性:
int waitStatus:节点状态
Node prev:同步对列中前驱节点
Node next:同步队列中后继节点
Thread thread:当前节点包装的线程对象
Node nextWaiter:等待队列中下一个节点
**节点状态值如下:**
int INITIAL = 0; //初始状态
int CANCELLED = 1; //当前接地单从同步队列中取消
int SIGNAL = -1; // 后继节点处于等待状态。如果当前节点释放同步状态会通知后继节点,使后继节点继续运行。
int CONDITION = -2; //节点处于等待队列中。其他线程对Condition调用signal()方法后,该节点会从等待队列移到同步队列中。
int PROPAGATE = -3; //共享式同步状态会无条件的传播。
AQS同步队列采用带有头尾节点的双向链表。
7. 独占锁的获取:acquire(int arg)
获取锁失败后调用AQS提供的acquire(int arg)模板方法。
tryAcquire(arg):再次尝试获取同步状态,成功直接方法退出。
失败调用:addWaiter();
addWaiter(Node.EXCLUSIVE, arg); //加快那个当前线程已指定的模式(独占式,共享式)封装为Node节点后置入公布队列。
**enq(Node node):**当前队列为空或CAS尾插失败调用此方法来初始化队列或补短板尾插。
acquireQueued() 获取锁成功条件:当前节点前驱为头结点并且再次获取同步状态成功。
节点在同步队列中获取锁失败后调用 shouldParkAfterFalledAcquire(Node pred, Node node)
此方法主要逻辑是:使用CAS将前驱节点状态置为SIGINAL,表示需要将当前节点阻塞。
如果CAS失败,不断自旋知道前驱节点状态置为SIGINAL为止。
acquireQueued():
- 如果当前的前驱节点为头结点并且能够成功获取同步状态,当前线程获取锁成功,方……
- 如果获取锁失败,先不断自旋将前驱节点状态置为SIGINAL,而后调用LockSupport.pack()方法将当前线程阻塞。
独占锁的释放:release()
unlock() 方法实际调用AQS提供的 release() 方法。
release() 方法是 unlock() 方法的具体实现。首先获取头结点的后继节点。当后继节点不为 null,会调用 LockSupport().unpark() 方法唤醒后继节点包装的线程。因此,每一次锁释放后就会唤醒该节点的后继节点所包装的线程。
独占锁的获取与释放的总结:
- 线程获取锁失败,将线程调用addWaiter()封装成Node进行入队操作。addWaiter()中方法enq()完成对同步对列的头结点初始化以及CAS尾插失败后的重试处理。
- 入队之后排队获取锁的核心方法acquireQueued(),节点排队获取锁是一个自旋过程。当且仅当当前节点的前驱节点为头节点并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。否则不满足条件时会不断自旋将前驱节点的状态置为SIGINAL而后调用LockSupport().park()将当前线程阻塞。
- 释放锁时会唤醒后继节点(后继节点不为null)。
独占锁特性:
获取锁是响应中断:
获取锁响应中断原理与acquire()几乎一样,唯一区别在于当parkCheckInterrupt()返回true时表示线程阻塞时被中断,跑出中断异常后线程退出。
超时等待获取锁:
tryAcquireNanos(),该方法在三种情况下会返回:
- 在超时时间内,当前线程成功获取到锁。
- 当前线程在阐释时间内被中断。
- 超时时间结束,该线程仍未获取到锁,线程退出返回false。
超时获取锁逻辑与可中断获取锁基本一致,唯一区别在于获取锁失败后,增加一个时间处理,如果当前时间超过截止时间,线程不在等待,直接的退出,返回false。否则江县城阻塞置为等待状态排队获取锁。
8. 再次理解ReentrantLock–独占式重入锁
8.1 重入如何实现
表示能够对共享资源重复加锁,即当前线程再次获取锁是不会被阻塞。
8.2 如何重入实现?
如果该同步状态不为0,表示该同步状态已被线程获取。再判断持有同步状态的线程是否是当前线程,如果是,同步状态再次加1并返回true,表示持有线程重入同步线程。
释放过程:
当且仅当同步状态减为0并且持有线程为当前线程时表示锁被正确是释放。否则,调用setState()将减为1后的状态设置回去。
IllegelMonitorStateException():非持有锁线程调用tryRelease()方法跑出异常。
8.3 公平锁 OR 非公平锁
公平锁:锁的获取顺序符合时间上的顺序,即等待时间最长的线程最先获取锁。
要使用非公平锁,调用ReentrantLock有参构造传入true,获取内置的公平锁。
public ReentrantLock(){
//默认为非公平锁
sync = new NonFairSync();
}
获取同步状态前先判断当前节点是否为前驱节点,如果有,获取失败。
公平锁实现:
获取同步状态之前先判断下当前节点是否有前驱节点,如果有,获取失败。
公平锁与非公平锁对比:
- 公平锁保证了获取锁的线程一点是等待时间最长的线程,保证了请求资源时间上的绝对顺序,需要频繁地进行上下文切换,性能开销较大。
- 非公平锁保证系统有更大的吞吐量(效率较高),但是会造成线程“饥饿状态”(有的线程可能永远无法获取到锁)。
9. ReentrantReadWriteLock 详解
读写锁:允许同一时刻被多个线程访问,但是在写线程访问时,所有的读线程与其他写线程均会阻塞。
写线程能够获取到锁的前提条件:没有任何读写线程拿到锁。
9.1 写锁的获取 WriteLock–独占锁
9.1.1 写锁获取–模板方法 tryAcquire()
protected final boolean tryAcquire(int acquires){...........}
同步状态的低16位表示写锁,高16位表示写锁。
写锁逻辑获取:
当读锁已被读线程获取或者写锁已被其他线程获取,则写线程获取失败;否则,当前同步状态没有被任何读写线程获取,当前线程获取写锁成功并且可重入。
写锁释放逻辑同独占式锁的释放(release)逻辑
9.2 读锁–共享式锁–tryAcquireShred()
9.3 缓存的实现应用读写锁
9.4 锁降级:写锁可以降级为读锁,反过来读锁不能降级为写锁。
10. Condition接口的 await、signal 机制
10.1 与内建锁wait、notify的区别
- Object类提供的wait与nitify方法是与对象监视器monitor配合完成的等待与通知机制,属于JVM底层实现。二ConditionyuLock配合完成的等待通知机制属于java语言级别,具有更高的机制与扩展性。
- Condition独有特性:
I. 支持不响应中断,而Object不支持。
II. 支持多个等待队列,而Object只有一个。
III. 支持超时间设置,而Object不支持。
等待方法
void await() throws InterruptedException-同Object.wait();
直到被中断或唤醒。
void awaitUninterruptibly() - 不响应中断,直到被唤醒。
boolean await(long time, TimeUnit throws InterruptedException,;
同Object.wait(long timeout)),多了自定义时间单位,中断、超时、被唤醒。
boolean awaitUntil(Date deadline) throws InterruptedException;
支持设置截止时间。
唤醒方法:
signal():唤醒一个等待在condition上的线程,将该线程有等待队列转移到同步队列中。
signalALL():将所有等待在condition上的线程全部转移到同步队列中。
10 .2 等待队列
等待队列与同步队列共享了Node节点类
等待队列是一个单向带有头尾节点的队列
11. 死锁
产生原因:
对并发资源的加锁成“环”。
解决:银行家算法
12. 线程池(juc包下)——面试重点*************
12.1 使用线程池的优点:
- 降低资源的消耗:通过重复利用已创建的线程降低线程创建与销毁带来的损耗。
- 提高响应速度:当新任务到达时,任务不需要等待线程创建就可以立即执行。
- 提高线程的可管理性:使用线程池可以统一进行线程分配、调度与监控。\
14.2 线程池执行任务的流程:
当一个Runnable或Callable对象到达线程池时,执行策略如下:
第一步:首先查看核心程池是否已满,如果未满,创建新的线程执行任务。如果核心线程池中有空闲线程,则将任务这直接分配给空闲线程,则将任务分配给空闲线程执行,否则执行第二步。
第二步:判断 工作队列(BlockingQueue)是否已满,如果工作队列没有满,将提交任务储存到到工作队列中等待核心池的调度。否则,若工作队列已满,进入第三步。
第三步:判断当前线程池中的线程数是否已将达到最大值maximumPoolSize,若已经达到最大值,将任务交给饱和策略处理,否则,继续创建新线程执行此任务。
12.3 线程池的使用
14.3.1 手工创建线程池************
通过创建ThreadPoolExecuttor来创建线程池:
-
corePoolSize(核心池的大小):当提交任务到线程池时,线程池会创建一个新的线程来执行任务,即使核心池中有其他空闲线程也会创建新线程,一直到线程数达到了核心池的大小为止。调用PrestartAllCoreThreads()线程池会提前创建并启动所有线程。
-
work(工作队列):用于保存等待执行任务的阻塞队列。可以选择以下几个阻塞队列:
I. ArrayBolckingQueue:基于数组结构的有界阻塞队列,此队列按照FIFO原则对元素进行排序。
II. LinkedBlockingQueue:基于立案表结构的阻塞队列,按照FIFO排序元素,吞吐量高于ArrarBolckingQueue,Executors.newFixedThreadPool()采用此队列。
III. synchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则,插入操作一直处于阻塞状态,通常吞吐量比LinkedBlockingQueue还要高,Executors.newFixedThreadPool()采用此队列。
IV. PriorityBlockingQueue:具有优先级的无界阻塞队列。 -
maximumPoolSize(线程池最大线程数量): 线程池允许创建最大的线程数。如果队列已满并且已创建的线程数小于此参数,则线程池会再创建新的线程执行任务。否则,调用饱和策略处理。采用无界队列,此参数无意义。
-
keepAliveTime(线程保持活动时间):线程池的工作线程空闲后,保持存活的时间。若任务很多,并且每个任务执行的时间较短,可以调大此参数来提高线程利用率。
-
TimeUnit(TV参数的时间单位)
-
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态。此时采用饱和策略来处理任务,默认采用AbortPolicy.JDK—共内置四个饱和策略。
I. AbortPolicy,表示无法处理新任务抛出异常,JDK默认采用此策略。
II. CallerRunsPolicy,等待调用者线程空闲后运行任务。
III. DiscardOldestPolicy,丢弃阻塞队列中最近的一个任务,并执行当前任务。
IV. DiscardPolicy,不处理,直接将新任务丢弃,也不报错。
FutureTask 类执行任务只执行一次,并且会阻塞其他线程。
Future.get() 会阻塞其他线程,一直等到当前Callable线程执行完毕拿到返回值为止。
JDK7 新增Fork-join()框架将大任务拆分子任务执行,最终结果由各个子任务汇总得来。
12.4 JDK内置四大线程池
普通调度池:
- 创建无大小限制的线程池:Executors.newCachedThreadPool()
适用于很多短期任务的小程序,负载较轻的服务器。 - 创建固定大小线程池:Executors.newFixedThreadPool(int nThreads)
适用于为了满足资源管理的需求而需要限制当前线程数量的应用场合,适用于负载比较重的服务器。 - 单线程池:Executors.newSingleThreadPool()
适用于需要保证顺序的执行各个任务,并且在任意时间点,不会有多个线程活动的场景。