美文网首页
并发编程

并发编程

作者: 技术灭霸 | 来源:发表于2020-04-25 23:48 被阅读0次

1、什么是线程安全,怎么保证线程安全?

线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题

如何保证线程安全

image.png

2、父子线程传递Threadlcoal值的问题

InheritableThreadLocal为什么能解决父子线程传递Threadlcoal值的问题。

  • 在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量
  • 在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去
  • 因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值
public class TestInheritableThreadLocal implements Runnable {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());
        Thread tt = new Thread(new TestInheritableThreadLocal());
        tt.start();
        System.out.println("----主线程结束");

    }

    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

3、volatile为什么不能保证原子性?

修改volatile变量分为四步:
1、读取volatile变量到local
2、修改变量值
3、local值写回
4、插入内存屏障,即lock指令,让其他线程可见这样就很容易看出来

前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。

并发编程中得了解的三个问题,可见性,原子性,有序性。volatile 原本的语义是禁用cpu缓存,也就是导致可见性的源头。原子性一般通过锁机制解决。

volatile 关键字通过内存屏障禁止了指令的重排序,并在单个核心中,强制数据的更新及时更新到缓存。在此基础上,依靠多核心处理器的缓存一致性协议等机制,保证了变量的可见性。

这里介绍几个状态协议,先从最简单的开始,MESI协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。

MESI 这种协议在数据更新后,会标记其它共享的CPU缓存的数据拷贝为Invalid状态,然后当其它CPU再次read的时候,就会出现 cache miss 的问题,此时再从内存中更新数据。

4、阻塞队列怎么实现?使用哪个场景

介绍:就是一个线程往队列里面放,而另外的一个线程从里面取,当线程持续的产生新对象并放入到队列中,直到队列达到它所能容纳的临界点

原理:LockSupport.park()、LockSupport.unPark()

场景:抽象为生产者消费者模型

5、阻塞,等待,挂起,休眠的区别

阻塞是线程的状态,等待、挂起和休眠是让线程进入阻塞状态的不同行为。等待是线程因为需要等待外部某个条件而进入阻塞,等条件满足后再继续运行(比如等待IO信号)。挂起线程主动让出CPU,等别的线程去唤醒它(比如如join)。休眠是线程主动让出CPU一段时间而进入阻塞状态,等时间到之后再继续运行(比如sleep(time))。

6、JUC大体讲一下,从宏观到微观

JUC大概包括:线程安全的集合类、锁、并发工具类、原子类、线程池。
而synchronized、volatile、CAS不属于

7、Synchronized与Lock的区别

1、简单对比

主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,当许多线程都在争用同一个锁时,使用 ReentrantLock 的总体开支通常要比 synchronized 少得多
synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

什么情况下可以使用 ReentrantLock

  • 使用synchronized 的一些限制:
  • 无法中断正在等候获取一个锁的线程;
  • 无法通过投票得到一个锁;
  • 释放锁的操作只能与获得锁所在的代码块中进行,无法在别的代码块中释放锁;
  • ReentrantLock 没有以上的这些限制,且必须是手工释放锁。

2、性能区别(悲观锁和乐观锁)

  • synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
  • synchronized采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其 他线程只能依靠阻塞来等待线程释放锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
  • Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就 是CAS操作(Compare and Swap)。

8、Java有哪些锁?

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

9、# jdk1.6以后对synchronized锁做了哪些优化

锁的级别从低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁


image.png

锁分级别原因:

没有优化以前,sychronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 sychronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。
两种情况轻量锁会升级到重量锁:

  1. 当自旋超过一定的次数时
  2. 第三个线程来访时

重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

10、两个线程同时执行i++100次

可能的结果:最小为2,最大为200

i++这种操作并不是原子性的, 实际上它的操作是首先从内存中取出数据放在cpu寄存器中进行计算, 然后再将计算好的结果返回到内存中。

最小值2的分析:

  • 假设两个线程a,b
  • 首先a执行99次,i为99,在未被写入内存时,b取i=0时执行1次,写入内存后i=1,此时覆盖掉了i=99的值;
  • 然后a取i=1执行1次,b取i=1执行99次,当a比b后写入内存时,a覆盖掉b,此时i=2

11、为什么wait方法在object类中?notify和wait为什么使用synchronize方法里面?为什么sleep方法在Thread类中


1、JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得
2、如果不用就会报错IllegalMonitorStateException,wait()方法的语义有两个,一是释放当前对象锁,另一个是进入阻塞队列,可以看到,这些操作都是与监视器相关的,而synchronized是由监视器实现的
3、最主要是sleep方法没有释放锁

12、多线程上下文切换的影响

多线程上下文切换的影响

  • 切换带来的性能损耗

引起上下文切换的原因

  1. 时间片用完,CPU正常调度下一个任务
  2. 被其他优先级更高的任务抢占
  3. 执行任务碰到IO阻塞,调度器挂起当前任务,切换执行下一个任务
  4. 用户代码主动挂起当前任务让出CPU时间
  5. 多任务抢占资源,由于没有抢到被挂起
  6. 硬件中断

如何减少上下文切换

  1. 无锁并发编程。多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据,队列实现异步串型无锁化。
  2. CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁
  3. 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
  4. 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

13、为什么java.util.concurrent 包里没有并发的ArrayList实现?

我认为在java.util.concurrent包中没有加入并发的ArrayList实现的主要原因是:很难去开发一个通用并且没有并发瓶颈的线程安全的List。

像ConcurrentHashMap这样的类的真正价值(The real point / value of classes)并不是它们保证了线程安全。而在于它们在保证线程安全的同时不存在并发瓶颈。举个例子,ConcurrentHashMap采用了锁分段技术和弱一致性的Map迭代器去规避并发瓶颈。

所以问题在于,像“Array List”这样的数据结构,你不知道如何去规避并发的瓶颈。拿contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list?

另一方面,Queue 和Deque (基于Linked List)有并发的实现是因为他们的接口相比List的接口有更多的限制,这些限制使得实现并发成为可能。

CopyOnWriteArrayList是一个有趣的例子,它规避了只读操作(如get/contains)并发的瓶颈,但是它为了做到这点,在修改操作中做了很多工作和修改可见性规则。 此外,修改操作还会锁住整个List,因此这也是一个并发瓶颈。所以从理论上来说,CopyOnWriteArrayList并不算是一个通用的并发List。

14、java Hashmap扩容时判断是否要重新hash的规则?

Java7扩容时,遍历每个节点,并重新hash获得当前数组的位置并添加到链表中;Java8进一步做了优化,将元素的hash和旧数组的大小(大小为2次幂)做与运算,为0则表示数组位置不变,不为0则表示需要移位,新位置为原先位置+旧数组的小大(新数组大小为旧数组翻倍),并将当前链表拆分为两个链表,一个链表放到原先位置,一个链路放到新位置,效率比Java7高。

15、TreeSet集合为什么要实现Comparable

TreeSet是用TreeMap来实现的,他是按照一定顺序存放的,这个排序的依据就是所存放元素的compareTo方法,放第一个的时候无需跟任何元素比较,所以不报错,但是从第二个开始就要比较以决定放置的位置了,这时候就要调用compareTo方法,怎么调用compareTo方法呢,只有将类强制转换为Comparable类型之后才能调用啊,但是若你没有实现Comparable接口,那当然就不能强制准换了,所以就报java.lang.ClassCastException

16、ConcurrentHashMap是如何在保证并发安全的同时提高性能

其实就是要控制锁的粒度,尽量避免锁的发生

ConcurrentHashMap使用了一些技巧来获取高的并发性能,同时避免了锁。这些技巧包括:

  1. 使用CAS乐观锁和volatile代替RentrantLock
  2. spread二次哈希进行segment分段。
  3. stream提高并行处理能力。

17、AQS是如何唤醒下一个线程的?

看出当前线程是否需要阻塞:

  1. 如果当前线程节点的前驱节点为SINGAL状态,则表明当前线程处于等待状态,返回true,当前线程阻塞
  2. 如果当前线程节点的前驱节点状态为CANCELLED(值为1),则表明前驱节点线程已经等待超时或者被中断,此时需要将该节点从同步队列中移除掉。最后返回false
  3. 如果当前节点节点前驱节点非SINGAL,CANCELLED状态,则通过CAS将其前驱节点的等待状态设置为SINGAL,返回false。

当线程释放同步状态后,则需要唤醒该线程的后继节点:

可能会存在当前线程的后继节点为null,超时、被中断的情况,如果遇到这种情况了,则需要跳过该节点,但是为何是从tail尾节点开始,而不是从node.next开始呢?原因在于node.next仍然可能会存在null或者取消了,所以采用tail回溯办法找第一个可用的线程。最后调用LockSupport的unpark(Thread thread)方法唤醒该线程。

18、线程池的非核心线程什么时候会被释放

当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过 keepAliveTime。

19、ConcurrentHashMap

插入流程

image.png

扩容流程

image.png

20、线程状态

image.png image.png

信号量同步是指在不同的线程之间,通过传递同步信号量来协调线程执行的 先后次序。这里重点分析基于时间维度 和 信号维度的两个类 : CountDownLatch、 Semaphore。

image.png

21、线程的 sleep()方法和 yield()方法有什么区别?

(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;

(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;

(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

22、Java 中 interrupted 和 isInterrupted 方法的区别?

  • interrupt:将被置为”中断”状态

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处。**支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

  • interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

  • isInterrupted:查看当前中断信号是true还是false

23、as-if-serial规则和happens-before规则的区别

as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。

as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

24、synchronized 的作用?

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

25、synchronized、volatile、CAS 比较

(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

(3)CAS 是基于冲突检测的乐观锁(非阻塞)

26、synchronized 和 volatile 的区别是什么?

  • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

27、 CAS到底最后加没加锁

首先使用Unsafe类中的compareAndSwapInt方法实现。
LOCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀

  1. 如果是多处理器,为cmpxchg指令添加lock前缀。
  2. 反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)

28、InheritableThreadLocal父子线程共享变量的原理

  • InheritableThreadLocal的源码非常简单,继承自ThreadLocal,重写其中三个方法。
  • InheritableThreadLocal本身并没做什么操作,唯一的可能就是Thread里做了手脚。目前的需求是要求将当前线程里的ThreadLocalMap共享到新开的线程,那么,因为不知道用户何时使用这个数据,所以新开的线程创建好后就必须能访问到这些数据
  • 如果当前线程的inheritableThreadLocals != null,新线程:this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
    传入当前线程的inheritableThreadLocals 。

29、CountDownLatch和CyclicBarrier源码上的区别

CountDownLatch底层是使用AQS

  • 当我们调用CountDownLatch countDownLatch=new CountDownLatch(4) 时候,此时会创建一个AQS的同步队列,并把创建CountDownLatch 传进来的计数器赋值给AQS队列的 state,所以state的值也代表CountDownLatch所剩余的计数次数;(state:同步状态,多少线程获取锁)
  • 当我们调用countDownLatch.wait()的时候,会创建一个节点,加入到AQS阻塞队列,并同时把当前线程挂起。
  • 当执行 CountDownLatch 的 countDown()方法,将计数器减一,也就是state减一,当减到0的时候,等待队列中的线程被释放。是调用 AQS 的 releaseShared 方法来实现的。(tryreleaseshared:通过设置同步状态尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false)
  • 因为这是共享型的,当计数器为 0 后,会唤醒等待队列里的所有线程,所有调用了 await() 方法的线程都被唤醒,并发执行。这种情况对应到的场景是,有多个线程需要等待一些动作完成。

CyclicBarrier底层是使用ReentrantLock(独占锁)和Condition

  • 每当线程执行await,内部变量count减1,如果count!= 0,说明有线程还未到屏障处,则在锁条件变量trip上等待。
  • 当count == 0时,说明所有线程都已经到屏障处,执行条件变量的signalAll方法唤醒等待的线程。
  • 其中 nextGeneration方法可以实现屏障的循环使用:重新生成Generation对象,恢复count值,如果generation.broken为true的话,说明这个屏障已经损坏,当某个线程await的时候,直接抛出异常
  • 在CyclicBarrier中,同一批的线程属于同一代,即同一个Generation;CyclicBarrier中通过generation对象,记录属于哪一代。当有parties个线程到达barrier,generation就会被更新换代。达到了循环使用

30、如何排查死锁?

使用 jps + jstack

  • jps -l
  • jstack -l 12316

31、比AtomicLong更高性能的LongAdder

LongAdder在高并发的场景下会比它的前辈————AtomicLong 具有更好的性能,代价是消耗更多的内存空间

AtomicLong在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况。

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

ConcurrentHashMap中的“分段锁”其实就是类似的思路。

32、ThreadLocal

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

原理:每个Thread类都有ThreadlocalMap属性,ThreadlocalMap定义在Threadlocal,真正的引用在Thread类,Map的key是ThreadLocal类的实例对象(注意不是线程id),value是用户的值。

相关文章

网友评论

      本文标题:并发编程

      本文链接:https://www.haomeiwen.com/subject/jxtfvhtx.html