IT评测·应用市场-qidao123.com技术社区

标题: 多线程进阶 [打印本页]

作者: 天津储鑫盛钢材现货供应商    时间: 2025-4-12 06:56
标题: 多线程进阶
进阶的内容,就关于线程的面试题为主了,涉及到的内容在工作中使用较少,但面试会考!!!
锁的策略

加锁的过程中,在处理冲突的过程中,涉及到的一些差别的处理方法,此处的锁策略,并非是 Java 独有的,需要重点明白一些相关的概念。
1. 乐观锁 和 灰心锁

这是两种差别的锁的实现方式
乐观锁: 在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候,就不会做太多的工作。加锁过程中做的事变比力少,加锁的速度可能就更快,但是更轻易引入一些其他的题目。(可能会消耗更多的 CPU 资源)
灰心锁:在加锁之前,预估当前出现锁冲突的概率比力大,因此在进行加锁的时候,做的事变就会更多,加锁的速度可能更慢,但是整个过程中不轻易出现其他的题目。
2. 轻量级锁 和 重量级锁

轻量级锁:加锁的开销小,加锁的速度更快 ==》 轻量级锁,一样平常就是乐观锁
重量级锁:加锁的开销大,加锁的速度更慢 ==》 重量级锁,一样平常便是灰心锁
   轻量级和重量级锁:是加锁之后,对结果的一种评价
  乐观锁和灰心锁:是在加锁之前,对未发生的事变进行的一种评估
  但整体来说,这两种角度,描述的是同一件事变
  3. 自旋锁 和 挂起等待锁

自旋锁:是轻量级锁的一种典范实现。进行加锁的时候,搭配一个 while 循环,假如加锁成功,自然循环竣事。假如加锁不成功,不是阻塞放弃 CPU,而是进行下一次循环,再次尝试获取到锁。
这个反复快速实行的过程,就称为“自旋”,一旦其他线程释放了锁,就能第一时间拿到锁,同时,如许的自旋锁,也是乐观锁,使用自旋锁的条件:就是预期锁冲突的概率不大,其他线程释放了锁,就能第一时间拿到锁。万一当前加锁的线程特殊多,自旋的意义就不大了,白白浪费 CPU 了
挂起等待锁:就是重量级锁的一种典范实现,同时也是一个灰心锁,在进行挂起等待的时候,就需要内核调度器介入了,这一块要完成的操作就很多了,真正获取到锁要花费的时间也就多了。但这个锁是可以适应于锁冲突激烈的环境。
   举个栗子:我是一个资深舔狗,每天都会向女神问候:早安午安晚安。有一天,我向女神表明,“女神女神,你能不能做我的女朋友”(尝试加锁),女神给了我一个字:“滚”。
  被女神拒绝之后,我有两种处理方式:
          1. 放弃了~~ 从此再也不联系女神了 ==》 我不想联系女神了,进入了阻塞等待,我就把 CPU 让出来了,可以安心学习了(但嘴上说,再也不联系了,身体上还是很诚实。)某一天,我通过其他途径,听说女神分手了,我的心思又活泛起来了,情不自禁又来找女神了,又尝试对女神加锁~~ (假如女神确实分手了,我是有可能上位的)
          但注意:这种策略中,我们获知女神分手了之后这个消息,一样平常是会在发生这个事变之后几个月才听说,我再尝试加锁,就不会像自旋锁那么快~~
          线程一旦进入阻塞,就需要重新到场系统的调度,什么时候可以或许再调度上 CPU 就是未知数了~~ 
          但是这种策略的好处是,在阻塞的过程中,把 CPU 的资源让出来了,可以乘隙做一点其他的事变~~(即使有很多人都是女神的备胎,也没关系,反正我是阻塞等待,我能乘隙去学习,当备胎 2 3 4 5 号他们和女神相处,我依然学我的,他们全分手了,我再去~~)
          
          2. 坚信一个道理:只要锄头挥的好,没有墙角挖不倒。仍旧每天向女神问候早安午安晚安,时不时的再表明一次。这种方式,就是自旋锁。
          当然这种环境,一旦女神分手了,我的机会就来了,就有很大的可能性,攻其不备,一举加上锁~~~
          加锁消耗的时间就比力短,这边一释放,我立即就加上锁。但是缺点就是比力消耗 CPU,每天都得花时间和女神交流(导致我这边就没故意思干别的事变)  
  自旋锁也是乐观锁,预估了锁竞争不太激烈才能使用,试想一下,假如女神的备胎不止我一个,有十几个兄弟都是备胎,也和我一样每天早安午安晚安一样问候,此时,女神就算分手了,也不一定轮得到我~~
Java 中的 synchronized 呢

 Java 中的 synchronized 算那种环境呢? ==》  synchronized 具有自适应的本领!!!
synchronized 在某些环境下,乐观锁 / 轻量级锁 / 自旋锁,在某些环境下,灰心锁 / 重量级锁 / 挂起等待锁
synchronized 的内部会自动的评估当前锁冲突的激烈程度。
假如当前锁冲突的激烈程度不大,就处于 乐观锁 / 轻量级锁 / 自旋锁
假如当前锁冲突的激烈程度很大,就处于 灰心锁 / 重量级锁 / 挂起等待锁
   上面 synchronized 会自适应的本质就是 JVM 的大佬们(他暖,我哭),为了让我们这些菜鸟程序员轻松一些,引入的一些优化方式,我们其实并不需要知道这几个锁策略具体是啥意思,就无脑用 synchronized 一样平常就不会有什么题目,并且还很高效~~~
  4. 普通互斥锁 和 读写锁

普通读写锁:就雷同于 synchronized 操作会涉及到 加锁 和 解锁
读写锁: 这里的读写锁,就把加锁分为两种环境了
        1. 加“读”锁
        2. 加“写”锁“
读锁和读锁之间,不会出现锁冲突(不会阻塞);写锁和写锁之间,会出现锁冲突(会阻塞);读锁和写锁之间,会出现锁冲突(会阻塞)
一个线程加 读锁 的时候,另一个线程,只能读,不能写
一个线程加 写锁 的时候,另一个线程,不能写,也不能读
   为什么要引入读写锁呢?
  假如两个线程读,自己就是线程安全的!!!不需要进行互斥!!!
  假如使用 synchronized 这种方式加锁,两个线程读,也会产生互斥,产生阻塞...如许的话又没必要,又会对性能产生一定的丧失~~
  完全给读操作不加锁,也不可,就怕一个线程读操作,一个线程写操作,可能会读到写了一样平常的数据...
  读写锁,就可以很好的办理上述题目~~~ 读写锁就能把这些并发读之间的锁冲突的开销给省下,对于性能的提升十分显着~~~
  在标准库中,也提供了专门的类,实现读写锁(本质上还是系统提供的读写锁,提供 API,JVM 中封装了 API 给 Java 程序员使用~~~),这里暂不介绍~~~
  5. 公平锁 和 非公平锁

和前面提过的”线程饿死“有一点关系
公平锁:服从”先来后到“,谁先来的,谁就在锁被释放后先获得
非公平锁:不服从”先来后到“
举个栗子:
当女神和男票恋爱中,兄弟们都在当备胎等待,A 兄弟已经追女神 1 年,B 兄弟追女神 1 个月,C 兄弟昨晚上才开始追女神

当女神分手后:公平锁的环境下,A 号大兄弟是最开始舔的,他就嗖嗖的上位追女神了,剩下两位老哥就继续等着

非公平锁:三位大兄弟不管谁先开始舔的,对着女神就是一拥而上~~~

注意:
        操作系统内部的线程调度就可以视为是随机的,假如不做任何额外的限制,锁就好坏公平的。假如要想实现公平锁,就需要依靠额外的数据布局,来记录线程们的先后顺序
        公平锁和非公平锁并没有优劣之分,关键还是看使用场景
6. 可重入锁 和 不可重入锁

可重入锁:”可以重新进入的锁“,即允许同一个线程多次获取到同一把锁,且不会死锁。比如一个递归函数中内里又加锁操作,递归的过程中,这个锁假如不会阻塞自己,那么这个锁就是可重入锁(因此,可重入锁也叫做递归锁)。可重入锁中需要记录持有锁的线程是谁,加锁的次数的计数
不可重入锁:一个线程针对一把锁,连续加锁两次,会产存亡锁。
Java 里只要一 Reentrant 开头命名的都是可重入锁,而且 JDK 提供的所有线程的 Lock 实现类,包罗 synchronized 都是可重入的锁
明白“把自己锁死”:
一个线程没有释放锁,然后又尝试加锁

按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁,但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥也不相干了,也就无法进行解锁操作,如许就会死锁

上面描述的锁,即为不可重入锁

上面的“锁策略”就是一大堆的名词表明,我们需要对这些词,有一个概念上的认识即可。
比方,对于 synchronized 来说
        1. 乐观锁 / 灰心锁 自适应
        2. 轻量级锁 / 重量级锁 自适应
        3. 自旋锁 / 挂起等待锁 自适应
        4. 不是读写锁
        5. 非公平锁
        6. 可重入锁
对于系统原生的锁(Linux 提供的 mutex 这个锁)
        1. 灰心锁
        2. 重量级锁
        3. 挂起等待锁
        4. 不是读写锁
        5. 非公平锁
        6. 不可重入锁
synchronized 内部的工作原理

synchronized 的内部优化好坏常好的,大部门环境下,使用 synchronized 不会有什么题目
但 synchronized 的加锁过程,尤其是“自适应” 的过程,到底是怎么回事呢???
当线程实行到 synchronized 的时候,假如当前这个对象处于未加锁的状态,就会经历以下过程:
1. 偏向锁阶段

核心思想是:懒汉模式,能不加锁,就不加锁,能晚加锁,就晚加锁。所谓偏向锁,并非是真的加锁了,而是做了一个非常轻量的标记。
换句话说,就是搞暧昧,就是偏向锁,只是做一个标记,并没有真的加锁(也不会有互斥),但假如发现有其他线程,来和我竞争这个锁,就会在另一个线程之前,先把锁获取到。然后就会从偏向锁升级为轻量级锁(升级为轻量级锁就是真加锁了,存在互斥了~)
假如在搞暧昧的过程中,没有人来竞争,就把加锁如许的操作就完全省略了~~~
非必要 不加锁 在碰到竞争的时候,偏向锁并没有提高效率,但是,假如在没有竞争的环境下,偏向锁也就大幅度的提高了效率~~
   举个栗子:
  假设我是一个美女(好看 & 有才华 & 琴棋书画样样俱全 & 故意计 & 时间管理大家)
  我谈了一个男票,谈了一段时间之后,假如我讨厌了,想换一个男朋友,效率是比力低的,要做两个事变:
          1. 想办法和现在的男朋友分手~~~(各种打骂,没事找事,冷暴力...)
          2. 和下一个小哥哥培养感情
  我们前面引入的池的概念就是对第二件事的优化 --> “备胎池”
  那怎么对第一阶段进行优化呢?? ==》 搞暧昧~~~
  当我最开始和这个小哥哥谈恋爱的时候,我就不和他确认关系~~~
  有情侣之实,但是无情侣之名,我们俩每天在一起干的事变,都是情侣之间的事变,但每当小哥哥提到说,我们是什么关系的时候,我就笑而不答,或者扯开话题。
  假如将来有一天,我厌倦了,我就直接可以把他拉黑,踹开一边即可,假如他再来胶葛我,我就补充一句,“我们只是朋友~~~”
  当前我在和这个小哥哥搞暧昧的时候,假如我突然发现,有其他的妹子,也在试图接近我家小哥哥,这个时候,我就立即和小哥哥确定关系,并且让其他妹子,离我家哥哥远点~~~(一切尽在把握之中,哥哥当然不会拒绝我~)
  2. 轻量级锁阶段

(假设有竞争 但不多)
此处是通过自旋锁的方式来实现的。
上风: 另外的线程把锁释放到了,就会第一时间拿到锁
劣势: 比力消耗 CPU 资源
与此同时, synchronized 内部也会统计,当前这个锁对象上,有多少个线程在到场竞争,这里当发现到场竞争的线程比力多了,就会进一步的升级到重量级锁(对于自旋锁来说,假如同一个锁竞争者很多,大量的线程都在自旋,整体 CPU 的消耗就很大了)
补充:偏向锁标记,是锁对象内里的一个属性,每个锁对象都有自己的标记,当这个锁首次被加锁的时候,先进入偏向锁,假如这个过程中,没有涉及到锁竞争,下次加锁还是先进入偏向锁,一旦这个过程中升级成为轻量级锁了,后续再针对这个对象加锁,都是轻量级锁了(跳过了偏向锁~~~)
3. 重量级锁阶段

此时拿不到锁的线程就不会再继续自旋了,而是进入“阻塞等待”,让出 CPU(不会让 CPU 的占用率太高)当线程释放锁的时候,就会由系统内核随机叫醒一个线程来获取锁了
   到底多少个线程算多呢???这是 JVM 源码内里的,我们要重点关注的是,会有这种”策略“,参数是可以随时调整的,策略是通用的!
  锁消除

也是 synchronized 中内置的优化策略,是编译器优化的一种方式,编译器在编译代码的时候,假如发现这个代码,不需要加锁,就会自动的把锁给干掉~~~
但这里的优化是比力守旧的,比如,就只有一个线程,在这一个线程里加锁了,或者说,加锁代码中,并没有涉及到”成员变量的修改“,只是一些局部变量的修改(假如加锁代码块中只涉及局部变量的修改,而没有对成员变量(类的属性)进行修改,也不需要加锁。这是由于局部变量是线程私有的,每个线程都有自己独立的副本,不会出现多个线程同时访问同一个局部变量的环境,也就不会有数据竞争题目。),是不需要加锁的。
其他模棱两可的环境,编译器也不确定的时候,是不会去消除的。
锁消除 ==》 针对 一眼看上去就完全不会涉及到线程安全题目的代码,就可以或许把锁消撤除
锁粗化

会把多个细粒度的锁,归并成一个粗粒度的锁
(synchronized { }  大括号内里包罗的代码越少,就会认为锁的粒度越细,包罗的代码越多,就会认为锁的粒度越粗)
通常环境下,让锁的粒度细一些, 是有利于多个线程并发实行的,但也有点时候,希望锁的粒度粗一些~~~

如上图,在代码实行的过程中,涉及很多加锁息争锁,即锁的粒度较细,每次加锁都是有可能涉及到阻塞的
如下图,编译器就会把三次细锁粒度的锁归并成一个粗粒度的锁了 ==》 粗化也是为了提高效率~~~

小结:

synchronized 背后是涉及了很多很多的“优化手段”
        1. 锁升级 ==》 偏向锁 -> 轻量级锁 -> 重量级锁
        2. 锁消除 ==》 自动干掉不必要的锁
        3. 锁粗化 ==》 把多个细粒度的锁归并成一个粗粒度的锁,减少锁竞争的开销
这些机制都是在内部默默发挥作用的,是 JVM 的大佬为我们默默奉献的(他暖,我哭~~~
CAS

什么是 CAS 

CAS:Compare and swap,字面意思:“比力并交换”,是一个特殊的 CPU 指令(严格的说,和 Java 无关)(JVM 中 关于 CAS 的 API 都是放在 unsafe 包里的,unsafe 即不安全的)
一个 CAS 就会涉及到一下操作:我们假设内存中的原数据为 V,寄存器中的值是 A,需要修改的是新值 B,会有三个操作:
        1. 比力原数据 V 和寄存器中的值 A 是否相等 (比力)
        2. 假如比力相等,把 B 写入 V。(交换)
        3. 返回操作是否成功
CAS 伪代码


此中,address 是内存地点中的值 expectValue 是寄存器中的旧值,swapValue 是寄存器中的新值。 if 语句中判断条件是,比力 address 内存地点中的值,是否和 expected 寄存器中的值相同,假如相同,就把 swap 寄存器的值和 address 内存中的值,进行交换,返回 ture;假如不相同,则啥都不敢,返回 false。(说是交换,也可以明白为“赋值”,我们往往只关注内存里最终的值,寄存器用完就不需要了~~)
CAS 一条 CPU 指令 就可以完成我们上述的功能 ==》单个 CPU 指令,自己就是原子的
CAS 的线程安全题目

基于 CAS 指令,就给线程安全题目的代码,打开了一个新世界的大门!!!我们之前为了实现线程安全,往往都是依靠加锁来保证的,但一旦有了加锁,就会导致阻塞,从而就会引起性能降低。
使用 CAS,不涉及加锁,就不会导致阻塞,公道使用也是可以保证线程安全的 ==》 无所编程(是多线程编程中的一个特殊技巧)
CAS 自己的 CPU 指令,操作系统又对指令进行了封装,JVM 又对操作系统提供的 API 封装了一层,有的 CPU 可能会不支持 CAS (但我们 x86 这种主流 CPU 都是没题目的)
Java 中的 CAS 的 API 放到了 unsafe 包内里(这内里的操作, 涉及到一些系统底层的内容,使用不妥的话可能会带来一些风险,一样平常不建议直接使用 CAS)
Java 的标准库,对于 CAS 又进行了进一步的封装,提供了一些工具类,供程序员们使用。
最主要的一个工具,叫做 “原子类”

在这个类中,就进行了一些封装,比如对 Integer 和 Long进行了封装,针对如许的对象进行多线程修改,就是线程安全的了。

示例代码:

这个代码就是我们之前典范的多线程可能不安全的代码,假如定义 count 的时候,是使用 private static int count = 0,然后在线程中均使用 count++的话,就是线程不安全的!!!
但是,假如是用 AtomicInteger 定义 count(此时是一个对象了),初始值传入参数为 0,然后再线程中,使用 getAndIncrement 方法,取代了 后置++,此时这个方法,就是通过 CAS 的方式实现的,这里的代码,就没有加锁,但也能保证线程的安全!!(并且这个代码要更为高效,没有锁,也就没有阻塞,也就不会消耗效率)
   之前的 count++ 是三个指令(多线程的三个指令,会相互的穿插实行,引起线程不安全,之前加锁,就是为了能让三个指令称为原子的)此处,这里的 getAndIncrement 对变量的修改,是 CAS 指令,CAS 指令自己就只是一条 CPU 指令,天然就是原子的
  原子类自增的源代码:

标准库中的代码,看起来有点复杂,我们可以用一段伪代码来明白:

再这段伪代码中,oldValue 渴望是一个放在寄存器内里的值,这个值就是初始化成 AtomicInteger 内里保存的整数值 value,假如内存地点的值 value 和 寄存器内里的值 oldValue 比力相同,则可以交换,oldValue + 1 和 value 交换,然后循环竣事,此时 value 已经更新成 value + 1了,假如没成功,就再来一次,直到成功为止
画图讲解:
如下图为多线程环境下:
最开始我们初始化 value 为 0
多线程实行 ==》 

t1 线程,将 value 赋值给 oldValue

然后调度到 t2 线程实行,t2 线程也赋值 oldValue 为 0

然后 t2 线程进入 while 循环,比力 value 和 oldValue 此时均为 0,此时还有一个寄存器三,为 oldValue + 1(即此时为 1)

会将 oldValue + 1 寄存器中的值 1 和 内存中的 0 进行交换

如许就通过线程 2,将 value 从 0 -> 1,将 value 重新赋给 oldValue 返回 oldValue。

然后 t1 线程又被调度上来了,再实行 t1 线程

注意,这个时候,t1 线程中再实行,value 的值已经过 0 变为 1了,但此时寄存器 1 的 oldValue 记录的仍旧是 0,这里就会发现 value 和 oldValue 差别,意味着在 CAS 之前,另一个线程修改了 value(通过这个方式,能辨认出是否有人修改)所以就不会进行交换,进入while 循环,将 value 的值,重新赋给 oldValue

然后再进入 while 循环,这时候 value 和 oldValue 的值就相同了,然后还有另一个寄存器存储 oldValue + 1

再进行交换,将 value 从 1 -> 2,然后将 value 再赋值给 oldValue 返回 oldValue

   之前的线程不安全,是内存变了,但是寄存器中的值没有跟着变,接下来的修改操作就会出错了,但使用 CAS 这种方式,通过一次内存和寄存器值的比力,就能确保辨认出内存的值是不是变了,不会,才会进行修改,假如变了,就会重新读取内存的值,确保是基于内存中的最新的值进行修改。非常巧妙的把之前的线程安全题目就办理了~~~
  实现自旋锁

基于 CAS 实现更灵活的锁,获得到更多的控制权
自旋锁伪代码:

当 owner 不为 null 的时候,意味着锁已经被其他线程持有。此时,当前尝试获取锁的线程并不会进入阻塞状态(不会像传统锁机制下调用 wait 方法一样阻塞)而是在这个 while 循环中不停的实行(“忙等”)。持续的尝试 CAS 操作区获取锁,只要获取不成功就不停循环,不放弃 CPU 资源,但也不到场 CPU 调度中的线程上下文切换等调度流程,制止了调度开销~~~但是这种方式的缺点就是自旋的锁会不停占用 CPU,需要消耗更多的 CPU 资源。
CAS 的 ABA 题目

   举个栗子:“翻新机”,我们以为买到的是一个新的极其,但实际上买到的是一个“二手的呆板“,外表看起来是极新的,但是内部已经是别人的形状了...
  CAS 在使用的时候,关键要点是:判定当前内存的值是否和寄存器中的值是一样的 ==》 假如是一样的,就进行修改,不一样,就什么也不做。(本质上是判定,但是假如当前代码实行过程中,有其他线程穿插进来了...可能存在如许的环境,比如数值本来是 0,实行 CAS 之前,另一个线程把这个值从 0 -> 100,又从 100 -> 0,虽然最终的结果仍旧是 0,但并不是没有别的线程穿插,而是其他线程穿插过程中,把值修改了,又改回去了)。一样平常来说,即使出现上述的环境,题目也不大,不会产生什么 bug,但是怕是一些极端的场景!!!
   假设,去银行取钱~~~
  初始环境下,账户余额 1000,要取 500。取钱的时候,ATM 卡了一下,按了一下没有反应(t1线程),又按了一下(t2线程)。此时产生了两个线程,去尝试进行扣款操作了。

  假如是按照上述的方法来实行,是可以正常实行的,没有题目。
  但假如,就在此时此刻,t3 线程,又给我们的账户,存了 500。此时,唉哟我嘞个豆

  t1 线程实行到这里,就不知道,当前的 balance 中的 1000 是个什么环境了,是始终没有变化呢? 还是变了又变返来了...那 t1 线程,假如认为是没有变化,继续减 500,那岂不是我亏大了~~~
  上面的 ATM 栗子,布满了假设和偶合,是一个非常极端的栗子
对于 ABA 题目,办理方案:
        1. 约定命据变化是单向的(只能增加或者减少),不能是双向的(既能增加,又能减少)
        2. 对于自己就必须双向变化的数据,可以给它引入一个版本号,版本号这个数字就是只能增加,不能减少的
补充: CAS 的操作,本是上还是 JVM 帮我们封装好的,上面所述的细节我们是没办法直接感知到的~~~
JUC(java.util.concurrent)的常见类

JUC 这个包内里,存放了一些进行多线程编程的时候的一些比力有用的类
Callable 接口

先回忆一下,我们之前创建线程的方法:
        1. 继续 Thread(包罗匿名内部类的方式)
        2. 实现 Runnable(包罗了匿名内部类的方式)
        3. 基于 lambda 表达式
        4. 就是根本 Callable(interface)
        5. 基于线程池
Runnable 关注的是过程,不关注实行结果,Runnable 提供的 run 方法,返回值范例是 void,Callable 要关注实行结果,Callable 提供的 call 方法,返回值是线程实行使命得到的结果
假如我们要编写多线程代码,希望关注线程中代码的返回值的时候,创建一个新线程,用新的线程实现 1 + 2 + 3 +.. + 1000
代码实现如下:

虽然上面的代码可以办理题目,但并不“优雅”(要在主线程中获取到线程中的盘算结果,还要再倒腾一个成员变量来获取)。
使用 Callable 可以更好的办理题目

这个泛型<V>,代表的是,渴望线程的入口方法中,返回值的范例
这里我们希望返回值是一个整数 --> Integer

如许使用 Callable,就不需要引入额外的成员变量了,直接借助这里的返回值即可~~
但是,当我们传入的时候发现,Thread 并没有提供构造函数来传入 callable

这里我们可以引入一个 FutureTask类,来作为 Thread 和 callable 的”粘合剂“。

futureTask ==》 将来的使命(使命可能还没实行完呢),既然这个使命是在将来实行完毕,最终去取结果的时候,就需要有一个凭据,这个凭借就是 futureTask(举个栗子:吃麻辣烫,选好菜之后,服务员会给我们一个小牌子,小牌子上有号码,到时候拿小牌子取餐~~~这个小牌子,就是 futureTask)。此期间码也不需要 t.join() 了~~~

注意,futureTask.get() ,这个操作也是具有阻塞功能的,假如线程还没实行完毕,get 就会阻塞,等到线程实行完毕了,return 的结果,就会被 get 给返回返来!
Callable 其实是一个 ”锦上添花“ 的东西,它能干的事变,其实 Runnable 也能干,不过,对于这种带有返回值的使命,在多线程中使用 Callable 的确会更好一些,代码更直观,更简朴~~~
不过还是需要重点明白 FutureTask 的作用!!!
完备代码如下:
  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. public class ThreadDemo46 {
  5.     public static void main(String[] args) throws ExecutionException, InterruptedException {
  6.         Callable<Integer> callable = new Callable<Integer>() {
  7.             @Override
  8.             public Integer call() throws Exception {
  9.                 int result = 0;
  10.                 for (int i = 0; i < 1000; i++) {
  11.                     result += i;
  12.                 }
  13.                 return result;
  14.             }
  15.         };
  16.         FutureTask<Integer> futureTask = new FutureTask<>(callable);
  17.         Thread t = new Thread(futureTask);
  18.         t.start();
  19.         System.out.println(futureTask.get());
  20.     }
  21. }
复制代码
ReentrantLock

ReentrantLock:是一种可重入锁,与 synchronized 定位雷同,都是用来实现互斥效果,保证线程安全(ReentrantLock 也是可重入锁,“Reentrant”的单词的原意就是“可重入”)
   synchronized 也是可重入锁呀。上古时期的 Java 中,synchronized 不敷强壮,功能也不敷强大,也并没有我们上面所述的各种优化,ReentrantLock 就是用来实现可重入锁的选择(历史遗留题目)厥后 synchronized 被各种优化的变的厉害了之后,ReentrantLock 就用的少了,但仍旧有一席之地~~
  ReentrantLock 是传统锁的风格,这个对象提供了两个方法:lock 和 unlock
这种写法,就轻易引起,我们加了锁之后,忘记 unlock 解锁了。或者是,在 unlock 之前,触发了 return 或者 异常,就可能 unlock 实行不到了。==》 正确使用 ReentrantLock 就需要把 unlock 的操作放到 finally 内里
ReentrantLock 与 synchronized 的区别

既然有了 synchronized(优化也非常好)那为什么还要有 ReentrantLock 呢???
        1. ReentrantLock 提供了 tryLock 操作。lock 是直接进行加锁,假如加锁不成功,就会阻塞。但 trylock,是尝试进行加锁,假如加锁不成功,不会阻塞,会直接返回一个 false。(提供了更多的“可操作空间”)
        2. ReentrantLock 提供了公平锁的实现(通过队列记录加锁线程的先后顺序)。synchronized 好坏公平锁。在 ReentrantLock 构造方法中填写参数,就可以将其设置为公平锁
        3. 搭配的等待通知机制不相同。对于 synchronized,搭配 wait / notify。 对于 ReentrantLock,搭配 Cindition 类,功能比 wait / notify 略强一点点,可以更准确控制叫醒某个指定的线程....
但是,在实际上绝大部门的开发中,使用 synchronized 就充足了!!!
信号量 Semaphore

信号量,用来表示“可用资源的个数”,本质上就是一个计数器
   举个栗子来明白:
  可以把信号量想象成是停车场的展示牌:当前有车位 100 个,表示有 100 个可用资源。当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1(这个称为信号量的 P 操作),当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1(这个称为信号量的 V 操作)。假如计数器的值已经为 0 了,该尝试申请资源,就会阻塞等待,知道其他线程释放资源。
  Semaphore 的 PV 操作中的加减计数器操作都是原子的,可用在多线程环境下直接使用
信号量也是操作系统内部给我们提供的一个机制,操作系统对应的 API 被 JVM 封装了一下,就可以通过 Java 代码来调用这里的相关操作了~~~
信号量是更广义的锁!!!
所谓的锁,本之上也是一种特殊的信号量。锁,可用认为就是计数值为 1 的信号量。释放状态,就是计数值为 1 的信号量,加锁状态,就是计数值为 0 的信号量。对于这种非 0 即 1 的信号量,称为 “二元信号量”。
代码示例:

作为锁使用:

CountDwonLatch

CountDownLatch 是针对特定场景来办理题目的小工具
比如,多线程实行一个使命,把大的使命,拆分成几个部门,由每个线程分别实行。
   举个栗子:“多线程下载”,比方 IDM 如许的软件。下载一个文件,这个文件可能很大,但是可用拆成多个部门,每个线程负责下载一部门,下载完成之后,最终把下载的结果都拼接到一起。在多线程下载的场景,最终实行完成之后,要把所有内容拼到一起,这个拼接必须要等到所有的线程实行完毕。
  使用 CountDownLatch 就可以很方便的感知到上面的这个事变(所有的线程实行完毕)(比我们调用多次 join 要简朴方便一些~~~)
假如使用 join 方式,就只能使每个线程只实行一个使命,借助 CountDownLatch 就可用让一个线程能实行多个使命~~~
示例代码:

线程安全的聚集类

原来的聚集类,大部门都不是线程安全的。但 Vector,Stack,Hashtable 是线程安全的,这三个类,在关键方法上加上了 synchronized,因此,这几个兄弟,无论怎样都得加锁,哪怕单线程的时候,也需要加锁,如许的做法是不科学的,这几个好兄弟,现在官方已经不建议使用了,可能在将来的某个版本就删掉了...
多线程环境使用 ArrayList

1. 程序员自己按照环境使用同步机制(synchronized 或者 ReentrantLock)

前面有讲解,此处不做重复阐明~
2. Collections.synchronizedList(new ArrayList)

这个包内里的方法,相当于是给 ArrayList 套了一个壳,ArrayList 自己的各种操作是不带锁的,但是通过上面的套壳操作之后,得到了新的对象,新的对象内里的方法就是都带有锁的,如许更方便我们灵活使用~~~
3. 使用 CopyOnWriteArrayList

这玩意是叫 写时拷贝
线程安全题目,本质上就是多个线程修改同一个数据的时候可能出现题目。
比方有一个顺序表如下:

假如多个线程,读这个程序表,是没有任何线程安全题目的。
但一旦有线程要修改内里的值,就可能引发线程安全题目

但假如使用CopyOnWriteArrayList,它假如发现有线程修改了内里的值,它就会把顺序表复制一份, 修改新的顺序表内容,并且修改引用的指向(这个操作是原子的,不需要加锁)
总结来讲就是:
当我们往一个容器添加元素的时候,不直接往容器内里添加,而是线将当前容器进行 Copy,复制出一个新的容器,然后在新的容器内里添加元素。添加完元素之和,再将原容器的引用指向新的容器。
如许做的好处是,可用对 CopyOnWrite 容器进行并发的读,而且不需要加锁,由于当前容器不会添加任何的元素。所以 CopyOnwrite 容器其实也是一种读和写分离的思想,读和写是差别的容器。
长处: 在读操作多,写操作少的场景下,性能不是很高,不需要加锁竞争
缺点:占用内存较多,并且新写的数据不能被第一时间读到
多线程环境使用哈希表

HashMap 自己不是安全的
在多线程环境下使用哈希表可用使用:Hashtable(在关键方法上添加了 synchronized) 和 ConcurrentHashMap
Hashtable

        1. 只是简朴的在关键方法加上了 synchronized 关键字

这相当于直接针对 Hashtable 对象自己加锁

此时,尝试修改两个差别链表的元素,都会触发锁冲突!!!(仔细观察,就会发现,假如修改两个差别链表上的元素,并不会涉及线程安全题目。假如修改的是同一个链表上的元素,才会可能涉及到线程安全题目~~~此时,针对同一个链表,是需要加锁的,假如针对的是差别链表进行操作,是不需要加锁的!!!)
        2. size 属性也是通过 synchronized 来控制线程同步是,也会比力满
        3. 一旦触发扩容,就由该线程完成整个扩容过程,这个过程就会涉及到大量的元素拷贝,效率非常低 ==》 不稳固~~~
ConcurrentHashMap

相比于 Hashtable 进行了一系列的改进和优化
(在 Java 1.7 及其之前,ConcurrentHashMap 是通过“分段锁”来实现的。给多少个链表分配一把锁,如许设定,不太合适,实现也复杂)
Java 1.8 中:
        1. 读操作没有加锁了(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁,加锁的方式仍旧是 synchronized,但不是锁整个对象,而是“锁桶”(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率。
ConcurrentHashMap 就是把锁变小了,给每一个链表都发了一个锁
此时,操作差别链表的时候,就不会产生锁冲突。而且上述设定,不会产生更多的空间代价。由于 Java 中任何一个对象都可用直接作为锁对象。自己哈希表中,就得有数组,数组的元素都是已经存在的,此时,只需要使用数组元素(链表头结点)作为加锁的对象即可。


        2. 充分利用 CAS 特性:比如 size 属性通过 CAS 来及逆行更新,制止出现重量级锁的环境。
synchronized 虽然刚开始是偏向锁 / 轻量级锁,但是有可能升级为重量级锁,且过程是不可控的
        3. 针对扩容操作的优化 --> 化整为零
扩容是一个重量操作,这里有一个概念是负载因子,即描述了每个桶上均匀有多少个元素,当同上的链表的元素个数不是太多,就能达到 O(1) 时间复杂度
(负载因子不是 0.75!!!0.75 是负载因子默认的扩容阈值,不是负载因子本体。负载因子是我们算出来的数,用实际的元素个数 / 数组的长度,那我们算出来的值和扩容阈值进行比力,来看是否需要扩容)
假如桶上的链表的元素个数太多 ==》 1. 变成树 2. 扩容
扩容,即创建一个更大的数组,把就的 hash 表的元素都给搬运到新的数组上,假如 hash 表自己元素非常多,这里的扩容操作就会消耗很长的时间!!!(hash 表平时都很快,O(1),突然间某个操作非常慢,然后过一会就又快了,如许的体现是不稳固的,无法控制什么时候触发扩容)
ConcurrentHashMap 就优化为了化整为零,蚂蚁搬家~~~
        1. 发现需要扩容的线程,会创建一个新的数组,同时只搬运几个元素已往
        2. 扩容期间,新老数组同时存在。
        3. 后续每个来操作 ConcurrentHashMap 的线程,都会到场搬运的过程,每个操作负责搬运一小部门元素~~~
        4. 搬完最后一个元素,再把老的数组删掉
        5. 这个期间,插入只往新数组中添加
        6. 这个期间,查找需要同时查新数组和老数组~~~

   HashMap 的扩容操作是一把梭哈,在某一次插入元素的操作中,整体完成扩容了
  ConcurrentHashMap 则是每次操作都只搬运一部门元素
  假设这里有 1kw 个元素,此时扩容的时候,每次插入 / 查找 / 删除,都会搬运一部门元素,一共会用多次搬运完成(花的时间会长一些,虽然总体时间变长了,但是每次操作的时间都不会很长,就制止出现很卡的环境了~~~)
  完!!!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4