Java多线程那点事儿

一、CyclicBarrier 和 CountDownLatch 的区别

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

1️⃣CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值 -1 而已,该线程继续运行。

2️⃣CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务。

3️⃣CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch 就不可再用了。

二、volatile 关键字的作用

理解 volatile 关键字的作用的前提是要理解 Java 内存模型。volatile 关键字的作用主要有两个:

1️⃣多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据。

2️⃣代码底层执行不像 Java 程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互。现实中,为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见java.util.concurrent包下的类,比如 AtomicInteger

三、Java 中如何获取到线程 dump 文件

死循环、死锁、阻塞、页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈。获取到线程堆栈有两步:

1️⃣【获取线程的 pid】通过使用 jps 命令。Linux 下还可以使用ps -ef | grep java

2️⃣【打印线程堆栈】可以通过使用jstack pid。Linux 下还可以使用kill -3 pid

Thread 类提供了一个 getStackTrace() 也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。

四、一个线程如果出现了运行时异常会怎么样

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放。

五、如何在两个线程之间共享数据

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

六、生产者消费者模型的作用

1️⃣通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。

2️⃣解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不会受到相互的制约。

七、synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量。ReentrantLock 比 synchronized 的扩展性体现在几点上:

  1. ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁;
  2. ReentrantLock 可以获取各种锁的信息;
  3. ReentrantLock 可以灵活地实现多路通知。

另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的是对象头中 mark word。

八、ConcurrentHashMap的并发度是什么

ConcurrentHashMap 的并发度就是 segment 的大小,默认为16,这意味着最多同时可以有 16 条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 都不能同时有两条线程获取 Hashtable 中的数据。

九、ReadWriteLock 是什么

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

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

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

十一、Thread.sleep(0) 的作用是什么

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

十二、什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

十三、什么是 Java 内存模型

Java内存模型定义了一种多线程访问 Java 内存的规范。Java 内存模型的几部分内容:

1️⃣Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去

2️⃣定义了几个原子操作,用于操作主内存和工作内存中的变量

3️⃣定义了 volatile 变量的使用规则

4️⃣happens-before,即先行发生原则,定义了操作 A 必然先行发生于操作 B 的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁 unlock 的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的 happens-before 规则,则这段代码一定是线程非安全的。

十四、CASAQS

1️⃣CAS 全称为 Compare and Swap,即比较-替换。假设有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当然 CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,永远都不可能成功。

2️⃣AQS 全称为 AbstractQueuedSychronizer,抽象队列同步器。如果说java.util.concurrent的基础是 CAS 的话,那么 AQS 就是整个 Java 并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS 实际上以双向队列的形式连接所有的 Entry,比方说 ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用 ReentrantLock 好了,则双向队列实际上的第一个 Entry 开始运行。AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方法,以实现自己的并发功能。

十五、Semaphore 有什么作用

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

十六、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”假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

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

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

举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:

1️⃣Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run() 是 Thread2 自己调用的;
2️⃣Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run() 是 Thread1 自己调用的。

十八、同步方法和同步块,哪个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好

虽说同步的范围越少越好,但是在 Java 虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer,它是一个线程安全的类,自然最常用的 append() 是一个同步方法,反复 append 字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着 Java 虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此 Java 虚拟机会将多次 append 方法调用的代码进行一个锁粗化的操作,将多次的 append 的操作扩展到 append 方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。

十九、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

1️⃣高并发、任务执行时间短的业务,线程池线程数可以设置为 CPU 核数 +1,减少线程上下文的切换。

2️⃣并发不高、任务执行时间长的业务要区分开看:

①假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO 操作并不占用 CPU,所以不要让所有的 CPU 闲下来,可以加大线程池中的线程数目,让 CPU 处理更多的业务。

②假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

③并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

二十、10 个线程和 2 个线程的同步代码,复杂度是相同的

从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。

二十一、如何调用 wait()?用 if 还是循环?为什么?

wait() 应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

// The standard idiom for using the wait method 
synchronized (obj) {
while (condition does not hold)
obj.wait(); 
// (Releases lock, and reacquires on wakeup)... 
// Perform action appropriate to condition
}

参见 Effective Java 第 69 条,获取更多关于为什么应该在循环中来调用 wait 方法的内容。

二十二、什么是多线程环境下的伪共享(false sharing)?

伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行,如图:

伪共享问题很难被发现,因为线程可能访问完全不同的全局变量,内存中却碰巧在很相近的位置上。如其他诸多的并发问题,避免伪共享的最基本方式是仔细审查代码,根据缓存行来调整你的数据结构。

二十三、什么是 Busy spin?为什么要使用它?

Busy spin 是一种在不释放 CPU 的基础上等待事件的技术。它经常用于避免丢失 CPU 缓存中的数据(如果线程先暂停,之后在其他 CPU 上运行就会丢失)。所以,如果工作要求低延迟,并且线程目前没有任何顺序,这样可以通过循环检测队列中的新消息来代替调用 sleep() 或 wait()。它唯一的好处就是只需等待很短的时间,如几微秒或几纳秒。LMAX 分布式框架是一个高性能线程间通信的库,该库有一个 BusySpinWaitStrategy 类就是基于这个概念实现的,使用 busy spin 循环 EventProcessors 等待屏障。

二十四、Swing 不是线程安全的

不能通过任何线程来更新 Swing 组件,如 JTable、JList 或 JPanel,事实上,它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中,可以一直等待,也可以通过异步更新直接返回结果。

二十五、什么是线程局部变量?

当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本。每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,是线程隔离的。线程隔离的秘密在于 ThreadLocalMap 类( ThreadLocal 的静态内部类)。

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

ThreadLocal 的方法:void set(T value)、T get()以及T initialValue()。

ThreadLocal 是如何为每个线程创建变量的副本的:

首先,在每个线程 Thread 内部有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个 threadLocals 就是用来存储实际的变量副本的,键值为当前 ThreadLocal 变量,value 为变量副本(即 T 类型的变量)。初始时,在 Thread 里面,threadLocals 为空,当通过 ThreadLocal 变量调用 set(),就会对 Thread 类中的 threadLocals 进行初始化,并且以当前 ThreadLocal 变量为键值,以 ThreadLocal 要保存的副本变量为 value,存到 threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过 get() 在 threadLocals 里面查找。

总结:

  1. 通过 ThreadLocal 创建的副本是存储在每个线程自己的 threadLocals 中的。
  2. 为何 threadLocals 的类型 ThreadLocalMap 的键值为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal 变量,就像上面代码中的 longLocal 和 stringLocal。
  3. 在进行 get 之前,必须先 set,否则会报空指针异常;如果想在 get 之前不需要调用 set 就能正常访问的话,必须重写 initialValue()。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值