一、创建线程有哪几种方式?
创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。
Runnable接口与Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常。
二、run()和start()有什么区别?
run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。
调用star()方法启动线程时,系统会把该run()方法当成线程执行体来处理。如果直接调用线程对象的run()方法, 系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
三、介绍一下线程的生命周期?
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
四、如果不使用synchronized和Lock,如何保证线程安全?
1、 volatile
volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是, volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
2、原子变量
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如Atomiclnteger表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换lnteger。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
3、本地存储
可以通过ThreadLocal类来实现线程本地存储的功能。每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。
4、不可变的对象
只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态,"不可变"带来的安全性是最直接、最纯粹的。Java语言中,如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,由于Java语言目前暂时还没有提供值类型的支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行。String类是一个典型的不可变类,可以参考它设计一个不可变类。
五、说说你了解的线程同步方式
java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized和Lock。
synchronized采用“CAS+Mark Word”实现,为了性能的考虑,并通过锁升级机制降低锁的开销。在并发环境中,synchronized会随着多线程竞争的加剧,按照如下步骤逐步升级:无锁、偏向锁、轻量级锁、重量级锁。
Lock则采用“CAS+volatile”实现,其实现的核心是AQS。AQS是线程同步器,是一个线程同步的基础框架,它基于模板方法模式。在具体的Lock实例中,锁的实现是通过继承AQS来实现的,并且可以根据锁的使用场景,派生出公平锁、不公平锁、读锁、写锁等具体的实现
六、线程的通信方式
在Java中提供了两种多线程通信方式分别是利用monitor实现通信方式和使用condition实现线程通信方式。使用不同的线程同步方式也就相应的使用不同的线程通信方式。当我们使用synchronize同步时就会使用monitor来实现线程通信,这里的 monitor其实就是锁对象,其利用object的wait,notify, notifyAll等 方法来实现线程通信。而使用Lock进行同步时就是使用Condition来实现线程通信,Condition对 象通过L ock创建出来依赖于L ock对象,使用其await, sign或signAll方 法实现线程通信。
七、sleep()和wait()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只 能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁, 并需要通过notify()/notifyAl()重新获取锁。
八、如何实现子线程先执行,主线程再执行?
启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。
Thread son = new Thread(() -> {
System.out.println("子线程");
});
son.start();
son.join();
System.out.println("主线程");
九、说一说synchronized与Lock的区别
参考答案
1.synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
2.synchronized可以用在代码块上、方法上;Lock只能写在代码里。
3.synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
4.synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
5.synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
6.synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。
十、说一说synchronized的底层实现原理
一、synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。
●monitorenter:
每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为O,再重新尝试获取monitor的所有权。
● monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。
二、方法的同步并没有通过 monitorenter和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
十一、 synchronized可以修饰静态方法和静态代码块吗?
synchronized可以修饰静态方法,但不能修饰静态代码块。
当修饰静态方法时,监视器锁(monitor) 便是对象的Class实例,因为Class数 据存在于永久代,因此静态方法锁相当于该类的一个全局锁。
十二、说一说Java中乐观锁和悲观锁的区别
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在JDK1.5中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
十三、说说你对读写锁的了解
与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥、读写互斥、写写互斥,而一般的独占锁是:读读互斥、读写互斥、写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。
在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
- 1.公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
- 2.可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁。
- 3.可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
十四、了解Java中的锁升级吗?
JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁。但是在JDK 1.6后,JVM为了提高锁的获取与释放效率对synchronized进行了优化,引入了偏向锁和轻量级锁,从此以后锁的状态就有了四种:无锁、偏向锁、轻量级锁、重量级锁。并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,这四种锁的级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁。
1.无锁
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
2.偏向锁
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是”偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程D也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
当一个线程访问同步代码块并获取锁时,会在MarkWord里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测MarkWord里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00的状态。
3.轻量级锁
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:1.当关闭偏向锁功能时;
2.由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为"释放",如果是则将其设置为"锁定",比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
4.重量级锁
重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。
十五、volatile关键字有什么用?
当一个变量被定义成volatile之后, 它将具备两项特性:
1.保证可见性
当写一个volatile变 量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
使用volatil关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有些规则:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
十六、谈谈volatile的实现原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障"来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时, 会多出一个lock前缀指令,lock前缀指令实际上相当于一 个内存屏障,内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 也不会把前面的指令排到内存屏障的后面; 即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3.如果是写操作,它会导致其他CPU中对应的缓存行无效。
十七、什么是CAS
CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
十八、java哪些地方用到了CAS
Java提供的API中使用CAS的地方有很多,比较典型的使用场景有原子类、AQS、并发容器。
1、原子类,以AtomicInteger为例,它的内部提供了诸多原子操作的方法。如原子替换整数值、增加指定的值、加1,这些方法的底层便是采用操作系统提供的CAS原子指令来实现的。
2、AQS,在向同步队列的尾部追加节点时,它首先会以CAS的方式尝试一次,如果失败则进入自旋状态,并反复以CAS的方式进行尝试。此外,在以共享方式释放同步状态时,它也是以CAS方式对同步状态进行修改的。
3、 对于并发容器,以ConcurrentHashMap为例,它的内部多次使用了CAS操作。在初始化数组时,它会以CAS的方式修改初始化状态,避免多个线程同时进行初始化。在执行put方法初始化头节点时,它会以CAS的方式将初始化好的头节点设置到指定槽的首位,避免多个线程同时设置头节点。在数组扩容时,每个线程会以CAS方式修改任务序列号来争抢扩容任务,避免和其他线程产生冲突。在执行get方法时,它会以CAS的方式获取头指定槽的头节点,避免其他线程同时对头节点做出修改。
十九、谈谈对AQS的理解
AQS是一个多线程同步器,是一个用来构建锁的基础框架,JUC包中很多组件都依靠他作为底层实现,例CountDownLatch,Semaphora等组件。
AQS提供了两种锁机制分别是共享锁(读锁)和排他锁(写锁)。
共享锁指多个线程共同竞争同一个共享资源时多个线程同时都能够获得,而排他锁再同一时间只能有一个线程能够获得共享资源。
AQS通过设置一个由volatile修饰的Int类型的互斥变量state来实现互斥同步,state=0时表名该锁可以被获取,state>=1时时表明该锁已经被获取。当当前锁为空闲时,多个线程同时使用CAS方式去修改State的值(保证了原子性,可见性),最后只有一个线程修改成功并获得锁。其他线程失败后便会执行unsafe类的park方法进行阻塞。这里的阻塞方式创建一个双向队列顺序存储锁竞争的线程,并先进先出的去获取锁(公平锁方式)。非公平锁方式是不管双向链表队列中是否有阻塞的线程它都不会进入队列而是直接去尝试修改互斥变量以获得锁。
二十、谈谈JUC
JUC是java.util.concurrent的缩写,是JSR 166标准规范的一个实现。JSR 166是一个关于Java并发编程的规范提案,在JDK中该规范由java.util.concurrent 包实现。即JUC是Java提供的并发包,其中包含了一些并发编程用到的基础组件。
JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具,大致可以分为以下几类:
- 原子类
Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。 - 锁和条件变量
jiava.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等等。
二十一、介绍下ThreadLocal
ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之记住一句话:ThreadLocal存储的变量属于当前线程。
ThreadLocal经典的使用场景是为每个线程分配一个JDBC连接Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection。另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。
二十二、ThreadLocal怎么解决hash冲突
ThreadLocalMap是ThreadLocal的内部类,每个数据用Entry保存,ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置,上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMa的效率高,为了避免哈希冲突,使用尽量少的thredlocal变量。
二十三、介绍下线程池
从Java 5开始,Java内建支持线程池。Java 5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
- newCachedThreadPool():创建一个具有缓存功能的线程池
- newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
- newSingleThreadExecutor():创建一个只有单线程的线程池.
- newScheduledThreadPool(intcorePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。
- newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
- newWorkStealingPool(int parallelism):
- newWorkStealingPool():
创建出来的线程池,都是通过ThreadPoolExecutor类来实现的。其主要有如下6个参数
- corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
- maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
- keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
- workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
- threadFactory(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用newThread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号, n为线程池内的线程编号)。
- handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略
其执行流程如下:
- 判断核心线程池是否已满,没满则创建一 个新的工作线程来执行任务。如果满了执行2
- 判断任务队列是否已满,没满则将新提交的任务添加在工作队列。如果满了执行3
- 判断整个线程池是否已满,没满则创建一个 新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
通常有以下四种策略:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- DiscardPolicy:也是丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
- CallerRunsPolicy:由调用线程处理该任务。
二十四、线程池都有哪些状态
线程池一共有五种状态,分别是:
- 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状态成功。
二十五、LongAdder解决了什么问题,它是如何实现的?
高并发下计数,一般最先想到的应该是AtomicL ong/AtomicInt, AtmoicXXX使用硬件级别的指令CAS来更新计数器的值,这样可以避免加锁,机器直接支持的指令,效率也很高。但是AtomicXXX中的 CAS操作在出现线程竞争时,失败的线程会白白地循环一次,在并发很大的情况下,因为每次CAS都只有一个线程能成功, 竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接诉自旋锁(spinlock)。计数操作本来是-个很简单的操作,实际雲要耗费的cpu时间应该是越少越好,AtomicXXX在高并发计数时,大最的cou时间都浪费会在自旋卜了、这很浪费、H降低了实际的计数效率。
LongAdder是jdk8新增的用于并发环境的计数器,目的是为了在高并发情况下,代替AtomicLong/Atomiclnt,成为一个用于高并发情况下的高效的通用计数器。说LongAdder比在高并发时比AtomicLong更高效,这么说有什么依据呢?LongAdder是根据锁分段来实现的,它里面维护一组按需分配的计数单元,并发计数时,不同的线程可以在不同的计数单元上进行计数,这样减少了线程竞争,提高了并发效率。本质上是用空间换时间的思想,不过在实际高并发情况中消耗的空间可以忽略不计。
现在,在处理高并发计数时,应该优先使用LongAdder,而不是继续使用AtomicLong。当然,线程竞争很低的情况下进行计数,使用Atomic还是更简单更直接,并且效率稍微高一些。其他情况,比如序号生成,这种情况下需要准确的数值,全局唯一的AtomicLong才是正确的选择,此时不应该使用LongAdder。