【成神之路】多线程并发相关面试题 - Go语言中文社区

【成神之路】多线程并发相关面试题


基本概念:

说说线程安全问题,什么是线程安全,如何保证线程安全

函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

(1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

(2)绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

(3)相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

(4)线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

高并发常见指标:

响应时间(Response Time)
吞吐量(Throughput)
每秒查询率QPS(Query Per Second)
并发用户数

什么是响应时间?
系统对请求做出响应的时间。
例如:系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。

什么是吞吐量?
单位时间内处理的请求数量。

什么是QPS?
每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。

什么是并发用户数?
同时承载正常使用系统功能的用户数量。
例如:一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
 
如何提升系统的并发能力?
互联网分布式架构设计,提高系统并发能力的方式,方法论上主要有两种:
垂直扩展(Scale Up)
水平扩展(Scale Out)

线程和进程的概念、并行和并发的概念

https://blog.csdn.net/w372426096/article/details/84793226

把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源

Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。

并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

Erlang 之父 Joe Armstrong 用一张比较形象的图解释了并发与并行的区别:

并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。
单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。

多线程是解决什么问题的?

(1)发挥多核CPU的优势

单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

(3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

创建线程的方式及实现

https://blog.csdn.net/w372426096/article/details/85234262

进程间通信的方式

信号量,生产者消费者模式

进程的状态转化(进程的三种状态间的基本转换)。

多核CPU线程调度怎么保证原子性

保证同步的前提下,用JUC中原子类

线程的生命周期,状态是如何转移的

关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:

新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。

就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。

在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。

阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。

等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。

计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,如下面示例:

public final native void wait(long timeout) throws InterruptedException;
终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。

什么时候会出现僵死进程?

1、应用本身程序的问题,造成死锁。

2、load 太高,已经超出服务的极限

3、jvm GC 时间过长,导致应用暂停

         因为出错项目里面没有打出GC的处理情况,所以不确定此原因是否也是我项目tomcat假死的原因之一。

4、大量tcp 连接 CLOSE_WAIT

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

TIME_WAIT 48

CLOSE_WAIT 2228

ESTABLISHED 86

常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关

一个线程连着调用start两次会出现什么情况?

由于状态只有就绪、阻塞、执行,状态是无法由执行转化为执行的,所以会报不合法的状态!

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

wait方法能不能被重写,wait能不能被中断;

wait是final方法,不能被重写;可以被中断

wait 方法:造成当前线程等待,直到另外一个线程调用了notify或者notifyAll方法 ,当前线程必须拥有该对象的 monitor 才可以,意味着当前线程在该对象的synchronized快里或者synchronized方法里面。

wait 方法的调用必须在synchronized快里或者synchronized方法里面调用才可以 不能被重写 wait(time) 方法, 定义等待线程多长时间

     synchronized (obj) {
         while (<condition does not hold>)
             obj.wait(); // 等待notify通知,但是得到通知了并不会一定会被唤醒,它必须要获取到该对象的锁才能继续往下执行
         ... // Perform action appropriate to condition

notify() 和notifyAll() 也是在synchronized快里或者synchronized方法里面调用才可以 不能被重写

当一个线程执行完毕,释放掉了对象的锁,调用了 notify()方法,就随机的选择了一个正在等待的线程被唤醒,这个线程不是由我们决定的,而是系统自己决定的,
调用了 notifyAll()方法,就通知所有正在等待的线程,然后多个线程通过竞争得到对象的锁。

wait 与 notify 方法都是定义在 Object 类中,而且是 final 的,因此会被所有的 Java类所继承并且无法重 写。这两个方法要求在调用时线程应该已经获得了对象的锁,因此对这两个方法的调用需要放在 synchronized 方法或块当中。当线程执行了 wait方法时,它会释放掉对象的锁。

sleep:另一个会导致线程暂停的方法就是 Thread 类的 sleep 方法,它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的 (这是跟wait方法不同的一点)。

wait方法一般都是要加异常处理的,这是因为:当某代码并不持有监视器的使用权时,去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

Java中有哪些同步方案

(重量级锁、显式锁、并发容器、并发同步器、CAS、volatile、AQS等)

Thread 类中的start() run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

Java中如何停止一个线程?

JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。点击这里查看示例代码。

如何在两个线程间共享数据?

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

notify notifyAll有什么区别?

notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

为什么wait, notify notifyAll这些方法不在thread类里面?要在同步块中被调用

要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。

wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别?

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

怎么检测一个线程是否持有对象监视器?

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。

不可变对象对多线程有什么帮助

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

什么是多线程的上下文切换?

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

Javainterrupted isInterruptedd方法的区别?

interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

有三个线程T1T2T3,怎么确保它们按顺序执行?

public class TestTwo {
        static TestTwo t=new TestTwo();
        class T1 extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //T1线程中要处理的东西
                System.out.println("T1线程执行")
            }
        }
        class T2 extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //T2线程中要处理的东西
                System.out.println("T2线程执行");
                t.new T1().start();
            }
        }
        class T3 extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //T3线程中要处理的东西
                System.out.println("T3线程执行");
                t.new T2().start();
            }
        }
        public static void main(String[] args) {
            t.new T3().start();
       //打印结果如下:
             //T3线程执行
        //T2线程执行
              //T1线程执行
        }
        
}

join方法:

https://www.cnblogs.com/lcplcpjava/p/6896904.html

Thread类中的yield方法有什么作用?

Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

什么是阻塞式方法?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

多线程中的忙循环是什么?

忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

如何强制启动一个线程?

这个问题就像是如何强制进行Java垃圾回收,目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。

Java多线程中调用wait() sleep()方法有什么不同?

sleep是Thread类的方法,wait是Object类的方法

Java程序中wait 和 sleep都会造成某种形式的暂停,可以用来放弃CPU一定的时间。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。

怎么唤醒一个阻塞的线程?

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

Java中用到的线程调度算法是什么?

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

Thread.sleep(0)的作用是什么?

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

线程类的构造方法、静态块是被哪个线程调用的?

记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    (1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    (2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

锁:

重入锁的概念,重入锁为什么可以防止死锁

详见https://blog.csdn.net/w372426096/article/details/89066535

产生死锁的四个条件(互斥、请求与保持、不剥夺、循环等待)

详见https://blog.csdn.net/w372426096/article/details/89066535

volatile 实现原理(禁止指令重排、刷新内存)

详见https://blog.csdn.net/w372426096/article/details/89066535

synchronized 实现原理(对象监视器)

详见https://blog.csdn.net/w372426096/article/details/89066535

synchronized 与 lock 的区别

详见https://blog.csdn.net/w372426096/article/details/89066535

AQS同步队列 ;AQS, 从unsafe一直到ReentrentLock花一下

详见https://blog.csdn.net/w372426096/article/details/89066535

synchronized与ReentraLock哪个是公平锁;

reentraLock通过传参数true设置公平锁

可重入锁中的lock和trylock的区别

使用lock()获取锁,若获取成功,标记下是该线程获取到了锁(用于锁重入),然后返回。若获取失败,这时跑一个for循环,循环中先将线程阻塞放入等待队列,当被调用signal()时线程被唤醒,这时进行锁竞争(因为默认使用的是非公平锁),如果此时用CAS获取到了锁那么就返回,如果没获取到那么再次放入等待队列,等待唤醒,如此循环。其间就算外部调用了interrupt(),循环也会继续走下去。一直到当前线程获取到了这个锁,此时才处理interrupt标志,若有,则执行 Thread.currentThread().interrupt(),结果如何取决于外层的处理。

使用tryLock()尝试获取锁,若获取成功,标记下是该线程获取到了锁,然后返回true;若获取失败,此时直接返回false,告诉外层没有获取到锁,之后的操作取决于外层
 

JUC相关:

偏向锁、轻量级锁、重量级锁、自旋锁的概念

详见https://blog.csdn.net/w372426096/article/details/89066535

CAS无锁的概念、乐观锁和悲观锁

详见https://blog.csdn.net/w372426096/article/details/89066535

CAS机制会出现什么问题;什么是ABA问题,出现ABA问题JDK是如何解决的

详见https://blog.csdn.net/w372426096/article/details/89066535

乐观锁的业务场景及实现方式

 1、悲观锁,前提是,一定会有并发抢占资源,强行独占资源,在整个数据处理过程中,将数据处于锁定状态。
          2、乐观锁,前提是,不会发生并发抢占资源,只有在提交操作的时候检查是否违反数据完整性。只能防止脏读后数据的提交,不能解决脏读。
          当然,还有其他的锁机制,暂时不多介绍,着重于乐观锁的实现。
          乐观锁,使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
           记录1,id,status1,status2,stauts3,version,表示有三个不同的状态,以及数据当前的版本
           操作1:update table set status1=1,status2=0,status3=0 where id=111;  
           操作2:update table set status1=0,status2=1,status3=0 where id=111;
           操作3:update table set status1=0,status2=0,status3=1 where id=111;
           没有任何控制的情况下,顺序执行3个操作,最后前两个操作会被直接覆盖。
           加上version字段,每一次的操作都会更新version,提交时如果version不匹配,停止本次提交,可以尝试下一次的提交,以保证拿到的是操作1提交后的结果。
          这是一种经典的乐观锁实现。
          另外,java中的compareandswap即cas,解决多线程并行情况下使用锁造成性能损耗的一种机制。
          CAS操作包含三个操作数,内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会西东将该位置值更新为新值。否则,处理器不做任何操作。
          记录2: id,stauts,status 包含3种状态值 1,2,3
           操作,update status=3 where id=111 and status=1;
           即 如果内存值为1,预期值为1,则修改新值。对于没有执行的操作则丢弃。

资源提交冲突,其他使用方需要重新读取资源,会增加读的次数,但是可以面对高并发场景,前提是如果出现提交失败,用户是可以接受的。因此一般乐观锁只用在高并发、多读少写的场景
其中:GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。

用过并发包下边的哪些类;

    提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。
    各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。
    各种并发队列实现,如各种 BlockedQueue 实现,比较典型的 ArrayBlockingQueue、 SynchorousQueue 或针对特定场景的 PriorityBlockingQueue 等。
    强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。

对于Java 并发包提供了哪些并发工具类,我是这么理解的:
1. 执行任务,需要对应的执行框架(Executors);
2. 多个任务被同时执行时,需要协调,这就需要Lock、闭锁、栅栏、信号量、阻塞队列;
3. Java程序中充满了对象,在并发场景中当然避免不了遇到同种类型的N个对象,而对象需要被存储,这需要高效的线程安全的容器类CountDownLatch:需求是每个对象一个线程,分别在每个线程里计算各自的数据,最终等到所有线程计算完毕,我还需要将每个有共通的对象进行合并.

常见的原子操作类

二、原子更新基本类型
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
(1)AtomicBoolean: 原子更新布尔类型。
(2)AtomicInteger: 原子更新整型。
(3)AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
(1)int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
(2)boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
(3)int getAndIncrement(): 以原子的方式将当前值加1,注意,这里返回的是自增前的值。
(4)void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
(5)int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。


Atomic包里的类基本都是使用Unsafe下的CAS方法实现,方法如下:
Unsafe
Unsafe只提供了三种CAS方法: compareAndSwapObject、compareAndSwapInt和compateAndSwapLong,其他类型都是转成这三种类型再使用对应的方法去原子更新的。
三、原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下的4个类:
(1)AtomicIntegerArray: 原子更新整型数组里的元素。
(2)AtomicLongArray: 原子更新长整型数组里的元素。
(3)AtomicReferenceArray: 原子更新引用类型数组里的元素。
这三个类的最常用的方法是如下两个方法:
(1)get(int index):获取索引为index的元素值。
(2)compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。
以AtomicReferenceArray举例如下:


AtomicReferenceArray
输出结果为:index100 n index1。
需要注意的是,数组value通过构造方法传递进去,然后AtoReferenceArray会将当前数组复制一份,所以当AtoReferenceArray对内部的数组元素修改的时候,不会影响原先传入的referenceArray数组。
四、原子更新引用类型
Atomic包提供了以下三个类:
(1)AtomicReference: 原子更新引用类型。
(2)AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
(3)AtomicMarkableReferce: 原子更新带有标记位的引用类型。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰,下面提供一段示例代码:


AtomicReferenceFieldUpdater
五、原子更新字段类
Atomic包提供了四个类进行原子字段更新:
(1)AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
(2)AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
(3)AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
(4)AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
这四个类的使用方式都差不多,示例代码如上一小节的AtomicReferenceFieldUpdater一样,要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段必须使用public volatile修饰。
AtomicInteger底层实现原理;

所有操作针对内部votile int value;使用Unsafe下的CAS方法实现,1.8之采用自选和CAS配合

其中lazySet方法与lock配合使用,减少不必要的内存屏障,提高程序执行效率;

由锁来保证共享变量的可见性,以设置普通变量的方式来修改共享变量,减少不必要的内存屏障,从而提高程序执行的效率。

atomic怎么实现的

Atomic包里的类基本都是使用Unsafe下的CAS方法实现,方法如下:
Unsafe
Unsafe只提供了三种CAS方法: compareAndSwapObject、compareAndSwapInt和compateAndSwapLong,其他类型都是转成这三种类型再使用对应的方法去原子更新的。

volatile 变量和 atomic 变量有什么不同?

volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

说说 CountDownLatch、CyclicBarrier 原理和区别

CyckicBarrier:

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

内部是使用重入锁ReentrantLock和Condition。它有两个构造函数:

    CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。

    CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

最重要的方法莫过于await()方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。

await()的处理逻辑还是比较简单的:如果该线程不是到达的最后一个线程,则他会一直处于等待状态,除非发生以下情况:

    最后一个线程到达,即index == 0
    超出了指定时间(超时等待)
    其他的某个线程中断当前线程
    其他的某个线程中断另一个等待的线程
    其他的某个线程在等待barrier超时
    其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态。

主要用在合并计算结果的应用场景

CountDownLatch:

CountDownLatch内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止。当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 – 1。当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置。

内部依赖Sync(内部类)实现,而Sync继承AQS。CountDownLatch仅提供了一个构造方法:CountDownLatch(int count);

采用共享锁来实现的。

await()方法中getState()获取同步状态,其值等于计数器的值,如果计数器值不等于0,则会调用(AQS的)doAcquireSharedInterruptibly(int arg),该方法为一个自旋方法会尝试一直去获取同步状态。

虽然,CountDownlatch与CyclicBarrier有那么点相似,但是他们还是存在一些区别的:

    CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
    CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier
    CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
    CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

场景:老板在会议室等员工到齐了了开会。

说说 Semaphore 原理

信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。

Semaphore,在API是这么介绍的:

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1;

Semaphore内部包含公平锁(FairSync)和非公平锁(NonfairSync),继承内部类Sync,其中Sync继承AQS(再一次阐述AQS的重要性)。

Semaphore提供了两个构造函数:

    Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore。
    Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore。

"公平信号量"和"非公平信号量"的区别

"公平信号量"和"非公平信号量"的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在CLH队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在CLH队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的tryAcquireShared()函数的实现不同。

说说 Exchanger 原理

我先到一个叫做Slot的交易场所交易,发现你已经到了,那我就尝试喊你交易,如果你回应了我,决定和我交易那么进入第2步;如果别人抢先一步把你喊走了,那我就进入第5步。
我拿出钱交给你,你可能会接收我的钱,然后把货给我,交易结束;也可能嫌我掏钱太慢(超时)或者接个电话(中断),TM的不卖了,走了,那我只能再找别人买货了(从头开始)。
我到交易地点的时候,你不在,那我先尝试把这个交易点给占了(一屁股做凳子上…),如果我成功抢占了单间(交易点),那就坐这儿等着你拿货来交易,进入第4步;如果被别人抢座了,那我只能在找别的地方儿了,进入第5步。
你拿着货来了,喊我交易,然后完成交易;也可能我等了好长时间你都没来,我不等了,继续找别人交易去,走的时候我看了一眼,一共没多少人,弄了这么多单间(交易地点Slot),太TM浪费了,我喊来交易地点管理员:一共也没几个人,搞这么多单间儿干毛,给哥撤一个!。然后再找别人买货(从头开始);或者我老大给我打了个电话,不让我买货了(中断)。
我跑去喊管理员,尼玛,就一个坑交易个毛啊,然后管理在一个更加开阔的地方开辟了好多个单间,然后我就挨个来看每个单间是否有人。如果有人我就问他是否可以交易,如果回应了我,那我就进入第2步。如果我没有人,那我就占着这个单间等其他人来交易,进入第4步。
6.如果我尝试了几次都没有成功,我就会认为,是不是我TM选的这个单间风水不好?不行,得换个地儿继续(从头开始);如果我尝试了多次发现还没有成功,怒了,把管理员喊来:给哥再开一个单间(Slot),加一个凳子,这么多人就这么几个破凳子够谁用!

Java 8并法包下常见的并发类

StampedLock:ReentrantReadWriteLock的增强,优化读写锁,可以相互转换

LongAdder:高并发时比AtomicLog好,代价消耗更多的内存空间

Java中的ReadWriteLock是什么?

公平,非公平;读写锁,支持Condition,AQS实现独占式写锁,共享式读锁

什么是FutureTask

在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor线程池来执行。

JavaRunnableCallable有什么不同?

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

Java中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?

    (1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

    (2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

Java中的fork join框架是什么?

ThreadLocal:

什么是ThreadLocal变量?

线程的局部变量:每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

ThreadLocal 原理分析;

ThreadLocal 的使用非常简单,最核心的操作就是四个:创建、创建并赋初始值、赋值、取值。

1、创建

ThreadLocal<String> mLocal = new ThreadLocal<>();

2、创建并赋初值。下面代码表示创建了一个 String 类型的 ThreadLocal 并且重写了 initialValue 方法,并返回初始字符串,之后调用 get() 方法获取的值便是initialValue 方法返回的值。

ThreadLocal<String> mLocal = new ThreadLocal<String>(){
            @Override
            protected String initialValue(){
                return "init value";
            }
        };
System.out.println(mLocal.get());

3、设置值

 mLocal.set("hello");

4、取值

mLocal.get()

ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。例如下面的 set 方法:

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 的 set 方法时,首先获取到了当前线程,然后获取当前线程维护的 ThreadLocalMap 对象,最后在ThreadLocalMap 实例中添加上。如果 ThreadLocalMap 实例不存在则初始化并赋初始值。

这里看到 set 方法的第一个参数是 thisthis即指的是当前的 ThreadLocal 对象,会看上看的代码就是指的 mLocal 这个对象。而在 ThreadLocalMap 的 set 方法中会根据当前 ThreadLocal 对象实例,做一些操作和判断,最终实现赋值操作(具体参考源码)。

所以说,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。

ThreadLocal为什么会出现OOM,出现的深层次原理

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 的时候,最好不要声明为静态的;(官方建议用静态是因为把生命周期变长,减少OOM)
  • 使用完 ThreadLocal ,最好手动调用 remove() 方法,例如上面说到的 Session 的例子,如果不在拦截器或过滤器中处理,不仅可能出现内存泄漏问题,而且会影响业务逻辑;

ThreadLocal的使用场景

就是当我们只想在本身的线程内使用的变量,可以用 ThreadLocal 来实现,并且这些变量是和线程的生命周期密切相关的,线程结束,变量也就销毁了。

所以说 ThreadLocal 不是为了解决线程间的共享变量问题的,如果是多线程都需要访问的数据,那需要用全局变量加同步机制。

最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。如:

        数据库连接:

  1. private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
  2.     public Connection initialValue() {  
  3.         return DriverManager.getConnection(DB_URL);  
  4.     }  
  5. };  
  6.   
  7. public static Connection getConnection() {  
  8.     return connectionHolder.get();  
  9. }  

        Session管理:

  1. private static final ThreadLocal threadSession = new ThreadLocal();  
  2.   
  3. public static Session getSession() throws InfrastructureException {  
  4.     Session s = (Session) threadSession.get();  
  5.     try {  
  6.         if (s == null) {  
  7.             s = getSessionFactory().openSession();  
  8.             threadSession.set(s);  
  9.         }  
  10.     } catch (HibernateException ex) {  
  11.         throw new InfrastructureException(ex);  
  12.     }  
  13.     return s;  

线程池:

线程池解决什么问题?

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目.

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。

讲讲线程池的实现原理?线程池的几种实现方式?

利用 Executors 提供的通用线程池创建方法,创建不同配置的线程池,主要区别在于不同的 ExecutorService 类型或者不同的初始参数。

Executors 目前提供了 5 种不同的线程池创建配置:

    newCachedThreadPool(),处理大量短时间工作任务的线程池,鲜明特点:试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
    newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
    newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
    newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
    newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

在大多数应用场景下,使用 Executors 提供的 5 个静态工厂方法就足够了,但是仍然可能需要直接利用 ThreadPoolExecutor 等构造函数创建.ExecutorService 除了通常意义上“池”的功能,还提供了更全面的线程管理、任务提交等方法。

Executor 是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点可以体会其定义的唯一方法。
void execute(Runnable command);
Executor 的设计是源于 Java 早期线程 API 使用的教训,开发者在实现应用逻辑时,被太多线程创建、调度等不相关细节所打扰。

ExecutorService 则更加完善,不仅提供 service 的管理功能,比如 shutdown 等方法,也提供了更加全面的提交任务机制,如返回Future而不是 void 的 submit 方法。<T> Future<T> submit(Callable<T> task);注意,这个例子输入的可是Callable,它解决了 Runnable 无法返回结果的困扰。

Java 标准类库提供了几种基础实现,比如ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool。这些线程池的设计特点在于其高度的可调节性和灵活性,以尽量满足复杂多变的实际应用场景。

Executors 则从简化使用的角度,为我们提供了各种方便的静态工厂方法。

下面我就从源码角度,分析线程池的设计与实现,我将主要围绕最基础的 ThreadPoolExecutor 源码。ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的扩展,主要是增加了调度逻辑。而 ForkJoinPool 则是为 ForkJoinTask 定制的线程池,与通常意义的线程池有所不同。

这部分内容比较晦涩,罗列概念也不利于你去理解,所以我会配合一些示意图来说明。在现实应用中,理解应用与线程池的交互和线程池的内部工作过程,你可以参考下图。

简单理解一下:

工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为 0 的 SynchronousQueue(使用 newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用 LinkedBlockingQueue。
private final BlockingQueue<Runnable> workQueue;
 
内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会在闲置一段时间(默认 60 秒)后结束线程。
private final HashSet<Worker> workers = new HashSet<>();
线程池的工作线程被抽象为静态内部类 Worker,基于AQS实现。

ThreadFactory 提供上面所需要的创建线程逻辑。

如果任务提交时被拒绝,比如线程池已经处于 SHUTDOWN 状态,需要为其提供处理逻辑,Java 标准库提供了类似ThreadPoolExecutor.AbortPolicy等默认实现,也可以按照实际需求自定义。

从上面的分析,就可以看出线程池的几个基本组成部分,一起都体现在线程池的构造函数中,从字面我们就可以大概猜测到其用意:
进一步分析,线程池既然有生命周期,它的状态是如何表征的呢?

这里有一个非常有意思的设计,ctl 变量被赋予了双重角色,通过高低位的不同,既表示线程池状态,又表示工作线程数目,这是一个典型的高效优化。试想,实际系统中,虽然我们可以指定线程极限为 Integer.MAX_VALUE,但是因为资源限制,这只是个理论值,所以完全可以将空闲位赋予其他意义。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    // 真正决定了工作线程数的理论上限
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
    // 线程池状态,存储在数字的高位
    private static final int RUNNING = -1 << COUNT_BITS;
    …
    // Packing and unpacking ctl
    private static int runStateOf(int c)  { return 
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/w372426096/article/details/89914454
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-24 09:56:42
  • 阅读 ( 1488 )
  • 分类:面试题

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢