并发包知识点总结

面试的时候总会遇到各种各样并发包的问题,因为用的不太多所以理解不够深入,所以今天总结下,加深下理解,欢迎各位的拍砖,吹水。讨论也是一种学习方式,有图有真相。
在这里插入图片描述

1.线程

1.1线程的五种状态

在这里插入图片描述

先来简单介绍一个整个图的流转过程。

通过调用start()方法,线程进入runable状态,即为可运行状态,但是不代表正在运行哦!它可能在等待资源同步锁(blocked)的释放,他可能在睡大觉(sleeping),也可能在等待(waiting)一个唤醒(notify或notifyAll),当它的执行条件都符合时,才开始真正的执行,执行完以后就进入终止(Terminated)的状态。如果现在还有其他的非守护线程在执行,则主线程继续执行,如果没有就,整个就终止。

new
new 就是线程创建完以后,但是还没有调用start()方法的状态。该状态,线程被创建,但是还没有开始执行
runnable
可运行状态,当你的线程进去runnable状态时,你可要注意了,他有可能在真的正在做事(正在运行),也很有可能在偷摸做以下三件事,你可要盯紧了。
blocked
第一件事,他有可能在等待某一个资源同步锁的释放,因为有些资源只允许一个线程访问他,所以当多个线程需要同时使用的时候就需要竞争,成功者得到资源使用权,失败者等待下次竞争,等待的过程就是blocked状态。
block状态时,会占用CPU,当锁被是否后,自动进入竞争状态,得到锁就进入runnable状态。
sleep
第二件事,他有可能在睡大觉,当线程启动(start())以后,当你调用sleep()方法时,线程就会进去睡眠状态,并且在睡眠时还会占用cpu,并且不会释放资源,等睡眠时间结束时,线程会进去runnable状态,继续执行。
wait
第三件事,他有可能进去了一个等待模式waiting,在等待模式,他不会像sleep一样自己回到runnable状态,他需要一个唤醒动作把他唤醒,才能回到runnable状态,在waiting状态时,不会占用CPU时间片,更不会占用资源,他只是在那里傻傻的等。这个唤醒可以是notify(),也有可能是notifyAll(),他俩的区别是,
notify(),只唤醒调用他的线程,而notifyAll()会唤醒所有在等待状态(waiting)的线程①,唤醒后,线程就来到了可运行状态(runnable)。
sleep 和wait状态的区别

状态是否占用CPU是否可以自动唤醒唤醒时机
sleep等待时间结束
wait等待notify()或notifuAll()

理论总是干干的,咱们来举个栗子,消化一下。
在这里插入图片描述
比如我去银行开张银行卡,我就是一个线程,你以为我去银行办事,到那就真的开始办了吗?NO,我又不是思聪,银行又不是我家开的。来了以后要先拿个号,因为开卡,我还需要先填个单子。当我填单子的时候,不是我到那就开始填了,单子可能很快就拿到手了,但是我得找根笔啊!你不会告诉我你去银行还带笔吧?

这时候一老太太(另一个线程)也要用笔(独占资源),我们俩就要竞争,看谁先抢到笔,谁就先填单子。我作为一个年轻小伙子,我怎么能跟老太太抢笔呢?老脸不要了?大气点,让她先填好了,这时候我就处在一个阻塞的状态(blocked)。

这时候,我就可以跟银行的小姐姐聊聊天,或者看看她的工装裙是不是合身,等到老太太填完了(独占资源释放了),我就可以获得笔(获取独占资源),开始填单子(runnable状态),当我填完单子了,看了一下我的号,发现前面还有五六个人,那索性就等会(wait()) 吧!过了一会叫到我的号了(notify()),我到柜台前,把单子交给柜台小妹,刚录了个姓名、地址,银行小姐姐接到通知,说突然要开个小会,我含情脉脉的看着柜台小姐姐,说大概要开多久,她说大概要十来分钟。因为昨晚没睡好,所以我就在定了个十分钟的闹钟,在柜台窗口趴着眯了十来分钟(sleep)醒来后,等柜台小姐姐回来,就办完事就撤了(Terminated)。

1.2线程实现方式

线程实现有三种方式,没有返回结果的线程方式有两种,继承Thread类和实现Runnable接口,有返回结果的线程时 Callable.

1.2.1没有返回结果的
1.2.1.1 继承Thread
继承Thread类,然后重写run方法,就可以通过new 创建线程了。
1.2.1.2 实现Runnable接口
实现Runnable接口,实现run方法,然后通过 new可以创建线程了。
因为java是类单继承和接口是多继承的,所以使用Runnable创建线程几乎不受限制,而如果你使用了继承Thread来创建线程,你就不能继承其他类了。
1.1.3 Callable和Future和FutureTask
(略)
这块内容太多了,等我有时间单独把这块写下【留坑】

1.3 守护线程

守护线程不会独立于线程单独的执行,他只会在有非守护线程执行时执行,好比是护士,如果有病人在,他就要工作,如果没有病人的话,他就不会单独工作了。
例子我就不行了,感兴趣的可以自己写下。

1.4同步的方式

同步的方式有三种,包括synchronized,Lock和ReentrantLock
1.4.1 synchronized
第一种是synchronized,这种大家应该都知道,他是一个关键字,他能修饰方法或代码块;他是JVM直接实现的锁,它能保证方法或者代码块在运行时,同一时刻只有一个方法进入临界区。并且他还能保证内存的可见性。但是他的功能还是比较初级,只能做一些粗粒度的同步,要想做细粒度的控制,还要看下面这位。
1.4.2 Lock
他就是 Lock,他也是一种同步方式,但是Lock不是一个关键字,他是一个接口,这样他就可以有好多实现类,有各种不同种类的实现,这就代表了他的实现有更多的灵活度。
他有常用的方法有lock和unlock 以及trylock
那哪些类实现了他呢?
最常用的还是ReentrantLock,他需要自己手动解锁,这样就使他可以更灵活,并且它提供了,lock() ,unlock(),tryLock(),并且他还可以区分公平锁和非公平锁
1.4.3 ReentrantLock
单线程是可重入的,但是要退出的次数要和加锁的次数相同。

1.5 锁

公平锁
在java锁机制中,公平和非公平的标准是什么呢?个人认为即为线程获得锁结果。如果一个线程组内,能保证每个人都能拿到锁,那么就是公平锁,反之,如果保证不了每个人都拿到锁,即为有的线程存在饿死的状态,那么即为非公平锁。常用的公平锁方案是队列FIFO,即为先进先出,而非公平锁,抢占锁的顺序是不固定的,所以有可能造成饥饿,甚至饿死的状态,但是非公平锁,效率高。

可重入锁
可重入锁即为,可以多次获取一个锁,但是不会造成死锁,最后退出的时候要跟加锁的次数相同才行,每加锁一次,请求计数器就会加1,每退出一次则减1,当计数器为0时,就可以被其他线程占用了。

自旋锁
正常情况下,如果一个线程占用了资源获得了锁,其他线程就会阻塞,直到资源被释放。但是线程阻塞,以及重新获取锁,都是有时间消耗的。当线程执行时间比较长时,这个消耗不算什么。但是如果线程执行时间较短的话,线程阻塞的时间就显得尤为耗时,所以有了自旋锁,自旋锁就是当线程被占用以后,其他线程不阻塞,而是不断的获得锁,当锁被释放后,立即有线程获得锁后执行。这样就执行效率就会更高一些。适用于单个线程占用时间较短且并发高的情况。

重量锁和轻量锁
重量锁,即为一个资源被占用是,其他线程阻塞。这种为重量锁【重量级锁和阻塞是否相同】
轻量锁,为一个资源被占用时,其他线程不阻塞,他会不断地请求线程,这样比较节省cpu时间片。
偏向锁,即为第一个获取的线程总是占用锁,被第一个线程拥有后,记录他的偏向线程ID,这样偏向线程就一直持有锁。以后每次同步,直接对比偏向线程ID是否和当前线程ID一致,如果一致,直接进入,不需要再同步,退出同步也不需要在去CAS更新对象头。如果不一致意味着产生了竞争,锁已经不再偏向一个线程了,这时候需要锁膨胀为轻量锁,才能保证线程公平竞争。

举个栗子
在这里插入图片描述
比如有一个摇摇车,A小朋友家里这个摇摇车比较近,吃完午饭,他就来到这个摇摇车这,正好现在没有人,他就可以一直玩啊玩(偏向锁),即便他口渴了,回家喝了袋牛奶,回来他还可以玩(重复进入),中午两点午睡时间结束以后,其他小朋友也过来了,他就不能一直玩了,这时他就要退下来(偏向锁变成轻量锁),与其他小朋友一起竞争,谁先抢到摇摇椅,谁先玩。

如果每个小朋友玩的都比较快,其他没抢到的小朋友为了更快的玩到,就会在旁边等着(自旋锁,减少换人浪费的时间);如果每个小朋友玩的时间,都比较长,这时其他没抢到的小朋友就不会再旁边等着了,换人的时间基本可以忽略不计了,其他小朋友就会分散开来各做各的游戏(阻塞,wait),等当前这个小朋友玩完了后,说:我不玩了,你们玩吧(notify)! 这时想玩的小朋友再去竞争。

当然如果每个小朋友,年龄,体格都差不多,基本都能竞争到车,就没问题。(非公平锁)

如果有一个或几个小朋友,太小了每次竞争都抢不到摇摇车,总是坐地上大哭,这就不好了,这就要想一种办法,让每个人都有机会玩到摇摇车,怎么办呢?那不如排队吧,想玩的小朋友在摇摇车前面排队,排到谁,谁玩。

可重入锁是什么呢?

比如摇摇车上有一个门,门内侧有一个锁门的锁,谁锁上这个门了,谁就可以玩了。这时,你可以在门上锁两把锁,当然你下来的时候,就要开两次锁,你才能下来。

1.6 常用关键字

1.6.1ThreadLocal
ThreadLocal 就是部分变量只在自己线程在使用,但是有可能Thread有不同的副本。那怎么区分呢,只能使用ThreadLocal泛型存储,并且ThreadLocal 一般不止一个对象,所以ThreadLocal内部维护了一个Map,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

1.6.2volatile
volatile有两层语义
第一层是,禁止指令重排序,因为计算机在执行的时候为了让程序计算的速度更快,会把语句进行重新排序执行。
第二层是,保持对象内存可见性,即为把线程本地的内存置为无效,直接从主内存读取共享变量。

2.同步的几种解决方案

2.1 CAS

CAS:Compare And Swap ,即比较再交换
他是基于synchronized实现的乐观锁机制,每次都认为线程不会把资源搞坏,所以就不加锁。每次对资源进行修改时,先进行比较,如果跟拿到的值和资源值一致才进行修改,如果不一致则不修改。适用于对最终一致性要求比较强,但对过程一致性要求不强的场景。

2.2 AQS

核心思想为:如果被请求的资源空闲着,就把它分配改当前求其的资源,并将共享资源设置为锁定状态,如果被请求的资源被占用了, 那就进入阻塞等待唤醒阶段,AQS的核心就在这,他用CLH队列锁实现的等待唤醒机制,即获取不到锁的线程进入到队列中。CLH队列是一个虚拟的双向队列,他不在真实的队列中,只是节点相关联罢了。
简单来说,AQS就是基于CLH队列,用volitile修改state状态符,然后线程通过CAS去改变状态符,成功则获取锁,失败就进入等待队列,等待被唤醒。

2.3 CopyOnWrite

CopyOnWrite 他也是一种无锁同步方式。它每次要修改资源时,先复制资源,在新资源上修改,修改完后,把资源的指针直接修改到新资源上,然后同步完成。
他这种相对来说,比较重,因为每次修改都需要复制嘛,但安全度比较高。

总结

并发包的知识还有很多,当前只说了个大概,如果各位感兴趣,有时间我再单点爆破下。谢谢各位的阅读。晚安

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值