Java多线程
- 进程与线程的区别:
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类和程序的单一进程。线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - 多线程编程的好处:
在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存(heap memory),因此创建多个线程去执行一些任务会比创建多个进程更好。举个例子,Servlets比CGI更好,是因为Servlets支持多线程而CGI不支持。 - 用户线程和守护线程:
当我们在Java程序中创建一个线程,它就被称为用户线程。一个守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。 - 线程优先级:
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。 - 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 - 在多线程中,什么是上下文切换(context-switching)
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。 - 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
Java中实现多线程有几种方法:
- 继承Thread类;
- 实现Runnable接口;
- 实现Callable接口通过FutureTask包装器来创建Thread线程;
- 使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)。
如何停止一个正在运行的线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
- 使用interrupt方法中断线程。
notify()和notifyAll()有什么区别
- notify可能会导致死锁,而notifyAll则不会;
- 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
- wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
- notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
sleep()和wait() 有什么区别
- 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
- 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()/notifyAll()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
volatile 是什么?可以保证有序性吗
- 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
禁止进行指令重排序。
volatile 不是原子性操作,使用 volatile 一般用于状态标记量和单例模式的双检锁。
“观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令”lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
volatile与synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
为什么wait, notify 和 notifyAll这些方法不在thread类里面
- 明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用
- 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
- 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
- 还有一个原因是为了避免wait和notify之间产生竞态条件。
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。
调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
Java中interrupted 和 isInterruptedd方法的区别
- interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有可能被其它线程调用中断来改变。
Java中synchronized 和 ReentrantLock 有什么不同
-
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。 -
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。 -
ReentrantLock获取锁定的三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁;
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断。
有三个线程T1,T2,T3,如何保证顺序执行
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
实际上先启动三个线程中哪一个都行,因为在每个线程的run方法中用join方法限定了三个线程的执行顺序。
public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
什么是线程安全
- 线程安全就是说多线程访问同一代码,不会产生不确定的结果。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。
但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
Thread类中的yield方法有什么作用
- yield()方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
Java线程池中submit() 和 execute()方法有什么区别
- execute():只能执行 Runnable 类型的任务。
- submit():可以执行 Runnable 和 Callable 类型的任务。
- Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
- 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
说一说自己对于 synchronized 关键字的了解
- synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行, 是由一对 monitorenter/monitorexit 指令实现的。另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果
要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,synchronized是可重入锁(可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况)。 - 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
- 锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化就是增大锁的作用域。
- 自旋锁: 是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
- 自旋锁的缺点
使用自旋锁会有以下一个问题:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
- 自旋锁的优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
- synchronized关键字最主要的三种使用方式:
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 总结:synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能。
常用的线程池有哪些
- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
- newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
- 线程池的好处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Lock 接口与synchronized
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定。 但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率,它为读和写分别提供了锁。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
- 举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
用 Java 实现阻塞队列
- 用wait()和notify();
public class BoundedBuffer_Synchronized {
private Object[] items = new Object[2];
private Object notEmpty = new Object();
private Object notFull = new Object();
int count,putidx,takeidx;
public void put(Object obj) throws InterruptedException{
synchronized(notFull){
while(count == items.length){
notFull.wait();
}
}
items[putidx] = obj;
if(++putidx == items.length){
putidx = 0;
}
count ++;
synchronized (notEmpty) {
notEmpty.notify();
}
}
public Object take() throws InterruptedException{
synchronized(notEmpty){
while(count == 0){ // 啥也没有呢 取啥
notEmpty.wait();
}
}
Object x = items[takeidx];
System.out.println("取第"+takeidx+"个元素"+x);
if(++takeidx == items.length){
takeidx = 0;
}
count --;
synchronized (notFull) {
notFull.notify();
}
return x;
}
public static void main(String[] args) throws InterruptedException {
final BoundedBuffer_Synchronized bb = new BoundedBuffer_Synchronized();
System.out.println(Thread.currentThread()+","+bb);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread()+","+bb);
bb.put("xx");
bb.put("yy");
bb.put("zz");
bb.put("zz");
bb.put("zz");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
bb.take();
bb.take();
}
}
- 用jdk1.5提供的Lock;
public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[2]; // 阻塞队列
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
System.out.println("进入put");
lock.lock();
System.out.println("put lock 锁住");
try {
while (count == items.length) { // 如果队列满了,notFull就一直等待
System.out.println("put notFull 等待");
notFull.await(); // 调用await的意思取反,及not notFull -> Full
}
items[putptr] = x; // 终于可以插入队列
if (++putptr == items.length)
putptr = 0; // 如果下标到达数组边界,循环下标置为0
++count;
System.out.println("put notEmpty 唤醒");
notEmpty.signal(); // 唤醒notEmpty
} finally {
System.out.println("put lock 解锁");
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
System.out.println("take lock 锁住");
try {
while (count == 0) {
System.out.println("take notEmpty 等待");
notEmpty.await();
}
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
System.out.println("take notFull 唤醒");
notFull.signal();
return x;
} finally {
lock.unlock();
System.out.println("take lock 解锁");
}
}
public static void main(String[] args) throws InterruptedException {
final BoundedBuffer bb = new BoundedBuffer();
System.out.println(Thread.currentThread()+","+bb);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread()+","+bb);
bb.put("xx");
bb.put("yy");
bb.put("zz");
bb.put("zz");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
bb.take();
}
}
线程安全性
- 所谓的线程安全性核心在于对访问状态操作进行管理,特别是对共享和可变的状态的访问。一个对象有时候不仅仅保存在对象本身,有时候还保存在许多和它关联的对象中,任何一个地方发现变更,都可能影响其他地方数据。
- 什么是线程安全
对象可以在多个线程之间调用,同时线程之间不会出现错误的交互。 - 不安全的线程出现的问题
竞态条件
并发编程中有一个情况叫做“竞态条件”,描述的是当两个线程对同一个资源进行竞争的时候因为不同的访问顺序导致出现不可预测的结果。
失效的数据
逸出
为什么说 Synchronized 是非公平锁?
-
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
-
为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的:
不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现需要线程挂起,所以被称为非阻塞同步。 -
乐观锁的核心算法是 CAS(Compare and Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS 具有原子性,它的原子性由 CPU 硬件指令实现保证,即使用 JNI调用 Native 方法调用由 C++ 编写的硬件级别指令,JDK 中提供了Unsafe 类执行这些操作。
-
乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
- 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
- 长时间自旋可能导致开销大。假如 CAS 长时间不成功而一直自旋,会给 CPU 带来很大的开销。
- ABA 问题。CAS 的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是 A,后来被一条线程改为 B,最后又被改成了 A,则 CAS 认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把
版本号加一。
那么请谈谈 AQS 框架是怎么回事儿
- AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器的框架 , 各种 Lock 包中的锁( 常用的有 ReentrantLock 、ReentrantReadWriteLock),以及其他如 Semaphore、CountDownLatch,甚至是早期的 FutureTask 等,都是基于 AQS 来构建。 AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。
- AQS 在内部定义了一个 volatile int state 变量,表示同步状态:当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1;如果 state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
- AQS 通过 Node 内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
Node 类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个 Node 结点关联其 prev 结点和 next 结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个 FIFO 的过程。
Node 类有两个常量,SHARED 和 EXCLUSIVE,分别代表共享模式和独占模式 。 所谓共享模式是一个锁允许多线程同时操作 ( 信号量Semaphore 就是基于 AQS 的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如 ReentranLock)。 - AQS 通过内部类ConditionObject构建等待队列( 可有多个 ),当Condition 调用 wait() 方法后 ,线程将会加入等待队列中 ,当Condition 调用 signal() 方法后,线程将从等待队列转移到同步队列中进行锁竞争。
- AQS 和 Condition 各自维护了不同的队列 , 在使用Lock 和Condition 的时候,其实就是两个队列的互相移动。
ReentrantLock 是如何实现可重入性的
- ReentrantLock 内部自定义了同步器 Sync(Sync 既实现了 AQS,又实现了 AOS,而 AOS 提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程 ID 和当前请求的线程 ID 是否一样,一样就可重入了。
Java 中的线程池是如何实现的
- 在 Java 中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类 Worker,它基于 AQS 实现,存放在线程池的 HashSetworkers 成员变量中;
而需要执行的任务则存放在成员变量 workQueue(BlockingQueue workQueue)中。
这样,整个线程池实现的基本思想就是:从 workQueue 中不断取出需要执行的任务,放在 Workers 中进行处理。
创建线程池的几个核心构造参数
-
Java 中的线程池的创建其实非常灵活,我们可以通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:
corePoolSize:线程池的核心线程数。
maximumPoolSize:线程池允许的最大线程数。
keepAliveTime:超过核心线程数时闲置线程的存活时间。
workQueue:任务执行前保存任务的队列,保存由 execute 方法提交的 Runnable 任务。 -
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于 corePoolSize,则任务根本不会存放,添加到 queue 中,而是直接开始运行)
如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
Java 中用到的线程调度算法是什么
-
抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
-
Thread.sleep(0)的作用是什么?
这个问题和上面那个问题是相关的,我就连在一起了。由于 Java 采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到 CPU 控制权的情况, 为了让某些优先级比较低的线程也能获取到 CPU 控制权,可以使用 Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡 CPU 控制权的一种操作。
Hashtable 的 size()方法中明明只有一条语句"return count",为什么还要做同步
- 这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是 size()方法明明只有一条语句,为什么还要加锁? 关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程 A 在执行 Hashtable 的 put 方法添加数据,线程 B 则可以正常调用 size()方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对 size++,线程 B 就已经读取size 了,那么对于线程 B来说读取到的 size一定是不准确的。而给 size()方法加了同步之后,意味着线程 B 调size()方法只有在线程 A 调用 put 方法完毕之后才可以调用,这样就保证了线程安全性;
2)CPU 执行代码,执行的不是 Java 代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到 Java 代码只有一行,甚至你看到 Java 代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句 "return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。
线程的几种可用状态
- 新建( new ):新创建了一个线程对象。
- 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。
- 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
- 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
(一). 等待阻塞:运行( running )的线程执行 wait ()方法, JVM 会把该线程放入等待队列( waitting queue )中。
(二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
(三). 其他阻塞: 运行( running )的线程执行 Thread.sleep ( long ms )或 t.join ()方法,或者发出了 I / O 请求时,JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。 - 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
- 当线程执行 wait() 方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis) 方法或 wait(long millis) 方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行完 Runnable的 run() 方法之后将会进入到 TERMINATED(终止) 状态。
并行和并发有什么区别
- 并行:多个处理器或多核处理器同时处理多个任务。
- 并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看并发的任务是同时执行;
- 简而言之:
并发=两个队列和一台处理器
并行=两个队列和两个处理器
start()和run()的区别
- Thread.run():在当前线程中,直接调用run()方法、相当于调用普通方法(同步);
- Thread.start():将创建的线程加入到线程组,启动线程、需等到获取了时间片才执行(异步)
线程池都有哪些状态
- 线程池的5中状态:
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
- SHUTDOWN:不再接受新提交的任务,但可以处理存量任务
- STOP:不再接受新提交的任务,也不处理存量任务
- TIDYING:所有的任务都已终止
- TERMINATED:terminated()方法执行完成后进入该状态
- 状态转换图:
在 Java 程序中怎么保证多线程的运行安全
- 方法一:使用安全类,比如 java. util. concurrent 下的类。
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。
// 手动锁 Java 示例代码如下:
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
死锁
-
什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。 -
怎么防止死锁?
-
尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
-
尽量使用 java.util.concurrent 并发类代替自己手写锁。
-
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
-
尽量减少同步的代码块。
- 死锁产生的必要条件:
- 互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
- 不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
- 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。
ThreadLocal
-
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是 ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
-
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
-
请谈谈 ThreadLocal 是怎么解决并发安全的
ThreadLocal 是 Java 提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,其实现原理是,在 ThreadLocal 类中有一个Map,用于存储每一个线程的变量的副本。
synchronized、volatile、Lock和ReentrantLock的区别
- synchronized 和 volatile的区别:
- volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- synchronized 和 Lock的区别:
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- synchronized 和 ReentrantLock 区别:
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下: - ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
- ReentrantLock 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
阻塞队列 (BlockingQueue)
-
阻塞队列:
阻塞队列 (BlockingQueue)是 java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。 -
BlockingQueue 的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类: -
ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
-
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现java.util.concurrent.Delayed 接口。
-
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
-
PriorityBlockingQueue : PriorityBlockingQueue 是一个无界的并发队列。它使用了和类java.util.PriorityQueue 一样的排序规则 。 你无法向这个队列中插入null值。所有插入到PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
-
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
-
ArrayBlockingQueue
ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标,takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外 notEmpty,notFull 条件变量用来进行出入队的同步。ArrayBlockingQueue 通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加 synchronized 的意味。其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了全局锁。 -
LinkedBlockingQueue
LinkedBlockingQueue 中也有两个 Node 分别用来存放首尾节点,并且里面有个初始值为 0 的原子变量 count用来记录队列元素个数,另外里面有两个 ReentrantLock 的独占锁,分别用来控制元素入队和出队加锁,其中 takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock 控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外 notEmpty 和 notFull 用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时有一个线程入队和一个线程出队,其实这个是个生产者-消费者模型。 -
PriorityBlockingQueue
PriorityBlockingQueue 内部有个数组 queue 用来存放队列元素,size 用来存放队列元素个数,allocationSpinLockOffset 是用来在扩容队列时候做 cas 的,目的是保证只有一个线程可以进行扩容。由于这是一个优先级队列所以有个比较器 comparator 用来比较元素大小。lock 独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty 条件变量用来实现 take 方法阻塞模式。这里没有 notFull 条件变量是因为这里的 put 操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。类似于 ArrayBlockingQueue 内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个 notEmpty 条件变量而没有 notFull 这是因为前者是无界队列,当 put 时候永远不会处于 await 所以也不需要被唤醒。PriorityBlockingQueue 始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过 cas 保证同时只有一个线程可以扩容成功。 -
SynchronousQueue
SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put 的时候),如果当前没有人想要消费产品(即当前没有线程执行 take),此生产线程必须阻塞,等待一个消费线程调用 take 操作,take 操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先 take 后 put,原理是一样的)。 -
DelayQueue
DelayQueue 的关键元素 BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue 是一个使用优先队列(PriorityQueue)实现的 BlockingQueue,优先队列的比较基准值是时间。
DelayQueue = BlockingQueue +PriorityQueue + Delayed
非阻塞队列
- 与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。在底层,非阻塞队列使用的是 CAS(compare and swap)来实现线程执行的非阻塞。
线程池的启动策略
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
同步线程及线程调度相关的方法
-
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
-
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 - InterruptedException 异常,不释放锁;
-
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; -
注意:java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。
公平锁、非公平锁,乐观锁、悲观锁,可重入锁
-
公平锁和非公平锁指的是线程在等待获取资源时,是否按照等待顺序进行获取。
-
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,直到第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制,例如:行锁,表锁,读锁,写锁,都是在操作前先上锁。java中的synchronized的实现也是一种悲观锁。
-
乐观锁:乐观锁概念为,每次拿数据的时候都认为别的线程不会修改这个数据,所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。
-
可重入锁:当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。防止死锁发生。synchronized 和 ReentrantLock 都是可重入锁。
CAS
-
CAS 原理:
CompareAndSwap 比较和交换,Atomic***类都是基于此实现。CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。 -
CAS 比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 )) -
CAS 是乐观锁的一种实现方式。
-
CAS 的缺陷:
- ABA 问题。通过添加时间戳的方式解决( AtomicStampedReference 带有时间戳的对象引用)
- 自旋时间过长
- 只能保证一个共享变量的原子操作, 从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
生产者消费者问题
- 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。
示意图:
- 实现方法:
- wait() / notify()方法
当缓冲区已满时,生产者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行;
当缓冲区已空时,消费者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行。
当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;
当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
public class Storage {
// 仓库容量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<>();
public void produce() {
synchronized (list) {
while (list.size() + 1 > MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
list.notifyAll();
}
}
}
//生产
public class Producer implements Runnable{
private Storage storage;
public Producer(){}
public Producer(Storage storage , String name){
this.storage = storage;
}
@Override
public void run(){
while(true){
try{
Thread.sleep(1000);
storage.produce();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//消费
public class Consumer implements Runnable{
private Storage storage;
public Consumer(){}
public Consumer(Storage storage , String name){
this.storage = storage;
}
@Override
public void run(){
while(true){
try{
Thread.sleep(3000);
storage.consume();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//测试
public class Main {
public static void main(String[] args) {
Storage storage = new Storage();
Thread p1 = new Thread(new Producer(storage));
Thread p2 = new Thread(new Producer(storage));
Thread p3 = new Thread(new Producer(storage));
Thread c1 = new Thread(new Consumer(storage));
Thread c2 = new Thread(new Consumer(storage));
Thread c3 = new Thread(new Consumer(storage));
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
notifyAll()方法可使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,还是要看JVM的具体实现吧。
- await() / signal()方法
在JDK5中,用ReentrantLock和Condition可以实现等待/通知模型,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Storage {
// 仓库最大存储量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的条件变量
private final Condition full = lock.newCondition();
// 仓库空的条件变量
private final Condition empty = lock.newCondition();
public void produce() {
// 获得锁
lock.lock();
while (list.size() + 1 > MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
empty.signalAll();
lock.unlock();
}
public void consume() {
// 获得锁
lock.lock();
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
full.signalAll();
lock.unlock();
}
}
//生产与消费代码同上。
- BlockingQueue阻塞队列方法
BlockingQueue是JDK5.0的新增内容,它是一个已经在内部实现了同步的队列,实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
import java.util.concurrent.LinkedBlockingQueue;
public class Storage {
// 仓库存储的载体
private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<>(10);
public void produce() {
try{
list.put(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
} catch (InterruptedException e){
e.printStackTrace();
}
}
public void consume() {
try{
list.take();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费了一个产品,现库存" + list.size());
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
- 信号量
Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。计数为0的Semaphore是可以release的,然后就可以acquire(即一开始使线程阻塞从而完成其他执行)。
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
public class Storage {
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 仓库的最大容量
final Semaphore notFull = new Semaphore(10);
// 将线程挂起,等待其他来触发
final Semaphore notEmpty = new Semaphore(0);
// 互斥锁
final Semaphore mutex = new Semaphore(1);
public void produce() {
try {
notFull.acquire();
mutex.acquire();
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
}
catch (Exception e) {
e.printStackTrace();
} finally {
mutex.release();
notEmpty.release();
}
}
public void consume() {
try {
notEmpty.acquire();
mutex.acquire();
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
mutex.release();
notFull.release();
}
}
}
- 管道
一种特殊的流,用于不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
inputStream.connect(outputStream)或outputStream.connect(inputStream)作用是使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。这种方式只适用于两个线程之间通信,不适合多个线程之间通信。
import java.io.IOException;
import java.io.PipedOutputStream;
//生产
public class Producer implements Runnable {
private PipedOutputStream pipedOutputStream;
public Producer() {
pipedOutputStream = new PipedOutputStream();
}
public PipedOutputStream getPipedOutputStream() {
return pipedOutputStream;
}
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
pipedOutputStream.write(("This is a test, Id=" + i + "!\n").getBytes());
}
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.io.PipedInputStream;
//消费
public class Consumer implements Runnable {
private PipedInputStream pipedInputStream;
public Consumer() {
pipedInputStream = new PipedInputStream();
}
public PipedInputStream getPipedInputStream() {
return pipedInputStream;
}
@Override
public void run() {
int len = -1;
byte[] buffer = new byte[1024];
try {
while ((len = pipedInputStream.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
//测试
public class Main {
public static void main(String[] args) {
Producer p = new Producer();
Consumer c = new Consumer();
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
try {
p.getPipedOutputStream().connect(c.getPipedInputStream());
t2.start();
t1.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
CycliBarriar 和 CountdownLatch
- CycliBarriar
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。 即:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。CyclicBarrier是当await的数量到达了设置的数量的时候,才会继续往下面执行,CyclicBarrier计数达到指定值时,计数置为0重新开始。
构造器:
public CyclicBarrier(int parties) {
this(parties, null);
}
1、创建一个新的 CyclicBarrier,会让给定数量(parties)的参与者(线程)在调用await()方法(达到了栅栏)后处于等待状态,直到所有给定线程(parties)都调用了await方法(即都达到了栅栏)时,各自才恢复运行;
2、参数:parties - 在达到栅栏(barrier) 前必须调用 await() 的线程数;
3、通俗的说:先到达栅栏(调用了await方法)的线程会被阻塞,直到同一组内的所有线程都达到栅栏时,大家才接着往后运行。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
// 1、用于在所有线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景
// 2、通俗的说:先到达栅栏(调用了await方法)的线程会被阻塞,当同一组内的所有线程都达到栅栏时,会从中随机选一个线程去执行barrierAction任务,此任务执行完成后,所有线程再接着往后运行。
import java.util.Random;
import java.util.concurrent.*;
/**
* Created by Administrator on 2019/3/11 0011.
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
/**
*与CountDownLatch一样,初始化将来要同步的线程数要与实际的相等
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
ExecutorService executorService = Executors.newFixedThreadPool(3);
System.out.println("主线程调用开始...");
for (int i = 0; i < 3; i++) {
/**新开3个线程执行任务,加入这是2个线程,那么它们最后会因为达不到初始化的3个,而一直等待*/
executorService.execute(new MyThread(cyclicBarrier));
}
System.out.println("主线程调用完毕...");
}
}
class MyThread implements Runnable {
private CyclicBarrier cyclicBarrier;
/**
* 参数传入
*/
public MyThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1 + new Random().nextInt(3));
System.out.println("玩家 " + Thread.currentThread().getName() + " 加入游戏,开始等待其它玩家...");
/**
* 到达栅栏时,可是等待其它线程,一直到调用await方法的线程个数达到new CyclicBarrier(3)初始化的个数后
* 所有的线程才会接着向后运行,否则一直阻塞在这里
*/
cyclicBarrier.await();
System.out.println("所有玩家准备完毕,开始游戏...");
TimeUnit.SECONDS.sleep(1 + new Random().nextInt(3));
System.out.println("玩家 " + Thread.currentThread().getName() + " 正在大杀特杀...");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
import java.util.Random;
import java.util.concurrent.*;
/**
* Created by Administrator on 2019/3/11 0011.
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
/**
*与CountDownLatch一样,初始化将来要同步的线程数要与实际的相等
* 当循环栅栏线程组中的线程都达到了栅栏后,会从中随机选一个来执行new Runnable任务
* 执行完后,循环栅栏组中的线程接着向后运行
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 所有线程都已经达到栅栏,开始生成地图...");
Thread.sleep(1000 + new Random().nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("主线程调用开始...");
for (int i = 0; i < 3; i++) {
/**新开3个线程执行任务,加入这是2个线程,那么它们最后会因为达不到初始化的3个,而一直等待*/
executorService.execute(new MyThread2(cyclicBarrier));
}
System.out.println("主线程调用完毕...");
}
}
class MyThread2 implements Runnable {
private CyclicBarrier cyclicBarrier;
/**
* 参数传入
*/
public MyThread2(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1 + new Random().nextInt(4));
System.out.println("玩家 " + Thread.currentThread().getName() + " 加入游戏,开始等待其它玩家...");
/**
* 到达栅栏时,开始等待其它线程,一直到调用await方法的线程个数达到new CyclicBarrier(3)初始化的个数后
* 所有的线程才会接着向后运行,否则一直阻塞在这里
*/
cyclicBarrier.await();
TimeUnit.SECONDS.sleep(1 + new Random().nextInt(4));
System.out.println("玩家 " + Thread.currentThread().getName() + " 正在大杀特杀...");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
- CountdownLatch
一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行。是并发包中提供的一个可用于控制多个线程同时开始某个动作的类,其采用的方法为减少计数的方式,当计数减至零时位于latch.Await()后的代码才会被执行,CountDownLatch是减计数方式,计数==0时释放所有等待的线程;CountDownLatch当计数到0时,计数无法被重置。
构造器:
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
- 只有如上所示一个构造器,参数 count 表示闭锁需要等待的线程数量,这个值只能被设置一次,CountDownLatch 没有提供任何机制去重新设置这个计数值。
- 如果需要重置计数,请考虑使用 CyclicBarrier。
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchTest {
public static void main(String[] args) {
try {
/**注意倒计数锁存器等待线程的个数要与实际一致*/
CountDownLatch countDownLatch = new CountDownLatch(2);
ExecutorService executorService = Executors.newCachedThreadPool();//无界线程池
for (int i = 0; i < 2; i++) {
/**假如实际开的线程小于CountDownLatch构造器中的值,那么主线程会永远休眠
* 假如实际开的线程大于CountDownLatch构造器中的值,那么当锁存器计数减到0时,主线程会提交运行*/
executorService.execute(new MyThread(countDownLatch));
}
System.out.println("主线程开始等待两个子线程执行任务...");
countDownLatch.await();//阻塞等待
System.out.println("两个子线程任务全部执行完毕,主线程开始关闭线程池...");
executorService.shutdown();//关闭线程池
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Created by Administrator on 2018/6/13 0013.
* 线程 任务
*/
class MyThread extends Thread {
/**
* 倒计数锁存器
*/
private CountDownLatch countDownLatch;
public MyThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println("子线程 " + Thread.currentThread().getName() + " 任务开始...");
Thread.sleep(1000 + new Random().nextInt(3000));//模拟线程在执行任务
System.out.println("子线程 " + Thread.currentThread().getName() + " 任务结束...");
/**线程执行完毕,递减锁存器的计数,即 CountDownLatch 总数减1
* 如果忘了计数递减,那么主线程可能会因为计数器迟迟不到0而一致等待
* */
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 它们的行为有一定相似度,区别主要在于:
- CountDownLatch 是不可以重置的,所以无法重用,CyclicBarrier 没有这种限制,可以重用。
- CountDownLatch 的基本操作组合是 countDown/await,调用 await的线程阻塞等待 countDown 足够的次数,不管你是在一个线程还是多个线程里 countDown,只要次数足够即可。 CyclicBarrier 的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。
- CountDownLatch 目的是让一个线程等待其他 N 个线程达到某个条件后,自己再去做某个事(通过 CyclicBarrier 的第二个构造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达到同样的效果)。而 CyclicBarrier 的目的是让 N 多线程互相等待直到所有的都达到某个状态,然后这 N 个线程再继续执行各自后续(通过 CountDownLatch 在某些场合也能完成类似的效果)。
线程池
- 线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
显然不是的。线程池默认初始化后不启动 Worker,等待有请求时才启动。
每当我们调用 execute() 方法添加一个任务时,线程池会做如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。 - Java中的线程池
-
SingleThreadExecutor线程池
这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
corePoolSize:1,只有一个核心线程在工作。
maximumPoolSize:1。
keepAliveTime:0L。
workQueue:new LinkedBlockingQueue(),其缓冲队列是无界的。 -
FixedThreadPool 线程池
FixedThreadPool 是固定大小的线程池,只有核心线程。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。
corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:new LinkedBlockingQueue(),其缓冲队列是无界的。 -
CachedThreadPool 线程池
CachedThreadPool 是无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为 1 的阻塞队列。缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的 daemon 型 SERVER 中用得不多。但对于生存期短的异步任务,它是 Executor 的首选。
corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:new SynchronousQueue(),一个是缓冲区为 1 的阻塞队列。 -
ScheduledThreadPool 线程池
ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在 DEFAULT_KEEPALIVEMILLIS 时间内回收。
corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:new DelayedWorkQueue() -
SingleThreadScheduledExecutor线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
// 创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度。
- WorkStealingPool线程池
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
-
这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建- ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
-
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小的子任务,只要它还能分割。
-
创建一个 ForkJoinPool:
可以通过其构造创建一个 ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望的并行级别。并行级别表示传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool的示例:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4); -
提交任务到 ForkJoinPool:
像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。 -
线程池的拒绝策略:
AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy,直接抛出异常也不处理。
CallerRunsPolicy:CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池所在的线程去执行被拒绝的任务。
DiscardPolicy:采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。
DiscardOldestPolicy策略的作用是,当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的任务,再把这个新任务添加进去。