资讯专栏INFORMATION COLUMN

浅谈java中的并发控制

Gilbertat / 1426人阅读

摘要:并发需要解决的问题功能性问题线程同步面临两个问题,想象下有两个线程在协作工作完成某项任务。锁可用于规定一个临界区,同一时间临界区内仅能由一个线程访问。并发的数据结构线程安全的容器,如等。

并发指在宏观上的同一时间内同时执行多个任务。为了满足这一需求,现代的操作系统都抽象出 线程 的概念,供上层应用使用。

这篇博文不打算详细展开分析,而是对java并发中的概念和工具做一个梳理。
沿着并发模型、并发要解决的问题、基本工具、衍生工具这一思路展开。

线程

首先线程是什么?线程是由OS抽象并实现的,我们知道OS的职责是管理并合理分配硬件资源,那么OS为了更好的管理、分配CPU资源,同时也为了满足同时执行任务这一需求,设计了线程这一概念。

虽然java程序运行在JVM虚拟机上,但是java的线程仍然是对操作系统原生线程的封装,同时,jvm对线程实现时也将jvm的运行栈设计成线程私有内存,因此,java线程和原生线程在理解上实际上没太大区别。

线程的五种状态:

graph LR
新建 --> 就绪;
就绪 --> 运行;
运行 --> 就绪;
运行 --> 阻塞;
阻塞 --> 就绪;
运行 --> 死亡;

先来看上面的就绪状态和运行状态。我们知道线程虽然宏观上是同时执行的,但是微观上使用如时间片轮转算法使得线程依次执行。那么,同一时间只有一个线程执行,其它需要执行的线程处于 就绪队列 中,等待自己被调度到。

而如果线程想要暂时放弃在CPU上运行的权利,就会阻塞自己。这时对应着阻塞状态,同时线程会从就绪队列中移除,进入等待队列。
很显然,阻塞线程被唤醒肯定是进入就绪队列等待调度,而不可能是直接分配到CPU上运行。

在线程同步时,线程可能由于以下情况被阻塞:

同步阻塞。就是被锁阻塞。

等待阻塞。被条件变量阻塞。

其它。调用sleep(), join()或等待IO操作时的阻塞。

并发需要解决的问题 功能性问题

线程同步面临两个问题,想象下有两个线程在协作工作完成某项任务。那么需要解决以下问题:

线程两个线程之间交互数据,必然涉及到数据共享。而某些数据资源无法被多个线程同时使用(临界区),这时需要,即线程互斥问题。

假如一个线程进行的太快,另外一个线程就需要等等它,即线程同步问题。

性能和可用性问题

在多线程程序的性能问题上,如果是对于同样一段临界区的多线程访问,那么则有以下几个思路:

互斥锁。互斥锁即保证同一时间只有一个线程访问临界区并完整执行完,其它线程在临界区外面等待。

无障碍或无锁。线程们一开始直接进入临界区执行,注意其中不能修改共享数据。执行完后再判断刚才这段时间是否有其它线程执行,没有的话才修改共享数据,如果有的话就回滚重来。

降低锁粒度。也即将这个大的临界区拆分成几个小的临界区,分别加互斥锁控制,这样提高了线程同时访问的临界区的机会变多,性能提高。显然这要对代码仔细推敲,考虑如何拆分锁粒度而不影响整体的语义。

以上三种思路的性能优劣没有一个普适的结果,和具体的场景相关。

并发中还会出现以下几种情况导致系统不可用:

死锁。不解释。

饥饿。线程调度算法如果不是平等分配的,那么就可能出现优先级高的线程长时间占用CPU,导致优先级低的线程无法得到执行机会。

活锁。这个我解释不来。。。

并发代码的几个性质

并发编程中需要考虑的几个概念:

原子性:指某个操作一旦被某个线程执行,直到该操作执行完毕都不会有其它线程来干扰。

可见性:指某个变量或某块内存如果被A线程修改,B线程能否马上读取到修改后的值。

有序性:A线程执行的代码序列,在B线程看来是否是有序的。

从我个人的理解来看,原子性属于由并发和线程这一理论概念自然而然推导衍生而来的概念,而可见性和有序性是具体的工程实践中产生的。
实际中,jvm并不能实现的特别完美,总会有工程上的妥协。理论模型与实际模型无法完美契合,总存在一定的偏差。
比如说,jvm为了向性能妥协使用了缓存机制,牺牲了数据一致性,这就产生了可见性的概念,需要程序员编程时自己控制。
jvm为了指令更高效率的执行进行了指令重排优化,则产生了有序性的问题。印象里以前大学里学过的CPU的流水线技术,为了指令能够更好的被CPU流水线利用,减少流水线的空闲时间,编译器编译时也会在不影响 串行语义 的前提下,进行指令重排。
总而言之,这是在性能和理论模型完整性之间的一种妥协。

并发的工具

技术上的工具、概念繁多复杂,但是如果我们能理解技术设计上无时无刻的不运用抽象和分层的手段,
那么,我们可以把技术上的工具分为两种:

最基本的、原生的工具。

在原生提供的工具上,进行封装得到的更高层次的工具。

更高层次的工具对基础工具进行了抽象和封装,屏蔽了其中的实现细节。
这里想强调的是,工具的接口实现是分开的,两者可以没有关系。
如java的监视器锁从接口上来看,其语义和互斥锁一样。然而它并不一定使用互斥锁实现,而是可以为了性能存在优化,只要最终的行为与接口相同即可。

基本工具 锁、条件变量、信号量

有三种用于线程同步的工具:

锁。锁可用于规定一个 临界区,同一时间临界区内仅能由一个线程访问。其他线程则在临界区外等待(阻塞)。

互斥锁。使用信号量实现。临界区外等待的线程会被阻塞。

自旋锁。临界区外等待的线程会忙等。

条件变量(Condition)。线程在某种条件不满足时阻塞自己,等待其它的线程条件满足时再唤醒它们。很显然所有等待的线程要放入一个数据结构中,这个数据结构就在条件变量内。

信号量。操作系统原生的机制。实际上,锁 + 条件变量可完成所有信号量可以完成的逻辑。

在java中,Object类有wait()、notify()和notifyAll()之类的方法。
这些方法可以认为每个对象都内置了一个条件变量,而这些方法是对这些条件变量的操作,因此,可以使用这些方法将对象当作条件变量使用,从而做到线程的同步。

无状态编程 底层机制直接对应得到的

底层机制的特点直接得到的:

1. java中的volatile关键字。
2. CAS。

volatile关键字能够保证变量的可见性,或者说是读或写的原子性。

CAS即compareAndSwap,原子操作
CAS操作直接能够对应到单条CPU指令,因此天然具有原子性。java中是通过JNI调用C语言从而调用CPU底层指令实现。

CAS的行为和以下代码一致:

int cas(long *addr, long old, long new)
{
    if (*addr == old) {
        *addr = new;
        return 1;
    } else {
        return 0; //*
    }
}

那么CAS可以做什么呢?很多乐观并发控制可以基于CAS实现。
比如说,通过一个标记变量来记录临界区被谁占有,线程进入临界区前不断的使用CAS操作判断标记变量是否为空同时将其记录为自己,来实现锁机制。这就是自旋锁的思路。

除此之外,乐观锁也能用CAS实现,比如java的Atomic系列,就是这样实现的。

由基本工具封装、优化而成的衍生工具 synchronized关键字

前面说到可以认为每个对象内置一个条件变量,同样,每个对象也内置一个锁。这个内置锁在Java中被抽象为监视器锁(monitor)。
synchronized关键字的使用实际上就相当于使用监视器锁定义了一个临界区。使用这种语法也特别直观简单,所以java经常用synchronizd来进行线程的同步。

JDK1.6之后,为了提升监视器锁的性能,java通过某些手段进行了优化。其中包含锁优化机制,对应三种锁:

1. 偏向锁
2. 轻量级锁
3. 重量级锁

一开始只有一个线程使用线程时使用偏向锁,当存在多个线程使用时膨胀为轻量级锁,而出现比较多的线程竞争时再膨胀为重量级锁。

并发的数据结构

线程安全的容器,如VectorConcurrentHashMap等。

读写锁,即java中的ReentrantReadWriteLock
读写锁又可以看做一个读锁和一个写锁组成的锁系统,读锁和写锁又叫共享锁和排它锁。

BlockedQueue,阻塞队列。

Atomic。 java提供的atomic包封装了一组常用原子类,使用无锁方式实现。

ThreadLocal。每个线程都拥有一份对象拷贝,互相不干扰。

其它:

双重检查锁

实际上是一种对于线程安全的懒汉单例模式的一种优化。

锁的属性

为了表达某种锁的特点,也会有着很多的概念。
但是这种概念对应的不是某一种锁,而是一类拥有特定属性的锁。如:

悲观锁和乐观锁。
悲观锁假设发生冲突,并使用各种方式保证一次只有一个线程使用临界区。
乐观锁放任线程去使用资源,在执行完后判断刚才是否有其它线程用过(破坏了数据完整性),如果是则撤回重试。

公平锁和非公平锁。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。java中的ReentrantLock是公平锁。

递归锁(可重入锁)/非递归锁(不可重入锁)
同一个线程可以多次获取同一个递归锁,不会产生死锁。
如果一个线程多次获取同一个非递归锁,则会产生死锁。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71470.html

相关文章

  • 浅谈Java并发编程系列(八)—— LockSupport原理剖析

    摘要:此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。阻塞当前线程,最长不超过纳秒,返回条件在的基础上增加了超时返回。唤醒线程唤醒处于阻塞状态的线程。 LockSupport 用法简介 LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。 LockSupport是用来创建锁和其他同步类的基本线程阻塞...

    jeyhan 评论0 收藏0
  • Java并发核心浅谈

    摘要:耐心看完的你或多或少会有收获并发的核心就是包,而的核心是抽象队列同步器,简称,一些锁啊信号量啊循环屏障啊都是基于。 耐心看完的你或多或少会有收获! Java并发的核心就是 java.util.concurrent 包,而 j.u.c 的核心是AbstractQueuedSynchronizer抽象队列同步器,简称 AQS,一些锁啊!信号量啊!循环屏障啊!都是基于AQS。而 AQS 又是...

    cppowboy 评论0 收藏0
  • 浅谈Java并发编程系列(五)—— ReentrantLock VS synchronized

    摘要:线程通过的方法获得锁,用方法释放锁。和关键字的区别在等待锁时可以使用方法选择中断,改为处理其他事情,而关键字,线程需要一直等待下去。拥有方便的方法用于获取正在等待锁的线程。 ReentrantLock是Java并发包中一个非常有用的组件,一些并发集合类也是用ReentrantLock实现,包括ConcurrentHashMap。ReentrantLock具有三个特性:等待可中断、可实现...

    Ocean 评论0 收藏0
  • 浅谈Java并发编程系列(二)—— Java内存模型

    摘要:物理计算机并发问题在介绍内存模型之前,先简单了解下物理计算机中的并发问题。基于高速缓存的存储交互引入一个新的问题缓存一致性。写入作用于主内存变量,把操作从工作内存中得到的变量值放入主内存的变量中。 物理计算机并发问题 在介绍Java内存模型之前,先简单了解下物理计算机中的并发问题。由于处理器的与存储设置的运算速度有几个数量级的差距,所以现代计算机加入一层读写速度尽可能接近处理器的高速缓...

    Edison 评论0 收藏0
  • Java学习路线总结,搬砖工逆袭Java架构师(全网最强)

    摘要:哪吒社区技能树打卡打卡贴函数式接口简介领域优质创作者哪吒公众号作者架构师奋斗者扫描主页左侧二维码,加入群聊,一起学习一起进步欢迎点赞收藏留言前情提要无意间听到领导们的谈话,现在公司的现状是码农太多,但能独立带队的人太少,简而言之,不缺干 ? 哪吒社区Java技能树打卡 【打卡贴 day2...

    Scorpion 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<