当前位置: 首页 > news >正文

联合智慧旅游建设旅游门户网站建设工程施工合同和承揽合同区别

联合智慧旅游建设旅游门户网站,建设工程施工合同和承揽合同区别,昆明网站排名优化报价,网站收录说明文章目录4. 多线程4.1 创建线程有哪几种方式#xff1f;4.2 说说Thread类的常用方法4.3 run()和start()有什么区别#xff1f;4.4 线程是否可以重复启动#xff0c;会有什么后果#xff1f;4.5 介绍一下线程的生命周期4.6 如何实现线程同步#xff1f;4.7 说一说Java多线程… 文章目录4. 多线程4.1 创建线程有哪几种方式4.2 说说Thread类的常用方法4.3 run()和start()有什么区别4.4 线程是否可以重复启动会有什么后果4.5 介绍一下线程的生命周期4.6 如何实现线程同步4.7 说一说Java多线程之间的通信方式4.8 说一说Java同步机制中的wait和notify4.9 说一说sleep()和wait()的区别4.10 说一说notify()、notifyAll()的区别4.11 如何实现子线程先执行主线程再执行4.12 阻塞线程的方式有哪些4.13 说一说synchronized与Lock的区别4.14 说一说synchronized的底层实现原理4.15 synchronized可以修饰静态方法和静态代码块吗4.16 谈谈ReentrantLock的实现原理4.17 如果不使用synchronized和Lock如何保证线程安全4.18 说一说Java中乐观锁和悲观锁的区别4.19 公平锁与非公平锁是怎么实现的4.20 了解Java中的锁升级吗4.21 如何实现互斥锁mutex4.22 分段锁是怎么实现的4.23 说说你对读写锁的了解4.24 volatile关键字有什么用4.25 谈谈volatile的实现原理4.26 说说你对JUC的了解4.27 说说你对AQS的理解4.28 LongAdder解决了什么问题它是如何实现的4.29 介绍下ThreadLocal和它的应用场景4.30 请介绍ThreadLocal的实现原理它是怎么处理hash冲突的4.31 介绍一下线程池4.32 介绍一下线程池的工作流程4.33 线程池都有哪些状态4.34 谈谈线程池的拒绝策略4.35 线程池的队列大小你通常怎么设置4.36 线程池有哪些参数各个参数的作用是什么4.36 线程池有哪些参数各个参数的作用是什么4. 多线程 4.1 创建线程有哪几种方式 参考答案 创建线程有三种方式分别是继承Thread类、实现Runnable接口、实现Callable接口。 通过继承Thread类来创建并启动线程的步骤如下 定义Thread类的子类并重写该类的run()方法该run()方法将作为线程执行体。创建Thread子类的实例即创建了线程对象。调用线程对象的start()方法来启动该线程。 通过实现Runnable接口来创建并启动线程的步骤如下 定义Runnable接口的实现类并实现该接口的run()方法该run()方法将作为线程执行体。创建Runnable实现类的实例并将其作为Thread的target来创建Thread对象Thread对象为线程对象。调用线程对象的start()方法来启动该线程。 通过实现Callable接口来创建并启动线程的步骤如下 创建Callable接口的实现类并实现call()方法该call()方法将作为线程执行体且该call()方法有返回值。然后再创建Callable实现类的实例。使用FutureTask类来包装Callable对象该FutureTask对象封装了该Callable对象的call()方法的返回值。使用FutureTask对象作为Thread对象的target创建并启动新线程。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。 扩展阅读 通过继承Thread类、实现Runnable接口、实现Callable接口都可以实现多线程不过实现Runnable接口与实现Callable接口的方式基本相同只是Callable接口里定义的方法有返回值可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。 采用实现Runnable、Callable接口的方式创建多线程的优缺点 线程类只是实现了Runnable接口或Callable接口还可以继承其他类。在这种方式下多个线程可以共享同一个target对象所以非常适合多个相同线程来处理同一份资源的情况从而可以将CPU、代码和数据分开形成清晰的模型较好地体现了面向对象的思想。劣势是编程稍稍复杂如果需要访问当前线程则必须使用Thread.currentThread()方法。 采用继承Thread类的方式创建多线程的优缺点 劣势是因为线程类已经继承了Thread类所以不能再继承其他父类。优势是编写简单如果需要访问当前线程则无须使用Thread.currentThread()方法直接使用this即可获得当前线程。 鉴于上面分析因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。 4.2 说说Thread类的常用方法 参考答案 Thread类常用构造方法 Thread()Thread(String name)Thread(Runnable target)Thread(Runnable target, String name) 其中参数 name为线程名参数 target为包含线程体的目标对象。 Thread类常用静态方法 currentThread()返回当前正在执行的线程interrupted()返回当前执行的线程是否已经被中断sleep(long millis)使当前执行的线程睡眠多少毫秒数yield()使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行 Thread类常用实例方法 getId()返回该线程的idgetName()返回该线程的名字getPriority()返回该线程的优先级interrupt()使该线程中断isInterrupted()返回该线程是否被中断isAlive()返回该线程是否处于活动状态isDaemon()返回该线程是否是守护线程setDaemon(boolean on)将该线程标记为守护线程或用户线程如果不标记默认是非守护线程setName(String name)设置该线程的名字setPriority(int newPriority)改变该线程的优先级join()等待该线程终止join(long millis)等待该线程终止,至多等待多少毫秒数。 4.3 run()和start()有什么区别 参考答案 run()方法被称为线程执行体它的方法体代表了线程需要完成的任务而start()方法用来启动线程。 调用start()方法启动线程时系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法则run()方法立即就会被执行而且在run()方法返回之前其他线程无法并发执行。也就是说如果直接调用线程对象的run()方法系统把线程对象当成一个普通对象而run()方法也是一个普通方法而不是线程执行体。 4.4 线程是否可以重复启动会有什么后果 参考答案 只能对处于新建状态的线程调用start()方法否则将引发IllegalThreadStateException异常。 扩展阅读 当程序使用new关键字创建了一个线程之后该线程就处于新建状态此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征程序也不会执行线程的线程执行体。 当线程对象调用了start()方法之后该线程处于就绪状态Java虚拟机会为其创建方法调用栈和程序计数器处于这个状态中的线程并没有开始运行只是表示该线程可以运行了。至于该线程何时开始运行取决于JVM里线程调度器的调度。 4.5 介绍一下线程的生命周期 参考答案 在线程的生命周期中它要经过新建New、就绪Ready、运行Running、阻塞Blocked和死亡Dead5种状态。尤其是当线程启动以后它不可能一直“霸占”着CPU独自运行所以CPU需要在多条线程之间切换于是线程状态也会多次在运行、就绪之间切换。 当程序使用new关键字创建了一个线程之后该线程就处于新建状态此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征程序也不会执行线程的线程执行体。 当线程对象调用了start()方法之后该线程处于就绪状态Java虚拟机会为其创建方法调用栈和程序计数器处于这个状态中的线程并没有开始运行只是表示该线程可以运行了。至于该线程何时开始运行取决于JVM里线程调度器的调度。 如果处于就绪状态的线程获得了CPU开始执行run()方法的线程执行体则该线程处于运行状态如果计算机只有一个CPU那么在任何时刻只有一个线程处于运行状态。当然在一个多处理器的机器上将会有多个线程并行执行当线程数大于处理器数时依然会存在多个线程在同一个CPU上轮换的现象。 当一个线程开始运行后它不可能一直处于运行状态线程在运行过程中需要被中断目的是使其他线程获得执行的机会线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后系统就会剥夺该线程所占用的资源让其他线程获得执行的机会。当发生如下情况时线程将会进入阻塞状态 线程调用sleep()方法主动放弃所占用的处理器资源。线程调用了一个阻塞式IO方法在该方法返回之前该线程被阻塞。线程试图获得一个同步监视器但该同步监视器正被其他线程所持有。线程在等待某个通知notify。程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁所以应该尽量避免使用该方法。 针对上面几种情况当发生如下特定的情况时可以解除上面的阻塞让该线程重新进入就绪状态 调用sleep()方法的线程经过了指定时间。线程调用的阻塞式IO方法已经返回。线程成功地获得了试图取得的同步监视器。线程正在等待某个通知时其他线程发出了一个通知。处于挂起状态的线程被调用了resume()恢复方法。 线程会以如下三种方式结束结束后就处于死亡状态 run()或call()方法执行完成线程正常结束。线程抛出一个未捕获的Exception或Error。直接调用该线程的stop()方法来结束该线程该方法容易导致死锁通常不推荐使用。 扩展阅读 线程5种状态的转换关系如下图所示 4.6 如何实现线程同步 参考答案 同步方法 即有synchronized关键字修饰的方法由于java的每个对象都有一个内置锁当用此关键字修饰方法时 内置锁会保护整个方法。在调用该方法前需要获得内置锁否则就处于阻塞状态。需要注意 synchronized关键字也可以修饰静态方法此时如果调用该静态方法将会锁住整个类。 同步代码块 即有synchronized关键字修饰的语句块被该关键字修饰的语句块会自动被加上内置锁从而实现同步。需值得注意的是同步是一种高开销的操作因此应该尽量减少同步的内容。通常没有必要同步整个方法使用synchronized代码块同步关键代码即可。 ReentrantLock Java 5新增了一个java.util.concurrent包来支持同步其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁它与使用synchronized方法和快具有相同的基本行为和语义并且扩展了其能力。需要注意的是ReentrantLock还有一个可以创建公平锁的构造方法但由于能大幅度降低程序运行效率因此不推荐使用。 volatile volatile关键字为域变量的访问提供了一种免锁机制使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新因此每次使用该域就要重新计算而不是使用寄存器中的值。需要注意的是volatile不会提供任何原子操作它也不能用来修饰final类型的变量。 原子变量 在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值可用在应用程序中如以原子方式增加的计数器但不能用于替换Integer。可扩展Number允许那些处理机遇数字类的工具和实用工具进行统一访问。 4.7 说一说Java多线程之间的通信方式 参考答案 在Java中线程通信主要有以下三种方式 wait()、notify()、notifyAll() 如果线程之间采用synchronized来保证线程安全则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法而是Object类中声明的方法。原因是每个对象都拥有锁所以让当前线程等待某个对象的锁当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁如果通过线程来操作就非常复杂了。另外这三个方法都是本地方法并且被final修饰无法被重写。 wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程使其进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程使它们进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。 每个锁对象都有两个队列一个是就绪队列一个是阻塞队列。就绪队列存储了已就绪将要竞争锁的线程阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后才会进入就绪队列进而等待CPU的调度。反之当一个线程被wait后就会进入阻塞队列等待被唤醒。 await()、signal()、signalAll() 如果线程之间采用Lock来保证线程安全则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法该接口是在Java 1.5中出现的它用来替代传统的waitnotify实现线程间的协作它的使用依赖于 Lock。相比使用waitnotify使用Condition的awaitsignal这种方式能够更加安全和高效地实现线程间协作。 Condition依赖于Lock接口生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内也就是说必须在lock.lock()和lock.unlock之间才可以使用。事实上await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即Conditon中的await()对应Object的wait()Condition中的signal()对应Object的notify()Condition中的signalAll()对应Object的notifyAll()。 BlockingQueue Java 5提供了一个BlockingQueue接口虽然BlockingQueue也是Queue的子接口但它的主要用途并不是作为容器而是作为线程通信的工具。BlockingQueue具有一个特征当生产者线程试图向BlockingQueue中放入元素时如果该队列已满则该线程被阻塞当消费者线程试图从BlockingQueue中取出元素时如果该队列已空则该线程被阻塞。 程序的两个线程通过交替向BlockingQueue中放入元素、取出元素即可很好地控制线程的通信。线程之间需要通信最经典的场景就是生产者与消费者模型而BlockingQueue就是针对该模型提供的解决方案。 4.8 说一说Java同步机制中的wait和notify 参考答案 wait()、notify()、notifyAll()用来实现线程之间的通信这三个方法都不是Thread类中所声明的方法而是Object类中声明的方法。原因是每个对象都拥有锁所以让当前线程等待某个对象的锁当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁如果通过线程来操作就非常复杂了。另外这三个方法都是本地方法并且被final修饰无法被重写并且只有采用synchronized实现线程同步时才能使用这三个方法。 wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程使其进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。notifyAll()方法用于唤醒所有正在等待相应对象锁的线程使它们进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。 每个锁对象都有两个队列一个是就绪队列一个是阻塞队列。就绪队列存储了已就绪将要竞争锁的线程阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后才会进入就绪队列进而等待CPU的调度。反之当一个线程被wait后就会进入阻塞队列等待被唤醒。 4.9 说一说sleep()和wait()的区别 参考答案 sleep()是Thread类中的静态方法而wait()是Object类中的成员方法sleep()可以在任何地方使用而wait()只能在同步方法或同步代码块中使用sleep()不会释放锁而wait()会释放锁并需要通过notify()/notifyAll()重新获取锁。 4.10 说一说notify()、notifyAll()的区别 参考答案 notify() 用于唤醒一个正在等待相应对象锁的线程使其进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。 notifyAll() 用于唤醒所有正在等待相应对象锁的线程使它们进入就绪队列以便在当前线程释放锁后竞争锁进而得到CPU的执行。 4.11 如何实现子线程先执行主线程再执行 参考答案 启动子线程后立即调用该线程的join()方法则主线程必须等待子线程执行完成后再执行。 扩展阅读 Thread类提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时调用线程将被阻塞直到被join()方法加入的join线程执行完为止。 join()方法通常由使用线程的程序调用以将大问题划分成许多小问题每个小问题分配一个线程。当所有的小问题都得到处理后再调用主线程来进一步操作。 4.12 阻塞线程的方式有哪些 参考答案 当发生如下情况时线程将会进入阻塞状态 线程调用sleep()方法主动放弃所占用的处理器资源线程调用了一个阻塞式IO方法在该方法返回之前该线程被阻塞线程试图获得一个同步监视器但该同步监视器正被其他线程所持有线程在等待某个通知notify程序调用了线程的suspend()方法将该线程挂起但这个方法容易导致死锁所以应该尽量避免使用该方法。 4.13 说一说synchronized与Lock的区别 参考答案 synchronized是Java关键字在JVM层面实现加锁和解锁Lock是一个接口在代码层面实现加锁和解锁。synchronized可以用在代码块上、方法上Lock只能写在代码里。synchronized在代码执行完或出现异常时自动释放锁Lock不会自动释放锁需要在finally中显示释放锁。synchronized会导致线程拿不到锁一直等待Lock可以设置获取锁失败的超时时间。synchronized无法得知是否获取锁成功Lock则可以通过tryLock得知加锁是否成功。synchronized锁可重入、不可中断、非公平Lock锁可重入、可中断、可公平/不公平并可以细分读写锁以提高效率。 4.14 说一说synchronized的底层实现原理 参考答案 一、以下列代码为例说明同步代码块的底层实现原理 public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println(Method 1 start);}} }查看反编译后结果如下图 可见synchronized作用在代码块时它的底层是通过monitorenter、monitorexit指令来实现的。 monitorenter 每个对象都是一个监视器锁monitor当monitor被占用时就会处于锁定状态线程执行monitorenter指令时尝试获取monitor的所有权过程如下 如果monitor的进入数为0则该线程进入monitor然后将进入数设置为1该线程即为monitor的所有者。如果线程已经占有该monitor只是重新进入则进入monitor的进入数加1。如果其他线程已经占用了monitor则该线程进入阻塞状态直到monitor的进入数为0再重新尝试获取monitor的所有权。 monitorexit 执行monitorexit的线程必须是objectref所对应的monitor持有者。指令执行时monitor的进入数减1如果减1后进入数为0那线程退出monitor不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。 monitorexit指令出现了两次第1次为同步正常退出释放锁第2次为发生异步退出释放锁。 二、以下列代码为例说明同步方法的底层实现原理 public class SynchronizedMethod {public synchronized void method() {System.out.println(Hello World!);} }查看反编译后结果如下图 从反编译的结果来看方法的同步并没有通过 monitorenter 和 monitorexit 指令来完成不过相对于普通方法其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的 当方法调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先获取monitor获取成功之后才能执行方法体方法执行完后再释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象。 三、总结 两种同步方式本质上没有区别只是方法的同步是一种隐式的方式来实现无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现被阻塞的线程会被挂起、等待重新调度会导致“用户态和内核态”两个态之间来回切换对性能有较大影响。 4.15 synchronized可以修饰静态方法和静态代码块吗 参考答案 synchronized可以修饰静态方法但不能修饰静态代码块。 当修饰静态方法时监视器锁monitor便是对象的Class实例因为Class数据存在于永久代因此静态方法锁相当于该类的一个全局锁。 4.16 谈谈ReentrantLock的实现原理 参考答案 ReentrantLock是基于AQS实现的AQS即AbstractQueuedSynchronizer的缩写这个是个内部实现了两个队列的抽象类分别是同步队列和条件队列。其中同步队列是一个双向链表里面储存的是处于等待状态的线程正在排队等待唤醒去获取锁而条件队列是一个单向链表里面储存的也是处于等待状态的线程只不过这些线程唤醒的结果是加入到了同步队列的队尾AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。 在同步队列中还存在2中模式分别是独占模式和共享模式这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒这两种模式分别对应独占锁和共享锁。 AQS是一个抽象类所以不能直接实例化当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state而ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock。 ReentrantLock 结构如下图所示 首先ReentrantLock 实现了 Lock 接口然后有3个内部类其中Sync内部类继承自AQS另外的两个内部类继承自Sync这两个类分别是用来公平锁和非公平锁的。通过Sync重写的方法tryAcquire、tryRelease可以知道ReentrantLock实现的是AQS的独占模式也就是独占锁这个锁是悲观锁。 4.17 如果不使用synchronized和Lock如何保证线程安全 参考答案 volatile volatile关键字为域变量的访问提供了一种免锁机制使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新因此每次使用该域就要重新计算而不是使用寄存器中的值。需要注意的是volatile不会提供任何原子操作它也不能用来修饰final类型的变量。 原子变量 在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值可用在应用程序中如以原子方式增加的计数器但不能用于替换Integer。可扩展Number允许那些处理机遇数字类的工具和实用工具进行统一访问。 本地存储 可以通过ThreadLocal类来实现线程本地存储的功能。每一个线程的Thread对象中都有一个ThreadLocalMap对象这个对象存储了一组以ThreadLocal.threadLocalHashCode为键以本地线程变量为值的K-V值对ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值使用这个值就可以在线程K-V值对中找回对应的本地线程变量。 不可变的 只要一个不可变的对象被正确地构建出来那其外部的可见状态永远都不会改变永远都不会看到它在多个线程之中处于不一致的状态“不可变”带来的安全性是最直接、最纯粹的。Java语言中如果多线程共享的数据是一个基本数据类型那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象由于Java语言目前暂时还没有提供值类型的支持那就需要对象自行保证其行为不会对其状态产生任何影响才行。String类是一个典型的不可变类可以参考它设计一个不可变类。 4.18 说一说Java中乐观锁和悲观锁的区别 参考答案 悲观锁总是假设最坏的情况每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上锁这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。 乐观锁顾名思义就是很乐观每次去拿数据的时候都认为别人不会修改所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型这样可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。 4.19 公平锁与非公平锁是怎么实现的 参考答案 在Java中实现锁的方式有两种一种是使用Java自带的关键字synchronized对相应的类或者方法以及代码块进行加锁另一种是ReentrantLock前者只能是非公平锁而后者是默认非公平但可实现公平的一把锁。 ReentrantLock是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的并且它的实现依赖于Java同步器框架AbstractQueuedSynchronizerAQSAQS使用一个整形的volatile变量state来维护同步状态这个volatile变量是实现ReentrantLock的关键。我们来看一下ReentrantLock的类图 ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。 public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}tryAcquire 是一个抽象方法是公平与非公平的实现原理所在。addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值而非公平锁在对于新晋线程有很大优势。acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。selfInterrupt 如果线程在阻塞期间发生了中断调用 Thread.currentThread().interrupt() 中断当前线程。 公平锁和非公平锁在说的获取上都使用到了 volatile 关键字修饰的state字段 这是保证多线程环境下锁的获取与否的核心。但是当并发情况下多个线程都读取到 state 0时则必须用到CAS技术一门CPU的原子锁技术可通过CPU对共享变量加锁的形式实现数据变更的原子操作。volatile 和 CAS的结合是并发抢占的关键。 公平锁FairSync 公平锁的实现机理在于每次有线程来抢占锁的时候都会检查一遍有没有等待队列如果有 当前线程会执行如下步骤 if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true;} 其中hasQueuedPredecessors是用于检查是否有等待队列的 public final boolean hasQueuedPredecessors() { Node t tail; // Read fields in reverse initialization order Node h head; Node s; return h ! t ((s h.next) null || s.thread ! Thread.currentThread());} 非公平锁NonfairSync 非公平锁在实现的时候多次强调随机抢占 if (c 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; }} 与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁被加入了等待队列后则跟公平锁没有区别。 4.20 了解Java中的锁升级吗 参考答案 JDK 1.6之前synchronized 还是一个重量级锁是一个效率比较低下的锁。但是在JDK 1.6后JVM为了提高锁的获取与释放效率对synchronized 进行了优化引入了偏向锁和轻量级锁 从此以后锁的状态就有了四种无锁、偏向锁、轻量级锁、重量级锁。并且四种状态会随着竞争的情况逐渐升级而且是不可逆的过程即不可降级这四种锁的级别由低到高依次是无锁、偏向锁轻量级锁重量级锁。如下图所示 无锁 无锁是指没有对资源进行锁定所有的线程都能访问并修改同一个资源但同时只有一个线程能修改成功。无锁的特点是修改操作会在循环内进行线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出否则就会继续循环尝试。如果有多个线程修改同一个值必定会有一个线程能修改成功而其他修改失败的线程会不断重试直到修改成功。 偏向锁 初次执行到synchronized代码块的时候锁对象变成偏向锁通过CAS修改对象头里的锁标志位字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后线程并不会主动释放偏向锁。当第二次到达同步代码块时线程会判断此时持有锁的线程是否就是自己持有锁的线程ID也在对象头里如果是则正常往下执行。由于之前没有释放锁这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个很明显偏向锁几乎没有额外开销性能极高。 偏向锁是指当一段同步代码一直被同一个线程所访问时即不存在多个线程的竞争时那么该线程在后续访问时便会自动获得锁从而降低获取锁带来的消耗即提高性能。 当一个线程访问同步代码块并获取锁时会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。 偏向锁只有遇到其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放锁线程是不会主动释放偏向锁的。关于偏向锁的撤销需要等待全局安全点即在某个时间点上没有字节码正在执行时它会先暂停拥有偏向锁的线程然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态则将对象头设置成无锁状态并撤销偏向锁恢复到无锁标志位为01或轻量级锁标志位为00的状态。 轻量级锁 轻量级锁是指当锁是偏向锁的时候却被另外的线程所访问此时偏向锁就会升级为轻量级锁其他线程会通过自旋的形式尝试获取锁线程不会阻塞从而提高性能。 轻量级锁的获取主要由两种情况 当关闭偏向锁功能时由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。 一旦有第二个线程加入锁竞争偏向锁就升级为轻量级锁自旋锁。这里要明确一下什么是锁竞争如果多个线程轮流获取一个锁但是每次获取锁的时候都很顺利没有发生阻塞那么就不存在锁竞争。只有当某线程尝试获取锁的时候发现该锁已经被占用只能等待其释放这才发生了锁竞争。 在轻量级锁状态下继续锁竞争没有抢到锁的线程将自旋即不停地循环判断锁是否能够被成功获取。获取锁的操作其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”如果是则将其设置为“锁定”比较并设置是原子性发生的。这就算抢到锁了然后线程将当前锁的持有者信息修改为自己。 长时间的自旋操作是非常消耗资源的一个线程持有锁其他线程就只能在原地空耗CPU执行不了任何有效的任务这种现象叫做忙等busy-waiting。如果多个线程用一个锁但是没有发生锁竞争或者发生了很轻微的锁竞争那么synchronized就用轻量级锁允许短时间的忙等现象。这是一种折衷的想法短时间的忙等换取线程在用户态和内核态之间切换的开销。 重量级锁 重量级锁显然此忙等是有限度的有个计数器记录自旋次数默认允许循环10次可以通过虚拟机参数更改。如果锁竞争情况严重某个达到最大自旋次数的线程会将轻量级锁升级为重量级锁依然是CAS修改锁标志位但不修改持有锁的线程ID。当后续线程尝试获取锁时发现被占用的锁是重量级锁则直接将自己挂起而不是忙等等待将来被唤醒。 重量级锁是指当有一个线程获取锁之后其余所有等待获取该锁的线程都会处于阻塞状态。简言之就是所有的控制权都交给了操作系统由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换线程的挂起和唤醒从而消耗大量的系统资。 扩展阅读 synchronized 用的锁是存在Java对象头里的那么什么是对象头呢我们以 Hotspot 虚拟机为例进行说明Hopspot 对象头主要包括两部分数据Mark Word标记字段 和 Klass Pointer类型指针。 Mark Word默认存储对象的HashCode分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间也就是在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Klass Point对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。 那么synchronized 具体是存在对象头哪里呢答案是存在锁对象的对象头的Mark Word中那么MarkWord在对象头中到底长什么样它到底存储了什么呢 在64位的虚拟机中 在32位的虚拟机中 下面我们以 32位虚拟机为例来看一下其 Mark Word 的字节具体是如何分配的 无锁 对象头开辟 25bit 的空间用来存储对象的 hashcode 4bit 用于存放对象分代年龄1bit 用来存放是否偏向锁的标识位2bit 用来存放锁标识位为01。偏向锁 在偏向锁中划分更细还是开辟 25bit 的空间其中23bit 用来存放线程ID2bit 用来存放 Epoch4bit 存放对象分代年龄1bit 存放是否偏向锁标识 0表示无锁1表示偏向锁锁的标识位还是01。轻量级锁在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针2bit 存放锁的标志位其标志位为00。重量级锁 在重量级锁中和轻量级锁一样30bit 的空间用来存放指向重量级锁的指针2bit 存放锁的标识位为11。GC标记 开辟30bit 的内存空间却没有占用2bit 空间存放锁标志位为11。 其中无锁和偏向锁的锁标志位都是01只是在前面的1bit区分了这是无锁状态还是偏向锁状态。关于内存的分配我们可以在git中openJDK中 markOop.hpp 可以看出 public:// Constantsenum { age_bits 4,lock_bits 2,biased_lock_bits 1,max_hash_bits BitsPerWord - age_bits - lock_bits - biased_lock_bits,hash_bits max_hash_bits 31 ? 31 : max_hash_bits,cms_bits LP64_ONLY(1) NOT_LP64(0),epoch_bits 2};age_bits 就是我们说的分代回收的标识占用4字节。lock_bits 是锁的标志位占用2个字节。biased_lock_bits 是是否偏向锁的标识占用1个字节。max_hash_bits 是针对无锁计算的hashcode 占用字节数量如果是32位虚拟机就是 32 - 4 - 2 -1 25 byte如果是64 位虚拟机64 - 4 - 2 - 1 57 byte但是会有 25 字节未使用所以64位的 hashcode 占用 31 byte。hash_bits 是针对 64 位虚拟机来说如果最大字节数大于 31则取31否则取真实的字节数。cms_bits 不是64位虚拟机就占用 0 byte是64位就占用 1byte。epoch_bits 就是 epoch 所占用的字节大小2字节。 4.21 如何实现互斥锁mutex 参考答案 在Java里面最基本的互斥同步手段就是synchronized关键字这是一种块结构Block Structured的同步语法。synchronized关键字经过Javac编译之后会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数那就以这个对象的引用作为reference。如果没有明确指定那将根据synchronized修饰的方法类型如实例方法或类方法来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。 自JDK 5起Java类库中新提供了java.util.concurrent包J.U.C包其中的java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。基于Lock接口用户能够以非块结构Non-Block Structured来实现互斥同步从而摆脱了语言特性的束缚改为在类库层面去实现同步这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。 4.22 分段锁是怎么实现的 参考答案 在并发程序中串行操作是会降低可伸缩性并且上下文切换也会减低性能。在锁上发生竞争时将通水导致这两种问题使用独占锁时保护受限资源的时候基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。 我们一般有三种方式降低锁的竞争程度 减少锁的持有时间降低锁的请求频率使用带有协调机制的独占锁这些机制允许更高的并发性。 在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解这称为分段锁。其实说的简单一点就是容器里有多把锁每一把锁用于锁容器其中一部分数据那么当多线程访问容器里不同数据段的数据时线程间就不会存在锁竞争从而可以有效的提高并发访问效率这就是ConcurrentHashMap所使用的锁分段技术首先将数据分成一段一段的存储然后给每一段数据配一把锁当一个线程占用锁访问其中一个段数据的时候其他段的数据也能被其他线程访问。 如下图ConcurrentHashMap使用Segment数据结构将数据分成一段一段的存储然后给每一段数据配一把锁当一个线程占用锁访问其中一个段数据的时候其他段的数据也能被其他线程访问能够实现真正的并发访问。所以说ConcurrentHashMap在并发情况下不仅保证了线程安全而且提高了性能。 4.23 说说你对读写锁的了解 参考答案 与传统锁不同的是读写锁的规则是可以共享读但只能一个写总结起来为读读不互斥、读写互斥、写写互斥而一般的独占锁是读读互斥、读写互斥、写写互斥而场景中往往读远远大于写读写锁就是为了这种优化而创建出来的一种机制。 注意是读远远大于写一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下读写锁由于需要额外维护读锁的状态可能还不如独占锁的效率高。因此需要根据实际情况选择使用。 在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock其提供了以下特性 公平性选择支持公平与非公平默认的锁获取方式吞吐量非公平优先于公平。可重入读线程获取读锁之后可以再次获取读锁写线程获取写锁之后可以再次获取写锁。可降级写线程获取写锁之后其还可以再次获取读锁然后释放掉写锁那么此时该线程是读锁状态也就是降级操作。 4.24 volatile关键字有什么用 参考答案 当一个变量被定义成volatile之后它将具备两项特性 保证可见性 当写一个volatile变量时JMM会把该线程本地内存中的变量强制刷新到主内存中去这个写会操作会导致其他线程中的volatile变量缓存无效。 禁止指令重排 使用volatile关键字修饰共享变量可以禁止指令重排序volatile禁止指令重排序有一些规则 当程序执行到volatile变量的读操作或者写操作时在其前面的操作的更改肯定全部已经进行且结果已经对后面的操作可见在其后面的操作肯定还没有进行在进行指令优化时不能将对volatile变量访问的语句放在其后面执行也不能把volatile变量后面的语句放到其前面执行。 即执行到volatile变量时其前面的所有语句都执行完后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。 注意虽然volatile能够保证可见性但它不能保证原子性。volatile变量在各个线程的工作内存中是不存在一致性问题的但是Java里面的运算操作符并非原子操作这导致volatile变量的运算在并发下一样是不安全的。 4.25 谈谈volatile的实现原理 参考答案 volatile可以保证线程可见性且提供了一定的有序性但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现加入volatile关键字时会多出一个lock前缀指令lock前缀指令实际上相当于一个内存屏障内存屏障会提供3个功能 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置也不会把前面的指令排到内存屏障的后面即在执行到内存屏障这句指令时在它前面的操作已经全部完成它会强制将对缓存的修改操作立即写入主存如果是写操作它会导致其他CPU中对应的缓存行无效。 4.26 说说你对JUC的了解 参考答案 JUC是java.util.concurrent的缩写该包参考自EDU.oswego.cs.dl.util.concurrent是JSR 166标准规范的一个实现。JSR 166是一个关于Java并发编程的规范提案在JDK中该规范由java.util.concurrent包实现。即JUC是Java提供的并发包其中包含了一些并发编程用到的基础组件。 JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具大致可以分为以下几类 原子更新 Java从JDK1.5开始提供了java.util.concurrent.atomic包方便程序员在多线程环 境下无锁的进行原子操作。在Atomic包里一共有12个类四种原子更新方式分别是原子更新基本类型原子更新 数组原子更新引用和原子更新字段。 锁和条件变量 java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。JUC 下的大多数工具类用到了Lock和Condition来实现并发。 线程池 涉及到的类比如Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。 阻塞队列 涉及到的类比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。 并发容器 涉及到的类比如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。 同步器 剩下的是一些在并发编程中时常会用到的工具类主要用来协助线程同步。比如CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。 4.27 说说你对AQS的理解 参考答案 抽象队列同步器AbstractQueuedSynchronizer 以下都简称AQS是用来构建锁或者其他同步组件的骨架类减少了各功能组件实现的代码量也解决了在实现同步器时涉及的大量细节问题例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。 AQS采用模板方法模式在内部维护了n多的模板的方法的基础上子类只需要实现特定的几个方法不是抽象方法不是抽象方法不是抽象方法就可以实现子类自己的需求。 基于AQS实现的组件诸如 ReentrantLock 可重入锁支持公平和非公平的方式获取锁Semaphore 计数信号量;ReentrantReadWriteLock 读写锁。 扩展阅读 AQS内部维护了一个int成员变量来表示同步状态通过内置的FIFO(first-in-first-out)同步队列来控制获取共享资源的线程。 我们可以猜测出AQS其实主要做了这么几件事情 同步状态state的维护管理等待队列的维护管理线程的阻塞与唤醒。 通过AQS内部维护的int型的state可以用于表示任意状态 ReentrantLock用它来表示锁的持有者线程已经重复获取该锁的次数而对于非锁的持有者线程来说如果state大于0意味着无法获取该锁将该线程包装为Node加入到同步等待队列里。Semaphore用它来表示剩余的许可数量当许可数量为0时对未获取到许可但正在努力尝试获取许可的线程来说会进入同步等待队列阻塞直到一些线程释放掉持有的许可state1然后争用释放掉的许可。FutureTask用它来表示任务的状态未开始、运行中、完成、取消。ReentrantReadWriteLock在使用时稍微有些不同int型state用二进制表示是32位前16位高位表示为读锁后面的16位低位表示为写锁。CountDownLatch使用state表示计数次数state大于0表示需要加入到同步等待队列并阻塞直到state等于0才会逐一唤醒等待队列里的线程。 AQS通过内置的FIFO(first-in-first-out)同步队列来控制获取共享资源的线程。CLH队列是FIFO的双端双向队列AQS的同步机制就是依靠这个CLH队列完成的。队列的每个节点都有前驱节点指针和后继节点指针。如下图 4.28 LongAdder解决了什么问题它是如何实现的 参考答案 高并发下计数一般最先想到的应该是AtomicLong/AtomicIntAtmoicXXX使用硬件级别的指令 CAS 来更新计数器的值这样可以避免加锁机器直接支持的指令效率也很高。但是AtomicXXX中的 CAS 操作在出现线程竞争时失败的线程会白白地循环一次在并发很大的情况下因为每次CAS都只有一个线程能成功竞争失败的线程会非常多。失败次数越多循环次数就越多很多线程的CAS操作越来越接近 自旋锁spin lock。计数操作本来是一个很简单的操作实际需要耗费的cpu时间应该是越少越好AtomicXXX在高并发计数时大量的cpu时间都浪费会在 自旋 上了这很浪费也降低了实际的计数效率。 LongAdder是jdk8新增的用于并发环境的计数器目的是为了在高并发情况下代替AtomicLong/AtomicInt成为一个用于高并发情况下的高效的通用计数器。说LongAdder比在高并发时比AtomicLong更高效这么说有什么依据呢LongAdder是根据锁分段来实现的它里面维护一组按需分配的计数单元并发计数时不同的线程可以在不同的计数单元上进行计数这样减少了线程竞争提高了并发效率。本质上是用空间换时间的思想不过在实际高并发情况中消耗的空间可以忽略不计。 现在在处理高并发计数时应该优先使用LongAdder而不是继续使用AtomicLong。当然线程竞争很低的情况下进行计数使用Atomic还是更简单更直接并且效率稍微高一些。其他情况比如序号生成这种情况下需要准确的数值全局唯一的AtomicLong才是正确的选择此时不应该使用LongAdder。 4.29 介绍下ThreadLocal和它的应用场景 参考答案 ThreadLocal顾名思义是线程私有的局部变量存储容器可以理解成每个线程都有自己专属的存储容器它用来存储线程私有变量其实它只是一个外壳内部真正存取是一个Map。每个线程可以通过set()和get()存取变量多线程间无法访问各自的局部变量相当于在每个线程间建立了一个隔板。只要线程处于活动状态它所对应的ThreadLocal实例就是可访问的线程被终止后它的所有实例将被垃圾收集。总之记住一句话ThreadLocal存储的变量属于当前线程。 ThreadLocal经典的使用场景是为每个线程分配一个 JDBC 连接 Connection这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作不会出现 A 线程关了 B线程正在使用的 Connection。 另外ThreadLocal还经常用于管理Session会话将Session保存在ThreadLocal中使线程处理多次处理会话时始终是同一个Session。 4.30 请介绍ThreadLocal的实现原理它是怎么处理hash冲突的 参考答案 Thread类中有个变量threadLocals它的类型为ThreadLocal中的一个内部类ThreadLocalMap这个类没有实现map接口就是一个普通的Java类但是实现的类似map的功能。每个线程都有自己的一个mapmap是一个数组的数据结构存储数据每个元素是一个Entryentry的key是ThreadLocal的引用也就是当前变量的副本value就是set的值。代码如下所示 public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals null; }ThreadLocalMap是ThreadLocal的内部类每个数据用Entry保存其中的Entry继承与WeakReference用一个键值对存储键为ThreadLocal的引用。为什么是WeakReference呢如果是强引用即使把ThreadLocal设置为nullGC也不会回收因为ThreadLocalMap对它有强引用。代码如下所示 static class Entry extends WeakReferenceThreadLocal? {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;} }ThreadLocal中的set方法的实现逻辑先获取当前线程取出当前线程的ThreadLocalMap如果不存在就会创建一个ThreadLocalMap如果存在就会把当前的threadlocal的引用作为键传入的参数作为值存入map中。代码如下所示 public void set(T value) {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null) {map.set(this, value);} else {createMap(t, value);} }ThreadLocal中get方法的实现逻辑获取当前线程取出当前线程的ThreadLocalMap用当前的threadlocak作为key在ThreadLocalMap查找如果存在不为空的Entry就返回Entry中的value否则就会执行初始化并返回默认的值。代码如下所示 public T get() {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null) {ThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)T result (T)e.value;return result;}}return setInitialValue(); }ThreadLocal中remove方法的实现逻辑还是先获取当前线程的ThreadLocalMap变量如果存在就调用ThreadLocalMap的remove方法。ThreadLocalMap的存储就是数组的实现因此需要确定元素的位置找到Entry把entry的键值对都设为null最后也Entry也设置为null。其实这其中会有哈希冲突具体见下文。代码如下所示 public void remove() {ThreadLocalMap m getMap(Thread.currentThread());if (m ! null) {m.remove(this);} }ThreadLocal中的hash code非常简单就是调用AtomicInteger的getAndAdd方法参数是个固定值0x61c88647。上面说过ThreadLocalMap的结构非常简单只用一个数组存储并没有链表结构当出现Hash冲突时采用线性查找的方式所谓线性查找就是根据初始key的hashcode值确定元素在table数组中的位置如果发现这个位置上已经有其他key值的元素被占用则利用固定的算法寻找一定步长的下个位置依次判断直至找到能够存放的位置。如果产生多次hash冲突处理起来就没有HashMap的效率高为了避免哈希冲突使用尽量少的threadlocal变量。 4.31 介绍一下线程池 参考答案 系统启动一个新线程的成本是比较高的因为它涉及与操作系统交互。在这种情形下使用线程池可以很好地提高性能尤其是当程序中需要创建大量生存期很短暂的线程时更应该考虑使用线程池。 与数据库连接池类似的是线程池在系统启动时即创建大量空闲的线程程序将一个Runnable对象或Callable对象传给线程池线程池就会启动一个空闲的线程来执行它们的run()或call()方法当run()或call()方法执行结束后该线程并不会死亡而是再次返回线程池中成为空闲状态等待执行下一个Runnable对象的run()或call()方法。 从Java 5开始Java内建支持线程池。Java 5新增了一个Executors工厂类来产生线程池该工厂类包含如下几个静态工厂方法来创建线程池。创建出来的线程池都是通过ThreadPoolExecutor类来实现的。 newCachedThreadPool()创建一个具有缓存功能的线程池系统根据需要创建线程这些线程将会被缓存在线程池中。newFixedThreadPool(int nThreads)创建一个可重用的、具有固定线程数的线程池。newSingleThreadExecutor()创建一个只有单线程的线程池它相当于调用newFixedThread Pool()方法时传入参数为1。newScheduledThreadPool(int corePoolSize)创建具有指定线程数的线程池它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数即使线程是空闲的也被保存在线程池内。newSingleThreadScheduledExecutor()创建只有一个线程的线程池它可以在指定延迟后执行线程任务。ExecutorService newWorkStealingPool(int parallelism)创建持有足够的线程的线程池来支持给定的并行级别该方法还会使用多个队列来减少竞争。ExecutorService newWorkStealingPool()该方法是前一个方法的简化版本。如果当前机器有4个CPU则目标并行级别被设置为4也就是相当于为前一个方法传入4作为参数。 4.32 介绍一下线程池的工作流程 参考答案 线程池的工作流程如下图所示 判断核心线程池是否已满没满则创建一个新的工作线程来执行任务。判断任务队列是否已满没满则将新提交的任务添加在工作队列。判断整个线程池是否已满没满则创建一个新的工作线程来执行任务已满则执行饱和拒绝策略。 4.33 线程池都有哪些状态 参考答案 线程池一共有五种状态, 分别是 RUNNING 能接受新提交的任务并且也能处理阻塞队列中的任务。SHUTDOWN关闭状态不再接受新提交的任务但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时调用 shutdown()方法会使线程池进入到该状态。STOP不能接受新任务也不处理队列中的任务会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时调用 shutdownNow() 方法会使线程池进入到该状态。TIDYING如果所有的任务都已终止了workerCount (有效线程数) 为0线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。TERMINATED在terminated() 方法执行完后进入该状态默认terminated()方法中什么也没有做。进入TERMINATED的条件如下 线程池不是RUNNING状态线程池状态不是TIDYING状态或TERMINATED状态如果线程池状态是SHUTDOWN并且workerQueue为空workerCount为0设置TIDYING状态成功。 下图为线程池的状态转换过程 4.34 谈谈线程池的拒绝策略 参考答案 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize如果还有任务到来就会采取任务拒绝策略通常有以下四种策略 AbortPolicy丢弃任务并抛出RejectedExecutionException异常。DiscardPolicy也是丢弃任务但是不抛出异常。DiscardOldestPolicy丢弃队列最前面的任务然后重新尝试执行任务重复该过程。CallerRunsPolicy由调用线程处理该任务。 4.35 线程池的队列大小你通常怎么设置 参考答案 CPU密集型任务 尽量使用较小的线程池一般为CPU核心数1。 因为CPU密集型任务使得CPU使用率很高若开过多的线程数会造成CPU过度切换。 IO密集型任务 可以使用稍大的线程池一般为2*CPU核心数。 IO密集型任务CPU使用率并不高因此可以让CPU在等待IO的时候有其他线程去处理别的任务充分利用CPU时间。 混合型任务 可以将任务分成IO密集型和CPU密集型任务然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距那么拆分没有意义。因为先执行完的任务就要等后执行完的任务最终的时间仍然取决于后执行完的任务而且还要加上任务拆分与合并的开销得不偿失。 4.36 线程池有哪些参数各个参数的作用是什么 参考答案 线程池主要有如下6个参数 corePoolSize核心工作线程数当向线程池提交一个任务时若线程池已创建的线程数小于corePoolSize即便此时存在空闲线程也会通过创建一个新线程来执行该任务直到已创建的线程数大于或等于corePoolSize时。 maximumPoolSize最大线程数线程池所允许的最大线程个数。当队列满了且已创建的线程数小于maximumPoolSize则线程池会创建新的线程来执行任务。另外对于无界队列可忽略该参数。 keepAliveTime多余线程存活时间当线程池中线程数大于核心线程数时线程的空闲时间如果超过线程存活时间那么这个线程就会被销毁直到线程池中的线程数小于等于核心线程数。 workQueue队列用于传输和保存等待执行任务的阻塞队列。 threadFactory线程创建工厂用于创建新线程。threadFactory创建的线程也是采用new Thread()方式threadFactory创建的线程名都具有统一的风格pool-m-thread-nm为线程池的编号n为线程池内的线程编号。 数。 IO密集型任务CPU使用率并不高因此可以让CPU在等待IO的时候有其他线程去处理别的任务充分利用CPU时间。 混合型任务 可以将任务分成IO密集型和CPU密集型任务然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距那么拆分没有意义。因为先执行完的任务就要等后执行完的任务最终的时间仍然取决于后执行完的任务而且还要加上任务拆分与合并的开销得不偿失。 4.36 线程池有哪些参数各个参数的作用是什么 参考答案 线程池主要有如下6个参数 corePoolSize核心工作线程数当向线程池提交一个任务时若线程池已创建的线程数小于corePoolSize即便此时存在空闲线程也会通过创建一个新线程来执行该任务直到已创建的线程数大于或等于corePoolSize时。maximumPoolSize最大线程数线程池所允许的最大线程个数。当队列满了且已创建的线程数小于maximumPoolSize则线程池会创建新的线程来执行任务。另外对于无界队列可忽略该参数。keepAliveTime多余线程存活时间当线程池中线程数大于核心线程数时线程的空闲时间如果超过线程存活时间那么这个线程就会被销毁直到线程池中的线程数小于等于核心线程数。workQueue队列用于传输和保存等待执行任务的阻塞队列。threadFactory线程创建工厂用于创建新线程。threadFactory创建的线程也是采用new Thread()方式threadFactory创建的线程名都具有统一的风格pool-m-thread-nm为线程池的编号n为线程池内的线程编号。handler拒绝策略当线程池和队列都满了再加入线程会执行此策略。
http://www.yutouwan.com/news/328260/

相关文章:

  • 儿童故事网站建设网站建设色系搭配
  • 网站制作代理平台小游戏网站建设
  • iis 网站打不开企业网站页面
  • 东莞市公司网站建设怎么样网页qq登陆保护
  • 计算机网站开发工作证学校校园网站
  • 转转怎么做钓鱼网站施工企业主要负责人包括
  • 简单网站开发实例网页版微信怎么加好友
  • 目前做那些网站致富apache 安装 wordpress
  • 福州企业网站制作wordpress添加表
  • 重庆企业模板建站信息wordpress插件放那个文件夹
  • 做网站优化两年遇到的SEO常态桔子建站官网
  • 网站首页打开速度洛阳做网站哪家专业
  • 做非法网站判刑多少年asp.net网站开发案例教程
  • 广州市企业网站制作个人备案做公司网站
  • 溧阳网站建设价格wordpress 调用文章简介
  • 免费自助建站软件网络空间搜索引擎
  • 网站搭建步骤原创文章代写平台
  • 岳阳公司网站开发服务器如何架设网站
  • 无站点推广就是不在网上推广专做美妆的视频网站
  • 技术专业网站建设wordpress屏蔽谷歌字体
  • 网站正在建设中备案燕郊 网站开发
  • html5移动端手机网站开发流程图卖水果做哪个网站好
  • 宜宾网站开发招聘有哪些做分析图用的网站
  • 阿里巴巴网站怎样做的漂亮wordpress加载latex慢
  • 网站建设优化去哪学网站右下角广告展示代码
  • 辽宁省建设工程招标投标协会网站wordpress用什么写的
  • 昆明云南微网站搭建网页微信登录不了
  • 泰安网站建设个人工作室校网站建设方案
  • 外贸建站新闻资讯7k7k电脑版网页游戏
  • 购物网站 缓存国内网站没备案