JUC并发

JUC并发编程

线程的状态:新建,就绪,阻塞,运行,死亡

多线程:在一个进程中,同时拥有多个线程,java支持多线程

优点:提高程序运行速度,提高cpu利用率

缺点:多个线程会对同一个数据进行访问,会引起线程安全问题。

如何解决多线程访问共享数据

加锁:

1.使用synchronized关键字

修饰方法:静态方法:锁对象是类的class对象,

非静态方法:锁的对象是this

编译方法后生成ACC_SYNCHRONIZED

修饰代码块:只对某一代码块进行加锁控制,需要自己传入一个锁对象,要求锁对象必须是唯一的,代码块编辑后,在加锁的部分添加了monitorenter指令

synchronized是依赖底层的指令控制是隐式的,可以自动的添加,自动的控制,一编译底层会生成相应的指令

多线程访问共享数据,就会出现问题

并发:宏观上同时执行,在一段时间内交替执行,做着多件事情

并行:微观上同时执行,在一个时间点上,同时做着多件事情

并发编程:在很多线程对共享资源进行访问时,需要通过控制让多个线程并发的对共享数据进行访问。

多线程执行的本质问题:

由于cpu,内存,硬盘三者之间的读写速度不一样导致的

1.多核cpu,每一个核中都有一个高速缓存,每一个高速缓存数据都不可见。(可见性)

2.线程中有IO操作,(从磁盘上进入IO操作)耗时较长,操作系统需要切换线程执行(原子性)

3.操作系统对指令进行优化,对指令进行重新排序(有序性)

缓存导致的可见性问题:

编译带来的有序性问题:

线程切换带来的原子性问题:

Volatile:关键字修饰变量,可以保证可见性和有序性,禁止指令重排。但是不能保证原子性。

Volatile的底层是如何实现的:

编译后生成: ACC_VOLATILE指令

底层使用Memory Barrier(内存屏障)也是一条指令

在对volatile修饰的变量读写操作前,可以添加内存屏障指令

禁止在该指令执行前插入其他指令,

在工作内存修改后,结合缓存一致性协议,将工作内存中数据更新到主内存,其他工作内存到内存中读取更新

cpu中的内存屏障:

写屏障(store barrier):强制所有在写屏障之前的指令在写屏障之前执行,并发送缓存失效信号。所有在写屏障之后的store指令,都要在写屏障之后执行,也就是禁止屏障前的指令和屏障后的store指令重排。

读屏障(load barrier):强制所有在读屏障之后的load指令,都在读屏障之后执行,也就是禁止读屏障之后的load操作重新排序。

全屏障(full barrier):全能型屏障。在全屏障前后的所有store/load操作,都要在对应的前后执行。也就是完全防止屏障前后的指令重排。

缓存一致性协议:MESI和Intel

MESI协议:MESI协议是保证每个缓存中使用的 变量的副本是一致的。

核心思想:当cpu写数据时,如果发现操作的变量是共享变量,即在其他cpu也存在该对象的副本,会通知其他cpu将该对象设置为无效状态,因此当其他cpu进行读取这个变量时,发现此变量已经失效,此时就会从内存中重新获取该变量。

原子性:

CAS机制:比较并且交换,是一种乐观锁(没有加锁)实现的,才用自旋(一遍一遍)的思想去比较

内部有三个值:

V:内存值,操作前将内存值读到工作内存中

A:预期值,在工作内存中修改了变量中的值之后,将要将修改后的变量写入内存中的时候,再次读取内存中的值

B:内部操作后的变量值

当向主存中写入数据时,必须要满足A==V,此时将B写入内存,负责就再读内存中值,更新到工作内存进行重复操作。

缺点:cas是无锁的,采用自旋的方式,线程不会阻塞,如有大量的线程此时进行尝试,就会导致cpu消耗较大,使用并发较小的情况。

这也会导致ABA问题:就是内存中的值有A改为B,再改为A时,CSA不知道值已经发生修改了。解决此类问题,加一个版本号,每次修改之后,修改版本号,此时比较的时候,就不光是比较值了,也会去比较版本号,就算值一样,版本号不一样,此时它就不是以前的那个东西了。

java中的锁:

不全是指锁,有的是指所得特性,有的是值锁的状态,有的是锁的设计。

乐观锁:采用CAS机制,认为不加锁是可以的,没有问题的。原子类采用CAS思想。

悲观锁:认为不加锁是会有问题的,真正意义上的锁。

可重入锁:又名递归锁,就是方法A有锁,方法B也有锁,此时线程进入方法A中去调方法B,也可以进行调用,再次获得锁,不会产生死锁,此时认为这样的锁就是可重入锁。synchronized和ReentrantLock都是可重入锁。

读写锁:里面维护两个锁实现,一个是写锁,一个是读锁,如果是写锁,一次只能有一个线程获得锁,读锁的话,一次可以有多个线程获得锁,写锁的优先级大于读锁。

分段锁:不是具体的锁,将所得粒度分的更小,以提高并发效率。

自旋锁:不断地重试去尝试获得锁,不会让线程进入阻塞状态,提高效率,但是消耗cpu

独占式:synchronized和ReentrantLock读写锁中的写锁,或者交互斥锁,一次只允许一个线程获得锁,

共享锁:读写锁中的读锁是共享锁,可以有多个线程获得

公平锁:可以按照请求的顺序分配锁,ReentrantLock中有公平锁,里面维护了一个队列,按照顺序排队获得锁。

非公平锁:不按照请求顺序分配锁,抢占式的,操作系统调度谁,抢到了就是谁的,synchronized和ReentrantLock都是的,ReentrantLock默认使用非公平的。

在synchronized中锁有三种状态:无锁状态,偏向锁,轻量级锁,重量级锁,

jdk1.6之前一直是重量级锁,但是1.6之后进行了大量优化,为了减少获得锁和释放锁带来的性能消耗而进行的优化。在线程冲突的情况下,可以获得和CAS类似的性能,而线程冲突的情况下,性能远高于CAS.

1.无锁:无锁的特点就是修改操作在循环内进行,线程会不断地尝试修改资源,如果没有冲突就修改成功并且退出,否则就会继续循环尝试,也就是CAS.

2.偏向锁:偏向锁是指一段代码块被一个线程同时访问,那么该线程会自动获取锁,降低获取锁的代价。也就是将线程id存到对象头中,下次该线程来获取锁时直接分配即可,减少CAS,只有在第一次访问时执行CAS操作,况且锁不会主动释放,只有存在另一个线程也来获取锁时,竞争时,才会释放锁。

3.轻量级锁:是指当锁是偏向锁时,又有一个线程进行访问,那么此时的锁才会升级为轻量级锁,其他线程不会阻塞,就会以自旋的方式尝试获取,从而提高性能。(总共两个先线程)

4.重量级锁:当锁的状态为重量级锁时,如果线程数量太多,有线程自旋超过10次或者线程数超过cpu核数一半,会从轻量级锁升为重量级锁,线程进入阻塞状态,有操作系统进行调度分配,减少行能消耗。

synchronized锁的实现:

synchronized可以修饰代码块,可以修饰方法。一次只允许一个线程进入。修饰方法的话,在编译后的指令中会添加ACC_SYNCHRONIZED,表示此方法是同步方法,有线程进入后其他线程不能进入,在对象头中锁标志+1,方法运行结束后,或者出现异常锁标志减一。

若修饰同步代码块,在进入代码前的位置加入monitorenter指令,对象锁标记位置+1,同步代码块结束或者出现异常执行monitorexit,锁标志减1,主要依靠底层指令实现。

AQS:AbstractQueuedSynchronizer 抽象对队列同步器

JUC java.util.concurrent Java并发包

AQS是JUC中实现线程安全的核心组件,是从java代码级别实现,内部维护了一个锁状态,Volatile stste内部还维护了一个队列,保存未获取到锁的线程,多个线程来访问如果有一个线程访问到了state就将其改为1,其他线程获取失败后就会添加到队列中,Node(Thread),内部还有一个变量,来记录当前加锁的线程。还维护了一些获取锁,添加线程到队列,释放一些锁的方法。

ReentrantLock是根据ASQ实现的,基于api显示加锁和释放锁,向外界提供锁实现的类,内部包含三个类

sync extends AQS

FairSync extends Sync 公平锁实现,里面是一个阻塞队列,来的迟的阻塞在队尾,先进先出。进来先判断当前有没有线程持有锁,无锁,判断线程等待队列是否有线程等待,有线程等待,进入排队,无等待,则抢锁,如果有线程持有锁,是否是当前线程,如果是,则重入。

NonfairSync extends Sync 默认的,非公平锁实现,支持线程直接的切换,从而提高效率。一上来看当前是否有线程有锁,state=0,抢,,失败了就进入AQS的模板方法acquire(1),还会进行一次抢锁,再失败的话,进入AQS队列,当锁的状态state=0时,接着抢,有锁的话,判断是否是当前线程,是的话,就重入。抢到就独占资源。

ConcurrentHashMap:并发线程安全的:

HashMap:线程不安全,使用在单线程情况下

ConcurrentHashMap:线程安全,支持并发访问,但是锁加在put方法上,线程低,一次只能有一个线程进入put方法上进行操作,

ConcurrentHashMap:是线程安全的,现在它的锁已经不在put方法上,而是更细分到了每一个位置上了,如果线程进来需要拿着hash值,需要再某一个位置上存放数据,先判断该位置上是否有元素,没元素的话,直接采用CAS机制,将元素存放进去,如果该位置已经有了元素,此时,它会采用将第一个节点做为锁标记对象,它将持有锁,此时别的线程进不来。以此来保证并发情况下线程是安全的。将锁的范围,粒度划的更小,以此来提高运行效率。

CopyOnWriteArrayList

写素组的拷贝,支持高并发,况且是线程安全的,读操作是无锁的,所有的可变操作都是通过对底层数组的一次复制实现的。

只有在操作影响数据的时候,加锁,保证修改数据是线程安全的,写入数据时,会创建一个新的数组的副本,将原来的数组copy过来,将新添加的元素写入新数组中,写完之后再将新数组的地址赋给底层原来的数组引用,读时没有任何操作。在添加数据的区间,要是有其他线程需要读取数据,此时读到的数据还时从那个旧数组中读的数据。所以写数据不影响读数据。

vector

添加和查询都添加了同步锁,所以在写数据还是读数据时,一次只能允许一个线程进去,它的效率是很低的,唯一亮点就是线程安全的。

CopyOnWriteArraySet:

底层实现和CopyOnWriteArrayList是一样的,但是它底层维护了添加的元素是不能重复的。

线程池

什么是线程池?

其实就是一个可以容纳多个线程的池子,其中的线程可以反复使用,线程在执行完任务后,会被丢进池子中,等待下一次任务分配,省去了频繁创建线程对象的操作,减少反复创建线程带来的资源消耗。

为什么用池?

pool每次连接数据库都要创建连接对象,用完销毁,比较费事,影响运行效率。

池子,它会事先创建好一些连接对象放在里面,每次获取直接从池子里面拿,用完直接还到池子避免重新重建,引起不必要的开销。

jdk5之后,ThreadPoolExecutor类实现了线程池的创建,这是推荐的,还有Executors类中提供创建。

优点:避免线程重复的创建和销毁,减少资源开销,提高系统运行速度。

统一管理线程,线程的创建和销毁都由线程池进行管理,

ThreadPoolExecutor

7大属性:

corePoolSize(必需):核心线程池的大小,创建线程对象后,核心线程池默认情况下,是没有线程的,当任务到达后,才会创建线程,但是当任务执行完之后,线程是不会销毁的,直到创建出与核心线程池大小相等的数量的线程。,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。不然他会一直存在。创建线程的方式:

maximumPoolSize(必须):线程池最大容纳线程数,当达到这个值后,线程会堵塞。也就是最大线程容纳数=核心+队列+非核心

keepAliveTime(必须):线程闲置时长,线程闲置时间超过这个时长时,非核心的线程池中的线程就会销毁。如果核心线程池中的allowCoreThreadTimeout 设置为true,它里面的线程满足条件时也会销毁。

unit(必须):指定keepAliveTime的时间单位,在TimeUtil类中有七种,小时,分钟,秒...

workQueue(必须):任务阻塞队列,用来存储等待线程,当核心的满了,进来的线程就会进入阻塞队列

threadFactory(可选):线程工厂,可指定线程池中创建线程的方式

handler(可选):拒绝策略,可指定当线程到达最大之后,在来的执行拒绝处理的册罗

任务来之后,线去核心线程池,核心线程池线程满着,此时进队列,队列满了之后,在进非核心线程池,非核心再满了,就执行拒绝策略。

handler拒绝策略:

AbortPolicy:报错

CallerRunsPolicy:由当前调用的线程执行(例如任务是指在main方法中执行的,就有main线程来执行)

DiscardOldestPolicy:丢弃队列里来的最早的任务,将后续任务加上

DiscardPolicy:直接将新来的任务丢弃了

对象引用

除了垃圾回收标记之外,还需要对垃圾对象进行状态分类管理

1.强引用,有引用指向对象,不是线程垃圾,回收器是不能回收的

2.软引用,使用softReference来管理的对象属于软引用,在内存充足时,回收器是不能进行回收的,当内存不足时,将这些软引用管理的对象进行回收。

3.弱引用:使用weakReference来管理对象属于弱引用,管理的对象只能存活到下次垃圾回收

4.虚引用(幽灵引用):使用phantomReference管理的对象,需要提供一个队列来维护,随时都可以被回收,主要是记录跟踪对象是否被回收。

ThreadLocal

线程并发的情况下使用。

创建一个ThreadLocal对象,赋值保存用来为每一个线程创建一份变量,实现传递数据,实现线程隔离。生命周期就是线程的生命周期。

ThreadLocal和synchronized的区别?

ThreadLocal和synchronized关键字都是用于处理多线程并发问题,不过两者处理问题的角度不同,synchronized采用同步机制,时间换空间,而ThreadLocal采用空间换时间,为每一个线程提供一个副本,从而实现数据相互隔离,

ThreadLocal执行效率更高,跟支持高并发,性能较好。

每一个Thread内部维护了一个ThreadLocalMap,ThreadLocalMap的key是ThreadLocal实例本身,value是要存储的对象,Threa内部的Map是由ThreadLocal维护的,由ThreadLocal负责想map获取和设置现成的变量。

jdk8的 设计好处:

 

1.每个map存储的Entry变少了,实际开发中,ThreadLocal的量少于thread

2.当线程销毁ThreadLocalMap也销毁,减少内存开支。

set方法,它先去判断以当前线程种的ThreadLocalMap,如果ThreadLocalMap不为null则以当前的的ThreadLocal的引用为key,设置的参数为value存储到ThreadLocalMap中。如果map为空则创建一个ThreadLocalMap放到,再进行存储。

 

get方法:获取当前线程,获取当前线程中Map,

如果map不为空,则以ThreadLocal的引用为key在map中获取Entry,如果e不为空则返回e.value,

map为空或者e为空时,通过initialValue函数获取初始值value(这个方法默认返回是object类型,返回null,需要重写),然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

 

如果一上来,没有set直接调用get则会触发这个initialvalue方法,去获取初始值,并以ThreadLocal的引用和value作为第一个key可value存储进去。默认值为null,该方法是protect方法,显然是为了让子类覆盖设计的。

 

 

 

使用弱引用,是为了ThreadLocal对象的生命周期和线程生命周期解绑,Entry是ThreadLocal的内部类。

强引用的情况:

 

在业务代码中,使用完ThreadLocal,和ThreadLocalRef应当被回收,但是ThreadLocalMap中的Entry中的key(ThreadLocal)使用了强引用,导致ThreadLocal无法被回收,在没有手动删除Entry和CurrentThread(当前线程)依然运行的前提下,始终有强引用指向entry(entry中包含ThreaLocal实列和value),entry不会被回收,造成内存泄露。

也就是说,ThreaLocal使用强引用是无法完全避免内存泄露的。

使用弱引用:

 

由于ThreaLocalMap只持有ThreaLocal的弱引用,没有任何指向ThreaLocal的强引用,所以ThreaLocal可以被gc垃圾回收,此时entry中的key为null,在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry,value就不会被回收, 而这块value永远不会被访问到了(因为key=null), 导致value内存泄漏,ThreaLocalMap中使用了弱引用key,也可能有内存泄露。

内存泄露的原因?

1.没有手动删除Entry

2.当前线程依旧在运行,ThreaLocalMap的生命周期和Threa一样长。

处理此类问题时,第一种方法要比第二要好,因为在使用线程池中的线程不会消亡。

为什莫使用弱引用?

在ThreaLocalMap中的set/getEntry方法中,会对key为null的,(也就是ThreaLocal为null)进行判断,如果为null的话会把value置为null

这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏

 

ThreadLocalMap中的hash冲突

ThreadLocalMap使用的时闭散列:(开放地址法或者也叫线性探测法),就是存一个数据,首先拿它的key计算出hash值,如果该位置已经存储元素了,他们hash值相同但是key不同,此时就接着找下一个位置,如果下一个位置也有元素,接着找,直至找到最后一个元素,从最后一个跳到第一个,接着找,直到找到空位置,插入。如果无空位置,则溢出。可看作一个环形数组。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值