java线程并发基础

CPU、进程、线程

我们知道进程是操作系统进行资源分配的最小单位,一个进程内部可以有多个线程进行资源的共享,线程作为CPU调度的最小单位,CPU会依据某种原则(比如时间片轮转)对线程进行上下文切换,从而并发执行多个线程任务。

打个比喻,CPU就像高速公路一样,每条高速公路会有并排的车道,而线程就像在路上行驶的汽车一样。

我们可以通过/proc/cpuinfo来查看服务器有几个CPU,以及每个CPU支持的核心线程数,这样我们就了解了服务器有几条高速公路,以及每条高速公路有几个并排的车道。

多线程引发的思考

粗粒度的来讲,JAVA对内存的划分可以分为:堆和栈。对于多线程而言,堆就好像主内存,而栈就像是工作内存。堆是多线程共享的,线程工作时要将堆中的数据 COPY TO 工作内存才能进行工作。

而线程什么时候COPY DATA TO工作内存?工作内存中的数据计算完毕又什么时候写回主内存?当多个线程之间对共享的数据进行读写,那么这一瞬间的读写是个什么顺序呢?一个线程能否看到或者什么时候才能看到另一个线程的改变呢?

读和读的线程是否不需要控制并发呢?当有写线程参与时,对读线程有什么影响呢?写线程存在时,读线程是否一定要等呢?线程需要完成一连串的读写操作,是否允许其他线程插入进来呢?

多线程的基础:可见性

正如上面所言,由于存在主内存以及工作内存,每一个线程都是在自己的工作内存中进行工作的,如果线程在自己的区域埋头苦干,却不知道其他线程已经对共享的数据做出修改,这将会引发“可见性”问题。

比如我们有一个配置文件,有一个写线程会对配置参数进行修改,其他很多读线程读取配置进行业务上的计算,如果写线程修改了,可是读线程依旧按照老的配置进行,也不知道读线程什么时候能“醒悟”,这多么可怕!

当然JAVA已经为我们提供了轻量级的volatile来解决这个问题。(volatile不仅仅提供可见性,而且对于CPU/编译器优化带来的代码重排性也做了限制)

不仅仅可见

可见,这只不过是一瞬间的事情,更多时候,我们要的是一段时间内的操作的封闭性,即原子性。

一个对象,它可以执行很多代码,但是我们希望它在执行某段代码(即临界区)时能够有一些限制,比如只允许一个线程对这个对象进行这段代码的操作,第二个线程要想操作必须等待第一个线程结束后。

说的直白点,这个对象就好像一把锁,它存在3个临界区,那么这个对象在任意时刻只能处在一个临界区内!

synchronized

通过synchronized来对对象的代码进行临界区划分,从而完成可见性以及原子性的要求。synchronized是隐式的锁方式,因为加锁和解锁的过程是JAVA帮助我们来进行的,无需我们关心。

正是由于这种隐式的方式,我们应重点关注的是synchronized锁住的是什么?锁住的是对象?还是锁住的是对象的临界区?锁对象的生命周期是什么?锁对象的粒度多大,是否可以优化?是否因为锁对象的粒度太大导致代码的串行,使得系统效率低下?

Lock

synchronized是JAVA最为古老的,也在不断优化的锁机制,在JAVA发展过程中也推出了新的锁机制:Lock。

Lock是显式的锁,需要手动的上锁以及解锁。特别需要注意的是必须fiannly解锁,否则会出现死锁现象。

第一个常用的锁是:ReentrantLock ,这是一个排他锁,和synchronized功能类似,不管线程是读,还是写,都是互斥的。

第二个常用的锁是:ReentrantWriteReadLock,这是读写锁,如果读,用readLock,如果写,用writeLock,从而达到读与读的并发,读写之间的互斥。

Atomic与CAS机制

很多时候,我们仅仅希望对某个变量做一系列简单的动作,希望保证可见性以及原子性的操作,JAVA已经为我们提供了Atomic相关的类,使用最为广泛的就是AtomicInteger。这类Atomic虽然没有利用synchronized/Lock这样的锁机制,但是通过CAS达到了同样的目的。

看一段AtomicInteger的代码:

一段死循环,先获取old值,然后尝试对比修改为新值,虽然没有临界区的锁控制,多个线程并发进行修改,但是显然compareAndSet保证了只会有一个线程能成功(相当于获得锁),这就是CAS机制。

如果我们将死循环改成有限几次尝试CAS修改的话,就是自己设置了自旋的次数了。

用空间换时间:CopyOnWrite机制

在前文涉及的锁机制,都无法避免一个问题:一旦存在写线程,那么读线程势必无法并发进行。那么可否让读写并发进行呢?

CopyOnWrite机制:对于一个容器而言,多个读线程可以并发的读取该容器的内容;如果存在写线程,那么先COPY一份此容器,写线程对COPY的容器进行操作,待写线程操作完毕后,将老的容器的引用重置为COPY后的容器。

这样一来,读写线程操作的容器不是同一个容器,当然可以并发进行操作。通过Copy的机制,利用空间来换取时间,需要注意的是当大量存在写线程时对内存的消耗。

并发编程集合类

StringBuffer 和 StringBuilder

StringBuffer的方法都打上了synchronized标签,自然是线程安全的;后来JDK走了一个极端,为我们提供了StringBuilder这样的非线程安全类,在单线程的环境下,提升了性能。

Hashtable 、 HashMap 、ConcurrentHashMap

Hashtable和HashMap同上面的StringBuffer/StringBuilder一样。

后来JDK出现了java.util.concurrent并发包,比如ConcurrentHashMap就通过分解锁的粒度,提高并发能力。下面我们来仔细剖析下ConcurrentHashMap的实现原理:

对于Hashtable/HashMap而言,其实里面存放的K/V并没有分层处理,对于Hashtable而言,如果锁,那么意味着锁住整个Hashtable的内容,意味着就算是读与读也得串行进行。

而ConcurrentHashMap则将K/V进行划分,多个K/V成为一个segment,默认有16个segment,显然不同segment之间的读写可以并发进行,自然将锁的粒度一下子降低16倍。

在每个segment内部,实际上借助于extends ReentrantLock实现读写互斥;而不同segment之间则不存在互斥关系。

CopyOnWriteArrayList 、 CopyOnWriteArraySet 、ArrayList 、Vector

Vector和ArrayList类似于StringBuffer/StringBuilder一样。

我们来看一段CopyOnWriteArrayList的代码,揭开CopyOnWrite机制:

add时利用排他锁达到互斥,在代码中可以看到Arrays.copyOf进行COPY,增加完元素后,利用setArray达到引用重置的目的。

再来看看获取元素的代码:

可以看到,没有锁的限制,读写并发进行操作!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值