
进程是资源分配的最小单位,线程是CPU调度的最小单位,一个进程包含多个线程
2、讲一讲线程的生命周期线程有五种状态:创建、等待、运行、阻塞、结束,线程有三种创建方式,两种没有返回值,一种有返回值,第一种是继承Thread类,第二种是实现Runnable接口,第三种是通过Callable和Future创建(重写Callable类的Call方法,通过get方法获取Call中返回值),线程创建好之后,通过调用start方法让线程进入等待状态,当线程被CPU调度的时候进入运行状态。线程如果调用sleep方法或者wait方法进入阻塞状态。然后通过中断方法interrupt抛出异常去唤醒被sleep的线程,或者通过notify、notifyAll去唤醒被wait的线程,进入对象锁池中。有三种情况线程会结束,第一种是run方法运行结束,第二种是调用stop方法强制结束,第三种是使用中断方法interrupt在合适的地方结束线程
3、wait()和sleep()的五个区别是什么?有五个区别:第一个是sleep不会让出cpu资源,wait会,第二个是sleep不会释放锁,wait会,第三个是sleep可以在任意位置调用,wait方法只能在同步块或者同步方法中调用,第四个是sleep唤醒之后是进入等待状态,而wait唤醒之后是在对象的锁池中争抢锁。第五个区别是sleep方法是线程的方法,而wait是Object的方法。
4、wait()、notify()、notifyAll()的区别是什么?wait方法让线程释放锁且让出CPU资源,notify会唤醒对象锁池中的随机一个线程,notifyall唤醒对象锁池中所
有的线程,优先级越高的线程争抢到锁的概率越大(setPriority() 1的优先级最低,10的优先级最高,默认值是5
,thread.setPriority(1))
保证线程之间的可见性,防止指令重排,每次读被修饰的变量的时候直接从主内存(线程共享区)中读取,而不
是从工作内存(线程私有区,栈)中读取,这样线程就能保证每次读取到的都是最新值。但是Volatile只能保证
线程间的可见性,而不能保证原子性,如果两个线程同时把变量拷贝到工作内存中,同时修改之后复制到主内存
,这时候就出问题了。像刚刚说的读取到工作内存其实就是被拷贝工作缓存区,工作缓存区是一个抽象的概念,
在jvm中是没有划分这块区域的,具体放在了哪里,是由虚拟机决定的,只要符合JMM规范即可。
不需要保证线程安全的场景,比如run方法中有一个以被Volatile修饰的变量为开关,当开关为false的时候退出循环
7、Volatile和Synchronized的三个区别是什么?第一个是volatile是轻量级同步(不排他,不保证原子性),synchronized是重量级同步(排他,CPU切换消耗资源)第二个是区别主要是volatile不保证原子性,而synchronized保证原子性,从而保证了可见性,第三个是volatile只能修饰变量,synchronized可以修饰代码块和方法
8、什么时候会发生死锁?如何避免死锁?发生死锁有四个条件:互斥使用、不可抢占、请求和保持、循环等待。避免死锁有两种方式:第一个是加锁顺序要一致,第二个是给锁设置超时时间。
9、如何给锁设置超时时间? 10、如何用代码实现生产者消费者问题?创建一个资源类,设置资源的最大个数为10,当前资源个数为0,类中有remove和put方法,remove方法中去判断当前资源个数是否为0,为0调用wait方法进行等待,不为0资源–,最后使用notify去唤醒其他线程(为什么不用notifyall唤醒全部呢?因为唤醒一个也是随机的,唤醒全部也是随机争抢锁的,当然数量越少越不消耗资源),put方法也是同理,去判断资源是否达到最大值,达到则等待,未达到则资源++,最后唤醒其他线程。消费者和生产者类分别使用组合调用资源类的remove和put方法,分别创建和启动消费者生产者线程
11、Synchronized的实现原理?在jdk1.6之前synchronized是重量级的,但是经过不断的优化在jdk1.6之后就变得很强大了,synchronized的锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁,随着竞争状态逐渐升级,不能降级在第一次获取锁的时候会通过CAS去修改对象头Mark word中的锁信息,锁会从CAS升级为偏向锁,Mark Word记录偏向线程的Id,如果没有其他线程竞争,就不需要去通过CAS加锁和解锁的,偏向线程会一直拥有偏向锁。当有线程要竞争偏向锁的时候会发生锁撤销,如果持有偏向锁的线程已经退出同步代码块或者不在活动状态,那么就会让持有偏向锁的线程在安全的地方暂定,然后释放锁,再唤醒持有偏向锁的线程(中断?),竞争的线程通过CAS去修改Mark word中的偏向线程Id,获取到偏向锁。如果发生竞争,持有偏向锁的线程未退出同步代码块,那么偏向锁就会升级成轻量锁,JVM会在偏向线程的栈中开辟一个存放锁信息的空间,存放对象头中MarkWord拷贝,在对象头中的MarkWord替换成指向栈中锁信息的指针。轻量级锁释放
的时候重新把MarkWord替换到对象头中,然后唤醒原持有偏向锁的线程。竞争线程通过尝试用指向栈中所信息的指针替换MarkWord,替换成功则获取轻量级锁,替换失败自旋,自旋次数过多升级成重量级锁,就是使用monitor重量级锁是基于monitorenter和monitorexit指令实现的,monitorenter插入到开始位置,monitorexit插入到结束位置,JVM需要保证每一个monitorenter都有一个monitorexit相关联,每一个对象都有一个monitor与之相关联,当一个monitor被持有之后,对象将处于锁定状态。当线程执行到monitorenter指令时会尝试获取对象锁对应的monitor所有权,即尝试获取对象锁。
this锁锁住的是当前实例,对象锁锁住的是指定对象,类锁锁住的是class对象,Synchronized修饰普通方法时是this锁,User.class.wait()释放锁,修饰静态方法时是类锁,修饰代码块时可以指定锁类型,本质使用的都是monitor
13、常见的锁类型有哪些?CAS锁:AS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。自旋锁:就是通过CAS不断去判断是否满足锁条件。乐观锁:一开始就认为一定会执行成功,通过版本号或者时间戳去控制,CAS就是乐观锁。悲观锁:一开始就认为一定不会执行成功,先争抢锁,再执行业务逻辑。重入锁:外层获取锁之后内层就不需要再次去获取锁了,避免了死锁,ReentrantLock和sychronized就是可重入锁。不可重入锁:外层获取锁之后,内层也要重新获取锁,容易产生死锁。公平锁:不同时间段进入对象锁池的按照先到先获取锁的规则获取锁。非公平锁:不同时间段进入对象锁池的按照一起争抢锁的规则获取锁
偏向锁、轻量级锁、重量级锁,谈Synchronized的实现原理。分段锁:谈ConcrrountHashMap。分布式锁:Redis、ZK等。读写锁:写的时候不让其他读和写,和zk同步的时候服务不让用一个道理。
ReentrantLock把所有Lock接口的操作委派给Sync类上,Sync类继承了AQS抽象类,Sync有两个子类,一个支持公平锁,
一个支持非公平锁。默认使用非公平锁。ReentrantLock.lock调用的是NonFairSync.lock方法(AQS只把尝试获取锁的
方法交给子类实现lock unlock holdLock()判断是否获取到锁)一般会在finally块中写unlock()以防死锁
第一个区别:一个是类,一个是关键字,第二个区别:一个可以判断是否获取到锁,一个不可以,第三个区别:一个是自动挡(指定开始位置和结束位置),一个是手动挡(修饰方法和代码块)
,第四个区别:lock是乐观锁,Synchronized原始用的是悲观锁,jdk1.6之后就变得很强大了可以锁升级。
阻塞线程,一般用来保证线程的执行顺序
17、run()和start()的区别?run()只是一个单纯的方法,并没有去创建线程,而start方法是去创建线程后执行run方法
18、CycliBarriar和CountdownLatch的两个区别?第一个区别是侧重点不同,CycliBarriar一般是用来阻塞一组线程至某个状态才执行,CountdownLatch是让某个线程等待其他线程
执行完任务之后再执行,第二个区别是CycliBarriar可以循环利用CountdownLatch是一次性的,到0之前都会阻塞线程
竞争条件:同时去操作一个非线程安全的数据,造成预期之外的结果
死锁:互相抢资源
活锁:互相让资源,解决,先到先服务
饥饿:资源不足
比如AtomicInteger类,可以调用方法进行自增或者自减,保证线程安全
21、什么是CAS锁?CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。
这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一
定会有其它线程去修改它,悲观锁效率很低。
CAS存在一个很明显的问题,即ABA问题;
如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它任然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段时间曾经被改成B,然后有改回A,那CAS操作就会误任务它从来没有被修改过。正对这种情况,java并发包提供了一
个带有标记的原子应用类AtomicStampedRefernce,它可以通过变量值的版本来保证CAS的正确性;
synchronized锁、lock
23、悲观锁的应用场景?并发量高的时候,如果追求响应快就用乐观锁,要么快速成功要么快速失败,如果重试代价大,使用悲观锁,悲观
锁可以保证成功率。
悲观锁适合读少写多的情况,乐观锁适合读多写少的情况
https://toutiao.io/posts/ljtjsk/preview
25、怎么快速判断是IO死锁问题而不是其他问题?(待定) 26、CLH知道么?虚拟队列。(待定) 27、双重校验加锁为什么需要volatile?https://juejin.cn/post/6844903913506734088
28、线程间的通信方式Synchronized
wait/notify 等待
Volatile 内存共享
CountDownLatch 并发工具
CyclicBarrier 并发工具
进程间通信方式进程间通信又称IPC(Inter-Process Communication),指多个进程之间相互通信,交换信息的方法。根据进程通信时信息量大小的不同,可以将进程通信划分为两大类型:
低级通信,控制信息的通信(主要用于进程之间的同步,互斥,终止和挂起等等控制信息的传递)
高级通信,大批数据信息的通信(主要用于进程间数据块数据的交换和共享,常见的高级通信有管道,消息队列,共享内存等)。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
① 管道(Pipe)及有名管道(named pipe)
管道通常指无名管道,是 UNIX 系统IPC最古老的形式,是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:
要关闭管道只需将这两个文件描述符关闭即可。
单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:
若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
FIFO,也称为命名管道,它是一种文件类型。
FIFO可以在无关的进程之间交换数据,与无名管道不同。FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
② 报文(Message)队列(消息队列)
消息队列是消息的链接表,包括Posix消息队列system V消息队列,存放在内核中并由消息队列标识符(即队列ID)标识。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
特点如下:
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
③ 共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
④ 信号(Signal)
信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。linux除了支持Unix早期信号语义函数signal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
⑤ 信号量(semaphore)
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。不是用于交换大批数据,而用于多线程之间的同步。常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
⑥ 套接口(Socket)
29、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择。
30、在多线程中,什么是上下文切换(context-switching)?单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。
操作系统中,CPU时间分片切换到另一个就绪的线程,则需要保存当前线程的运行的位置,同时需要加载需要恢复线程的环境信息。
31、用户线程和守护线程有什么区别?守护线程都是为JVM中所有非守护线程的运行提供便利服务: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。
因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
32、如何创建守护线程?以及在什么场合来使用它?任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
守护线程相当于后台管理者 比如 : 进行内存回收,垃圾清理等工作
33、线程的状态转换?[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FnPjLUqk-1633419634497)(java全占知识体系-JUC.assets/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f77323876756b78616277326b6967393366623037353532692f696d6167655f31626a7332716f746539743865713863756f31626363626d6f6d2e706e67)]](https://camo.githubusercontent.com/6ffc2482690d88a43be62bcddfebc83b474578274ae3ed05a1f27549c11bb1a0/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f77323876756b78616277326b6967393366623037353532692f696d6167655f31626a7332716f746539743865713863756f31626363626d6f6d2e706e67)
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁) (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。 (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程中断是否能直接调用stop,为什么?Java提供的终止方法只有一个stop,但是不建议使用此方法,因为它有以下三个问题:
答: ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态; ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常; ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
请说出与线程同步以及线程调度相关的方法。答:
答:如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
不使用stop停止线程?当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
使用自定义的标志位决定线程的执行情况
public class SafeStopThread implements Runnable{
private volatile boolean stop=false;//此变量必须加上volatile
int a=0;
@Override
public void run() {
// TODO Auto-generated method stub
while(!stop){
synchronized ("") {
a++;
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
a--;
String tn=Thread.currentThread().getName();
System.out.println(tn+":a="+a);
}
}
//线程终止
public void terminate(){
stop=true;
}
public static void main(String[] args) {
SafeStopThread t=new SafeStopThread();
Thread t1=new Thread(t);
t1.start();
for(int i=0;i<5;i++){
new Thread(t).start();
}
t.terminate();
}
}
Java中如何实现线程?各有什么优缺点,比较常用的是那种,为什么?
在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。
Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。
如何控制某个方法允许并发访问线程的大小?Semaphore两个重要的方法就是semaphore.acquire() 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)semaphore.release()释放一个信号量,此时信号量个数+1
public class SemaphoreTest {
private Semaphore mSemaphore = new Semaphore(5);
public void run(){
for(int i=0; i< 100; i++){
new Thread(new Runnable() {
@Override
public void run() {
test();
}
}).start();
}
}
private void test(){
try {
mSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 进来了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 出去了");
mSemaphore.release();
}
}
在Java中什么是线程调度?
线程调度是指系统为线程分配处理器使用权的过程。 主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。
协同式线程调度:线程的执行时间由线程本身控制,当线程把自己的工作执行完了之后,主动通知系统切换到另一个线程上。
抢占式线程调度:每个线程由系统分配执行时间,不由线程本身决定。线程的执行时间是系统可控的,不会有一直阻塞的问题。
Java使用抢占式调度
Java中用到的线程调度算法是什么?抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
线程类的构造方法、静态块是被哪个线程调用的?线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
在实现Runnable的接口中怎么样访问当前线程对象,比如拿到当前线程的名字?Thread t = Thread.currentThread();
String name = t.getName();
System.out.println("name=" + name);
什么是线程池?为什么要使用它?为什么使用Executor框架比使用应用创建和管理线程好?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。
为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
常用的线程池模式以及不同线程池的使用场景?以下是Java自带的几种线程池: 1、newFixedThreadPool 创建一个指定工作线程数量的线程池。 每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
2、newCachedThreadPool 创建一个可缓存的线程池。 这种类型的线程池特点是:
3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。
在Java中Executor、ExecutorService、Executors的区别?Executor 和 ExecutorService 这两个接口主要的区别是:
Executors 类提供工厂方法用来创建不同类型的线程池。
比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
如何创建一个Java线程池?Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Thread 类中的start() 和 run() 方法有什么区别?start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
Java线程池中submit() 和 execute()方法有什么区别?两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
Java中notify 和 notifyAll有什么区别?notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。
为什么wait, notify 和 notifyAll这些方法不在thread类里面?一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用?主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
最主要的原因是为了防止以下这种情况
// 等待者(Thread1)
while (condition != true) { // step.1
lock.wait() // step.4
}
// 唤醒者(Thread2)
condition = true; // step.2
lock.notify(); // step.3
在对之前的代码去掉 synchronized 块之后,如果在等待者判断 condition != true 之后而调用 wait() 之前,唤醒者**将 condition 修改成了 true 同时调用了 notify() **的话,那么等待者在调用了 wait() 之后就没有机会被唤醒了。
讲下join,yield方法的作用,以及什么场合用它们?join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。
yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
sleep方法有什么作用,一般用来做什么?sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复。注意这里的恢复并不是恢复到执行的状态,而是恢复到可运行状态中等待CPU的宠幸。
Java多线程中调用wait() 和 sleep()方法有什么不同?Java程序中wait和sleep都会造成某种形式的暂停,它们可以满足不同的需要。
不能被重写,线程的很多方法都是由系统调用的,不能通过子类覆写去改变他们的行为。
为什么Thread类的sleep()和yield()方法是静态的?Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。
该代码只有在某个A线程执行时会被执行,这种情况下通知某个B线程yield是无意义的(因为B线程本来就没在执行)。因此只有当前线程执行yield才是有意义的。通过使该方法为static,你将不会浪费时间尝试yield 其他线程。
什么是阻塞式方法?只能给自己喂安眠药,不能给别人喂安眠药。
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情。
ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。
此外,还有异步和非阻塞式方法在任务完成前就返回。
如何强制启动一个线程?在Java里面没有办法强制启动一个线程,它是被线程调度器控制着
一个线程运行时发生异常会怎样?简单的说,如果异常没有被捕获该线程将会停止执行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
在线程中你怎么处理不可控制异常?在Java中有两种异常。
非运行时异常(Checked Exception):这种异常必须在方法声明的throws语句指定,或者在方法体内捕获。例如:IOException和ClassNotFoundException。
运行时异常(Unchecked Exception):这种异常不必在方法声明中指定,也不需要在方法体中捕获。例如,NumberFormatException。
因为run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行异常时,我们必须捕获并且处理它们。当运行时异常从run()方法中抛出时,默认行为是在控制台输出堆栈记录并且退出程序。
好在,java提供给我们一种在线程对象里捕获和处理运行时异常的一种机制。实现用来处理运行时异常的类,这个类实现UncaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法。示例:
package concurrency;
import java.lang.Thread.UncaughtExceptionHandler;
public class Main2 {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
class Task implements Runnable{
@Override
public void run() {
int numero = Integer.parseInt("TTT");
}
}
class ExceptionHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been capturedn");
System.out.printf("Thread: %sn", t.getId());
System.out.printf("Exception: %s: %sn", e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %sn",t.getState());
}
}
当一个线程抛出了异常并且没有被捕获时(这种情况只可能是运行时异常),JVM检查这个线程是否被预置了未捕获异常处理器。如果找到,JVM将调用线程对象的这个方法,并将线程对象和异常作为传入参数。
Thread类还有另一个方法可以处理未捕获到的异常,即静态方法setDefaultUncaughtExceptionHandler()。这个方法在应用程序中为所有的线程对象创建了一个异常处理器。
当线程抛出一个未捕获到的异常时,JVM将为异常寻找以下三种可能的处理器。
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
1、一般来说,wait肯定是在某个条件调用的,不是if就是while 2、放在while里面,是防止出于waiting的对象被别的原因调用了唤醒方法,但是while里面的条件并没有满足(也可能当时满足了,但是由于别的线程操作后,又不满足了),就需要再次调用wait将其挂起。 3、其实还有一点,就是while最好也被同步,这样不会导致错失信号。
while(condition){
wait();
}
多线程中的忙循环是什么?
忙循环就是程序员用循环让一个线程等待,不像传统方法wait()、 sleep() 或 yield(),它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。
这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
什么是自旋锁?没有获得锁的线程一直循环在那里看是否该锁的保持者已经释放了锁,这就是自旋锁。
什么是互斥锁?互斥锁:从等待到解锁过程,线程会从sleep状态变为running状态,过程中有线程上下文的切换,抢占CPU等开销。
自旋锁的优缺点?自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁。
虽然自旋锁效率比互斥锁高,但它会存在下面两个问题: 1、自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。 2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
如何在两个线程间共享数据?同一个Runnable,使用全局变量。
第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的Runnable
第二种:将这些Runnable对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作
class ShareData {
private int x = 0;
public synchronized void addx(){
x++;
System.out.println("x++ : "+x);
}
public synchronized void subx(){
x--;
System.out.println("x-- : "+x);
}
}
public class ThreadsVisitData {
public static ShareData share = new ShareData();
public static void main(String[] args) {
//final ShareData share = new ShareData();
new Thread(new Runnable() {
public void run() {
for(int i = 0;i<100;i++){
share.addx();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
for(int i = 0;i<100;i++){
share.subx();
}
}
}).start();
}
}
Java中Runnable和Callable有什么不同?
Runnable和Callable都是接口, 不同之处: 1.Callable可以返回一个类型V,而Runnable不可以 2.Callable能够抛出checked exception,而Runnable不可以。 3.Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的 4.Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadTestB {
public static void main(String[] args) {
ExecutorService e=Executors.newFixedThreadPool(10);
Future f1=e.submit(new MyCallableA());
Future f2=e.submit(new MyCallableA());
Future f3=e.submit(new MyCallableA());
System.out.println("--Future.get()....");
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e1) {
e1.printStackTrace();
}
e.shutdown();
}
}
class MyCallableA implements Callable{
public String call() throws Exception {
System.out.println("开始执行Callable");
String[] ss={"zhangsan","lisi"};
long[] num=new long[2];
for(int i=0;i<1000000;i++){
num[(int)(Math.random()*2)]++;
}
if(num[0]>num[1]){
return ss[0];
}else if(num[0]
Java中CyclicBarrier 和 CountDownLatch有什么不同?
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
- CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
CountDownLatch的用法:
public class Test {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CyclicBarrier用法:
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N,new Runnable() {
@Override
public void run() {
System.out.println("当前线程"+Thread.currentThread().getName());
}
});
for(int i=0;i
Java中interrupted和isInterrupted方法的区别?
interrupt方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
isInterrupted 只是简单的查询中断状态,不会对状态进行修改。
ConcurrentHashMap的源码理解以及内部实现原理,为什么他是同步的且效率高
ConcurrentHashMap 分析
ConcurrentHashMap的结构是比较复杂的,都深究去本质,其实也就是数组和链表而已。我们由浅入深慢慢的分析其结构。
先简单分析一下,ConcurrentHashMap 的成员变量中,包含了一个 Segment 的数组(final Segment[] segments;),而 Segment 是 ConcurrentHashMap 的内部类,然后在 Segment 这个类中,包含了一个 HashEntry 的数组(transient volatile HashEntry[] table;)。而 HashEntry 也是ConcurrentHashMap 的内部类。HashEntry 中,包含了 key 和 value 以及 next 指针(类似于 HashMap 中 Entry),所以 HashEntry 可以构成一个链表。
所以通俗的讲,ConcurrentHashMap 数据结构为一个 Segment 数组,Segment 的数据结构为 HashEntry 的数组,而 HashEntry 存的是我们的键值对,可以构成链表。
首先,我们看一下 HashEntry 类。
HashEntry
HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。其类的定义为:
static final class HashEntry {
final int hash;
final K key;
volatile V value;
volatile HashEntry next;
HashEntry(int hash, K key, V value, HashEntry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
...
}
HashEntry 的学习可以类比着 HashMap 中的 Entry。我们的存储键值对的过程中,散列的时候如果发生“碰撞”,将采用“分离链表法”来处理碰撞:把碰撞的 HashEntry 对象链接成一个链表。
如下图,我们在一个空桶中插入 A、B、C 两个 HashEntry 对象后的结构图(其实应该为键值对,在这进行了简化以方便更容易理解): [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1SWIHGsI-1633419634499)(java全占知识体系-JUC.assets/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f3662696e656d6f6e66666761676774306839647362657a6b2f696d6167655f31626b746a3372766a31306a6676613536766d716861713161392e706e67)] 图1
Segment
Segment 的类定义为static final class Segment extends ReentrantLock implements Serializable。其继承于 ReentrantLock 类,从而使得 Segment 对象可以充当锁的角色。Segment 中包含HashEntry 的数组,其可以守护其包含的若干个桶(HashEntry的数组)。Segment 在某些意义上有点类似于 HashMap了,都是包含了一个数组,而数组中的元素可以是一个链表。
table:table 是由 HashEntry 对象组成的数组如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表table数组的数组成员代表散列映射表的一个桶每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16。
count 变量是计算器,表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 的链表)包含的HashEntry 对象的个数。之所以在每个Segment对象中包含一个 count 计数器,而不在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响并发性。
static final class Segment extends ReentrantLock implements Serializable {
transient volatile HashEntry[] table;
transient int count;
transient int modCount;
final float loadFactor;
}
我们通过下图来展示一下插入 ABC 三个节点后,Segment 的示意图: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lSg6PMKB-1633419634500)(java全占知识体系-JUC.assets/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f763069717961386778626b363676666239723938653530682f696d6167655f31626b746a3667346931336d726f746f6b32743462356263316d2e706e67)] 图2
其实从我个人角度来说,Segment结构是与HashMap很像的。
ConcurrentHashMap
ConcurrentHashMap 的结构中包含的 Segment 的数组,在默认的并发级别会创建包含 16 个 Segment 对象的数组。通过我们上面的知识,我们知道每个 Segment 又包含若干个散列表的桶,每个桶是由 HashEntry 链接起来的一个链表。如果 key 能够均匀散列,每个 Segment 大约守护整个散列表桶总数的 1/16。
下面我们还有通过一个图来演示一下 ConcurrentHashMap 的结构: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6iz9QxEQ-1633419634500)(java全占知识体系-JUC.assets/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f71796f687573767777327a32333135686b763775656a32792f696d6167655f31626b746a3732676665663734677376766c6835663130333531332e706e67)] 图3
并发写操作
在 ConcurrentHashMap 中,当执行 put 方法的时候,会需要加锁来完成。我们通过代码来解释一下具体过程: 当我们 new 一个 ConcurrentHashMap 对象,并且执行put操作的时候,首先会执行 ConcurrentHashMap 类中的 put 方法,该方法源码为:
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + Sbase)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
我们通过注释可以了解到,ConcurrentHashMap 不允许空值。该方法首先有一个 Segment 的引用 s,然后会通过 hash() 方法对 key 进行计算,得到哈希值;继而通过调用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法进行存储操作。该方法源码为:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//加锁,这里是锁定的Segment而不是整个ConcurrentHashMap
HashEntry node = tryLock() ? null :scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
//得到hash对应的table中的索引index
int index = (tab.length - 1) & hash;
//找到hash对应的是具体的哪个桶,也就是哪个HashEntry链表
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解锁
unlock();
}
return oldValue;
}
关于该方法的某些关键步骤,在源码上加上了注释。
需要注意的是:加锁操作是针对的 hash 值对应的某个 Segment,而不是整个 ConcurrentHashMap。因为 put 操作只是在这个 Segment 中完成,所以并不需要对整个 ConcurrentHashMap 加锁。所以,此时,其他的线程也可以对另外的 Segment 进行 put 操作,因为虽然该 Segment 被锁住了,但其他的 Segment 并没有加锁。同时,读线程并不会因为本线程的加锁而阻塞。
正是因为其内部的结构以及机制,所以 ConcurrentHashMap 在并发访问的性能上要比Hashtable和同步包装之后的HashMap的性能提高很多。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。
总结
在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。
ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
ConcurrentHashMap 的高并发性主要来自于三个方面:
- 用分离锁实现多个线程间的更深层次的共享访问。
- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
使用分离锁,减小了请求 同一个锁的频率。
通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。
BlockingQueue的使用?
BlockingQueue的原理
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue的核心方法:
1)add(E e): 添加元素,如果BlockingQueue可以容纳,则返回true,否则报异常
2)offer(E e): 添加元素,如果BlockingQueue可以容纳,则返回true,否则返回false.
3)put(E e): 添加元素,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(long timeout, TimeUnit timeUnit): 取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等timeout参数规定的时间,取不到时返回null
5)take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
BlockingQueue常用实现类
1)ArrayBlockingQueue: 有界的先入先出顺序队列,构造方法确定队列的大小.
2)linkedBlockingQueue: 无界的先入先出顺序队列,构造方法提供两种,一种初始化队列大小,队列即有界;第二种默认构造方法,队列无界(有界即Integer.MAX_VALUE)
4)SynchronousQueue: 特殊的BlockingQueue,没有空间的队列,即必须有取的方法阻塞在这里的时候才能放入元素。
3)PriorityBlockingQueue: 支持优先级的阻塞队列 ,存入对象必须实现Comparator接口 (需要注意的是 队列不是在加入元素的时候进行排序,而是取出的时候,根据Comparator来决定优先级最高的)。
BlockingQueue<> 队列的作用
BlockingQueue 实现主要用于生产者-使用者队列,BlockingQueue 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的
这是一个生产者-使用者场景的一个用例。注意,BlockingQueue 可以安全地与多个生产者和多个使用者一起使用 此用例来自jdk文档
//这是一个生产者类
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while(true) {
queue.put(produce());
}
} catch (InterruptedException ex) {
... handle ...
}
}
Object produce() {
...
}
}
//这是一个消费者类
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) {
consume(queue.take());
}
} catch (InterruptedException ex) {
... handle ...
}
}
void consume(Object x) {
...
}
}
//这是实现类
class Setup {
void main() {
//实例一个非阻塞队列
BlockingQueue q = new SomeQueueImplementation();
//将队列传入两个消费者和一个生产者中
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
ThreadPool的深入考察?
引言
合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。
线程池的使用
我们可以通过ThreadPoolExecutor来创建一个线程池。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
创建一个线程池需要输入几个参数:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- linkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于linkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
- keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
向线程池提交任务
我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
Future
线程池的关闭
我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
线程池的分析
流程分析:线程池的主要工作流程如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSTWzqqY-1633419634501)(java全占知识体系-JUC.assets/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f75716f6b6a7432306a6f6b6f7335646e3772336368666a642f696d6167655f31626b746c6464753631653830316f7667326a6a6268303135353032302e706e67)]
从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
- 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
- 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
- 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
源码分析
上面的流程分析让我们很直观的了解了线程池的工作原理,让我们再通过源代码来看看是如何实现的。线程池执行任务的方法如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数小于基本线程数,则创建线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,
则创建一个线程执行任务。
else if (!addIfUnderMaximumPoolSize(command))
//抛出RejectedExecutionException异常
reject(command); // is shutdown or saturated
}
}
工作线程。线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的run方法里看到这点:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
合理的配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。
线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }
Java中Semaphore是什么?
Java中的Semaphore是一种新的同步类,它是一个计数信号。
从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。
但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。
信号量常常用于多线程的代码中,比如数据库连接池。
同步方法和同步代码块的区别是什么?
同步方法默认用this或者当前类class对象作为锁; 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法; 同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){代码内容}进行修饰;
如何确保N个线程可以访问N个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
volatile 变量和 atomic 变量有什么不同?
首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。
Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
Java中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
Vector是一个线程安全类吗?
Vector 是用同步方法来实现线程安全的
ReadWriteLock是什么?
一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。
Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。
什么是FutureTask?
在Java并发程序中FutureTask表示一个可以取消的异步运算。
它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。
每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。
首先,通过复用减少了代价高昂的对象的创建个数。 其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。
什么是Java线程转储(Thread Dump),如何得到它?
线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。
有很多方法可以获取线程转储——使用Profiler,Kill-3命令,jstack工具等等。有的更喜欢jstack工具,因为它容易使用并且是JDK自带的。由于它是一个基于终端的工具,所以可以编写一些脚本去定时的产生线程转储以待分析。
如果你提交任务时,线程池队列已满。会时发会生什么?
如果你使用的linkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为linkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;
如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
线程之间是如何通信的?
当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。
Object类中wait()notify()notifyAll()方法可以用于线程间通信关于资源的锁的状态。
怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候,才会返回true.注意这是一个static方法,这意味着"某条线程"指的是当前线程。
什么是死锁(Deadlock)?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
锁的分类
1、自旋锁 2、自旋锁的其他种类 3、阻塞锁 4、可重入锁 5、读写锁 6、互斥锁 7、悲观锁 8、乐观锁 9、公平锁 10、非公平锁 11、偏向锁 12、对象锁 13、线程锁 14、锁粗化 15、轻量级锁 16、锁消除 17、锁膨胀 18、信号量
死锁发生的几个条件是什么
- 因为系统资源不足。
- 进程运行推进的顺序不合适。
- 资源分配不当。
实现一个死锁?
产生死锁的四个必要条件:
- 互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁?
打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
- 打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
- 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
- 打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。 四.打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。
Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。
一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。
简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
死锁与饥饿的区别?
饥饿是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死。
死锁是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源。
相同点:二者都是由于竞争资源而引起的。
不同点:
- 从进程状态考虑,死锁进程都处于等待状态,忙等待(处于运行或就绪状态)的进程并非处于等待状态,但却可能被饿死;
- 死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给自己的资源,表现为等待时限没有上界(排队等待或忙式等待);
- 死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死;
- 死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。
- 在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃。
什么是乐观锁和悲观锁
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。
什么是对象锁?
对象锁是指Java为临界区synchronized(Object)语句指定的对象进行加锁,对象锁是独占排他锁。
对于对象锁,是针对一个对象的,它只在该对象的某个内存位置声明一个标志位标识该对象是否拥有锁,所以它只会锁住当前的对象。一般一个对象锁是对一个非静态成员变量进行syncronized修饰,或者对一个非静态方法进行syncronized修饰。对于对象锁,不同对象访问同一个被syncronized修饰的方法的时候不会阻塞住。
怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
Java中synchronized 和 ReentrantLock 有什么不同?
Java在过去很长一段时间只能通过synchronized关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过Lock接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。
可重入锁的含义
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在Java环境下 ReentrantLock 和synchronized 都是可重入锁
什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功
有三个线程T1,T2,T3,怎么确保它们按顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
单例模式的双检锁是什么?
这个问题在Java面试中经常被问到,但是面试官对回答此问题的满意度仅为50%。一半的人写不出双检锁还有一半的人说不出它的隐患和Java1.5是如何对它修正的。它其实是一个用来创建线程安全的单例的老方法,当单例实例第一次被创建时它试图用单个锁进行性能优化,但是由于太过于复杂在JDK1.4中它是失败的,我个人也不喜欢它。无论如何,即便你也不喜欢它但是还是要了解一下,因为它经常被问到。你可以查看how double checked locking on Singleton works这篇文章获得更多信息
如何在Java中创建线程安全的Singleton?
这是上面那个问题的后续,如果你不喜欢双检锁而面试官问了创建Singleton类的替代方法,你可以利用JVM的类加载和静态变量初始化特征来创建Singleton实例,或者是利用枚举类型来创建Singleton,我很喜欢用这种方法。你可以查看这篇文章获得更多信息。
写出3条你遵循的多线程最佳实践
给你的线程起个有意义的名字。
这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
避免锁定和缩小同步的范围
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。 多用同步类少用wait 和 notify 首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
多用并发集合少用同步集合
这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。
面经
ThreadLocal的原理
什么是ThreadLocal变量
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
ThreadLocal实现原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:
内存泄漏问题
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
建议回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally块进行回收
为什么key使用弱引用可以解决一部分内存泄漏,但是不能完全解决
因为value是强引用,在gcroot 中一直存在,所以不会给gc掉
ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例(每个线程需要使用自己的变量)
- 实例需要在多个方法中共享,但不希望被多线程共享
多线程参数
public ThreadPoolExecutor(int corePoolSize, //核心线程的数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue workQueue, //保存待执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂
RejectedExecutionHandler handler // 当任务无法执行时的处理器
) {…}
(线程比作员工,线程池比作一个团队,核心池比作团队中核心团队员工数,核心池外的比作外包员工)
有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做
需要获取全局锁
核心员工已经最多了,HR 不给批 HC 了,那这个需求只好攒着,放到待完成任务列表吧
如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧
需要获取全局锁
如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了
线程池基本知识
线程池的基本知识点应该都了解了,不过这里还是列出几点作为阅读源码的基础,以下是创建线程池最关键的7个参数:
corePoolSize:线程池核心线程数量;
maximumPoolSize:线程池会创建最大线程的数量;
keepAliveTime:线程池中大于 corePoolSize 的那部分线程的最大空闲存活时间。
Unit:存活时间单位;
workQueue:保存等待执行的任务的一个阻塞队列,当线程池所以线程都在运行中时再次提交任务,任务会保存在阻塞队列中;
threadFactory:创建线程的一个工厂, 默认为DefaultThreadFactory类;
handler:线程饱和策略,如果线程池所有线程都在执行任务,并且等待队列也满了的情况下,指定的处理方法,默认为ThreadPoolExecutor.AbortPolicy。
execute源码步骤
梳理了execute方法源码执行步骤如下图:
从上图可以把execute方法主要分三个步骤:
首先如果当前工作线程数小于核心线程,则调用addWorker(command, true)方法创建核心线程执行任务。
其次如果当前线程大于核心线程数则判断等待队列是否已满,如果没有满则添加任务到等待队列中去,如果工作线程数量为0则调用addWorker(null, false)方法创建非核心线程,并从等待队列中拉取任务执行。
最后如果队列已满则会调用addWorker(command, false)方法创建一个非核心线程执行任务。如果创建失败则会拒绝任务。
简单来说就是优先核心线程,其次等待队列,最后非核心线程。
addWorker方法
可以看到execute中最关键的就是addWorker方法,它接受两个参数:
第一个参数是要执行的任务,如果为null那么会从等待队列中拉取任务;
第二个参数是表示是否核心线程,用来控制addWorker方法流程的;
addWorker方法实现主流程如下图:
流程中去除一些异常情况,只留了主要流程,流程中有一步验证线程数大于核心线程或者最大线程数,如果传递的参数core等于true那么运行线程数量不能大于核心线程数量,如果为false则当前线程数量不能大于最大。
addWorker只有两个作用:增加工作线程数量、创建一个Worker并加到工作线程集合中。
线程池执行过程
线程池任务提交与运行
直接看结果,主流程如下:
线程池调用execute提交任务—>创建Worker(设置属性thead、firstTask)—>worker.thread.start()—>实际上调用的是worker.run()—>线程池的runWorker(worker)—>worker.firstTask.run();
线程池的execute的作用是把任务放到等待队列中或者新建worker并把任务放到worker的firstTask,最后执行worker中的thread;
Worker中的thread的start方法会执行Worker的run方法;
Worker的run方法会调用线程池的runWorker方法;
runWorker方法则是调用worker的firstTask的run方法,达到目的;
好处就是可以重复利用Worker与Worker中的thread,这也是线程池的优势。
说说四个拒绝策略的英文,我不想听中文,中文你八股文肯定背过了。
l ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
l ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
l ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
l ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
如何检测死锁
1、Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
首先,我们通过jps确定当前执行任务的进程号:
jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
复制代码
可以确定任务进程号是1362,然后执行jstack命令查看当前进程堆栈信息:
jonny@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01
Deadlock Detection:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
which is held by "Thread-1"
Found a total of 1 deadlock.
复制代码
可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象
2、JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”
RunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
l ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
l ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
如何检测死锁
1、Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
首先,我们通过jps确定当前执行任务的进程号:
jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
复制代码
可以确定任务进程号是1362,然后执行jstack命令查看当前进程堆栈信息:
jonny@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01
Deadlock Detection:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
which is held by "Thread-1"
Found a total of 1 deadlock.
复制代码
可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象
2、JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”
[外链图片转存中…(img-FWW63biA-1633419634503)]
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”
[外链图片转存中…(img-6VVPYyng-1633419634504)]