[Java复习04] 并发 JUC

Q1:为什么非常高的并发请求下AtomicLong的性能会有很大影响?有没有什么更好的替代方案?

虽然AtomicLong使用CAS但是CAS失败后还是通过无限循环的自旋锁不断尝试的,在高并发下N多线程同时去操作一个变量会造成大量线程CAS失败然后处于自旋状态,这大大浪费了CPU资源,降低了并发性。

JDK8提供的LongAdder。该类也可以保证Long类型操作的原子性,相对于AtomicLong,LongAdder有着更高的性能和更好的表现,可以完全替代AtomicLong的来进行原子操作。

低竞争下直接更新base,类似AtomicLong。高并发下,会将每个线程的操作hash到不同的cells数组中,从而将AtomicLong中更新一个value的行为优化之后,分散到多个value中从而降低更新热点,而需要得到当前值的时候,直接将所有cell中的value与base相加即可。

Q2: LockSupport工具类的作用?与Object的wait/notify的区别?

LockSupport提供一组公共静态方法,提供最基本的线程阻塞和唤醒功能。park()阻塞, unpark()唤醒。

区别:1.LockSupport不需要在同步代码块中,所以线程也不需要维护一个共享同步对象,实现线程解耦。

2. Unpark可以先于park调用,不用担心线程间的执行先后顺序。

Q3: 锁是基于AQS实现,什么是AQS(抽象队列同步器),AQS框架的核心思想,实现流程?

AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的框架。

JUC包中几乎所有关于锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS框架。

AQS提供一个模板框架维护了一个资源state(volatile int)和一个同步队列。

核心思想(原理简述):

1. 同步状态(synchronization state)管理

基于volatile int state的变量表示同步状态,配合Unsafe工具类对其原子性操作来实现对当前锁状态的修改。暴露出getState、setState以及compareAndSetState操作来读取和更新这个状态。

2. 阻塞/唤醒线程的操作

JUC包LockSupport类来作为线程阻塞和唤醒的工具

3. 通过FIFO队列来完成资源获取线程的排队工作

AQS框架包含两种可供选择的实现方式:独占(Exclusive)和共享(Share)。

AQS框架支持中断、超时,支持Condition条件等待。

独占锁流程:

首先调用acquire(1), 之后进入tryAcquire(1)尝试获取锁,若成功则返回。

若失败则将当前线程构造为Node节点,通过addWaiter(mode)里enq(node)自旋+CAS插入到同步队列尾部。

自旋时判断其前驱节点是否为头节点,并且是否成功获取同步状态,二者皆成立则当前节点设置为头节点,否则挂起当前线程等待被前驱节点唤醒。

共享锁流程:

首先调用acquireShared(1), 之后进入tryAcquireShared(1)获取同步状态,返回值不小于0则说明同步状态有剩余,获取成功直接返回。

若返回值小于0则说明获取同步状态失败,构造Node节点,自旋+CAS插入同步队列尾部,并检查前驱节点是否为头节点且成功获取同步状态, 若是则当前节点设置为头结点,否则挂起等待被前驱节点唤醒。

释放时调用releaseShared(acquires)释放同步状态,之后遍历整个队列唤醒所有后继节点。

独占锁和共享锁实现区别:

1. 独占锁的state=1,同一时刻只有一个线程成功获取同步状态。共享锁state>1,取值由自定义同步器决定。

2.独占锁队列头节点运行完毕释放锁后唤醒直接后继节点,共享锁唤醒所有后继节点。

Q4: 可重入锁(ReentrantLock)的实现?公平锁fair和非公平锁nofair?

ReentrantLock和synchronized都是可重入锁,synchronized由JVM实现,重入锁实现时最主要的逻辑是判断上次获取锁的线程是否为当前线程,ReentrantLock基于AQS实现,提供公平锁和非公平锁两种方式。

公平锁的实现逻辑如下,与非公平锁的区别为判断当前节点是否存在前驱节点,只有等待前驱节点释放后才能获取锁。

原理是将state变量的高16位和低16位拆分,高16位表示读锁(共享锁),低16位表示写锁(独占锁)。

Q5: ReentrantReadWriteLock读写锁的实现?

写锁实现:

获取同步状态,分离低16位的写锁状态。同步状态不为0,则存在读锁或写锁。若存在读锁则不能获取写锁,若当前线程不是上次获取写锁的线程,也不能获取写锁。通过以上判断后对低16位(写锁state)进行CAS修改,并把当前线程设置为写锁获取线程。

读锁实现:

获取同步状态,计算高16位位读锁状态+1后的值,若存在写锁且当前线程不是写锁获取者,则获取读锁失败。若上述判断都通过,则利用CAS重新设置读锁的同步状态。

锁降级:当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。

Q6:JDK8新增StampedLock?

StampedLock: 读写锁的改进版。读写锁虽然读写分离,但是读和写之间依然冲突,读锁会完全阻塞写锁,使用的是悲观锁策略。如果有大量读线程,写线程很少时,容易引起写线程饥饿。

StampedLock提供一种乐观锁的读策略,类似于无锁操作,完全不会阻塞写线程。

StampedLock是不可重入的。

StampedLock三种访问模式:Reading, Writing, Optimistic reading(乐观读模式):优化的读模式。

StampedLock提供读锁和写锁的相互转换。

Q6: CopyOnWriteArrayList为什么是线程安全的容器(相对于ArrayList)?底层实现原理?

CopyOnWriteArrayList 有什么优点?如何保证写时线程安全的?适用场景是什么?

是ArrayList的并发实现。底层是用volatile transient声明地数组 array , 保证读可见性。

ReentrantLock重入锁保证写操作同步。

写时复制,读写分离,写的时候复制出一个新数组,完成增删改后将新数组赋值给array。

利用了快照的概念从而使读和迭代器遍历操作无须同步加锁。读取是当前数组快照,所以不会出现ConcurrentModificationException异常。

增删改都需要获得锁,并且锁只有一把,而读操作不需要获得锁,支持并发读。

Q7:为什么增删改中都需要创建一个新的数组,操作完成之后再赋值给原来的引用?

为了保证get的时候,都能获取到元素,如果在增删改的过程直接修改原来的数组,可能会造成执行读操作获取不到数据。

适用场景:读取和遍历多,并不适合高并发写的场景,因为数组拷贝非常耗时。

Q8: 假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?

1. 用join  2. 利用JUC包下的CountDownLatch. 3. 利用JUC包下的CyclicBarrer

Q9:ConcurrentHashMap原理?

原理概述:

在ConcurrentHashMap中通过一个Node<K,V>[]数组来保存添加到map中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化(懒加载),否则如果只是初始化一个ConcurrentHashMap对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容。

Put方法: 第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。

Get方法:取元素的时候,通过计算hash来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断key和key的hash,取出value值。

扩容:通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过hash值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。

Q10:线程池的作用?

1. 不使用线程池的时候,线程的创建和销毁都需要开销。使用线程池,池里的线程可以复用,不用每次都重新创建和销毁线程。

2. 提供资源限制和管理手段,可以限制线程个数、动态新增线程等。

Q11: ThreadPoorExecutor的数据结构,原理?

1. ctl:线程池状态, AtomicInteger原子对象,记录“线程池中任务数量"和”线程池状态”,共32位,高3位表示"线程池状态",低29位表示"线程池中的任务数量"。

有RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。

corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;

maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;

keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

unit:keepAliveTime的单位;

workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;

threadFactory:线程工厂,用于创建线程,一般用默认即可;

handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

AbortPolicy策略(抛异常),CallerRunsPolicy策略(重试添加当前的任务),DiscardOledestPolicy策略(抛弃旧的任务 ),DiscardPolicy策略(抛弃当前的任务)

execute方法:若使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。

原文地址:https://www.cnblogs.com/fyql/p/11235206.html

时间: 2024-11-06 03:42:28

[Java复习04] 并发 JUC的相关文章

[Java复习] 多线程 并发 JUC 补充

线程安全问题? 当多个线程共享同一个全局变量,做写的操作时,可能会受到其他线程的干扰.读不会发生线程安全问题. --  Java内存模型. 非静态同步方法使用什么锁? this锁 静态同步方法使用什么锁? 当前类的字节码文件 什么是ThreadLocal? ThreadLocal是给每个线程提供局部变量,每个线程可独立改变自己的副本,不会影响其他线程所对应的副本,解决线程安全问题. ThreadLocal底层原理是map集合. ThreadLocal内存泄漏问题? 由于ThreadLocalMa

java 复习001

java 复习001 比较随意的记录下我的java复习笔记 ArrayList 内存扩展方法 分配一片更大的内存空间,复制原有的数据到新的内存中,让引用指向新的内存地址 ArrayList在内存不够时默认是扩展为1.5倍 + 1个 ArrayList,LinkedList,Vector 区别 Vector内存扩展和ArrayList一样,不过Vector是默认扩展为2倍 Vector支持线程的同步,因此牺牲了访问性能 ArrayList,Vector都是使用数组实现,插入删除效率低 Linked

JAVA多线程和并发性知识点总结

转载请注明出处:http://blog.csdn.net/zhoubin1992/article/details/46861397 上次我总结了一份JAVA 面向对象和集合知识点总结: http://blog.csdn.net/zhoubin1992/article/details/46481759 受到了博友们的激励,这次对JAVA多线程和并发性相关知识点进行总结,方便各位博友学习以及自己复习之用. 一. 什么是进程.线程?线程和进程的区别? 1. 进程 当一个程序进入内存运行时,即变成一个进

java之高并发与多线程

进程和线程的区别和联系 从资源占用,切换效率,通信方式等方面解答 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务.在引入了线程的操作系统中,通常一个进程都有若干个线程,至少需要一个线程.下面,我们从调度.并发性. 系统开销.拥有资源等方面,来比较线程与进程. 1.调度 在传统的操作系统中,拥有资源的基本单位和独立调度.分派的基本单位都是进程.

Java实现高并发秒杀API--Service层2

今天完成了整个Java实现高并发秒杀API--Service层的学习: 1.接口的编码以及实现类的逻辑编写 2.利用spring ioc对Service进行管理 3.利用spring声明式事务对事务进行控制: 事务主要配置: <!--配置事务管理器 -->    <bean id="transactionManager"        class="org.springframework.jdbc.datasource.DataSourceTransacti

EFFECTIVE JAVA 第十章 并发

EFFECTIVE  JAVA  第十章  并发 66.同步访问共享的可变数据 *java语言规范保证读或写一个变量是原子的(可以保证返回的值是某个线程保存在该变量中的),除非这个变量的类型为long或double.(但并不保证一个线程写入的值对于另一个线程是可见) *synchronized修饰方法.synchronized代码块可以实现同步 *volatile修饰的变量只保证读取的是主存里最新的值而不是内存中该值的拷贝,使用volatile变量必须遵循(即变量真正独立于其他变量和自己以前的值

Java基础】并发 - 多线程

Java基础]并发 - 多线程 分类: Java2014-05-03 23:56 275人阅读 评论(0) 收藏 举报 Java 目录(?)[+] 介绍 Java多线程 多线程任务执行 大多数并发应用程序时围绕执行任务(task)进行管理的:所谓任务就是抽象的,离散的工作单元. 围绕执行任务来管理应用程序时,第一步是要指明一个清晰的任务边界.大多数应用服务器程序都选择了下面这个自然的任务辩解:单独的客户请求: 任务时逻辑上的单元: 任务 Runnable 表示一个任务单元(java.lang)

JAVA多线程和并发基础面试问答【转】

JAVA多线程和并发基础面试问答 多线程和并发问题是Java技术面试中面试官比较喜欢问的问题之一.在这里,从面试的角度列出了大部分重要的问题,但是你仍然应该牢固的掌握Java多线程基础知识来对应日后碰到的问题.(校对注:非常赞同这个观点) Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环境是一个包含了不同的类和程序的单 一进程.线程可以被称为轻量

[连载]Java程序设计(04)---任务驱动方式:工资结算系统

任务:还是在上一家公司,该公司将职员分为三类:部门经理.技术员和销售员.在发工资的时候,部门经理拿固定月薪8000元,技术人员按每小时100元领取月薪,销售人员按照500元底薪加当月销售额的4%进行提成,设计并实现一个工资结算系统. 分析:不管是部门经理.技术员还是销售员都具有员工的共同特征,可以先设计一个员工类(Employee),并将结算工资的方法设计为抽象方法,因为不同的员工有不同的结算工资的方式,需要进行多态实现.所谓的抽象方法就是没有方法体并被abstract修饰符修饰的方法.如果一个