java 并发编程

目录

1. 进程&线程是什么?

2. 并发&并行区别?

3.  JAVA中创建线程几种方式

4. Thread类中 stop、interrupt、isInterrupt、interrupted 区别

5.  线程生命周期

6. synchronized内置锁 与 volatile

7. 并发工具ThreadLocal

7.1 ThreadLocal 实现原理

7.2 ThreadLocal 内存泄漏 

7.3 ThreadLocal 使用注意

 8. 强、软、弱、虚 引用

9. wait、notify、notifyAll 使用

10. yield、sleep、wait、notify 锁的释放 

11. CountDownLatch & CyclicBarrier

 11.1 CountDownLatch 使用 

 11.2 CyclicBarrier 使用

11.3 CountDownLatch & CyclicBarrier 总结

12. Semaphore 使用

13. Exchange 数据交换器

 14. FutureTask & Callable 使用

14. Fork/Join 使用 

15. 原子操作CAS

15.1 什么是原子操作CAS

15.2 原子操作原理

15.3 原子操作存在的问题

15.4 Jdk中提供一些原子操作类 

16. 显示锁&AQS同步器

16.1 什么是显示锁

16.2 公平锁&非公平锁 

16.3 共享锁与排他锁&读写锁 

16.4 了解LockSupport 工具类中方法

16.5 AbstractQueuedSynchronized AQS同步器 

16.5.1 AbstractQueuedSynchronized 需要自己实现的方法 

16.5.2 AbstractQueuedSynchronized 其他方法

16.5.3 Node&ConditionObject

16.5.4 独占锁原理&流程

17. JDK中显示锁

17.1 ReentrantLock 如何避免死锁

17.2 ReentrantReadWriteLock 实现原理

18. JDK 1.7 ConCurrentHashMap 

18.1 数据结构

18.2 初始化 

18.3 Hash值计算

 18.4 元素定位

18.5 HashEntry[] table 扩容

18.6 如何保证线程安全?

18.7 Size实现

19. JDK1.8 ConcurrentHashMap

19.1 数据结构

19.2 初始化

19.3 Hash值计算

19.4 table 扩容

19.5 如何保证线程安全

19.6 Size 实现

19.7 ConcurrentHashMap 1.7&1.8区别

20. 跟多并发容器        

20.1 ConcurrentSkipListMap & ConcurrentSkipListSet

20.2 CopyOnWriteArrayList&ConpyOnWriteArraySet 写时复制容器

20.3 阻塞队列

21. 线程池

21.1 为什么使用线程池

21.2 线程池中参数含义

21.3 如何合理配置线程数量

21.4 JDK中默认线程池实现 Executors.new***()

22. PS


1. 进程&线程是什么?

  • 进程:操作系统分配资源的最小单位

  • 线程:cpu调用最小单位

2. 并发&并行区别?

  • 并行:并行 指某个时间的处理能力(4核cpu同一时刻能在运行4个线程那么并行就是4)

  • 并发:并发 指某个时间段的处理能力(晚上十一点到十二点之间某平台下了1000w订单,那么并发就是1000w)

3.  JAVA中创建线程几种方式

两种方式(jdk Thread类官方翻译两种 )

  • 继承Thread类 实现run方法 第一种

  • 实现Runable接口 第二种

  • 实现Callable接口+FutureTask

4. Thread类中 stop、interrupt、isInterrupt、interrupted 区别

  • stop:stop方法调用 会直接强制中断

  • interrupt:标记这个线程需要中断 仅仅是标记 需自己实现中断逻辑

  • isInterrupted:判断线程是否需要中断

  • interrupted:Thread类中的静态方法,判断当前线程是否需要中断,判断后会将当前线程中断状态标记为false(不建议使用)

5.  线程生命周期

  1. 新建:线程刚被创建new Thread()

  2. 就绪:start方法调用 排队等待cpu执行

  3. 运行:run方法被调用

  4. 阻塞:调用 wait、sleep

  5. 死亡:run方法执行完

     Thread类相关方法

  1. start 启动当前线程

  2. join 假设A线程调用B线程join(A放弃cpu执行权给B线程-start方法调用后调用否则失效)

  3. yield 释放当前cpu执行权 重新去排队获取(可能当前线程会立即获取cpu执行权)

  4. setDaemon 设置当前线程为守护线程(守护线程 finally快不一定执行)

6. synchronized内置锁 与 volatile

  • synchronized  锁定某个资源(类、方法、实例)使其同一时刻只能由一个线程访问

  • volatile            volatile 关键字修饰属性,每次都是查询直接内存而不是访问线程中内存副本                         (保正属性可见性)

PS:synchronized 关键字 锁的对象不能改变,改变会导致锁失效,不能保证线程安全(基本类型 参考反编译文件)

.java文件中一段代码

int num=0;

num++;

那么编译后 就会变成这段

Integer num=0;

num=Integer.valueOf(num.intValue());

valueOf 实际上是new 了一个对象

如果我们用synchronized(Integer num) 的时候锁就失效了,不是同一个对象

7. 并发工具ThreadLocal

7.1 ThreadLocal 实现原理

ThreadLocal get set remove 操作,都会去调用Thread.currentThread()拿到当前线程

而每个Thread对象中,都有ThreadLocal.ThreadLocalMap threadLocals 变量,

这个threadLocals map key是当前这个ThreadLocal 对象

所以每次ThreadLocal操作,最终会去操作当前线程中threadLocals 变量中的值

7.2 ThreadLocal 内存泄漏 

ThreadLocalMap 中 key 是一个ThreadLocal 的弱引用 当jvm内存不够用是可能会回收掉 key 导致,key 一直是空的,一直会保留在内存中,无法释放

threadLocal=null 会将ThreadLocal变成一个弱引用,可能会被gc释放(假设被gc放), 会导致ThreadLocalMap中存留key=null value=object 这样一个结构,导致永远无法被gc 释放

7.3 ThreadLocal 使用注意

ThreadLocal 每次使用后 不要忘记 使用remove 释放内存 或者 申明Thread变量为static f inal的 这样这个ThreadLocal 一直都是强引用

ThreadLocal set的值 确保不是共享的,会导致线程不安全

 8. 强、软、弱、虚 引用

  • 强引用:Object object=new Object() ;对象引用数量=0 才会被释放

  • 软引用:SoftReference jvm内存不足时,释放弱引用,内存还不够用,再释放软引用

  • 弱引用:WeakReference jvm内存不足时 一定会释放

  • 虚引用:

9. wait、notify、notifyAll 使用

  1. wait wait方法调用 当前线程会阻塞

  2. notify|notifyAll 唤醒阻塞线程 重新排队获取cpu执行权

生产者:

synchronized(object){

while(业务条件不满足)

//释放锁 阻塞当前线程

object.wait();

}

消费者:

synchronized(object){

//业务操作,操作完之后通知其他等待的线程,唤醒其他线程

object.notifyAll(); //同步代码块最后面调用

}

PS:建议使用notifyAll ,notify方法 只唤醒一个线程 

10. yield、sleep、wait、notify 锁的释放 

  • yield: 不释放

  • sleep: 不释放

  • wait: 释放锁

  • notify 不释放

11. CountDownLatch & CyclicBarrier

 11.1 CountDownLatch 使用 

假设有20个用户需要插入数据库,一个线程太慢我们用10个线程处理 每个线程插入2个(不一定是2个)

 11.2 CyclicBarrier 使用

假设同样需求20个用户 10个线程执行 每个线程插入2个

11.3 CountDownLatch & CyclicBarrier 总结

  • CountDownLatch: CountDownLatch 仅仅是一个多线程 计数器工具,跟调用多少次countLatch()方法有关,跟线程数量没有任何关系,只跟countLatch()方法调用次数有关,每调用一次countLatch()计数器-1;计数器>0 外面业务线程一直被阻塞,计数器=0 外面线程恢复正常,调用其他业务逻辑;

  • CyclicBarrier: 多线程协调工具,他的值=线程数量

12. Semaphore 使用

假设我们有个login业务,这个登录接口某个时间段(并发量)只允许10个用户登录

PS:Semaphore 使用注意点,可能有坑

由于代码逻辑问题,先调用release()释放许可证,再去调用acquire 去拿许可证

假设许可证数量 为10 ,由于程序逻辑bug

先调用release

再调用qcquire

会导致这个工具完全失效,永远不会限制许可证数量

13. Exchange 数据交换器

假设,有两个同学正在写作业,小明、小红,小明不学好作业没写,小红作业写好了,小明使坏

小明:小红 我作业写的很好,要不我们交换下吧,你拿我作业我能保证你明天拿一个小红花

小红:ヾ(≧▽≦*)o 好的好的!

 14. FutureTask & Callable 使用

假设业务场景:我与张三一起玩耍,不开心了, 张三不讲武德,上来就给了我一拳,我又打不过他,怎么办?我就回去叫我家大黄(汪汪汪~),让我家大黄替我教训他(咬他-启动线程托管,教训张三的事情我不管了),大黄放出去了,但是不能乱咬啊,如果咬错了,我要上门道歉啊,所以就得问大黄,他今天咬谁(返回值)了。

又假设业务场景,大黄今天发疯了,一直咬,一直咬个不停,只有我能管得住(良民大大的,不能把事情闹大,得把大黄阻止住)cancle()方法调用

PS:cancle()方法调用其实就是调用Thread.interrupt() 具体中断逻辑得自己实现

14. Fork/Join 使用 

  • RecursiveTask 有返回值

  • RecursiveAction 没返回值

假设计算0-100w数据可分割多个任务,交给多个线程去执行,再将执行结果合并

15. 原子操作CAS

15.1 什么是原子操作CAS

谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何cpu上下文切换 (来源百度百科)

15.2 原子操作原理

原子操作不是由操作系统 编程语言 支持,而是由硬件cpu支持的

如果原子操作不成功,他会一直循环直到成功

15.3 原子操作存在的问题

  • ABA问题 变量的修改 不知道有没有修改过 修改过几次?

  • 开销问题 原子操作再一个do while中(操作不成功会一直循环,直到成功)

  • 只能保证一个共享变量的原子操作

15.4 Jdk中提供一些原子操作类 

  • 基本类型 AtomicBoolean AtomicInteger AtomicLong ......

  • 数组 AtomicIntegerArray AtomicLongArray AtomicReferenceArray ......

  • 引用类型

    • AtomicReference

    • AtomicMarkableReference 提供是否被修改过的原子操作

    • AtomicStampReference 提供版本的原子操作

  • filed字段类型 AtomicReferenceFiledUpdate AtomicIntegerFiledUpdate AtomicLongFieldUpdate ......

16. 显示锁&AQS同步器

16.1 什么是显示锁

显示锁(编程式锁)上锁、释放锁,由程序员通过coding决定,是再代码中能看见的

16.2 公平锁&非公平锁 

  • 公平锁 ABCD四个线程抢锁,Z线程占用锁,这时候又new了一个E线程(拿到 cpu执行权,拿不到锁 先挂起),这时候Z线程释放锁,那么获取锁会 从我们队列开头(线程A)去拿锁,A拿锁又要进行cpu上下文切换,A 拿到cpu执行权后再去拿锁。这种拿锁方式是一个队列形式(A-B-C-D- E)

  • 非公平锁 ABCD四个线程抢锁,Z线程占用锁,这时候又new了一个E线程(拿到 cpu执行权),这时候正好Z线程释放锁,那么E线程也刚好获取锁,E 线程 正好获取到。这种拿锁方式跟队列无关

  • 非公平锁性能>公平锁

  • ReentrantLock 默认是非公平锁

16.3 共享锁与排他锁&读写锁 

  • 共享锁 允许多人上锁

  • 排他锁|独占锁 不允许其他人上锁

  • 读锁 允许其他读线程上锁                      共享锁

  • 写锁 不允许其他任何读写线程上锁        排他锁

  • 读锁,n个读线程上锁,写锁,只能一个线程上锁,读写互斥(上了读锁不能上写锁,上了写锁就不能上读锁)

16.4 了解LockSupport 工具类中方法

  • park*() 阻塞线程

  • unpark*() 唤醒线程

16.5 AbstractQueuedSynchronized AQS同步器 

AbstractQueuedSynchronized 是java 提供给开发者 实现 显示锁的一个模板

16.5.1 AbstractQueuedSynchronized 需要自己实现的方法 

  • tryAcquire() 获取排他锁

  • tryRelease() 释放排他锁

  • tryAcquireShared() 获取共享锁

  • tryReleaseShared() 释放共享锁

  • isHeldExcluively() 是否属于排他锁|独占锁

16.5.2 AbstractQueuedSynchronized 其他方法

  • getState 获取同步器状态

  • setState 设置同步器状态

  • compareAndSetState 使用CAS方式设置同步器状态

  • setExclusiveOwnerThread 设置当前占用锁的线程

16.5.3 Node&ConditionObject

  • Node 当一个线程拿锁失败打包一个Node对象(双向链表)以便其他线程释放锁唤醒其他线程

    • waitStatus几种状态

      • CANCELLED 线程等待超时,或者被中断(从队列移除)

      • SIGNAL 表示需要通知后续节点拿锁

      • CONDITION 当前节点处于等待队列中

      • PROPRGTE 表示当前节点被唤醒后,同时也要唤醒后续节点

      • INITALIZE =0 初始化状态

    • head节点 头节点会一直自旋去拿锁

    • tail节点 每当有线程获取锁失败 放到tail节点

  • ConditionObject 当有线程去释放锁,通过ConditionObject 去唤醒在等待 队列 (Node)中线程

    • singal*() 唤醒线程

    • await*() 阻塞线程

16.5.4 独占锁原理&流程

释放锁流程:

tryRelease() 返回true 释放锁成功,唤醒头节点的下一个节点

拿锁流程:

  1. tryAcquire()调用成功直接返回,调用不成功打包成Node

  2. addWaiter() 将当前获取锁失败的节点 加到尾节点

  3. acquireQueued() 拿到自己前置节点 如果前置节点等于队列头节点,一直自旋拿锁,直到拿锁成功,拿到锁之后将自己设置为头节点。如果前置节点不是头节点,wait阻塞当前线程

17. JDK中显示锁

  • Lock接口 JDK实现锁的规范接口

  • ReentrantLock 一个可以避免死锁的显示锁(迭代锁)

  • ReentrantReadWriteLock 读写锁

17.1 ReentrantLock 如何避免死锁

加锁:

解锁:

17.2 ReentrantReadWriteLock 实现原理

ReentrantReadWriteLock 关键在于 AQS 同步器

ReadLock WriteLock 构造函数都需要一个同步器,而这个同步器是使用了同一个的

而这个同步器的一个state 状态 是int类型(Java 一个int类型 是 32bit)

state 分左半边(高位)表示共享锁|读锁数量 右半边(低位)排他锁|写锁 数量

读锁:拿读锁时判断(低位不等于0)是否有排他锁|写锁,没有排他锁时 才能拿到,有排他锁加入队列里,再阻塞当前线程

写锁: state 不等于0 说明有锁 ,什么锁 不知道 去计算w(写锁|排他锁)数量

第一种 情况:w写锁数量等于0 说明有读锁存在 返回false 阻塞

第二种 情况:w不等于0(有写锁)如果当前线程不等于持有锁线程 返回false 阻塞

能走第二个if:说明 当前这个写锁 是自己加的

state 等于0 说明读锁写锁都没有 直接原子操作去维护状态和持有锁线程

18. JDK 1.7 ConCurrentHashMap 

18.1 数据结构

Concurrent 中 segments属性 Segment数组 组成

Segment 中 table属性 HashEntry组成

Segment 继承 ReentrantLock 本身是一个锁

HashEntry 是一个单向链表

18.2 初始化 

ConcurrentHashMap Segment默认大小为16 默认并发量为16

Segement中table 默认大小为2

ConcurrentHashMap Segment默认初始化1个

18.3 Hash值计算

key.hashCode 再进行散列(Wang/Jenkins hash)

 18.4 元素定位

通过自己hash()算法 再与segementShift 和 segmentMask 计算取余 定位具体segement

再通自己hash()算法 与 segment中table 数量计算取余 定位具体 HashEntry

遍历HashEntry 判断 key 是否相等

18.5 HashEntry[] table 扩容

table 扩容 每次扩容是翻倍x2

扩容后 会 reHash()重新计算(部分)HashEntry落点

18.6 如何保证线程安全?

put操作时由于Segement extents ReentrantLock 代表他自己本身就是一个锁

put时 会将自己上锁保证同一时刻只能有一个线程put成功

可见性 由 JDK中 volatile 关键字实现

18.7 Size实现

不加锁情况便利每个元素 (2次)

如果两次的值一样 返回

如果两次值不一样 锁住所有Segment 在进行统计返回

19. JDK1.8 ConcurrentHashMap

19.1 数据结构

Node 数组

Node=链表|红黑树结构TreeBin

链表长度为8时,转换为红黑树结构

19.2 初始化

1.8中 没做任何事情

初始容量也是16

table 初始化在put操作时 判断 table 是否为空 仅new 了一个 Node[]

初始化table后 sizeCtl 默认为(16*0.75)12

Node初始化 每次put时 定位Table[i] 为空 再初始化Node

19.3 Hash值计算

再散列(散列规则与1.7不一样Wang/jenkins)

同样是再散列

19.4 table 扩容

翻倍扩容

扩容时判断 Node节点大小 >8 转树结构 <8 转链表结构

多个线程put 时,会由多个线程并发扩容

19.5 如何保证线程安全

通过Synchronized 内置锁,锁住Node

可见性 同样使用volatile

19.6 Size 实现

内部 使用 ConterCell [] 数组 count累加 根据当前线程计算坐标于哪个ConterCell

然后会 进行快速一次 cas操作

如果失败会调用fullAddCount 方法 自旋cas

而size方法 统计 就是 遍历 这个ConterCell数组 累加value属性

19.7 ConcurrentHashMap 1.7&1.8区别

  • 1.7 使用 链表结构

  • 1.8 使用 链表+红黑树结构

  • 初始化

    • 1.7 一开始就实例化Segrament

    • 1.8 什么事都没做,初始化在put时候实时的

  • hash值计算

    • 两者都是 再hash 1.7 与 1.8 再散列算法不一样

  • 扩容

    • 都是按照双倍扩容

    • 1.8中使用多线程扩容,到了阈值75%就扩容 1.7>阈值

  • 如何保证线程安全

    • 1.7 使用 ReentrantLock 锁住 segrament

    • 1.8 使用 Synchronized 内置锁

    • 两个put性能几乎一样 可能1.8慢点 1.8中还要维护count

  • size()

    • 1.7 会先遍历整个map 2次,如果2次结果不一样,锁住整个map 再统计

    • 1.8 put时根据当前线程去维护CounterCell数组,统计时遍历 CounterCell数组

    • 两者都是弱一直性

    • 1.8 比 1.7 快 准确性1.7高一点(锁整个map) 可用性1.8高一点,因为1.8中仅仅是遍历CounterCell数组

20. 跟多并发容器        

20.1 ConcurrentSkipListMap & ConcurrentSkipListSet

  1. TreeMap TreeSet 并发版本

  2. SkipList 以空间换时间的一种数据结构 Skip List--跳表(全网最详细的跳表文章没有之一) - 简书

  3. redis zSet 使用

  4. 性能接近于 平衡二叉树

  5. 使用更多空间 保存 索引(空间换时间

20.2 CopyOnWriteArrayList&ConpyOnWriteArraySet 写时复制容器

  1. 原理 调用add方法 使用ReentrantLock 上锁,复制一份List 进行add

  2. 读操作任何时刻不影响(不会加锁 ,只有写操作加锁)

  3. 只能保证最终一直性 ,不能保证实时一直性

  4. 读多写少场景,对数据实时性无要求(黑白名单)

20.3 阻塞队列

  • 无界阻塞队列 队列大小没有限制

  • 有界阻塞队列 队列大小有限制

  • ArrayBlockingQueue

    • 由数组结构组成的有界阻塞队列

    • 按照先进先出原则

    • 使用一个锁

    • 要求设置初始化大小

  • LinkedBlockingQueue

    • 由链表结构组成的有界阻塞队列

    • 按照先进先出原则

    • 可以不设置大小 默认= Integer.MAX_VALUE;

    • 使用两个锁

  • PriorityBlockingQueue

    • 支持优先级排序的无界阻塞队列

    • 默认按照自然顺序

    • 可以实现自己的排序规则

  • DelayQueue

    • 使用优先级排序的无界阻塞队列

    • 支持延时获取的队列

  • SynchronousQueue

    • 一个不存储数据的阻塞队列|同步队列

    • 每一个put操作都要等待一个take操作

    • 要求生产者&消费者 处理能力相等

  • LinkedTransferQueue

    • 由链表结构组成的无界阻塞队列

    • 可以将消息直接发送给消费者,省去放入队列操作

    • transfer() 必须消费者消费后才会返回,反之阻塞

    • tryTransfer() 无论消费者是否接受,方法立即返回

  • LinkedBlockingDeque

    • 由双向链表结构组成阻塞队列

    • 可以从队列 头&尾 拿消息

21. 线程池

21.1 为什么使用线程池

线程创建-上下文切换(wait notify)时非常消耗cpu的

项目中如果频繁去创建线程,用一次就丢掉,会浪费大量cpu性能

如果将n个线程做统一管理 可以避免频繁创建线程而浪费cpu性能

21.2 线程池中参数含义

  • corePoolSize

    • 当由任务提交时,会创建corePoolSize个线程执行任务

  • maxnumPoolSize

    • 有任务提交时 当前线程数量>maxnumPoolSize 时不会创建线程,将任务放到阻塞队列中

  • BlockingQueue<Runnable> workQueue 具体队列的实现

  • RejectedExecutionHandler handle 阻塞队列满时采用哪种方式处理

    • Jdk默认实现策略

    • DiscardOldersPolicy 丢弃队列最靠前任务执行当前任务

    • AbortPolicy 抛出异常

    • CallerRunspolicy 谁提交任务由谁执行

    • DiscardPolicy 直接丢弃当前任务

  • keepAliveTime 线程空闲时间 到期回收

21.3 如何合理配置线程数量

  • 三种场景

    • CPU密集型 大量cpu计算 (正则)

    • IO密集型 大量下载文件,保存文件(磁盘,网络)

    • 混合型 计算+IO

  • cpu密集型

    • cpu核心数量+1

  • IO密集型

    • 最佳线程数=NCPU*UCPU*(1+W/C)

    • NCPU 处理器核心数

    • UCPU CPU期望利用率(0~1)

    • W/C 等待时间比计算时间 比率

    • 不确定W/C 2*CPU核心数

    • 等待时间与计算时间在Linux vmstat或者top命令查看

  • 混合型

    • 尽量拆分处理

21.4 JDK中默认线程池实现 Executors.new***()

  • FixedThreadPool

    • 创建固定线程数量的线程池

    • 内部使用LinkedBlockingQueue

  • SingleThreadExecutor

    • corePoolSize&maxnumPoolSize 都等于1 单线程的线程池

    • 内部使用LinkedBlockingQueue

  • CachedThreadPool

    • 线程最大值=Integer.MAX_VALUE();

    • 内部使用同步队列SynchronousQueue

    • 使用需谨慎

  • WorkStealingPool

    • 默认大小等于当前cpu逻辑线程数

    • 使用Fork/Join

  • ScheduledThreadPoolExecutor

    • 延时任务式线程池

    • 内部使用DelyedWorkQueue

    • schedule() n时间后执行一次

    • scheduleWithFixedDelay() 每n时间后执行

    • 前一个任务超时影响后一个任务执行时间

    • Runable 中 发生异常会影响整个任务队列 (catch 异常)

  • CompletionService & CompletionFutuer

22. PS

仅仅自己对并发编程一些理解,不喜勿喷。

原文档地址 https://docs.qq.com/doc/DSEJjVnBQZW1XWWti

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值