文章目录

  • 一.内置锁
  • 二.线程状态
    • 线程的5种状态
    • 线程状态图
    • 线程释放锁的情况
    • 线程阻塞和线程等待的区别
    • sleep、join、yield、wait区别
      • yield不释放锁案例
      • sleep不释放锁案例
  • 三.监视器(monitor)以及锁池和等待池概念
    • 初识监视器(monitor)
    • 初识锁
    • 监视器(monitor)概念描述
    • 监视器(monitor)概念加强
      • 个人理解

一.内置锁

Java提供了一种内置的锁机制来支持原子性可见性同步代码块(Synchronized Block)

synchronized的原理有两个:

  • 内置锁
  • 互斥锁

Java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。线程进入同步代码块或方法的时候会自动获得该锁,并且在退出同步代码块时(正常返回,或者是异常退出)会自动释放锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

  • 而Java的内置锁又是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须 等待(WAITING)或者阻塞(BLOCKED),直到线程A释放这个锁,如果A线程不释放这个锁,那么B线程将永远等待下去。

同步代码块以关键字synchronized修饰,例如:

synchronized(锁对象引用){//锁保护的代码块
}

如果synchronized修饰的是对象的方法,被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的对象。

public class SyncTest {public synchronized void method() {//方法体就是同步代码块}
}

如果synchronized修饰的是静态方法,那被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的Class对象。

public class SyncTest {public static synchronized void method() {//方法体就是同步代码块}
}

如果synchronized修饰的是某一代码块,需要指定synchronized的锁对象引用

内置锁的特性

  • 互斥:同一时间最多只有一个线程能够持有这种锁。
    当一个线程尝试获取一个被其它线程占用的内置锁,其他线程必须等待(自旋)或者阻塞(自旋策略失效),并且 因为请求内置锁而被阻塞的线程不能被中断
  • 可重入:也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。作用是防止在同一线程中多次获取锁而导致死锁发生。

实现原理:为每个锁关联一个请求计数器和一个占有它的线程。当计数值为0,表示这个锁没有被任何线程持有。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数器将递增;

  • 每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

二.线程状态

线程的5种状态

线程有多种状态的切换,在早期的jdk版本中,线程之间的切换主要是通过join,sleep,wait,notify,notifyAll等方法来进行状态转换的。

线程共包括以下5种状态。
大致分为创建(New)、可运行(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)等五个状态。

  1. 可运行(Runnable)状态和运行(Running)状态可以相互转换阻塞状态(Blocked)和可运行(Runnable)状态可以相互转换。
  2. 线程只能从就绪状态进入到运行状态。
  1. 新建(NEW):新创建了一个线程对象。

  2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中获取cpu 的使用权

  3. 运行(RUNNING)可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu时间片暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片 转到运行(running)状态。阻塞的情况分三种:

    • .等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞:运行(running)的线程在获取"对象"的同步锁synchronized时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态
  5. 死亡(DEAD)线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程状态图

线程之间的切换状态如下图所示:

二.初始状态

  • 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态

三.可运行状态

  • 调用的start()方法,线程进入可运行状态。
  • 可运行状态只是说明有资格运行,线程调度程序没有挑选到你,你就永远是可运行状态
  • 当前线程sleep()结束,其他线程join()结束,阻塞式IO方法返回,某个线程拿到对象锁,这些线程也将进入可运行状态
  • 当前线程时间片用完了 或者 调用当前线程的yield(),当前线程进入可运行状态
  • 锁池里的线程拿到对象锁后,进入可运行状态。
  • 线程调用wait()进入WAITING状态,其他线程调用notify()/nofifyAll()唤醒相关线程
  • 处于挂起状态的线程调用了resume()恢复线程
  • 线程调用的阻塞IO已经返回 或者 阻塞方法执行完毕

四.运行状态

  • 线程调度程序从可运行池(锁池)中选择一个线程作为当前线程时所处的状态, 真正开始执行run()方法,这也是线程进入运行状态的唯一一种方式

五.死亡状态

  • 当线程的run()方法完成时,或者主线程的main()方法完成时, 异常退出run方法,我们就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。
  • 在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive()。如果是可运行或被阻塞,返回true; 如果线程仍旧是new状态不是可运行的, 或者线程死亡了,则返回false.

六.阻塞状态

  • 当前线程T调用Thread.sleep()方法,主动放弃占用的cpu资源,当前线程进入阻塞状态。
  • 运行在当前线程里的其它线程t2调用join()方法,当前线程进入阻塞状态。
  • 线程调用了阻塞式IO方法,在方法返回前,该线程被阻塞
  • 线程尝试获得一个锁,但是该锁正被其他线程所持有
  • 程序调用了suspend()方法,挂起该线程(此方法容易导致死锁,应该避免调用)
  • 线程等待某个通知

线程释放锁的情况

  1. 执行完同步代码块会释放对象锁;
  2. 执行同步代码块的过程中,如果遇到异常导致线程终止,锁也会被释放;
  3. 执行同步代码块的过程中,执行了锁所属对象的wait()方法,此线程会释放对象锁,进入等待队列中,等待被唤醒。

线程阻塞和线程等待的区别

  1. 线程阻塞(BLOCKED) : 一个处于就绪状态(Running)的线程尝试去获取锁,但锁已经被其他线程占用, 导致当前线程被阻塞的状态(synchronize 关键字产生的状态)

    进入BLOCKED状态的只有synchronize关键字,ReentrentLock.lock()底层调用的是LockSupport.park(),因此ReentrentLock.lock()进入的是WAITING状态

  2. 线程等待(WAITING) : 一个线程已经获取到了锁,但是需要等待其他线程执行某些操作。时间不确定
    当钱线程调用wait,join,park方法时,进入WAITING状态。(前提是这个线程已经拥有锁)

  3. 超时等待(TIMED_WAITING) : 一个线程已经获取到了锁,但是需要等待其他线程执行某些操作。时间确定
    通过sleep(int timeout)Wait(int timeout)方法进入的限时等待的状态)

实际上可以不用区分两者, 因为两者都会暂停线程的执行.
两者的区别是:

  1. 进入WAITING状态是线程主动的, 而进入BLOCKED状态是被动的.
  2. 更进一步的说, 进入BLOCKED状态是在同步代码块之外的, 而进入WAITING状态是在同步代码块之内.

sleep、join、yield、wait区别

  • sleep 不释放当前对象监视器的锁、释放cpu
  • join 释放对象监视器的锁、抢占cpu
  • yield 不释放对象监视器的锁、释放cpu
  • wait 释放对象监视器的锁、释放cpu

记住一句话:cpu是非常宝贵的,所以只有running的时候才会获取CPU时间片。

join底层还是wait()实现的,会释放锁,进入waiting状态(简单说,在哪个线程的线程体中调用join,哪个线程就会进入等待状态,并且释放锁)

yield不释放锁案例

线程让步 yield(): 不释放锁,释放CPU,让当前线程从“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;·也有可能是当前线程又进入到“运行状态”继续运行!·

public class YieldLockTest {private static Object obj = new Object();public static void main(String[] args) {ThreadA t1 = new ThreadA("t1");ThreadA t2 = new ThreadA("t2");ThreadA t3 = new ThreadA("t3");t1.start();t2.start();t3.start();}static class ThreadA extends Thread {public ThreadA(String name) {super(name);}@Overridepublic void run() {// 获取obj对象的同步锁synchronized (obj) {for (int i = 0; i < 10; i++) {System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);// i整除4时,调用yield,if (i % 4 == 0) {Thread.yield();}}}}}
}


主线程main中启动了两个线程t1和t2,t3。t1和t2,t3,在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.yield(),释放cpu执行权;但是,t2,t3是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!

sleep不释放锁案例

线程休眠sleep(): 不释放锁,释放CPU,让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。线程休眠时间结束时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

//还是上面的代码,i % 4 == 0 调用 Thread.sleep(100) 休眠 100毫秒// 获取obj对象的同步锁synchronized (obj) {try {for (int i = 0; i < 10; i++) {System.out.printf("%s: %d\n", this.getName(), i);// i能被4整除时,休眠100毫秒if (i % 4 == 0) {Thread.sleep(100);}}} catch (InterruptedException e) {e.printStackTrace();}}


结果说明:
主线程main中启动了两个线程t1和t2,t3。t1和t2,t3在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.sleep(100)释放cpu执行权;但是,t2,t3是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!

三.监视器(monitor)以及锁池和等待池概念

初识监视器(monitor)

可以将监视器比作一个建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据。 一个线程从进入这个房间到它离开前,它可以独占地访问房间中的全部数据。

如果用一些术语来定义这一系列动作:

  • 进入这个建筑叫做“进入监视器”
  • 进入建筑中的那个特别的房间叫作“获得监视器”
  • 占据房间叫做“持有监视器”
  • 离开房间叫做“释放监视器”
  • 离开建筑叫做“退出监视器”

初识锁

  1. 虽然叫做锁,但是其实相当于临界区大门的一个钥匙,那把钥匙就放到了临界区门口,有人进去了就把钥匙拿走揣在了身上,结束之后会把钥匙还回来只有拿到了指定临界区的锁,才能够进入临界区,访问临界区资源,当离开临界区时释放锁,其他线程才能够进入临界区

  2. 而对于锁本身,也是一种临界资源,是不允许多个线程共同持有的,同一时刻,只能够一个线程持有;

  1. Java中任何一个对象都可以被当做锁
  2. 在Java对象头中有一部分数据用于记录线程与对象的锁之间的关系,通过这个对象锁,进而可以控制线程对于对象的互斥访问,在JVM中每个对象中都拥有这样的数据
  3. 如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(也就是在指定的内存区域中进行一些数据的写入)

一个线程拥有了一个对象的锁之后,他就可以再次获取锁,也就是经常说的可重入

如下图所示,两个方法共用同一个锁, 在methodA中调用了methodB,如果不可重入的话: 一个线程获取了锁,进入methodA然后等待进入methodB的锁,但是他们是同一个锁,自己等待自己,岂不是死锁了所以锁具有可重入的特性

public class ReentrantTest {public static void main(String[] args) {ReentrantTest reentrantTest = new ReentrantTest();new Thread(()-> {reentrantTest.methodA();}).start();new Thread(()-> {reentrantTest.methodA();}).start();new Thread(()-> {reentrantTest.methodA();}).start();}private static final Object obj = new Object();public  void methodA() {synchronized (obj) {System.out.println(Thread.currentThread().getName()+"=>methodA start="+  System.currentTimeMillis());try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}methodB();//进入methodBSystem.out.println(Thread.currentThread().getName()+"=>methodA end="+  System.currentTimeMillis());}}public void methodB() {synchronized (obj) {System.out.println(Thread.currentThread().getName()+"=>methodB start="+  System.currentTimeMillis());try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"=>methodB end="+  System.currentTimeMillis());}}
}

  1. 对于锁的可重入性,JVM会维护一个计数器,记录对象被加锁了多少次没有被锁的对象是0,后续每重入一次计数器加1 只有自己可以重入,别人是不可以,是互斥的)只有计数器为0时,其他的线程才能够进入,所以,同一个线程加锁了多少次,也必然对应着释放多少次

  2. JVM会帮助我们解决计数器的维护,锁的获取与释放等,因此开发人员不需要直接接触锁

监视器(monitor)概念描述

java虚拟机给每个对象的class字节码都设置了一个监听器Monitor。
我们可以把Monitor想象成一个保险箱,里面专门存放一些需要被保护的数据。
Monitor每次只允许一个线程进入,当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列和wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。

再说一下wait-set队列。当一个线程拥有Monitor后,经过某些条件的判断(比如用户取钱发现账户没钱),这个时候需要调用Object的wait方法,线程就释放了Monitor,进入wait-set队列,等待Object的notify方法(比如用户向账户里面存钱)。当该对象调用了notify方法或者notifyAll方法后,wait-set中的线程就会被唤醒,然后在wait-set中被唤醒的线程和entry-set中的线程一起通过CPU调度来竞争对象的Monitor,最终只有一个线程能获取对象的Monitor


The Owner: 同步代码
Entry Set:(锁池): 保存等待获取对象锁的所有线程
Wait Set:(等待池): 保存执行了objectX.wait()/wait(long)的状态为WAITTING的所有线程
enter:进入锁池
acquire: 获取到锁
release: 释放锁进入Wait Set(等待池)
release and exit: 释放锁和退出同步代码块

监视器(monitor)概念加强

Java中每个对象都有一个唯一与之对应的内部锁(Monitor)JVM会为每个Monitor维护两个“队列-> Entry Set 和 Wait Set”(姑且称之为“队列”,尽管它不一定符合数据结构上队列的“先进先出”原则)

Entry Set和Wait Set,也有人翻译为锁池和等待池,意思基本一致。其实个人的理解可以认为是就绪队列和等待队列
锁池是在同步的环境下才有的概念,一个对象对应一个锁池。

  • 一个叫Entry Set(入口集),另外一个叫Wait Set(等待集)。对于任意的对象objectX, objectX的Entry Set用于保存等待获取objectX对应的内部锁的所有线程。objectX的Wait Set用于保存执行了objectX.wait()/wait(long)的线程。

    对于Entry Set(锁池): 如果线程A已经持有了对象锁(注意:不是类),此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。
    对于Wait Set(等待池): 如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING BLOCKED状态。

    线程B想要获得对象锁,一般情况下有两个先决条件,1是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),2是当前线程已处于RUNNABLE状态
    .
    对于Wait Set(等待池)中的线程,当对象的notify()/notifyAll()方法被调用时,JVM会唤醒处于Wait Set(等待池)中属于某一个或者全部线程,这些线程的状态就从WAITING转变为RUNNABLE,在等待池中被唤醒的线程和Entry-set(锁池)中的线程一起通过CPU调度来竞争对象的锁,最终只有一个线程能获取对象的锁。
    .
    每当对象的锁被释放后,所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set(锁池)中等待下一次机会。

假设objectX是任意一个对象,monitorX是这个对象对应的内部锁,现有线程A、B、C同时申请monitorX

  • 由于任意一个时刻只有一个线程能够获得 (占用/持有) 这个锁,因此除了胜出 (即获得了锁) 的线程 (这里假设是B) 外,其他线程 (这里就是A和C) 都会被暂停 (线程的生命周期状态会被调整为BLOCKED)

这些 因申请锁而落选的线程 就会被存入objectX对应的 Entry Set(以下记为entrySetX)之中。当monitorX被其持有线程 (这里就是B) 释放时,entrySetX中的一个任意 (注意是“任意”,而不一定是Entry Set中等待时间最长或者最短的) 线程会被唤醒 (即线程的生命周期状态变更为RUNNABLE) 。这个被唤醒的线程会与其他活跃线程 (即不处于Entry Set之中,且线程的生命周期状态为RUNNABLE的线程) 再次抢占monitorX。这时,被唤醒的线程如果成功申请到monitorX,那么该线程就从entrySetX中移除。否则,被唤醒的线程仍然会停留在entrySetX,并再次被暂停,以等待下次申请锁的机会。

如果有个线程执行了 objectX.wait(),那么该线程就会被暂停 (线程的生命周期状态会被调整为WAITTING) 并被存入objectX的 Wait Set (以下记为waitSetX)之中。此时,该线程就被称为 objectX的等待线程 。当其他线程执行了 objectX.notify()/notifyAll() 时 ,waitSetX 中的一个 (或者多个,取决于被调用的是notify还是notifyAll方法) 任意 (注意是“任意”,而不一定是Wait Set中等待时间最长或者最短的) 等待线程会被唤醒 (线程的生命周期状态变更为RUNNABLE)这些被唤醒的线程会与entrySetX中被唤醒的线程以及其他(可能的)活跃线程共同参与抢夺monitorX。如果其中一个被唤醒的等待线程成功申请到锁,那么该线程就会从waitSetX中移除。否则,这些被唤醒的线程仍然停留在waitSetX中,并再次被暂停,以等待下次申请锁的机会。

个人理解

调用对象的 notifyAll方法后,Wait Set 上的线程不会会加入到 EntrySet 中

从Java虚拟机性能的角度来说,Java虚拟机没有必要在notifyAll调用之后“将Wait Set中的线程移入Entry Set”

  • 首先,从一个“队列”移动到另外一个“队列”是有开销的,其次,虽然notifyAll调用后Wait Set中的多个线程会被唤醒,但是这些被唤醒的线程极端情况下可能没有任何一个能够获得锁(比如被其他活跃线程抢先下手了)或者即便可以获得锁也可能不能继续运行(比如这些等待线程所需的等待条件又再次不成立)。那么这个时候,这些等待线程仍然需要老老实实在wait set中待着。因此,如果notifyAll调用之后就将等待线程移出Wait set会导致浪费(白白地进出“队列”)。

这点可以参考显式锁的实现:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(Node, int)

 /*** Acquires in exclusive uninterruptible mode for thread already in* queue. Used by condition wait methods as well as acquire.** @param node the node* @param arg the acquire argument* @return {@code true} if interrupted while waiting*/final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

使用显式锁时,被唤醒的线程获取到锁tryAcquire调用返回true)之后才被从wait set中移出(setHead调用)。

Lock接口和synchronzied关键字对比主要有以下:

  • Lock需要显示地获取和释放锁,繁琐能让代码更灵活
  • Synchronized不需要显示地获取和释放锁,简单

Lock接口主要功能有以下:

  • 使用Lock可以方便的实现公平性
  • 非阻塞的获取锁
  • 能被中断的获取锁
  • 超时获取锁
  • java对象在内存中的结构(HotSpot虚拟机)
  • java锁与监视器概念
  • 多线程与Java-集合

【Java多线程】了解线程的锁池和等待池概念相关推荐

  1. Java多线程:线程8锁案例分析

    线程8锁案例分析 通过分析代码,推测打印结果,并运行代码进行验证 1.两个线程调用同一个对象的两个同步方法 被synchronized修饰的方法,锁的对象是方法的调用者.因为两个方法的调用者是同一个, ...

  2. Java多线程之线程同步机制(锁,线程池等等)

    Java多线程之线程同步机制 一.概念 1.并发 2.起因 3.缺点 二.三大不安全案例 1.样例一(模拟买票场景) 2.样例二(模拟取钱场景) 3.样例三(模拟集合) 三.同步方法及同步块 1.同步 ...

  3. java多线程:线程同步synchronized(不同步的问题、队列与锁),死锁的产生和解决

    0.不同步的问题 并发的线程不安全问题: 多个线程同时操作同一个对象,如果控制不好,就会产生问题,叫做线程不安全. 我们来看三个比较经典的案例来说明线程不安全的问题. 0.1 订票问题 例如前面说过的 ...

  4. java多线程及线程池使用

    Java多线程及线程池的使用 Java多线程 一.Java多线程涉及的包和类 二.Java创建多线程的方式 三.Java线程池 1. 创建线程池ThreadPoolExecutor的7个参数 2. 线 ...

  5. Java多线程之线程池配置合理线程数

    Java多线程之线程池配置合理线程数 目录 代码查看公司服务器或阿里云是几核的 合理线程数配置之CPU密集型 合理线程数配置之IO密集型 1. 代码查看公司服务器或阿里云是几核的 要合理配置线程数首先 ...

  6. Java多线程之线程池的手写改造和拒绝策略

    Java多线程之线程池的手写改造和拒绝策略 目录 自定义线程池的使用 四种拒绝策略代码体现 1. 自定义线程池的使用 自定义线程池(拒绝策略默认AbortPolicy) public class My ...

  7. Java多线程之线程池7大参数、底层工作原理、拒绝策略详解

    Java多线程之线程池7大参数详解 目录 企业面试题 线程池7大参数源码 线程池7大参数详解 底层工作原理详解 线程池的4种拒绝策略理论简介 面试的坑:线程池实际中使用哪一个? 1. 企业面试题 蚂蚁 ...

  8. Java多线程之线程池详解

    Java多线程之线程池详解 目录: 线程池使用及优势 线程池3个常用方式 线程池7大参数深入介绍 线程池底层工作原理 1. 线程池使用及优势 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务 ...

  9. java 多线程使用线程池_Java多线程:如何开始使用线程

    java 多线程使用线程池 什么是线程? (What is a Thread?) A thread is a lightweight process. Any process can have mul ...

最新文章

  1. 详解音视频直播中的低延时
  2. android SDK manager 无法获取更新版本列表【转载】
  3. 托管与非托管的混合编程问题
  4. yum更新php版本,yum php版本太低怎么办
  5. IE8浏览器跨域接口访问异常的解决办法
  6. Service Work生命周期
  7. java 集合转字符串工具类,浅谈常用字符串与集合类转换的工具类
  8. 配置本地yum源文件
  9. 计算机科学与技术专业认证研讨,CNCC丨一流本科专业建设暨工程认证研讨会
  10. tensorflow-serving源码阅读1
  11. 手机连不上wifi,一直显示正在获取ip地址
  12. Python的环境安装
  13. Navigating to current location (/login) is not allowed
  14. 物流服务--查询物流
  15. photoshop cs5 安装过程及序列号
  16. 第一课 以太坊开发从入门到精通学习导航
  17. matlab计算幂律分布,Matlab拟合曲线之幂律分布
  18. 一个食品专业本科生的自白:能不吃最好别吃
  19. cocos creator 如何制作九宫格抽奖
  20. GDKOI-PJ-2021 Day1总结

热门文章

  1. c语言语言写压缩软件,哈弗曼压缩软件C语言代码.pdf
  2. InfluxDB1.8
  3. 常考的Ajax面试题
  4. mbp touchbar设置_揭秘MBP的Touch Bar:原来它并非看上去那么简单
  5. 关于Android 11HDMI设置-显示-HDMI无法选择呈灰色的定位流程及方式
  6. 百度网盘 分享链接批量转存方法【2020-10】
  7. 电脑网速慢怎么解决?4个方法有效提升电脑网速!
  8. 基于WinRAR软件的文件自动打包与异地备份方案
  9. 我的世界服务器物品发送到,我的世界更好的传送弓箭 添加物品到服务器
  10. cn.bing.com