问题整理 - 高并发

一、HashMap 是不是线程安全?

1 hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同一时间片同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
2 HashMap的get操作可能因为resize而引起死循环
HashMap的扩容机制就是重新申请一个容量是当前的2倍的桶数组,然后将原先的记录逐个重新映射到新的桶里面,然后将原先的桶逐个置为null使得引用失效,
线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [1,A], next = [2,B]。线程thread2被调度执行并且扩容 此时 [2,B]的next 为[1,A]在取链表的时候从是从尾部开始取,形成了环形链表,如果get的key的桶索引会陷入死循环

    1.1、如何变得安全:
          Hashtable:通过 synchronized 来保证线程安全的,独占锁,悲观策略。吞吐量较低,性能较为低下
          ConcurrentHashMap:JUC 中的线程安全容器,高效并发。ConcurrentHashMap 的 key、value 都不允许为 null
     1.2、jdk1.8相对于jdk1.7的优化
         由 数组+链表 的结构改为 数组+链表+红黑树。
         拉链过长会严重影响hashmap的性能,所以1.8的hashmap引入了红黑树,当链表的长度大于8时,转换为红黑树的结构
         优化了高位运算的hash算法:h^(h>>>16) 将hashcode无符号右移16位,让高16位和低16位进行异或。

二、ConcurrentHashMap 的实现方式
1.7
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁
put将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
1. 计算键所对应的 hash 值;2. 如果哈希表还未初始化,调用 initTable() 初始化,否则在 table 中找到 index 位置,并通过 CAS 添加节点。如果链表节点数目超过 8,则将链表转换为红黑树。如果节点总数超过,则进行扩容操作
get将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上(get():无需加锁,直接根据 key 的 hash 值遍历 node),由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值
ConcurrentHashMap的get方法采用了unsafe方法,来保证线程安全
ConcurrentHashMap迭代器是强一致性,hashmap强一直性(ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException)
     1.1、jdk1.8相对于jdk1.7的区别
             jdk1.7:Segment+HashEntry来进行实现的;
             jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
             jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node(将 1.7 中存放数据的 HashEntry 改                       为 Node,但作用都是相同的)
数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized

三、CountDownLatch 和 CyclicBarrier
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作
countdownlatch中有个计数器,当计数器减少到0的时候,释放所有等待的线程,coutDown()会让计数器的值减少,await() 进入阻塞状态,直到count为0为止,所有等待的线程都会开始执行。
而且CountDownLatch只有一次的机会,只会阻塞线程一次
CyclicBarrier:是回环栅栏,只有等待线程积累到一定的数量的时候才会释放屏障,在释放屏障的时候还可以使用接口初始化 是可以重重复使用的

四、怎么控制线程,尽可能减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程
无锁并发编程:多线程处理数据时,可以使用一些方法来避免使用锁。如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据(currenthashmap分段锁思想)
CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

五、乐观锁和悲观锁
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现
     1.1、乐观锁的ABA 问题
           如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能              的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题
     JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是       否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

六、并发特性 - 原子性、有序性、可见性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行,不进行指令重排列
1.原子性:提供互斥访问,串行线程(atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则 A操作一定在B操作之前,而是A操作的影响能被操作B观察到)

七、synchronized
synchronized锁可以修饰在 普通方法中、静态方法中、代码块,
synchronized是内置的语言实现,jvm编译器(monitor)去保证锁的加锁和释放 ,synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
解决多线程并发访问共享数据的竞争问题
synchronized使用的锁对象是存储在Java对象头的Mark Word内,Mark Word存储对象的HashCode、分代年龄、锁
其中锁分为:偏向锁、轻量级锁、自旋锁
jdk1.6以后对synchronized做了优化, 如自旋锁、偏向锁、轻量级锁、自旋锁等技术来减少锁操作的开销
一个线程获得了锁,进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁,但锁竞争比较激烈的场合,偏向锁就失效
转换为轻量级锁,不存在竞争, 轻量级锁失败后
转换为自旋锁,若干次循环后 去竞争锁

八、volatile
变量定义为 volatile 之后 具备两种特性:保证此变量对所有的线程的可见性;禁止指令重排序优化
volatile变量通过内存屏障是一个CPU指令,指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么

九、JMM
JMM 规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序
一方面提供足够强的内存可见性保证
一方面计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排
在并发编程模式中 线程安全考虑会有3个概念:
1、可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
对于串行程序来说,可见性是不存在的在多线程环境中线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中共享变量x进行操作。
2、有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的
3、原子性
一个操作或者多个操作要么全部执行要么全部不执行。

通过 volatile、synchronized、final、concurrent 包等 实现。

十、队列 AQS 队列同步器
AQS 是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等), 包含了实现同步器的细节(获取同步状态、FIFO 同步队列)。AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。
维护一个同步状态 state。当 state > 0时,表示已经获取了锁;当state = 0 时,表示释放了锁。
1、如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程
2、当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态
AQS 内部维护的是** CLH 双向同步队列**

十一、锁的特性
可重入锁:指的是在一个线程中可以多次获取同一把锁。 ReentrantLock 和 synchronized 都是可重入锁。
可中断锁:顾名思义,就是可以相应中断的锁。synchronized 就不是可中断锁,而 Lock 是可中断锁。
公平锁:即尽量以请求锁的顺序来获取锁。synchronized 是非公平锁,ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

十二、ReentrantLock
ReentrantLock可重入锁、显示锁ReentrantLock 提供了比synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率
ReentrantLock 实现 Lock 接口,基于内部的 Sync 实现
Sync 实现 AQS ,提供了 FairSync(公平锁) 和 NonFairSync(非公平锁) 两种实现
Condition 和 Lock 一起使用以实现等待/通知模式,通过 await()和singnal() 来阻塞和唤醒线程。

十三、ReentrantReadWriteLock
读写锁维护着一对锁,一个读锁和一个写锁。分离读锁和写锁
在同一时间,可以允许多个读线程同时访问,但是,在写线程访问时,所有读线程和写线程都会被阻塞

十四、Synchronized 和 Lock 的区别
synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。Lock 是一个接口 JDK自带
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行ReentrantLock 提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票
ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized 而言,ReentrantLock 会不容易产生死锁些。
ReentrantLock 支持中断处理

十五、Java 中线程同步的方式
sychronized 同步方法或代码块
volatile、Lock、ThreadLocal、阻塞队列(LinkedBlockingQueue)、使用原子变量(java.util.concurrent.atomic)

十六、多线程下为什么不使用 int 而使用 AtomicInteger。
Concurrent 包下的类的源码时,发现无论是 ReentrantLock 内部的 AQS,还是各种 Atomic 开头的原子类,内部都应用到了 CAS
CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值 V 的值修改为 B,否则什么都不干
Unsafe 是 CAS 的核心类,Java 无法直接访问底层操作系统,而是通过本地 native` 方法来访问。不过尽管如此,JVM 还是开了一个后门:Unsafe ,它提供了硬件级别的原子操作
valueOffset 为变量值在内存中的偏移地址,Unsafe 就是通过偏移地址来得到数据的原值的
在多线程环境下,int 类型的自增操作不是原子的,线程不安全

十七、线程池
使用线程池目的:
1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
2、对线程进行一些简单的管理(延时执行、定时循环执行的策略等) 利于扩展
3、线程并发数量过多,运用线程池能有效的控制线程最大并发数,防止抢占系统资源从而导致阻塞
线程池有五种状态:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。
参数:
corePoolSize线程池中核心线程的数量
maximumPoolSize 线程池中允许的最大线程数
keepAliveTime线程空闲的时间,线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime
unit:keepAliveTime 的单位
workQueue:用来保存等待执行的任务的阻塞队列 (可选ArrayBlockingQueue、LinkedBlockingQueue 等)
handler:线程池的拒绝策略 (向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务)

十八、死锁与活锁的区别
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象
产生死锁的必要条件:
互斥条件:所谓互斥就是进程在某一时间内独占资源。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败

十九、FutureTask
用ExecutorService启动任务,FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法
只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞

二十、什么是竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件

二十一、volatile 变量和 atomic 变量的不同
Volatile不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的
AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作

二十二、i++
i++: 读值,+1,写值。在这三步任何之间都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。
如果是方法里定义的,一定是线程安全的,因为每个方法栈是线程私有的。
如果是类的静态成员变量,i++则不是线程安全的,每个线程需要对共享变量操作的时候必须先把共享变量从主内存load到自己的工作内存,登完成对共享变量的操作时再保存到主内存。如果一个线程运算完成后还没刷到主内存,此时这个共享变量的值被另一个线程从主内存读取到了,这个时候读取的数据就是脏数据了
解决:使用循环CAS使用支持原子性操作的类AtomicInteger

posted @ 2020-02-12 20:45  吴某1  阅读(202)  评论(0编辑  收藏  举报