并发和JUC

一、并发

1、线程的状态

  • new:线程对象被创建,并且没有调用start方法
  • Ready:线程等待分配时间片
  • RUNNABLE:线程分配到时间片正在执行状态
  • BLOCKET:线程阻塞状态,发生在线程等待锁的状态
  • WAITING:线程在调用不带等待时间的wait()、join()、park方法的时候
  • TIME_WAITING:线程在调用带等待时间的wait()、join()、park()、sleep()方法的时候

2、创建线程的方法

  • Tread类和Runnable接口
  • Callable接口,通过实现Callable接口创建线程可以获取线程的返回值Future对象,Future对象调用get()方法可以获取具体返回值,但是这个方法是阻塞的

3、synchronized

  1. 锁方法和锁代码块的区别:锁方法的时候默认锁的是当前的对象,而且对整个方法进行加锁。锁代码块的时候锁的粒度是被锁包围的代码块,并且锁的对象可以自己选取。
  2. 锁普通方法和锁静态代码块的区别:锁普通方法默认锁的是当前对象,锁静态方法锁的是当前类。
  3. 锁升级:当锁粒度从偏向锁向轻量级锁升级的时候,会产生STW,所以当线程竞争比较多的时候可以禁用synchronzied的偏向锁。

4、wait和notify

  • wait和notify的方法都属于Object的方法,所以wait方法和notify方法之间是需要同步进行调用的,所以在调用这两个方法之前需要获取synchronized锁
  • 为什么wait和notify要放在Object里面呢,因为synchronized可以锁任何的Object。

5、线程中断(interrupt)

  • 线程中断的方法:Thread.interrupt():给线程发送一个中断信号,线程被打上中断标记;Thread.interrupted():获取线程的中断标记,并且会重置线程的中断标记;Thread.isInterrupted():获取线程的中断标记,并且不会重置中断标记
  • 只有线程调用了显式抛出了InterruptedException的方法的时候,调用Thread.interrupt()方法才会抛出InterruptedException(sleep(),wait(),join())。否则只是将线程的打断标识设置为打断。

6、并发问题

  1. 并发问题产生的原因:由于多个线程同时竞争同一个共享资源
  2. 死锁:
    死锁产生的原因:两个线程相互持有对方等待的锁,导致线程相互阻塞发生死锁
    发现死锁的方法:1、通过jdk自带的工具检测(jdk/bin/jconsole.exe、jdk/bin/jvisualvm.exe、jdk/bin/jmc.exe)。2、非本地环境不能使用jdk自带的工具情况下,可以使用jstack命令打印线程栈信息,如果有死锁将被日志记录。
  3. 活锁:在死锁的时候,线程自动放弃自己所持有的锁。两个线程都是这样,导致一直无限循化下去

7、JMM内存模型

  1. cpu三级缓存cpu缓存分为L1、L2、L3三级缓存,L1、L2是是每个核所独有的,L3是所有核所共享的。但是又cpu缓存一致性协议的存在,所以三级缓存中不会发生缓存不一致的情况。但是为了保持缓存一致性这样会造成性能的损耗,一个核中的L1或者L2缓存发生变化之后需要同步到L3中,并且其它核如果与该核缓存了同样的数据,此时数据也会失效,并且需要重新到主存中读取。这也是导致伪共享的根本原因(需要解决伪共享就需要保证每个核缓存的数据没有公共的部分。因为cpu读取数据到缓存中是按照一个单元长度去读取的,所以为了没次都整取,整取避免了同一个数据需要读多次或者同一数据被其它从cpu也缓存了,避免伪共享)。
  2. cpu其它缓存:因为cpu的三级缓存必须遵循缓存一致性协议,该协议又会损耗性能,所以cpu又引入了Store Buffer、Load Buffer(还有其他各种Buffer),引入了这些缓存提高了性能但是又引入了新的问题那就是缓存不一致性和cpu的内存重排序问题。向内存中写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入L1中,同时同步写入主内存中。

8、指令重排序和内存可见性

  1. 指令重排序:分为编译器指令重排序-编译器会对于没有先后依赖关系的语句进行重排序。重排序的好处就是将属于不同线程或方法的相同的操作进行批量的执行,就想手机工厂中,有人负责屏幕安装有人负责电池安装,操作都是批量进行同一个,这样可以提高工作效率;cpu指令重排序-让没有依赖关系的指令同时执行;CPU内存重排序-由于cpu的缓存没有及时写入主存造成。cpu内存重排序是造成内存可见性的主要原因。
  2. 内存屏障:为了禁止编译器重排序和 CPU 重排序,在编译器和 CPU 层面都有对应的指令,也就是内存屏障。比如写入指令后面加一个读屏障,则写入成功同步到主存之前是不能够读取该变量的指令的。这也正是JMM和happen-before规则的底层实现原理。
  3. as-if-serial:指令的重排序要遵守单线程下,重排序之后不会改变执行结果
  4. happen-before:就是写入数据必须发生在读取数据之前,也就是读取数据一定是写入的最新数据。开发者可以通过使用volatile、synchronized、final等关键字来告诉编译器或者cpu不能进行指令重排,使之遵循happen-before。对volatile变量的写happen-before变量的读。对synchronized的加锁happen-before解锁。happen-before具有传递性。

二、JUC

1、并发容器

  1. BlockingQueue阻塞队列:相当于一个消费者和生产者模型。接口中的添加方法有add()、put()、offer(),其中put()是阻塞的即添加的时候如果队列满了则阻塞等待。而add()队列满时会抛出异常,offer会返回false。取数据的方法有-remove()、take()和pull()其中take()和pull()是阻塞的。
    ArrayBlockingQueue源码分析:

初始化一个数组用来容纳元素,并且创建一个锁,当调用只有一个容量大小参数的构造方法的时候默认创建的是一个非公平锁,即等待添加元素或者获取元素而阻塞的线程是非公平竞争的。这里创建了两个Condition,将非空状态和非满状态分开这样可以在非空的时候只唤醒等待获取元素的线程,而不会唤醒等待插入的线程,这就比wait和notify方法要智能。

put方法先获取锁,可以看到这里加锁的方式是可以打断的锁,可以打断的锁和不可以打断的锁的区别后面说到锁的时候再进行分析。当队列满的时候就等待,不然就调用enqueue(e)方法添加元素。 

 enqueue(e)方法向数组中添加元素,并且唤醒应队列为空而获取元素时阻塞的线程。再来看一下另一个阻塞的方法,take()方法。

同样,这里跟put方法获取的是同一把锁,所以put和take方法是相互阻塞的。然后加上可以打断的锁,判断如果队列为空,则take方法等待进入阻塞,如果不为空则调用dequeue方法。

 应为取走了一个元素,所以唤醒因为队列满了而不能添加元素而阻塞的线程。

2、一般的阻塞队列都是按照元素先进先出的顺序获取元素,接下来我们看一个比较特殊的队列,它会按照元素的分值对元素进行排序而不是按照队列的先进先出的顺序。

PriorityBlockingQueue

构造方法中有两个参数,一个是阻塞队列容量的大小,一个是队列中的值的比较策略。比较策略默认是null,此时阻塞队列中的元素都必须实现Comparable接口,否则会报错。

接下来看一下该队列的put方法,put方法中调用offer(E e)方法。

主要的操作为先添加锁,然后判断阻塞队列的容量是否达到的最大值,如果达到了最大值,这里跟ArrayBlockingQueue以及LinkBlockingQueue不同的是,这里不是阻塞添加线程二十对队列进行扩容。扩容方法即是对数组进行扩容这里不进行扩展。如果容量没有达到最大则添加元素到队列中。这里根据是否有自定义的Comparator接口函数进行不同的处理。

该队列的底层是通过数组实现了一个小顶堆,每次添加数据的时候需要对堆进行调整,每次取数据直接从堆顶取出就可以了。

DelayQueue:延迟队列,这是要给非常重要的队列,有许多延迟的任务调度的地方都使用了延迟队列。下面我们看一下延迟队列的实现方式。

 延迟队列是基于PriorityBlockingQueue队列实现的,排序则依据的是延迟的时间。

 

 我们看一下延迟队列的take()方法。

添加元素的大概流程为:先上锁,这是操作所有队列的必须流程,获取 PriorityBlockingQueue中的堆顶元素,如果堆顶元素等于空,则线程阻塞等待。如果不等于空则判断当前堆顶元素是不是已经达到了延迟时间,如果时间达到了则返回该数据,如果没达到,则判断leader变量是否为空,如果为空的话,则将leader变量赋值为当前线程,并且等待堆顶元素的延迟时间,如果不为空,则说明有线程在之前已经尝试过取元素,则当前线程陷入无限等待中。put方法则是向PriorityBlockingQueue中添加一个元素,这里不再赘述。

CopyOnWrite:

  copyOnWrite是一个重要的思想,即在读取的时候不进行加锁,在更改的时候更改数据的副本,再通过悲观锁或者是乐观锁的方式进行写入。提高了并发量,再读多写少的场景下非常的适合。

CopyOnWriteArrayList:我们通过这个CopyOnWriteArrayList来详细分析一下CopyOnWrite的思想。

这个List就是一个数组和一把锁组成的, 先看一下它的读方法:读方法直接是从数组里面获取数据,并且没有加锁,不加锁那是如何保证读取方法的安全性的呢,我们来看一下添加元素的方法add(E e)。

这里先获取锁,然后将原数组复制一遍,然后在新数组里面添加元素之后,将老数组添加成新数组组。这里也可以看出,copyonwrite是不具备强一致性的。

ConcurrentHashMap:这个在我另外一篇文章里面已经说过了,这里就不再说了ConcurrentHashMap源码分析

ConcurrentSkipListMap/Set:为什么要出现这个map或者是set,这是对于非并发包中的HashMap的TreeMap,即有序的map。说到这个ConcurrentSkipListMap/Set的时候我们先说一下跳跃表。

跳跃表:在链表中隔一定的数抽出一个元素,将多个该元素组成一个新的链表作为原链表的范围索引。

查询数据的时候先查询最外层索引,一步步定位到数据的范围,然后在范围内进行查找,提高了查找的效率。添加元素的时候则需要对链表以及索引链表进行修改操作,这里为了提高性能,采取了链表的无锁插入或者删除。

  1. 删除元素时将当前节点的next指针指向一个mark节点,表示逻辑删除。删除的时候先判断该元素的next指针是否指向的时mark节点,如果是,则表示已经被删除了,则执行物理删除,如果不是则进行标记。这样每次只有一个动作保证的原子性。
  2. 添加元素的时候同样是判断前面或者后面的节点是否被mark标记。 

2、同步工具

同步工具中包括Semaphore、CountDownLatch、CyclicBarrier、Exchanger、Phaser、Lock中都是通过AbstractQueuedSynchronizer简称AQS来实现同步功能的。AQS中有一个链表用来存放同步等待的线程,还有一个state变量。通过CAS来操作这个state变量来达到并发状态下的同步功能,接下来我们就一个个的攻破这些同步工具,来深入了解一下AQS。

Semaphore:信号量,创建Semaphore的时候可以指定一次多少个信号量,线程开始执行的时候从Semaphore中取得信号量之后才能继续执行,每次取走一个信号量,总的信号量就减少。等信号量都取完之后后面的线程想要执行就只能等待前面的线程执行完成之后释放信号量了。

Semaphore类中有一个Sync类型的对象,Sync又是继承自AQS。还有NofairSync非公平同步和FairSync公平同步都是继承自Sync。

构造方法中传入信号量个数,默认使用的是非公平的同步方式。

最终是调用了setState(int newState)方法 将AQS中的state变量设置为信号量的值。再来看一下主要方法,获取信号量的方法。

点进sync的acquireSharedInterruptibly方法

 如果线程被打断过则抛出InterruptedException。如果没打断执行tryAcquireShared方法

因为默认创建的是非公平的同步,所以最终会执行上图的方法。如果是当前state的值小于0或者是state值大于0但是对于state的CAS操作成功,即获得了信号量则返回当前的state值。根据上上图的方法,当返回的state值小于0时会执行doAcquireSharedInterruptibly(arg)方法 

方法主要是讲线程放入到等待队列的队尾,放入的过程中会检查当前线程的节点是否是等待队列的队列的头节点了,如果是头节点,说明在添加节点的时候有其它的节点删除了,所以需要重新尝试去获取信号量,调用之前上面的方法tryAcquireShared(arg),如果获取到了则可以继续执行了,如果仍然没有获取到信号量则将线程park。

CountDownLatch:使用场景为主线程需要等待多个线程执行完成之后在执行。此时使用CountDownLatch,每个线程执行完成之后会将CountDownLatch中的数字减1,主线程判断数字不大于0的时候则可继续执行,不然阻塞等待。

 CountDownLatch内部也是依赖AQS进行同步的,Sync这个静态内部类继承了AbstractQueuedSynchronizer。查看构造函数

也是将AQS中的state变量设置成传入的count值。查看countDown方法,这里是通过CAS将state的值减1。再看await方法

先判断当前的state变量是否为0,如果不为0则执行doAcquireSharedInterruptibly方法。方法里面是将当前线程放到AQS的等待队列中。后面几个同步工具的原理也都是通过AQS来实现线程的同步。

Atomic:原子操作做

Atomic类可以将多个非原子的操作合并成一个原子操作而不需要使用锁。比如i++这类操作是一个非原子操作,所以是非线程安全的,我们就可以使用AtomitInteger或者AutomicLong来操作这种累加或者累减。它们的底层都是使用了unsafe来操作变量。Unsafe里面的所有的方法都是native的,具体都是通过CAS操作来更改数据,使用do{}while()来循化进行CAS操作直到修改成功。

其它的引用类型要进行原子操作需要使用AtomicStampedReference<T>,该类还可以防止ABA的问题发生。比如A->B->A这样看来是没有修改的,其实是修改了两次,所以存在问题,AtomicStampedReference<T>会添加一个版本号来防止ABA的问题。

AtomicMarkableReference<T>这个和AtomicStampedReference<T>是一样的,只是版本号使用的是boolean类型只能标记是否修改了,修改了几次并不能体现,所以不能完全避免ABA问题。

由于篇幅过长所以后面的锁以及线程池放在后面讲解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值