赞
踩
自定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是该线程的线程执行体。
创建该实现类的实例,并以此实例来创建Thread对象。
MyRunnable mr = new MyRunnable();
new Thread(mr);
调用线程对象的start方法来启动该线程。
创建Callable接口的实现类并实现call()方法,该方法就是线程执行体,有返回值。
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call()方法的返回值。
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
使用该FutureTask对象来创建Thread对象。
new Thread(futureTask);
使用FutureTask对象的get()方法来获取子线程执行结束后的返回值。
使用Executors类中的newFixedThreadPool(int num)方法来创建一个线程数量为num的线程池。
调用线程池中的execute()方法执行由Runnable接口创建的线程;调用submit()方法来执行有Callable接口创建的线程。
使用shutdown()方法关闭线程池。
阿里Java开发手册明确规定不能用Executors去创建线程,而是通过ThreadPoolExecutor的方法。
FixedThreadPool和SingleThreadPool允许请求队列的长度为Integer.MAX_VALUE,可能会堆积大量的请求导致OOM。
CachedThreadPool和ScheduledTheadPool允许创建线程的数量为Integer.MAX_VALUE,可能会创建大量的线程导致OOM。
线程的七种状态:新建(NEW)、就绪(READY)、运行(RUNNABLE)、阻塞(BLOCKED)、终止(TERMINATED)、等待(WAITING)、超时等待(TIME_WAITING)。
新建:使用new关键字创建了一个线程之后,线程就处于新建状态。此时仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
就绪:线程对象调用start()方法之后,线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。
运行:处于就绪状态的线程获得了CPU,开始执行线程执行体,线程处于运行状态。如果计算机只有一个CPU,那么任何时刻只有一个线程处于运行状态。在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
阻塞:
终止:
等待:一个线程获得了锁,但是需要等待其他线程执行某些操作,这个等待时间是不确定的。
超时等待:与等待的区别就是,该等待时间是确定的。
一些其他要注意的点:
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
Thread提供的让一个线程等待另一个线程完成的方法。某个程序执行中的线程A调用了线程B的join方法,则线程A进入等待状态,直到线程B执行完成后,才会继续执行。join()是会释放锁的。
public final void join() throws InterruptedException
这个是一直等待这个线程终止。public final void join(long millis) throws InterruptedException
等待这个线程死亡的时间最多为millis毫秒,如果为0表示一直等待。如果是负数则直接抛出IllegalArgumentException异常。public final void join(long millis, int nanos) throws InterruptedException
等待最多millis毫秒加上这个线程死亡的nanos纳秒,如果millis为负数或者nanos不在0-999999范围则直接抛出IllegalArgumentException异常。Thread类中的静态方法,当一个执行中的线程调用了Thread的sleep()方法,调用线程会进入阻塞状态而让出CPU,但是不释放锁。如果时间到了就会正常返回,然后线程处于就绪状态参与CPU调度,获取到CPU时间片就可以执行。
该方法声明抛出了InterrupedException。
Thread.sleep(0)表示这次调用该方法的线程被冻结了一下,让其他线程有机会执行。即重新触发一次CPU竞争(依然按照线程的优先级)
线程通信的方式之一,这三个方法必须由同步监视器对象来调用
该线程的存在是为其他线程提供服务的。JVM的垃圾回收线程就是典型的守护线程。其有个典型的特征:如果所有的前台线程都死亡,则后台线程自动死亡。当整个虚拟机只剩下后台线程时,程序就没有运行的必要,直接退出。
每个线程都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围在1-10之间(1的优先级最低,10的最高),也可以使用MAX_PRIORITY值为10、MIN_PRIORITY值为1、NORM_PRIORITY值为5。一般不建议乱设置。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。其中,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应当避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
public class DeadLockDemo { private static final Object resource1 = new Object(); private static final Object resource2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + " get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + " waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + " get resource2"); } } }, "线程1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + " get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + " waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + " get resource1"); } } }, "线程2").start(); } } 输出结果: Thread[线程2,5,main] get resource2 Thread[线程1,5,main] get resource1 Thread[线程2,5,main] waiting get resource1 Thread[线程1,5,main] waiting get resource2
看代码不难看出,首先线程2获得了锁synchronized (resource2)
,然后由通过Thread.sleep(1000)
休眠1s让线程1开始执行,然后线程1执行完后也进行了Thread.sleep(1000)
休眠1s。这两个线程休眠结束后都企图去获得对方所持有的资源而陷入互相等待的状态,这就造成了死锁。
借助Object类的wait()、notify()、notifyAll()三种方法。
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,即不能用上述三种方法。 Condition类提供了await()、signal()、signalAll()三种方法,这三种方法与上述三个方法类似。获取指定Lock对象对应的Condition:private final Condition cond=lock.newCondition()
下面直接用cond调用方法即可。
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该进程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
如果线程A修改了主存中的某一数据但是没有及时写回主存,而线程B又去进行读取数据操作,则读取到的是过期的数据。
主内存中i = 0
线程1: load i from 主存 // i = 0
i + 1 // i = 1
线程2: load i from 主存 // 线程1还没将i的值写回主存,所以i还是0
i + 1 // i = 1
线程1: save i to 主存
线程2: save i to 主存
现在主存中的值还是1,可我们的预期值是2
该问题可以通过同步机制(控制不同线程间操作发生的相对顺序)或者通过volatile关键字使每次修改都能够及时强制刷新到主存,从而对每个线程可见。
原子性:对于基本数据类型的读取和赋值操作都是原子性操作。即这些操作是不可中断的,要么做完,要么不做。
经典案例:如果有两个线程同时对i进行赋值,一个赋值为1,另一个为-1,则i的值要么为1要么为-1。
i = 2; //1
j = i; //2
i++; //3
i = i + 1; //4
其中,1是赋值操作,是原子操作,而234都不是原子操作。
2是读取赋值
3和4都是读取,修改,赋值
JMM只保证单个操作具有原子性,并不保证整体原子性。保证整体原子性可以使用Atomic下的类或者synchronized。
可见性:一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
有序性:在本线程内观察,所有的操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的。无序是因为发生了指令重排序和工作内存与主内存同步延迟。
其中,synchronized具有原子性、可见性、有序性;volatile具有有序性和可见性;final具有可见性。
现在的CPU都是采用流水线来执行指令的,一个指令的执行有:取指、移码、执行、访存、写回五个阶段,多条指令可以同时存在流水线中同时被执行。流水线是并行的,也就是说不会在一条指令上耗费很多时间而导致后续的指令都卡在执行之前的阶段。我们编写的程序都要经过优化后(编译和处理器对我们编写的程序进行优化后以提高效率)才被运行。优化分为很多种,其中一种就是重排序。即重排序就是为了提高性能。在JMM中,允许编译器和处理器对指令进行重排序,重排序的过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序:由于处理器使用缓存和IO缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
重排序的两大规则:as-if-serial规则和happens-before规则。
as-if-serial:指令不管怎么重排序,单线程程序的执行结果不能被改变。
编译器,JRE和处理器必须遵守该规则。编译器和处理器不会对存在数据依赖关系的操作做重排序,如果不存在数据依赖关系,那么这些操作可能被编译器和处理器重排序。
数据依赖性:如果两个操作访问同一个变量,且这两个操作有至少有一个为写操作,此时这两个操作就存在数据依赖性
int a = 2; //A
int b = 4; //B
int c = a * b; //C
AC存在数据依赖关系,BC也存在,而AB不存在,所以在最终执行指令序列的时候,C不能排在AB的前面(这样会改变程序的结果),但是AB并没有数据依赖性关系。也就是说编译器和处理器可以重排AB之间的执行顺序:先B后A,先A后B都可以。as-if-serial规则把单线程程序保护了起来,这也就就是说遵守as-if-serial语义的编译器、JRE和处理器给了我们一个幻觉:单线程的程序是按照顺序来执行的。其实并不是,as-if-serial语义使程序员无需担心重排序的影响,也无须担心内存可见性的问题。
happens-before:如果操作A先行发生与操作B,则在操作B发生之前,操作A的影响(修改主内存中共享变量的值、调用方法等)是操作B可见的。
JMM向我们保证:如果线程A的写操作write和线程B的读操作read之间存在happens-before关系,尽管write和read在不同的线程中执行,但JMM向程序员保证write操作对read操作可见。
JMM规定的天然现行发生关系,如果两个操作之间没有下面的关系,且无法从下面的关系推导,则JVM可以对其进行随意的重排序:
两个操作之间存在happens-before关系,并不意味具体实现时必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按happens-before关系来执行的结果一致。那么这种重排序在JMM之中是被允许的。
重排序带来的问题:
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a * a; //4 …… } } }
开两个线程AB,分别执行writer和reader,flag为标志位,用来判断a是否被写入,则我们的线程B执行4操作时,能否看到线程A对a的写操作?不一定,12操作并没有数据依赖性,编译器和处理器可以对这两个操作进行重排序,也就是说可能A执行2后,B直接执行3,判断为true,接着执行4,而此时a还没有被写入,这样多线程程序的语义就被重排序破坏了。
编译器和处理器可能会对操作重排序,这个是要遵守数据依赖性的,即不会改变存在数据依赖关系的两个操作的执行顺序。这里所说的数据依赖性仅仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑,所以在并发编程下这就有一些问题了。volatile关键字利用内存屏障来禁止重排序带来的问题。
为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序:
四种内存屏障
屏障类型 | 指令类型 | 说明 |
---|---|---|
LoadLoadBarriers | Load1;LoadLoad;Load2 | 确保Load1的数据的装载先于Load2及所有后续装载指令的装载。 |
StoreStoreBarriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储。 |
LoadStoreBarriers | Load1;LoadStore;Store2 | 确保Load1的数据的装载先于Store2及所有后续存储指令的存储 |
StoreLoadBarriers | Store1;StoreLoad;Load2 | 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载 |
对于JMM的八种操作,volatile规定:read(读取)、load(载入)、use(使用)动作必须连续出现;assign(赋值)、store(存储)、write(写入)动作必须连续出现。即每次读取前必须先从主内存刷新最新的值,每次写入后必须立即同步回主内存当中。
缓存行:为了增加CPU的访问速度,通常会在CPU和内存之间增加多级缓存:
L3为全局缓存,L2L1为核心独享的缓存。
根据局部性原理,CPU每次访问主存时都会读取至少一个缓存行的数据(通常一个缓存行为64字节,哪怕读取4字节数据,也会连续的读取该数据之后的60个字节)。
volatile的底层实际上就是加一个lock指令,**lock指令会锁定共享变量所在的所有缓存行,变量更新完成同步回内存后再释放。**这样就会产生一个性能问题,当一个线程更新变量时,其他线程都无法对该变量的相邻变量操作了(相邻变量被预读取在同一缓存行)。
public class VolatileExample { private static volatile int counter = 0; public static void main(String[] args) { //开十个线程,让他们每个都自增10000次,理论上应该得到10000 for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) counter++; } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }
多次运行上述代码,每次都得不到10000,这说明volatile并不能保证整体原子性,问题就是counter++不是一个原子性操作:
如果线程1读取counter到工作内存后,其他线程对这个值已经做了自增操作,那么线程A的这个值自然就是一个过期的值,造成了数据的脏读,因此结果必然小于10000。如果想让volatile保证整体原子性,必须符合:
synchronized关键字使每个线程依次排队操作共享变量,也就是用来处理共享数据的安全性问题。不过这种同步机制的效率很低。
如果锁的是类对象的话,不管new多少个实例对象,他们都会被锁住,即线程之间保证同步关系。其实无论对一个对象进行加锁还是对一个方法进行加锁,实际上都是对对象进行加锁。被加了锁的对象就叫锁对象,在Java中任何一个对象都能成为锁对象。
synchronized的问题:保证同一时刻只有一个线程能够获取到对象的监视器,从而进入到同步代码块或者同步方法中,表现为互斥性。这种方式的效率十分低下,每次只能过一个线程。在jdk1.6之后对synchronized进行优化。synchronized的优化,其实就是锁的四种状态的转变。
锁的四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。 这几个状态会随着竞争情况而逐渐升级,锁可以升级但不能降级。这种升级不降级的策略目的是为了提高获得锁和释放锁的效率。
synchronized并非一开始就给该对象加上重量级锁,而是从偏向锁到轻量级锁再到重量级锁的演变。假如我们一开始就知道某个同步代码块竞争很激烈的话,那么我们一开始就要使用重量级锁,从而减少锁转换的开销。如果我们只有一个线程在运行,那偏向锁则是一个很好的选择。而当某个同步代码块竞争不是那么很激烈的时候,我们就可以考虑使用轻量级锁。
CXQ队列(_cxq):竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。CXQ队列是一个临界资源,并不是一个真正的Queue,只是一个虚拟队列,由Node和next指针构成,每次新加入Node都会在队头进行,通过CAS改变第一个结点的指针为新增结点(新线程),同时设置新增节点的next指向后续节点,而取数据则发生在队尾。通过这种方式减轻了队列取数据时的争用问题。而且该结构是个Lock-Free的队列无锁队列(实际上就是通过CAS不断的尝试来实现的)。
EntryList:CXQ队列中有资格成为候选资源的线程会被移动到该队列中。
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck。
**Owner:获得锁的线程称为Owner。**初始时为NULL,当有线程占有该monitor的锁的时候,Owner标记为该线程的唯一标识,当线程释放monitor时,Owner又恢复为NULL。无法获得锁则会依然留在EntryList中,在EntryList中的位置不发生变化。
**WaitSet:如果Owner线程被wait方法阻塞,则转移到WaitSet队列。**当wait线程在某个时刻被notify/notifyAll之后,会将对应的ObjectWaiter从WaitSet移动到EntryList或CXQ队列中。如何移动取决于Monitor的策略:
WaitSet存放的是处于等待状态的线程,这些线程在等待某种特定的条件变成真,所以又称为条件队列。这个Monitor的wait/notify/notifyAll方法实际上是为上层提供的操作API。所以要调用这个条件队列的方法,必须先拿到这个Monitor,相应的,对于同步方法或者同步代码块中,就会有一个推论就是“wait/notify/notifyAll方法只能出现在相应的同步块或同步方法中”。如果不在同步方法或同步块中,运行时会报IllegalMonitorStateException。
每个等待锁的线程都会被封装成ObjectWaiter对象,保存了Thread(当前线程)以及当前的状态ThreadState等数据。
volatile | synchronized | |
---|---|---|
本质 | 告诉jvm当前线程的工作变量可能是不正确的,需要从主内存中重新读取 | 锁定当前共享变量,只有获取锁的线程才能访问该共享变量 |
使用范围 | 变量 | 方法,代码块 |
特性 | 有序性,可见性,无法保证原子性 | 原子性,有序性,可见性 |
线程阻塞 | 不会造成线程阻塞 | 会阻塞线程 |
ABA问题:如果内存地址存放的值由A变成了B,然后又由B变回了A,此时CAS检查的时候发现共享内存的值并没有变化依然为A,但是实际上却是发生了变化。如果基本类型问题不大,如果是引用类型就会有一些问题。
自旋时间过长:CAS是一种非阻塞同步,线程不会自己被挂起,而是不停的尝试而自旋,自旋时间过长就会造成CPU很大的性能消耗。解决方案:破坏for循环使其超过一定时间或者一定次数时,return退出。
jdk1.8提供了一个LongAdder类,使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS的性能。
public class LongAdder extends Striped64 implements Serializable
/**
* cell数组,大小总是2的幂次方
*/
transient volatile Cell[] cells;
/**
* 基本值,主要在没有争用的情况下使用,在表的初始化的时候也作为一个基础值。通过CAS更新。
*/
transient volatile long base;
如果发现并发更新的线程数量不是很多,就直接给base值进行累加。如果发现并发更新的数量过多,就开始实行分段CAS机制,系统将该变量拆分为多个变量并把这些线程分配到不同的cell数组元素中,来实施分段CAS。
假设当前有80个线程进行一变量的自增操作,cell数组长度为8,则每一组都有10个线程,每一组对cell数组的其中一个元素做自增,最后cell数组8个元素的值都为10,累加得到80。这就等于80个线程对i进行了80次自增操作。
自动迁移机制:
只能保证一个共享变量的原子操作:如果对多个共享变量同时进行操作,CAS不保证其原子性。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){...}
corePoolSize:线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。在初次创建了线程池后,线程池中其实是没有任何线程的,而是等待有任务到来才创建线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到任务队列(阻塞队列)当中。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
maximumPoolSize:线程池的最大线程数量。当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务。
keepAliveTime:空闲线程的存活时间。**如果当前线程池的线程个数已经超过了corePoolSize且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁。**这么做尽可能的降低了系统资源损耗。
unit:keepAliveTime的时间单位。
workQueue:用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
threadFactory:创建线程的工厂类。可以用来设定线程名、是否为daemon线程等等。
handler:饱和策略。当阻塞队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,说明当前线程池已经处于饱和状态了,这时候有任务提交就需要采用一种策略来处理这种情况。
拒绝策略是一个接口。可以实现该接口去定制拒绝策略,也可以使用jdk提供的四种拒绝策略。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
AbortPolicy:中断策略,直接拒绝所提交的任务,抛出RejectedExecutionException异常。是线程池默认的拒绝策略,在任务不能再提交时抛出异常,及时反馈程序的运行状态。如果是比较关键的业务,建议使用此拒绝策略,这样的话在系统不能承受更大的并发量的时候能够及时的通过异常发现。
CallerRunsPolicy:调用者运行策略,只让调用者所在的线程来执行任务。只要线程池不关闭,则会在调用excute的线程时执行这个被拒绝的任务。这种情况是要让所有任务都执行完毕,适合大量计算的任务类型去执行。
DiscardPolicy:舍弃策略,不处理直接丢掉任务。一般建议无关紧要的业务采用此策略。
DiscardOldestPolicy:舍弃最旧任务策略,丢弃掉任务队列中存放时间最久的任务,执行当前任务。
线程池内部将运行状态(runState)和线程数量(workerCount)两个关键参数的维护放在了一起:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
这一个变量维护了两个信息高3位用来保存runState,低29位则用来保护workCount,两者互不干扰。这样做的好处就是可以避免在做相关决策时出现不一致的情况,不必为了维护两者的一致去占用锁资源。
ThreadPoolExecutor运行状态5种:
作用:用来存储等待执行的任务
线程公平访问队列:指阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞先访问的形式。在队列为空时,获取元素的线程会等待队列变为非空,当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
常见的阻塞队列:
ArrayBlockingQueue:用数组实现的有界阻塞队列,按照FIFO的原则对元素进行排序。支持公平锁和非公平锁。
LinkedBlockingQueue:用链表实现的有界阻塞队列,默认最长为Integer.MAX_VALUE。吞吐量高于ArrayBlockingQueue。按照FIFO的原则对元素进行排序,使用较多。通常在Executors.newFixedThreadPool()使用。
SynchronousQueue:不存储元素(无容量)的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,常用于生产者消费者模型,吞吐量高,使用较多。
PriorityBlockingQueue:支持优先级的无阻塞队列,默认进行自然排序,两个元素优先级相同不保证顺序。
public static ExecutorService newXxxThreadPool()
newSingleThreadPool:一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)去执行。
newFixedThreadPool:创建一个定长的线程池。每提交一个任务,则创建一个工作线程,达到最大线程数目后,进入任务队列。该线程池不会释放空闲线程,对资源有一定的占用和浪费。(可以控制最大并发数)
newCacheThreadPool:弹性缓存线程池,创建的时候线程池中没有线程,只有当通过execute()和submit()方法提交任务时,如果有空的线程则让空的线程执行该任务,否则创建新的线程来执行该任务。创建线程的最大值取决于默认最大值,如果空闲时间超过60s,则进行回收线程操作。
newScheduleThreadPool:创建定长的线程池,支持定时以及周期性的任务执行。
线程池提交任务:
execute():提交不需要返回值的任务
void execute(Runnable command);
//该方法无返回值,提交后无法判断该任务是否被线程池执行成功
submit():提交需要返回值的任务,提供了三个重载
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
//可以看出既支持Callable参数或者Runnable,同时返回一个Future对象,可以通过该方法来判断任务是否执行成功
//使用Future.get()方法获取执行结果,该方法会阻塞当前线程直到任务完成
阿里巴巴开发手册禁止直接创建上面四个线程池:
AQS是一同步器,该类在java.util.cocurrent.locks包下面,其用来构建锁和其他同步组件的基础框架,例如ReentrantLock,ReentrantReadWriteLock等。它主要依赖一个int成员变量state来表示同步状态以及通过一个FIFO队列构成等待队列。其子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。
AQS维护了一个 volatile int state(表示当前同步状态)和一个FIFO的线程等待队列(多线程争用资源被阻塞时会进入此队列)。juc就是基于AQS实现的。底层核心数据结构:双向链表+state(锁状态),底层的操作为CAS。
AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。
AQS可重写的方法如下:
实现同步组件时AQS提供的模板方法如下:
模板方法举例:
AQS中有一方法tryAcquire:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
而ReentrantLock的内部类Sync继承了AQS,Sync的子类NonfairSync有一个方法tryAcquire重写了AQS的tryAcquire:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
AQS中的acquire方法又调用了该NonfairSync的方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时,NonfairSync在调用模板方法acquire的时候,就会调用被自己重写的tryAcquire方法。
AQS定义了两种资源访问方式:
实现了AQS的锁有:自旋锁。互斥锁、读锁写锁。
在线程获取锁时会调用AQS的acquire方法,该方法第一次尝试获取锁如果失败,会将该线程加入到该同步队列中,加入同步队列中的线程状态为阻塞。AQS中的同步队列是通过链式方式进行实现的一个双向链表。
AQS中有一个静态内部类Node,对每一个等待获取资源线程的封装,里面包含了需要同步的线程本身以及其等待状态,例如是否被阻塞、是否等待唤醒、是否已经被取消等待。
static final Node SHARED = new Node(); //指示结点正在共享模式下等待的标记
static final Node EXCLUSIVE = null; //指示结点正在独占模式下等待的标记
volatile int waitStatus; //结点状态
volatile Node prev; //当前结点/线程的前驱节点
volatile Node next; //当前结点/线程的后继节点
volatile Thread thread; //加入同步队列的线程引用
Node nextWaiter; //等待队列中的下一个节点
结点的状态有这些:
static final int CANCELLED = 1 //表示当前结点已经取消调度。当timeout或响应中断,会出发变更为此状态,进入该状态后的结点将不会再变化
static final int SIGNAL = -1 //表示当前结点后继的线程处于等待状态,如果当前结点释放同步状态会通知其后继,使得后继节点的线程能够运行
static final int CONDITION = -2 //表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待状态转移到同步队列中,等待获取同步锁
static final int PROPAGATE = -3 //表示在共享模式下,前驱结点不仅会唤醒后继结点,同时也可能唤醒后继的后继结点
static final int INITIAL = 0; //新结点入队的默认状态
负值表示有效的等待状态,正值表示结点以及被取消。所以可用waitStatus是否大于等于0来判断结点的状态是否正常。
jdk1.5后出现,替代Object中的wait()、notify()、notifyAll()方法来实现线程间的通信,使线程协作更加安全和高效。其就是一个条件队列,区分与前面说的等待队列。条件队列是由Condition形成的条件队列,线程被await操作挂起后就会被放入条件队列,这个队列中的节点都被挂起,他们都等待被signal后进入阻塞队列再次获取锁
Condition为一个接口:
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
Condition中有两个变量firstWaiter和lastWaiter,类似于头结点和尾结点。
Condition中的node有两个状态:
当运行中的线程被await操作后就会被加入条件队列:
当调用condition的signal或者signalAll方法时,会将等待队列中的结点移动到同步队列中,使该结点有机会获得lock。
ReentrantLock:重入锁,是实现Lock接口的一个实现类,支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁后,再次获取不被阻塞。**ReentrantLock还支持公平锁和非公平锁。**ps:synchronized关键字隐式支持重入性,通过获取自增,释放自减来实现重入。
非公平锁:static final class NonfairSync extends Sync
公平锁:static final class FairSync extends Sync
与非公平锁的获取释放差不多,只不过判断了一下当前线程是否在同步队列的最前面。
如果一个锁是公平的,那么锁的获取顺序就应该是符合请求的绝对时间顺序,类似排队,满足FIFO。公平锁每次都是从同步队列的第一个结点获取到锁,而非公平锁则是有可能刚释放的线程能再次获取到锁。
其中ReentrantLock默认的无参构造就是非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
还有一个带boolean类型的有参构造,true为公平,false为非公平:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
详情看前面的synchronized
详情看前面的ReentrantLock
多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,通常可以使用Thread的join方法,让主线程等待被join的线程执行完之后,才继续往下执行。CountDownLatch就是一个类似于倒计时的工具类,可以方便的完成这种场景。
其构造方法为:public CountDownLatch(int count)。构造方法会传入一个整型数N,之后调用CountDownLatch的countDown方法会对N减一,直到N减到0的时候,当前调用await方法的线程继续执行。
一个很通俗的例子,运动员进行跑步比赛时,假设有6个运动员参与比赛,裁判员在终点会为这6个运动员分别计时,可以想象每当一个运动员到达终点的时候,对于裁判员来说就少了一个计时任务。直到所有运动员都到达终点了,裁判员的任务也才完成。这6个运动员可以类比成6个线程,当线程调用CountDownLatch.countDown方法时就会对计数器的值减一,直到计数器的值为0的时候,裁判员(调用await方法的线程)才能继续往下执行。
CyclicBarrier也是一种多线程并发控制的实用工具,和CountDownLatch一样具有等待计数的功能,但是相比于CountDownLatch功能更加强大。CyclicBarrier在使用一次后,下面依然有效,可以继续当做计数器使用,这是与CountDownLatch的区别之一。
CyclicBarrier提供了一个构造方法:public CyclicBarrier(int parties, Runnable barrierAction),意思是当指定的线程都到达了指定的临界点的时,接下来执行的操作可以由barrierAction传入即可。
信号量Semaphore,主要是用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待。当执行完业务功能后,需要通过release()
方法将许可证归还,以便其他线程能够获得许可证继续执行。
Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有多个线程读取数据后,需要将数据保存在数据库中,而可用的最大数据库连接只有10个,这时候就需要使用Semaphore来控制能够并发访问到数据库连接资源的线程个数最多只有10个。在限制资源使用的应用场景下,Semaphore是特别合适的。
Semaphore的构造方法中还支持指定是否具有公平性,默认非公平。
线程间交换数据的一个工具类,提供了一个交换的同步点,在这同步点两个线程能够交换数据,使用exchange方法来实现,如果一个线程先执行exchange方法,那么它会同步等待拎一个线程也执行exchange方法,这时候两个线程就都达到了同步点,可以交换数据。
其提供了一个无参构造方法,还有两个主要方法:
理解:下课期间,男生经常会给走廊里为自己喜欢的女孩子送情书,相信大家都做过这样的事情吧。男孩会先到女孩教室门口,然后等女孩出来,教室那里就是一个同步点,然后彼此交换信物,也就是彼此交换了数据。
ThreadLocal是java.lang包下的线程变量,该变量属于当前线程,对于其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程去访问自己内部的副本变量。
多线程中,我们解决共享资源的安全问题时一般就是synchronized、lock,这样控制会让未获取到锁的线程进行阻塞等待,时间效率不好。线程安全问题的核心就是多个线程会对同一个临界区共享资源进行操作。TreadLocal就是让每个线程使用自己的共享资源,不会影响其他线程,让多个线程达到隔离状态,是一种空间换时间的方案。
什么时候用:数据库连接管理类,使用TreadLocal给每个线程的连接创建副本,在线程内部的任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题。
set:
get:
remove:
面试题:为什么是ThreadLocal和Entry的key是弱引用?
原因:由于ThreadLocal实例与Entry中key存在的引用是弱引用,当threadLocal外部的强引用(ThreadLocal Instance == null)的话,threadLocal实例就没有一条引用链路可达。在gc的时候势必会被回收,这就会存在key为null,无法通过一个key为null去访问到该entry的value。同时也存在了一条强引用链:
threadLocal ref -》 currentThread ref -》 currentThread -》 threadLocalMap -》 Entry -》 value -》 memory
导致垃圾回收的时候,key为null的entry是无法被回收的,且该entry永远无法被访问到,造成内存泄露。如果线程结束后,threadLocal、threadRef、thread等就会断掉,上述的threadLocalMap、entry也会被回收。不过实际使用中我们都是会用线程池去维护线程,例如Executors.newFixedThreadPool()时,为了复用线程是不会结束的。
ThreadLocal做的改进:
Thread.exit()会令threadLocal = null,这意味着gc的时候可以对threadLocalMap进行垃圾回收,即threadLocal的生命周期与thread的生命周期相同。每次使用完ThreadLocal,都应该调用其remove方法清除数据。在使用线程池的情况下,必须及时清理ThreadLocal,不仅是内存泄露的问题,更严重的是可能导致业务逻辑出现问题。
根本区别 | 资源开销 | 包含关系 | 内存分配 | 影响关系 | 执行过程 | |
---|---|---|---|---|---|---|
进程 | 操作系统资源分配的基本单位 | 每个进程都有独立的代码和数据空间,进程之间切换会有较大的开销 | 一个进程内有多个线程,进程的执行过程是多个线程共同完成的 | 进程之间的地址空间和资源是相互独立的 | 一个进程崩溃后,在保护模式下不会对其他进程造成影响 | 每个独立的进程有程序运行的入口、顺序执行序列和程序出口 |
线程 | 处理器任务调度和执行的基本单位 | 同一类线程共享代码和数据空间,每个线程都有自己独立运行的栈和程序技术器,线程之间切换开销小 | 进程的一部分 | 同一进程的线程共享本进程的地址空间和资源 | 一个线程崩溃后整个进程都会死掉 | 线程不能独立执行,必须依赖于进程,由进程提供多个线程的执行控制 |
在Java中,平时所说的并发编程、多线程其实都是用户线程,而对应到操作系统,还有另外一种线程叫做内核线程。用户线程和内核线程之间必然存在某种关系,这种对应关系最常见有三种方法:多对一模型、一对一模型和多对多模型。
多对一又叫用户级线程模型,即多个用户线程对应到同一个内核线程上,线程的创建调度同步等所有细节全部由进程的用户控件线程库来处理。
优点:用户线程很多操作对于内核线程来说都是透明的,不需要用户态和内核态频繁的切换,使线程的创建、调度、同步等非常快。
缺点:
一对一模型,又叫做内核级线程模型,即一个用户线程对应一个内核线程,内核负责每个线程的调度,可以调度到其他处理器上面。
优点:实现简单
缺点:
Java使用的就是一对一线程模型。
多对多模型,又叫作两级线程模型,它是博采众长之后的产物,充分吸收前两种线程模型的优点且尽量规避它们的缺点。在此模型下,用户线程与内核线程是多对多(m : n,通常m>=n)的映射模型。
首先,区别于多对一模型,多对多模型中的一个进程可以与多个内核线程关联,于是进程内的多个用户线程可以绑定不同的内核线程,这点和一对一模型相似;其次,又区别于一对一模型,它的进程里的所有用户线程并不与内核线程一一绑定,而是可以动态绑定内核线程, 当某个内核线程因为其绑定的用户线程的阻塞操作被内核调度让出CPU时,其关联的进程中其余用户线程可以重新与其他内核线程绑定运行。
优点:
缺点:实现复杂
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。