并发编程中的锁

一、并发与并行

1、并发:一个处理器同时处理多个任务,逻辑上的同时发生

2、并行:多个处理器或者多核处理器同时处理不同的任务,物理上的同时发生

并发是一个人同时吃三个馒头,并行是三个人同时吃三个馒头

二、进程与线程

进程是操作系统的结构基础,进程也是操作系统进行资源分配的独立单元;一个进程就是一个程序的运行,可将.exe程序看成是一个进程,进程中有很多的子任务,每个子任务对应一个线程,线程是大多数操作系统调度的基本单元;比如在qq.exe进程中,有好友在线视频线程,有文件上传下载线程,这些线程拥有自己特有的计数器、栈、、堆、局部变量表等属性,并且可以访问共享内存的变量。

三、为什么使用并发编程

1、更多的处理器核心

随着处理器核心处越来越多,以及超线程技术的广泛运用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式也从更高的主频向更多的核心发展。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的运行效率。相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多的核心加入而变得更有效率。

2、更快的响应时间

例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从点击“购买”开始,就要等这些操作全部完成之后才能看到购买成功的结果。这时候可以采用多线程技术,就是将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),这样做的好处是响应用户请求的线程可以尽快的处理完成,提升了用户体验。

扫描二维码关注公众号,回复: 2599918 查看本文章

3、更好的编程模型

Java为多线程提供了良好、考究并且一致的编程模型。

四、并发编程的挑战

并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程就能让程序最大限度的并发执行,并发编程会面临很多的挑战,比如上下文的切换、死锁、以及受限软件和硬件的资源限制问题

1、创建销毁线程消耗资源

2、上下文切换

Cpu通过时间片分配算法来循环执行任务,在切换前会保存上一个任务的状态,以便下回切换为这个任务,可以加载这个任务的状态。任务从保存到再次加载的过程就是一次上下文的切换,这样的切换会造成线程线程的阻塞和唤醒。

java中的线程是映射到操作系统原生线程之上的,如果阻塞或者唤醒一个线程就会需要操作系统的介入,需要在用户态和核心态之间切换,这种切换会消耗大量的系统资源。因为用户态和核心态都有各自的专用内存空间、专用寄存器等。用户态切至核心态会传递大量的变量、参数给内核,内核需要保存用户态的参数变量等,以便于内核态调用结束后切换回用户态继续工作。

上下文每秒钟切换1000多次,如何减少?

(1)无锁并发编程:多线程竞争锁时会引起上下文的切换,将数据的ID按照hash算法取模分段,不同的线程处理不同段的数据。

(2)CAS算法:使用cas算法更新数据不用加锁(Atomic包)

(3)使用最少线程:

(4)协程:在单线程里实现多线程的调度,并且在单线程里维持多个任务之间的切换

3、死锁:两个或者多个线程互相持有对方所需要的资源,并且互相等待对方释放资源。如果都不主动释放资源,将会产生死锁

避免死锁?

(1)避免一个线程同时获得多个锁

(2)避免一个线程在锁内同时占用多个资源

(3)使用定时锁

检测死锁?

(1)Jconsole:图形化工具,用于连接jvm进程以监控java代码

(2)Jstack:命令行工具,用于生成jvm当前时刻的线程快照

4、受限资源

(1)软件:数据库、socket连接数,可以使用资源池将资源复用来解决。

(2)硬件:宽带的上传下载速度、硬盘的读写速度、cpu的处理速度等。解决办法:使用集群并行执行程序,不同的机器处理不同的数据。使用“数据ID%机器数”得到机器编号,由对应机器编号处理这笔数据。

五、锁的升级与优化

Jdk为了减少获得/释放锁带来的消耗,引入了偏向锁和轻量级锁

1、锁的状态(可升级不可降级)

无锁状态→偏向锁(乐观锁)→轻量级锁(乐观锁)→重量级锁(悲观锁)

2、乐观锁和悲观锁

乐观锁:假定不会发生数据冲突,只是在数据提交的时候才去检查是否违反数据完整性,核心思路:每次不加锁而是假定没有冲突去完成某次操作,如果因为冲突失败就重试,直到成功为止。CAS、偏向所、轻量级锁

悲观锁:假定会发生冲突,屏蔽一切可能违反数据完整性的操作。

3、锁的升级

A:偏向锁:只有一个线程进入临界区、两个或者多个线程交替进入(之前进入的线程都不在进入临界区)

B:轻量级锁:两个线程交替进入(之前进入的线程(一条线程)还会进入临界区)、两个线程同时进入临界区

C:重量级锁:两个线程同时进入临界区、多个线程同时进入临界区(多个线程交替进入临界区我不知道)

Synchorized(lock){

//doSomething

}
假设有Thread#1和Thread#2分三种情况:

情况一:只有Thread#1进入临界区

情况二:Thread#1和Thread#2同时进入临界区

情况三:Thread#1和Thread#2同时进入临界区,再进来Thread#3
情况一是偏向锁的应用场景:只有Thread#1进入临界区,jvm将lock的对象头Mark Word的锁标志位设为01,同时使用cas操作将获取到Thread#1的线程ID记录到Mark Word中。如果cas成功,则以后Thread#1在进入或者退出同步块时不需要使用cas操作来进行加锁或者解锁,只是简单测试对象头中Mark Word里是否存储指向当前线程的偏向锁,对比ID,不在进行cas操作

情况二:若Thread#2尝试进入临界区时,因为偏向锁使用了一种等到竞争出现才会释放锁的机制,因此Thread#2可以看到对象偏向状态,这时候表示已经存在竞争了,检查持有该锁的线程是否存活,如果挂了(Thread#1已死),则可以将对象变成无锁状态,然后重新偏向Thraed#2,如果原来线程依旧存活,则马上执行Thread#1的操作数栈检查该对象的使用情况,如果还需要持有偏向锁,则升级为偏向锁,如果不存在使用了(不在进入临界区),则将对象恢复无锁状态重新偏向。

情况三:轻量级锁认为竞争存在,但是竞争程度很低,一般两个线程对一个锁的操作会错开或者稍等一下(自旋)另一个线程就会释放锁。但是当自旋超过一定次数或者有一个线程持有锁,另一个在自旋这时候又有第三个线程来访时,轻量级锁膨胀为重量级锁,重量级锁除了持有锁的线程以外的线程都阻塞,防止cpu空转

4、锁的对比

偏向锁:优点加锁或者解锁都不需要额外的消耗,与执行非同步方法仅存在纳米级差距,缺点如果存在竞争带来额外撤销锁的消耗

轻量级锁:优点竞争的线程不会阻塞,缺点自旋会消耗cpu

重量级锁:优点竞争不会自旋消耗cpu,缺点线程阻塞,响应时间缓慢

5、锁的优化

1、自旋锁和自适应锁

同步互斥对性能最大的影响就是阻塞的实现,挂起线程和恢复线程的实现均需要转入内核态完成,共享数据的锁状态只会维持很短时间,为了这段时间去挂起线程和恢复线程不值得,因此可以让后面的线程稍等一下,给线程执行忙循环,这就是自旋锁。自旋锁会浪费cpu,自旋锁不能代替阻塞,当自旋次数超所一定的次数(默认10次),则使用传统挂起线程。

但是在jdk1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不在固定。如果对于同一个锁,自旋等待刚刚成功,并且持有锁的线程正在运行,那么jvm会将自旋等待延迟更久(比如100个循环)。如果对于某个锁,自旋很少得到锁,jvm会忽略以后这个锁的自旋过程,以避免浪费cpu

2、锁清除

锁清除指的是JIT在运行时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行,则会将其消除。主要依据逃逸分析的数据支持,如果一段代码中,堆上所有数据都不会逃逸出去而被其他线程访问到,则可以认为线程私有无需加锁

3、锁粗化

如果连续一系列操作对同一个锁进行加锁解锁,甚至加锁会出现在循环体中(如连续的append()操作),会把整个加锁同步的范围扩展到整个操作序列的外部。

猜你喜欢

转载自blog.csdn.net/AUBREY_CR7/article/details/81294949