线程并发基础

前言java

本篇博客将对线程并发的一些基础知识进行阐述,你们也能够参考楼主之前关于线程的2篇博客:《Java多线程感悟一》《Java多线程感悟二》
编程


CPU、进程、线程安全

咱们知道进程是操做系统进行资源分配的最小单位,一个进程内部能够有多个线程进行资源的共享,线程做为CPU调度的最小单位,CPU会依据某种原则(好比时间片轮转)对线程进行上下文切换,从而并发执行多个线程任务。打个比喻,CPU就像高速公路同样,每条高速公路会有并排的车道,而线程就像在路上行驶的汽车同样。咱们能够经过/proc/cpuinfo来查看服务器有几个CPU,以及每一个CPU支持的核心线程数,这样咱们就了解了服务器有几条高速公路,以及每条高速公路有几个并排的车道。
服务器


多线程引起的思考多线程

粗粒度的来说,JAVA对内存的划分能够分为:堆和栈。对于多线程而言,堆就好像主内存,而栈就像是工做内存。堆是多线程共享的,线程工做时要将堆中的数据 COPY TO 工做内存才能进行工做。而线程何时COPY DATA TO工做内存?工做内存中的数据计算完毕又何时写回主内存?当多个线程之间对共享的数据进行读写,那么这一瞬间的读写是个什么顺序呢?一个线程可否看到或者何时才能看到另外一个线程的改变呢?读和读的线程是否不须要控制并发呢?当有写线程参与时,对读线程有什么影响呢?写线程存在时,读线程是否必定要等呢?线程须要完成一连串的读写操做,是否容许其余线程插入进来呢?并发


多线程的基础:可见性ide

正如上面所言,因为存在主内存以及工做内存,每个线程都是在本身的工做内存中进行工做的,若是线程在本身的区域埋头苦干,殊不知道其余线程已经对共享的数据作出修改,这将会引起“可见性”问题。好比咱们有一个配置文件,有一个写线程会对配置参数进行修改,其余不少读线程读取配置进行业务上的计算,若是写线程修改了,但是读线程依旧按照老的配置进行,也不知道读线程何时能“醒悟”,这多么可怕!固然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的代码:


wKioL1blH5ixjXhrAAAWUfXLJbQ920.png


一段死循环,先获取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机制:


wKiom1blKN6Sb8muAAAvM-vLojs480.png

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


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

wKiom1blKZzxQH9ZAAAKwZuOXkg475.png

wKioL1blKsuh_oNWAAAI6PlkqUQ571.png

能够看到,没有锁的限制,读写并发进行操做!

相关文章
相关标签/搜索