并发的三个重要特性:原子性,可见性,有序性
- 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
- **可见性 :**当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
- **有序性 :**代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
1.什么是线程和进程?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
**总结:**线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程
则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
2.线程的上下文切换
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,所以任务从保存到再加载的过程就是一次上下文可以再加载这 个任务的状态。切换。
- 主动让出 CPU,比如调用了
sleep()
,wait()
等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html
3.如何创建线程?
一般来说,创建线程有很多种方式,例如继承Thread
类、实现Runnable
接口、实现Callable
接口、使用线程池、使用CompletableFuture
类等等。
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()
创建。不管是哪种方式,最终还是依赖于new Thread().start()
。
4.说说线程的生命周期和状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
5.指令重排
为了保证处理器的运算单元被充分利用,处理器可能会对输入的代码进行乱序执行优化
6.程序计数器为什么是私有的?
如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
7.死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁必须具备以下四个条件:
-
互斥条件:该资源任意一个时刻只由一个线程占用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
导致死锁的条件有四个,也就是这四个条件同时满足就会产生死锁。
- 互斥条件,共享资源 X 和 Y 只能被一个线程占用;
- 请求和保持条件,线程 T1 已经取得共享资源 X,在等待共享资源Y 的时候,不释放共享资源 X;
- 不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待条件,线程 T1 等待线程 T2 占有的资源,线程T2 等待线程T1占有的资源,就是循环等待。
按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏的,因为这是互斥锁的基本约束,
其他三方条件都有办法来破坏:
- 对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
- 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
- 对于“循环等待”这个条件,可以靠按序申请资源来预防。
所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
8.Thread#sleep() 方法和 Object#wait() 方法对比
共同点:两者都可以暂停线程的执行。
区别:
sleep()
方法没有释放锁,而wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒。sleep()
是Thread
类的静态本地方法,wait()
则是Object
类的本地方法。为什么这样设计呢?下一个问题就会聊到。
9.为什么 wait() 方法不定义在 Thread 中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
类似的问题:**为什么 sleep()
方法定义在 Thread
中?**因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
10.可以直接调用 Thread 类的 run 方法吗?
new 一个 Thread
,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
11.mesi缓存一致性协议
解决多核cpu之间操作变量的一致性问题
m:修改
e:独占,互斥
s:共享
i:无效
通过总线嗅探机制,监听其他cpu是否再操作同一个变量
12.JMM模型
概念:Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步(解决硬件底层不同的抽象协议)
是围绕原子性,有序性,可见性展开 【https://blog.csdn.net/lxm55913153/article/details/79208126】
8种状态:
- lock :加锁 锁定操作变量
- unlock:解锁
- load:加载
- read:读取
- write:写入
- store:存储
- use:使用
- assign:赋值
13.volatile
- 保证可见性
- 防止指令重排
- 不能保证原子性
主要时使用了内存屏障(读屏障和写屏障),cpu的指令:
lock,unlock 【volatile的有序性】
lock,store
同样的,JVM在volatile变量写操作之后插入存储屏障,在读操作之前插入加载屏障,==保证volatile变量的可见性==
总线风暴
由于volatile的mesi缓存一致性协议需要不断的从主内存嗅探和cas不断循环无效交互导致总线带宽达到峰值
14.CAS
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
cas是高效的的原子操作
缺点:无法避免aba问题,自旋开销大,只能操作一个共享变量
是一种乐观锁思想,在无锁情况下保证线程操作共享数据原子性,底层使用Unsafe类的方法来实现,(拿预期值与现有值比较,如果相同就修改,无法避免aba问题,自旋)
JUC中用到了CAS的操作的类:
- AbstractQueueSynchronizer(AQS框架)
- **AtomicXXX类 原子类:**AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
15.synchronized
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。
1).synchronized 关键字最主要的三种使用方式:
-
修饰实例方法,锁的是实例对象。
-
修饰静态方法,锁的是类class。
-
修饰代码块,锁定的类class。
2).讲一下 synchronized 关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面。
3).说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
4).锁主要存在四种状态
依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
5).锁升级
synchronized再jdk1.6之后进行了大量的优化,增加了偏向锁,自旋锁,轻量级锁,锁的粗化,锁消除,重量级锁
1.偏向锁: 一段很长时间都只被一个线程使用的锁,可以使用偏向锁,第一次获取锁时,会有一次CAS操作,编程偏向锁结构,之后线程获取在获取锁,只要判断 mark word 中是否有自己的线程id即可,而不是开销相对较大的CAS命令。(只有一个线程使用)
2.轻量级锁(自旋锁):线程加锁时间是错开的(即无竞争),可以使用轻量级锁来优化。轻量级锁修改了锁对象头的锁标志,相较于重量级锁性能提升了很多,每次修改都是CAS操作,保证了原子性(不同线程交替使用)
**3.重量级锁(多线程争抢):**底层使用monitor实现,涉及到用户态和内核态的切换、线程上下文切换,成本较高,性能相对较低
偏向锁:的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模 式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需 再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从 而也就提供程序的性能。
所以,对于没有锁竞争的场合,偏向锁有很好的优化效 果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激 烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相 同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏 向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种 称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量 级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同 步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应 的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就 会导致轻量级锁膨胀为重量级锁。
自旋锁:
偏向锁:刚执行到Synchronized关键字时的锁对象为偏向锁(偏向第一个申请到它的线程)(通过CAS操作修改对象头里的锁标志位),当该线程执行完之后,锁不会被释放;当第二次执行到同步代码块时,线程会判断当前持有锁的线程是否就是自己(对象头里有持有锁的线程ID),若是则继续往下执行,不需要重新加锁;若不是,则会把偏向锁升级为轻量级锁。
轻量级锁: 当有第二个线程加入锁竞争时,偏向锁就会升级为轻量级锁。轻量级锁是自旋锁,即当一个线程申请锁而不得时,该线程就会进入自旋(为什么是自旋而不是挂起呢?因为挂起和恢复需要在用户态和内核态之间切换,会造成较大的开销,而短时间的自旋开销更小,不需要切换状态)。
重量级锁:若某线程忙等次数过多大于设置的阈值,说明锁竞争情况严重(长时间的自旋会造成CPU资源的浪费,开销变大),因此这个达到最大自旋次数的线程就会将轻量级锁升级为重量级锁(CAS操作修改锁标志位),将自己挂起,放弃CPU,等待未来被唤醒。
6).锁的粗化和锁消除
1.锁消除:是在程序编译阶段的优化手段(编译器和 JVM 会检测当前代码是否是多线程执行或是否有必要加锁。如果无必要,但又把锁给写了,那么在编译的过程中就会自动把锁去掉。)
2.锁粗化:锁的粒度指的是 synchronized 代码块中包含代码的多少。代码越多,粒度越大;代码越少,粒度越小。(锁的粒度小就意味着串行执行的代码更少,并发执行的代码更多)
如果某个场景需要频繁地加锁解锁,此时编译器就可能把这个操作优化成个粒度更粗的锁,即锁的粗化。
7).volatile和synchronized
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以**volatile 性能肯定比synchronized关键字要好。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块**。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
16.ReentrantLock
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类。
17.synchronized&ReentrantLock
1).两者都是可重入锁
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
2).synchronized是基于jvm实现的
3).reentrantLock是基于cpu的特殊指令实现的
4).相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
-
等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-
可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
-
可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
5).锁synchronized和ReentrantLock的区别
* 1.syncchronized是隐式锁
2.syncchronized只能修饰静态方法,实例方法,代码块。而ReentrantLock 只能用在代码块
3.syncchronized是jvm层面的锁,是java的关键字,通过monitor对象来完成,ReentrantLock是java的api层面实现的底层是基于cas+aqs多线程同步器实现
4.两者都是可重入锁
5.syncchronized只能是非公平锁,ReentrantLock是公平锁和非公平锁
6.synchronized 不需要用户去手动释放锁,ReentrantLock使用lock加锁,unlock释放锁,需要手动释放
7.synchronized是不可中断的锁,ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
8.synchronized不能绑定Condition; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
9.synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
18.AQS:多线程同步器
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
当线程申请共享资源时,共享资源空闲则将线程设置为有效工作线程,并将共享资源锁定,当共享资源被占用时,需要一个能处理线程的等待和唤醒时锁定分配机制。
内部是一个CLH虚拟队列。他是一个双向链表,遵循**fifo(先进先出)**原则,他的节点就是我们的等待的线程。
1.aqs为什么使用双向链表:
1)没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态。这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。
2)第二个方面,在 Lock 接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。(如图)被标记为 CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。
3)第三个方面,为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。
羊群效应,公平锁,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。所以,(如图)为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。
aqs的其他实现:
CountDownLatch(减法计数器):
只有数到0了才会运行 await 后面的代码(无参的时候)
CountDownLatch#getCount() 获取当前剩余数
CountDownLatch#countDown() 当前剩余数减一 - 原子操作
CountDownLatch#await() 等待计数器归0,再向下执行 每次有线程调用 countDown() 数量-1,当计数器变为0,countDownLatch.await()就会被唤醒,继续往下执行!
CountDownLatch#await(long, TimeUnit) 等待计数器归0,或者已过指定时间,再向下执行
使用场景:
将DB的1000W条数据导入到es中,使用 CountDownLatch 记录总执行次数,进行次数控制
CyclicBarrier(加法计数器):
到达指定等待数时,第一个线程 会运行构造后的方法体内容
CyclicBarrier#CyclicBarrier(int parties, Runnable barrierAction) CyclicBarrier构造函数
parties 指定等待数
barrierAction 当满足执行条件(到达等待时间或者达到指定等待数时)后待执行的逻辑
CyclicBarrier#await() 阻塞获取当前等待数 - 会导致被阻塞的线程一直阻塞
CyclicBarrier#await(long, TimeUnit) 超时阻塞获取当前等待数 - 推荐此方法,
Semaphore(信号量(默认为非公平的) 底层使用AQS):
可用于限流,多个共享资源互斥的使用!并发限流,控制最大的线程数!
Semaphore#acquire() 阻塞获取访问权限 - 会导致被阻塞的线程一致阻塞
Semaphore#tryAcquire(int) 阻塞获取指定数量的访问权限 - 会导致被阻塞的线程一致阻塞
Semaphore#tryAcquire(long, TimeUnit) - 超时返回false
Semaphore#tryAcquire(int, long, TimeUnit) - 超时返回false
Semaphore#tryAcquire() 阻塞获取当前等待数 - 会导致被阻塞的线程一致阻塞
Semaphore#release() 释放一个访问权限
Semaphore#release(int) 释放指定数量的访问权限
ForkJoin: 并行执行任务,【只能将任务1个切分为两个,不能切分为3个或其他数量】
实现
19.线程的6种状态
- new创建
- runnable 可运行状态
- blocked 状态,获取锁失败的状态
- waiting状态 调用sleep或wait的状态
- time waiting 设置超时时间的wait
- terminated 退出或结束
20.线程间通信的定义
线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
对象的wait()方法:当前线程就进入阻塞状态,并释放同步监视器
对象的notify()方法:一旦执行此方法,就会唤醒被阻塞的进程,如果有多个被wait(),就唤醒优先级最高的
==sleep()不会释放锁,wait()会释放锁==
21.线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
1).ThreadPoolExecutor
corePoolSize: 核心线程数,核心线程数目
CPU密集型(多于计算,减少线程上下文的切换): 线程数 = CPU核数 + 1
IO密集型: 线程数 = CPU核数 * 2 + 1
maximumPoolSize: 最大线程数(核心线程 + 救济线程的最大数目)
keepAliveTime: 生存时间,救急线程的生存时间,生存时间内没有新任务,此线程的资源会被释放
unit: 时间单位 救急线程的生存时间单位
workQueue: 当无空闲核心线程时,新来任务加入到此队列排队,队列满了就会创建救急线程执行任务
ThreadPoolExecutor线程池推荐了三种等待队列,它们是:SynchronousQueue 、LinkedBlockingQueue 和 ArrayBlockingQueue。
1)有界队列:
SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于 阻塞状态,
吞吐量通常要高于LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。
一旦创建了这样的缓存区,就不能再增加其容量。
试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
2)无界队列:
LinkedBlockingQueue:【指定容量的场景使用的多】,基于链表结构的无界阻塞队列,
它可以指定容量也可以不指定容量(实际上任何无限容量的队列/栈都是有容量的,这个容量就是Integer.MAX_VALUE)
PriorityBlockingQueue:是一个按照优先级进行内部元素排序的无界阻塞队列。队列中的元素必须实现 Comparable 接口,这样才能通过实现compareTo()方法进行排序。
优先级最高的元素将始终排在队列的头部;PriorityBlockingQueue 不会保证优先级一样的元素的排序。
注意:keepAliveTime和maximumPoolSize及BlockingQueue的类型均有关系。
如果BlockingQueue是无界的,那么永远不会触发maximumPoolSize,自然keepAliveTime也就没有了意义。
threadFactory: 线程工厂 定制线程实例的创建,如设置线程名、是否是守护线程等
handler: 拒绝策略,当所有线程都在繁忙,并且工作队列也放满了的时候,就会触发拒绝策略
拒绝策略
- AbortPolicy 直接抛异常阻止系统正常运行
- CallerRunsPolicy 由调用线程处理该任务 (采用)
- DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- DiscardPolicy 也是丢弃任务,但是不抛出异常。
当调用线程池 execute() 方法添加一个任务时,线程池会做如下判断:
corePoolSize -> 阻塞队列 -> maximumPoolSize -> 拒绝策略
如果有空闲线程,则直接执行该任务;
如果没有空闲线程,且当前运行的线程数少于 corePoolSize,则创建新的线程执行该任务(无视其他工作线程处于空闲状态);
如果没有空闲线程,且当前的线程数等于 corePoolSize,同时阻塞队列未满,则将任务入队列,而不添加新的线程;
如果没有空闲线程,且阻塞队列已满,同时池中的线程数小于 maximumPoolSize ,则创建新的线程执行任务;
如果没有空闲线程,且阻塞队列已满,同时池中的线程数大于等于 maximumPoolSize ,则根据构造函数中的 handler 指定的策略来拒绝新的任务。
线程池 五 个状态:
RUNNING:该状态下,线程池可以接受新任务,并能够处理阻塞队列中的任务
SHUTDOWN:该状态下,线程池不再可以接受新任务,但能够继续处理阻塞队列中的任务
STOP:该状态下,线程池不再可以接受新任务,也不会继续处理阻塞队列中的任务。同时会中断正在处理的任务
TIDYING:该状态下,线程池中的工作线程数量为0。并且会调用terminated()钩子方法(hook method)
TERMINATED:当terminated()钩子方法(hook method)执行完毕后,线程池进入该状态
线程池的shutdown() 方法,将线程池由 RUNNING(运行状态)转换为 SHUTDOWN状态
线程池的shutdownNow()方法,将线程池由RUNNING 或 SHUTDOWN 状态转换为 STOP 状态。
状态生命周期:RUNNING ---> SHUTDOWN/STOP ---> TIDYING ---> TERMINATED
不建议使用 Executors 创建的线程池:
1.0 newCachedThreadPool 可以创建一个最大线程数为 Integer.MAX_VALUE 的线程池,创建大量线程,可能会导致 OOM
2.0 newSingleThreadExecutor 和 newFixedThreadPool 可以创建一个阻塞队列为 Integer.MAX_VALUE 的线程池,堆积大量请求,可能会导致 OOM
2).Future 类有什么用?
Future
类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future
类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。
3).Callable 和 Future 有什么关系?
FutureTask
提供了 Future
接口的基本实现,常用来封装 Callable
和 Runnable
,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。ExecutorService.submit()
方法返回的其实就是 Future
的实现类 FutureTask
。
4).CompletableFuture 类有什么用?
Future
在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get()
方法为阻塞调用。
Java 8 才被引入CompletableFuture
类可以解决Future
的这些缺陷。CompletableFuture
除了提供了更为好用和强大的 Future
特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
22.java的四种引用
ThreadLocal 资源对象的线程隔离,各自线程的线程变量,存储在 Thread.threadLocals(ThreadLocal.ThreadLocalMap 类型但未实现Map接口)
set get remove操作
内存溢出原因:
ThreadLocalMap 中 Entry 的 key 是弱引用,所以遇到 GC 时会被回收,但是 Entry 存储的 value 是强引用,还会留在内存中,
所以积累起来会导致线程本地数据越存越多,从而导致OOM
建议 使用完之后 调用 remove 删除对应的value
引用:
强引用:
对象处于有用且必须的状态
只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
即便出现OOM,GC在对象的使用期间也不会回收的实例 最为普遍的方式 new 关键字创建的对象(非弱引用 软引用 虚引用类的实例)
弱引用:
对象处于可能有用但非必须的状态
无论内存是否足够,GC时,都会被回收
也可以使用引用队列
WeakReference reference = new WeakReference(obj);
软引用:
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
内存不够时,GC是会被回收,如果内存足够,则不会被回收
也可以使用引用队列
SoftReference reference = new SoftReference(obj);
虚引用:
必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler线程 调用与引用相关方法释放直接内存,释放外部资源
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference referenceQueue = new PhantomReference(obj, referenceQueue);
23.如何知道线程池中的任务是否已经执行完成?
1.通过submit提交的返回值future.get()方法,阻塞
2.通过线程池的isTemnated()方法来循环判断,但是需要调用shutdown()方法.(一般不会采用这种方法)
3.通过coundownLatch
24.Thread和Runnable区别是什么?
thread是一个类型,runable是一个接口。runable是一个任务。而thread是一个真正处理线程的实现。
25.ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
26.逃逸分析
就是即时编译时(jit),编译器分析当前代码不会被线程外应用的,则会自动进行逃逸分析
27.线程的生命周期状态?
new 初始
runable 运行
blocked 阻塞
wating 等待
time_wating 超时等待
terminated 终止
其他问题
1.如何知道线程池中的任务是否已经执行完成?
答:1.通过submit提交的返回值future.get()方法,阻塞
2.通过线程池的isTemnated()方法来循环判断,但是需要调用shutdown()方法.(一般不会采用这种方法)
3.通过coundownLatch
2.Thread和Runnable区别是什么?
答:thread是一个类型,runable是一个接口。runable是一个任务。而thread是一个真正处理线程的实现。
本文由 zzpp 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2024/09/12 09:06