1:volatile与synchronized,lock与final

Volatile:通过四种内存屏障实现

Volatile的写之前插入StoreStore屏障,写之后插入StoreLoad屏障

Volatile的读之后插入LoadLoad屏障,读之后插入LoadStore屏障

写一个变量的时候,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。实质是该线程向接下里要读取这个volatile变量的某个线程发出消息。

当读一个变量的时候,JMM会把该线程对应的本地内存置为无效,接下来,线程将从主内存中读取共享变量。实质是该线程接收了之前某个线程发出的消息。

一个线程A写,随后一个线程B读。实质是A通过主内存向B发送消息。

线程间的通信方式有共享内存和消息传递两种。共享内存是隐式通信,显式同步,消息传递是显式通信,隐式同步。默认采用的是共享内存方式。

Synchronized(独占式的可重入锁):通过monitor实现

每个对象有一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下:

 1 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为1,该线程即为 monitor的所有者。

 2 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加1.

 3 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。

当线程释放锁的时候,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。实质是该线程向接下里要读取这个volatile变量的某个线程发出消息。

当线程获取锁的时候,JMM会把该线程对应的本地内存置为无效,接下来,线程将从主内存中读取共享变量。实质是该线程接收了之前某个线程发出的消息。

一个线程A释放锁,随后一个线程B获取锁。实质是A通过主内存向B发送消息。

Final:修饰的类不可继承;修饰的方法不可重写;修饰的变量不可改变引用,但可以改变引用内容。

读final域的重排序规则:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

写final域的重排序规则:禁止把final域的写重排序的构造函数之外。即在对象引用为任意线程可见之前,对象的final域已经被正确的初始化。

volatile和synchronized区别

1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.

2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.

3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.  
  4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.

5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等

6、使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

synchronized和lock区别

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;synchronized使用 Object对象本身的wait、notify、notifyAll 调度机制,Lock 可以使用 Condition 进行线程之间的调度,完成synchronized实现的所有功能。

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)synchronized既可以加在方法上也可以加在代码块上。而Lock需要显示指定起始位置和终止位置。synchronized加锁是托管给 JVM 执行的,而Lock的锁定是通过代码实现的。

5)synchronized是隐式的获取和释放锁,便捷。Lock是显式的获取和释放,但是却具有可操作,可中断,超时获取等新特性。

ReenTrantLock和Synchronized比较

1.   ReenTrantLock 等待可中断:在持有锁的线程长期不释放锁的时候,等待线程可以选择放弃等待,可以避免出现死锁

2.  ReenTrantLock公平锁:按照申请锁的顺序来一次获取锁称为公平锁,但synchronized是非公平锁,ReentrantLock可以通过构造函数实现公平锁

3.  ReenTrantLock  绑定多个Condition:通过多次newCondition可以获得多个Condition对象,可以简单实现比较复杂的线程同步功能

在资源竞争不是很激烈的情况下,synchronized的性能优于ReetrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降非常快,而ReetrantLock的性能基本保持不变。


2:happen-before

的:在不改变程序执行结果的前提下。尽可能提高程序的执行并行度。

定义:1】如果一个操作happen-before另外一个操作,则第一个操作的结果对第二个操作可见,而且第一个操作的执行顺序排在第二个之前(JMM对程序员的承诺,但是并不表示执行顺序也是的这样的前后顺序)

2】如果重排序后的结果和按照happen-before指定的顺序执行结果一致,则允许这种重排序(JMM对编译器和处理器的承诺,在不违背执行结果前提下,可以进行优化)

规则:时间先后顺序和先行发生原则之间没有关系

程序顺序规则:在一个线程中的每个操作都happen-before于该线程的任意后续操作。可以看做是as-if-serial的封装。若是不同线程则不适用这条规则

Volatile规则。Volatile域的写happen-before读。其底层是通过插入内存屏障实现

锁规则。锁的释放happen-before锁的获取。其底层是通过监视器实现

传递规则

Start规则。若线程A执行ThreadB.start(),则线程A的ThreadB.start()操作happen-before线程B的任意操作

Join规则。若线程A执行ThreadB.join()并成功返回,则线程B的任意操作happen-before线程A的ThreadB.join()操作成功返回。


3:Executor框架

Executor将工作单元(runnable和callable)和执行机制(Executor)分开。Executors 类提供工厂方法用来创建不同类型的线程池

Executor框架包含的主要类和接口

Executor:一个接口,是整个框架的基础,其由executorService接口继承(接口是可以继承接口的,并且可以是继承多个接口)。executorService又两个抽象类实现,AbstractExecutorService和ScheduledExecutorService.

ThreadPoolExecutor继承了抽象类AbstractExecutorService,用来执行被提交的任务。其下面有SingleThreadExecutor,FixedThreadExecutor,CachedThreadExecutor三个子类。

SingleThreadExecutor:适用于需要保证顺序的执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。默认使用的是无界双端队列LinkedBlockingQueue,核心线程池数量和最大线程池数量都被指定为1。适用于串行执行任务

FixedThreadExecutor:适用于负载较重的服务器,为了满足资源管理的需求,而限制当前的线程数量。默认使用的是无界双端队列LinkedBlockingQueue,核心线程池数量和最大线程池数量都被指定为创建时指定的参数大小

当一个任务的执行时间比较长的时候。会导致阻塞队列的任务越来越多,极端情况下会导致机器的内存飙升,从而产生OOM异常。一般比较适用于CPU密集型的任务

CachedThreadExecutor:适用于执行很多的短期异步任务。默认使用synchronousQueue作为阻塞队列,核心线程池的数量是0,但是最大线程池的数量是inter.maxvalue,即最大线程池数量是无界的。因此当线程提交的任务快于最大线程池中线程的处理速度,则会因为创建过多线程而耗尽cpu和内存。适用于并发执行大量短小的任务

ScheduledThreadPoolExecutor继承了抽象类ScheduledExecutorService,可以在给定的延迟后运行命令,或者定期执行命令。其下面有ScheduledThreadPoolExecutor,SingleThreadScheduledExecutor二个子类

ScheduledThreadPoolExecutor:适用于需要多个线程执行周期任务,同时为了满足资源管理需求而限制线程的数量。适用于周期性执行任务。

SingleThreadScheduledExecutor:适用于需要单个线程执行周期任务,同时需要保证顺序的执行各个任务。

ScheduledThreadPoolExecutor的具体实现:

ScheduledThreadPoolExecutor采用的delayQueue无界队列作为阻塞队列。阻塞队列delayQueue封装了一个priorityQueue阻塞队列,按照每一个任务的time进行排序,time小的排在前面,时间早的任务先被执行,当time一致的时候按照提交的先后顺序排序,先提交的先执行。

线程执行周期任务的过程:

第一步:线程从DelayQueue中获取已经到期的任务(DelayQueue.take()。

DelayQueue.take()

获取lock

获取周期任务(一直循环,直至获取成功):

若当前DelayQueue为null,则当前线程到condition等待

若当前DelayQueue不为null,则看DelayQueue队列头部的任务执行时间是否大于当前时间,若大于说明该队列中所有任务的时间都没有到,因为这是一个按照time进行排序的优先级队列。则当前线程到condition等待任务的time到达

若DelayQueue不为null,且队首任务是一个可以被执行的任务,则执行该任务,然后判断队首任务执行完成后DelayQueue是否为null,若不null,则唤醒等待在condition上的其他所有线程(因为这是多线程执行周期任务)

释放lock

第二步:线程执行取到的任务

第三步:线程执行完成后,将该任务的time设置为下次将要被执行的时间

第四步:线程将修改time的任务再次放入到DelayQueue(DelayQueue.add()),等待下一次被调用执行

DelayQueue.add()

获取lock

添加任务

因为是无界队列,所有的新任务都是可以被成功添加到DelayQueue

添加任务后,判断新添加的任务是不是DelayQueue的头元素,若是说明DelayQueue前一段时间为null,没有可执行的任务导致线程在condition上等待,此时唤醒所有等待线程,若不是则不做唤醒操作

释放lock

Future和实现Future接口的FutureTask用来表示异步执行的结果

Future就是对于具体的 Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了FutureTask。FutureTask 类实现了 RunnableFuture 接口,而 RunnableFuture 继承了 Runnable 接口和 Future 接口。所以FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future得到Callable的返回值。

FutureTask:未启动状态(run没有执行),已启动状态(run正在执行),已完成状态(run正常执行完成或执行过程中抛出异常导致的异常结束或调用cancel导致的强制结束)

并发相关

FutureTask.get()

调用AQS.acquireSharedInterruptibly方法,该方法调用子类中实现的tryAcquireShared方法来判断acquire是否可以操作成功,成功的条件是FutureTask任务的状态是完成或者已取消,并且runner不null

如果成功则立即返回,否则当前线程加入到等待队列中等待

当其他线程执行release操作(run或cancel)会唤醒当前线程,当前线程再次执行tryAcquireShared返回1,当前线程离开等待队列的时候会唤醒它的后续线程

返回计算的结果或者抛出异常

FutureTask.run()

执行在构造函数中指定的任务

以原子形式更新同步状态(调用AQS.compareandsetstate,设置state为执行完成状态),若更新成功则调用AQS.releaseShared

AQS.releaseShared方法调用子类中实现的tryReleasedhared方法,然后唤醒等待队列上的一个线程

Runnable和Callable接口用来表示执行单元,可以被ScheduledThreadPoolExecutor和ThreadPoolExecutor调用execute或者submit执行。Runnable可以被包装成Callable,还可以将Runnable和待返回的结果包装成Callable.


4:线程池

预先创建若干线程,并且不可以由用户直接对线程的创建进行控制,在这个前提下重复使用固定数量的线程来完成任务的执行。这样避免了操作系统频繁的进行进程上下文切换,无故的增加系统开销,同时线程的创建和消亡都是需要耗费系统资源的。

线程池的优势:

1 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  2 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  3 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定 性,使用线程池可以进行统一的分配,调优和监控。

提交任务到线程池(execute,submit)的执行流程

1:判断核心线程池的线程(工作线程)是否都在执行任务,若不是,则创建一个新的工作线程来执行当前任务,即使此时的阻塞队列(7种)不满,也是新开辟工作线程而不是加入到阻塞队列中

2:当核心线程池的线程都在执行任务,则判断阻塞队列是否为满,若不满则将当前任务加入到阻塞队列中

3:当阻塞队列也满,则判断最大线程池的线程是否都在执行任务,若不是,则开辟新的工作线程来执行当前任务,

4:当最大线程池的线程也都在执行任务,则交给饱和策略处理当前任务。

之所以先判断能否加入到阻塞队列,再判断最大线程池的线程是否都处于工作状态,是因为创建工作线程是需要获取全局锁,有一定的开销代价的。这样做尽可能减少获取全局锁的次数,增加效率。

线程池创建线程的6个参数:

CorePoolSize:线程池的基本大小。当提交新任务到线程池的时候,线程池会创建一个线程来执行任务,即使其他空闲的基本线程(当前阻塞队列中没有了任务)能执行该任务也会创建新的。直到任务数大于基本线程数。

MaximumPoolSize:线程池的最大容量。当任务数超过该量,则会进入饱和策略处理。若阻塞队列是无界的,该参数没有用,因为不管多少线程都会在执行的第二步被加入到阻塞队列中。因此建议阻塞队列选择有界的!

Handle:饱和处理策略。有AbortPolicy(直接抛出异常,默认的策略),CallerRunsPolicy(只用调用者所在的线程来处理该任务),DiscardOldestPolicy(丢弃阻塞队列里时间最久任务,并执行当前任务),DiscardPolicy(直接丢弃,不做处理)

KeepAliveTime:线程池的工作线程空闲后,可以存活的时间。即超过该时间后这个线程会被线程池回收。因此对于任务较多,但是每一个任务执行时间都很短的情况可以调大该时间。这样可以避免线程的反复创建,提高已创建线程的利用率

Timeunit:存活时间的单位。可以是天,时,分,秒,毫秒,纳秒

RunnableTaskQueue:具体可参考7并发集合的阻塞队列。

线程池提交线程执行的2个方法:

execute:用于提交没有返回值的任务。无法判定当前任务是否被线程执行完成

submit:用于提交有返回值的任务。返回的是future类型,可以通过future.get获取返回值,并且该方法会阻塞当前线程直至任务执行完成获得返回值。

Execute和submit的区别

1 可接受的任务类型不同。Execute 只能接受 Runnable 类型的任务,submit 不管是 Runnable 还是 Callable 类型的任务都可以接受,但是 Runnable 返回值均为 void,所以使用 Future 的 get()获得的还是 null。

2 返回值不同。execute没有返回值,submit有返回值,所以需要返回值的时候必须使用 submit。

3 异常处理不同。execute 中的是 Runnable 接口的实现,所以只能使用 try、catch 来捕获 CheckedException,通过实现 UncaughtExceptionHande 接口处理 UncheckedException。submit 中不管是 CheckedException 还是 UncheckedException,直接抛出即可。

线程池关闭线程的2个方法(底层调用的是interrupt,还有一种关闭是用close):

shutdown:中断所有没有正在执行任务的线程,已经开始执行任务的线程将可以把自己的任务执行完。通常用此方法,要是任务不一定要执行完则用下面的

shutdownNow:停止所有正在执行任务的线程,并返回等待执行任务的列表。

线程池参数的选择和配置:

对于I/O繁忙型,因为cpu的利用率不高,因此可以将线程的数量调大,这样在cpu空闲等待i/o操作的时候可以处理别的任务,提高cpu利用率。

对于cpu繁忙型,因为cpu空闲时间较少,此时应减少线程的数量。因为再增加线程的数量,cpu也无法处理,且需要在线程间来回切换。

对于任务具有优先级或者执行时间不同来说,可以设置priorityBlockingQueue阻塞队列来实现。将优先级高的或者执行时间短的排在队列前面,让其先执行。


5:J.U.C脑图

并发相关

并发容器collections具体见知识点7,工具tools具体见知识点12,executor线程具体见知识点3,4,13.原子操作具体见知识点6,locks具体见知识点9,10,11.


6:原子操作类

基本都是使用Unsafe实现的包装类(CAS)

原子更新基本数据类型

AtomicBoolean,AtomicLong,AtomicInteger

原子更新数组

AtomicReferenceArray,AtomicLongArray,AtomicIntegerArray

原子更新引用类型

AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference

原子更新字段类

AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference


7:并发集合

ConcurrentHashMap(1.7版本)

多线程的情况下,hashmap在resize的时候可能产生环造成死循环;hashtable用synchronized修饰保证线程安全,但是是所有访问hashtable的线程竞争同一把锁,会导致效率低下。ConcurrentHashMap采用的是锁分段技术,将数据一段一段存储,每一段分配一个锁。当阻塞一个段上的数据的时候,其他线程可以访问别的段上的数据。Segment是可重入锁

Get:与hashtable的区别是在整个get的过程中是不需要进行加锁的。将使用的共享变量全部定义成volatile类型,由JMM的happen-before原则保证多线程的情况下读取的数据是最新的。与hashmap的区别是需要两次定位,定位Segment –> 定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry(该方法是Unsafe类中方法,主要用于获取obj对象中offset偏移地址对应的Object型field属性值,支持Volatile读内存语义)

Ssize:segment数组的长度,即锁的个数。

Sshift=ssize以2为底的对数

Segmentshift:32-sshift

SegmentMask=ssize-1

每一个segment对应的数组长度:initialCapacity除以ssize,(结果大于1,则取大于等于的最小2的N次方,否则等于1)

定位segment:int index=(hash>>>segmentShift)&segmentMask

定位hashentry:int index=hash&(tab.length-1)

Put:hashmap是在插入后进行一次扩容判断,如果达到了进行扩容,但是很有可能扩容之后就没有新元素的插入,相当于是进行了一次无效扩容。而Concurrenthashmap是先判断再插入,在扩容之后插入对应的元素Concurrenthashmap。扩容只针对某一个锁segment对应的数组进行扩容而不是所有的segment全部扩容。

1)如果没有初始化就先调用initTable()方法来进行初始化过程

2)如果没有hash冲突就直接CAS插入

3)如果还在进行扩容操作就先进行扩容,调用helpTransfer()方法帮助扩容,目的就是调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作

4)如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,

5)最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环

6)如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

Size:会先尝试用2次不加锁的方式获取count,若两次获取的时候不一致则采用加锁的方式计算(此时要锁住所有的segment中的clean,put,delete操作)。出现不一致是通过modCount进行判断,因为每一次clean,put,delete操作均会使modCount的值加1。

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作。

1.7和1.8区别

1.JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

2.JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了

3.JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表

ConcurrentLinkedQueue:

非阻塞式的链表结构,使用循环CAS实现的。该队列由head和tail两个节点组成(其中的tail节点并不是永远指向队列的尾节点的)队列中的每一个结点由元素item和指向下一个节点的next引用组成。

入队:将当前入队节点插入到队列尾部

1:定位尾节点(可能是tail节点也可能是tail的next节点)

2:设置入队节点为尾节点

3:根据hops更新tail节点

出队:从队列弹出一个元素,但并不是每一次都更新头结点head。当头结点元素是null则更新,当不null的时候,将该节点的元素置null,不更新头结点。

CopyOnwritearrayList:

CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationExceptio异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了

缺点:缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

 阻塞队列:支持两个附加操作的队列

概念:

1:支持阻塞的插入:当队列满时,队列会阻塞继续插入元素的线程,直到队列不满

2:支持阻塞的删除:当队列空时,队列会阻塞继续删除元素的线程,直到队列不空

处理方式:一直阻塞(take,put),抛出异常(add,remove),返回特殊值(poll(time,unit),offer(time,unit)),超时退出(poll,offer)

对null值处理:不接收null值的插入,相应的方法在碰到null值的时候会抛出异常。因为null值在阻塞队列里作为poll的特殊值返回,若是允许插入null值,则获取的时候无法判断是失败还是说请求的数据就是null。

分类及用途:

ArrayBlockingQueue:数组实现的有界阻塞队列,按照FIFO原则对元素排序,但是不保证线程公平的访问

以put和take为例解释底层实现:

主要用的是ReentrantLock和condition实现,

 private static final long serialVersionUID = -817911632652898426L;//因为阻塞队列是支持序列化的,所以会产生一个序列化id,具体可见知识点十五的12;

Int takeIndex//take,poll,remove的脚标

Int  putIndex//put,offer,add的脚标

Int count//容器的容量

final ReentrantLock lock;/** Main lock guarding all access */

  private final Condition notEmpty;  /** Condition for waiting takes */

private final Condition notFull;   /** Condition for waiting puts */

    public void put(E e) throws InterruptedException {

        checkNotNull(e);

        final ReentrantLock lock = this.lock;

        lock.lockInterruptibly();

        try {

            while (count == items.length)

                notFull.await();

            enqueue(e);

        } finally {

            lock.unlock();

        }

    }

    private void enqueue(E x) {

        // assert lock.getHoldCount() == 1;

        // assert items[putIndex] == null;

        final Object[] items = this.items;

        items[putIndex] = x;

        if (++putIndex == items.length)

            putIndex = 0;

        count++;

        notEmpty.signal();

    }

    public E take() throws InterruptedException {

        final ReentrantLock lock = this.lock;

        lock.lockInterruptibly();

        try {

            while (count == 0)

                notEmpty.await();

            return dequeue();

        } finally {

            lock.unlock();

        }

    }

    private E dequeue() {

        // assert lock.getHoldCount() == 1;

        // assert items[takeIndex] != null;

        final Object[] items = this.items;

        @SuppressWarnings("unchecked")

        E x = (E) items[takeIndex];

        items[takeIndex] = null;

        if (++takeIndex == items.length)

            takeIndex = 0;

        count--;

        if (itrs != null)

            itrs.elementDequeued();

        notFull.signal();

        return x;

    }

LinkedBlockingQueue:链表实现的无界阻塞队列

PriorityBlockingQueue:支持优先级的无界阻塞队列,默认按照自然顺序排列,可自定义类实现compare接口,或者初始化的时候指定构造函数Comparator进行排序。但是这个阻塞队列是不会阻塞生产者的,而只会在没有可消费的数据的时候阻塞消费者。因此当生产者的速度快于消费者的速度时候,时间一长,会最终耗尽所有可用的堆内存空间。

DelayQueue:延迟获取元素的无界阻塞队列。常用在缓存系统的设计,定时任务的调度等。当消费者从队列里获取元素的时候,若该元素没到到达延时时间,就阻塞当前线程,即只会阻塞消费者,不会阻塞生产者

SynchronousQueue:不存储元素的阻塞队列。但是所有的put操作必须等待一个take操作,否则不能继续添加元素。

LinkedTransferQueue:

LinkedBlockingDeque:由链表实现的双向阻塞队列,可以用在Fork-join的框架中

实现:(可以类似操作系统中一组生产者,一组消费者,公用n个缓冲区的pv操作)


8:基本概念

内存模型的三大特性:

有序性:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile和synchronized实现

可见性:当一个线程修改一个共享变量的时候,另外一个线程可以读到这个修改后的值。具体实现方式有:volatile,synchronized,final

原子性:在执行过程中不可中断的一个或一系列操作。通过锁总线或者锁缓存(推荐)的方式实现。下面的8个操作都是原子性的

工作内存和主内存之间的交互操作

Lock:作用于主内存变量,它把一个变量标识为一条线程独占的状态

Unlock:作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

Read:作用于主内存变量,把一个变量的值从主内存传输到工作内存。以便后面的load使用

Load:作用于工作内存变量,将read读到的值放入到工作内部的变量副本中

Use:作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎

Assign:作用于工作内存变量,把一个从执行引擎收到的值赋给工作内存变量

Store:作用于工作内存变量,把工作内存中的一个变量的值传送给主内存,以便随后的write使用

Write:作用于主内存变量,把store操作从工作内存取得的变量值放入到主内存中。

这8中基本操作满足以下规则:

Read,load和store,write每一对操作必须同时出现。且每一对的两个操作是按顺序执行,但是不保证连续执行。即在read和load之间是可以插入其他指令。

一个变量同一时刻只允许一条线程进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock后,该变量才会被释放。

对一个变量执行lock操作会清空工作内存中此变量的值,在执行引擎使用这个变量前会重新load。对一个变量执行unlock操作,必须将工作内存中此变量同步到主内存中。

并发和并行:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行

线程优先级:对于偏重计算即cpu繁忙型的线程应该降低优先级,防止独占cpu

Daemon线程:是一种支持型线程。当一个java虚拟机栈不存在非Daemon线程的时候java虚拟机栈就会退出,此时的Daemon线程的finally代码块不一定执行。

等待队列和同步队列(阻塞队列)的关系

调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。

并发相关

几个方法的比较

Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。

Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。

obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。

obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

LockSupport.park()/LockSupport.parkNanos(longnanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

线程安全的实现方式:

不可变:不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。比如被final修饰的基本数据类型,String,枚举类型。

互斥同步:synchronized,ReentrantLock

非阻塞同步:

原子操作:具体见6原子操作类

CAS(基于冲突检测和乐观并发。先进行操作,如果没有其他线程争用共享数据则操作成功,若是有争用,则表示发生冲突,采取不断重试,直至成功的补偿措施)。

CAS的问题:

1:ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类(12个原子操作类之一)来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

2:循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

3:只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

栈封闭:多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

线程本地存储:ThreadLocal:线程局部变量,是一种多线程间并发访问变量的解决方案。当使用 ThreadLocal维护变量时, ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。                     

ThreadLocal用在什么地方?

1:保存线程上下文信息,在任意需要的地方可以获取。比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

2:线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失

ThreadLocal 为什么保证线程私有?

每个线程中都有一个ThreadLocalMap对象,该map以Threadlocal的实例为key,以存储的值为value。每个线程通过ThreadLocal设置或者获取变量时都是操作自己内部的 ThreadLocalMap,与其他线程无关,所以保证了线程私有。即每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的。

ThreadLocalMap为什么会引起内存泄漏?

并发相关

由于ThreadLocalMap的生命周期跟Thread一 样长, 如果没有手动删除对应key会导致内存泄漏 。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现 key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。因此需要自己在finally中强制remove掉。

将ThreadLocal对象定义为static修饰的好处:

1:这个变量是针对一个线程内所有操作共享的,设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用的时候装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

2:由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除。

同步代码块和同步方法的区别:

对于普通方法,锁是当前的实例对象,

对于静态方法,锁是当前类的class对象

对于同步代码块,锁是Synchronized括号里配置的对象

代码块锁的粒度更小,可以选择只同步会发生同步问题的部分代码而不是整个方法。相反同步方法默认用this或者当前类class对象作为锁。


9:锁的分类

  1. 从其它等待中的线程是否按顺序获取锁的角度划分--公平锁与非公平锁

公平锁:在绝对时间上,同步队列中等到时间最长的线程最优先获得锁,即锁的获取顺序是按照请求的顺序执行,符合FIFO原则

非公平锁:不按照请求顺序获得锁,释放后线程自己去竞争获得。但是由于空间局部性的原理,上次获得锁的线程再次获得锁的概率比其他线程要大。

公平锁保证了锁的的获取顺序,不会造成饥饿甚至饿死的现象,但是代价是要进行大量的线程切换,效率相对来说不高。非公平锁可能造饥饿现象,但是进程切换的代价小(因为可重入的前提下,同一个线程获取锁不需要进行线程的切换),效率高。

  1. 数据库中常用到的锁--共享锁、排它锁

共享锁:也称读锁或S锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。 

排它锁:也称独占锁、写锁或X锁。如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。

  1. 从一个线程能否递归获取自己的锁的角度划分--重入锁(reentrantLock)与自旋锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法(内层方法也需要获取同一对象的锁)会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

 自旋锁 VS 适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁(不放弃cpu时间片,减少cpu切换和线程的恢复操作)。自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

  1. 从锁的设计理念来分类--悲观锁、乐观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

  1. 读写锁ReentrantReadWriteLock

读锁:可重入的共享锁(读锁和读锁是兼容的)

写锁:可重入的排他锁

读写状态设计:在独享锁中state这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)

锁的降级:当前拥有写锁,再获取到读锁,然后释放先前拥有的写锁。将写锁降级为读锁的过程。注意拥有写锁,释放写锁,再获得读锁的过程不叫做降级。

  1. 对锁的不同效率进行的分类--偏向锁、轻量级锁和重量级锁(针对的是synchronized)

偏向锁(需要显示开启)和轻量级锁都是乐观锁,重量级锁是悲观锁

无锁是通过CAS实现,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒造成的用户态和心态的切换而影响性能。重量级锁是将除拥有锁的线程以外的线程都阻塞。

并发相关

1、偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

2、偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后判断锁对象目前是否处于锁定状态。若未锁定则偏向撤销后恢复到未锁定,若锁定则设置为轻量级锁的状态。

3、轻量级锁的加锁过程(争取线程都可以成功的拿到对象的锁,则不用升级为重量锁)

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态.

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

4.轻量级锁的解锁过程:轻量级锁解锁时,把复制的对象头替换回去(CAS)如果替换成功(就是要把无锁的状态放回去给对象头,之后锁继续被拿还是轻量级锁,但是如果锁已经是重量级锁了,那么就失败,之后锁就是重量级的锁了),锁结束,之后别的线程来拿还是轻量级锁,如果失败,说明已有竞争,释放锁,此时把对象头设为重量级锁,并notify 唤醒其他等待线程。

重量级锁

就是让争抢锁的线程从用户态转换成内核态,不管是阻塞还是唤醒都需要进行状态的切换。让cpu借助操作系统进行线程协调。

synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成。

在代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和异常结束处)织入monitorexit指令实现。线程执行到monitorenter处,将会获取锁对象对应的monitor的所有权,即尝试获得对象的锁。(任意对象都有一个monitor与之关联,当且一个monitor被持有后,他处于锁定状态)

1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。

4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

6. 如果自旋成功则依然处于轻量级状态。

7. 如果自旋失败,则升级为重量级锁。


10:等待通知机制

Synchronized与对象object的wait,notify等方法配合实现等待通知机制

调用wait和notify以及notifyAll的前提是必须获得对象的锁。WaitThread首先获得对象的锁,然后调用对象的wait()方法,从而放弃了对象的锁进入到对象的等待队列WaitQueue中,状态由RUNNABLE到WAITING。由于WaitThread释放了对象的锁,NotifyThread随后获得对象的锁,并调用对象的notify方法,此时将WaitThread从WaitQueue移到SynchronizedQueue中,状态由WAITING到BLOCKED。但此时WaitThread依旧不会从wait返回,当且仅当WaitThread再次获得对象的锁才会返回,状态由BLOCKED到READY

Lock与Condition配合实现等待通知机制

其作用机制和synchronized类似。不同的是每一个监视器对象有一个同步队列和一个等待队列组成。而AQS同步器由一个同步队列和多个等待队列组成。


11:AQS队列同步器(reentrantlock,semphore,countdownlatch,futuretask,reentranreadwritelock)

同步器的设计是基于模板方式的。使用这需要继承同步器并重写指定的方法(tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared,isHeldExclusively只有这五种),然后将同步器组合在自定义的同步组件中(自定义同步组件可以实现Lock接口,也可以不实现),并调用同步器的模板方法(模板方法都是被final修饰,常见的有acquire,acquireInterruptibly,tryAcquireNanos,acquireShared,acquireSharedInterruptibly,tryAcquireSharedNanos,release,releaseShared,getQueueThreads),模板方法会调用用户重写的方法。

独占式同步状态的获取与释放:

1:调用acquire模板方法,进而调用重写的tryAcquire方法,当获取成功的时候退出返回,否则进入步骤2

2:获取失败的线程会被构造成Node.EXCLUSIVE同步节点,调用addWaiter(Node.EXCLUSIVE)方法加入到同步器内置的FIFO双向队列中。

3:将同步节点添加到队列会首先进行快速尝试在尾部添加,当尾节点不null的时候会通过compareAndSetTail(Node except,Node update)确保节点可以被线程安全的添加。当为null的时候,会调用enq(final Node update)方法进行添加,这是一个通过死循环确保正确添加。在死循环中只有通过CAS将节点设置为尾节点之后,当前线程才能够返回。

4:节点进入到同步队列后,调用acquireQueued(addWaiter(Node.EXCLUSIVE))方法进行自旋,每一个结点(线程)都在自省的观察,当条件满足,获取到同步状态就会从自旋退出,释放同步状态,唤醒下一个节点。

5:步骤4的条件有2个。一是当前节点的前驱节点是头结点,二是当前节点成功的获取到同步状态。

6:步骤5设置条件一的原因有2个。一:头结点是成功获取到同步状态的节点,而头结点释放了同步状态后需要唤醒其后继节点,因此后继节点被唤醒的线程需要检查自己的前驱节点是不是头结点。二:维护同步队列的FIFO原则。


12:并发工具类

CountDownLatch:

一个线程或多个线程等待其他线程完成操作,或线程中的一步或多步操作等待其他操作完成。构造函数接收的int数值表示等待计数器。调用countDown会使计数器的值减1,减到为0的时候,那些因为调用了await()方法而被阻塞的线程将会被唤醒。

用join也可以实现该作用。

CyclicBarrier:

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障是,所有被屏障拦截的线程会被执行。但此时不保证执行的顺序。调用await表示自己到达屏障,然后自己被阻塞。高级的构造函数中可以指定一组线程到达屏障后先执行的任务操作(实现runnable,是指定的任务,不是指定的线程)。

CyclicBarrier和CountDownLatch的区别:

1、CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

2、CyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

3、CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

4,、CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

Seamphore:

信号量机制,用来控制同时访问特定资源的线程数量,通过协调各个线程,以保障合理的使用公共资源。常用在流量控制(数据库允许的链接数量等)。调用acquire获取公共资源的一个使用许可,调用release释放归还一个公共的许可。

Exchanger:

用作线程间的协作,进行数据交换。一个线程执行exchange方法后,会等到第二个线程执行exchange方法,到达同步点后两个线程交换数据。如果有多个线程调用exchange方法,则是按照线程执行的顺序(程序中线程的调用顺序不代表是线程的执行的顺序)进行两两交换。当出现单独线程的时候,没有配对线程交换数据的时候,该单独线程会一直等待,一直阻塞,只能手动强制结束。


13:fork-join框架

一个并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每一个小任务的结果后得到大任务的结果。

工作窃取:某个线程从其他队列里窃取任务执行。将一个大任务分割成若干个小任务,每一个小任务放到不同的队列(这里用双端队列),并且为每一个队列分配一个线程执行。当某线程将自己队列的任务执行完后就去别的队列里窃取任务执行。被窃取任务的线程永远从队列的头部拿任务,窃取任务的线程从队列的尾部拿任务,这样可以充分利用线程进行并行计算,减少线程竞争。当且仅当双端队列里只有一个任务的时候会发生竞争。

实现:

1将任务定义为forkjoin任务,需要继承RecursiveTask(有返回值)或者RecursiveAction(没有返回值),重写其中的compute方法

2:compute方法定义处理的逻辑和阈值。调用fork和join方法执行任务

3:创建ForkJoinPool,因为forkjoin任务需要ForkJoinPool来执行。


14:并发的实际场景题

 两个线程并发执行以下代码,假设a是全局变量,初始为1。计算所有的输出可能

1

2

3

4

void foo(){

    a=a+1;

    printf("%d ",a);

}

多个线程在操作一个全局变量的时候会有以下三步

1:首先会从主内存中拷贝一个副本到自己的工作内存,然后内部的操作都是针对副本,对全局变量的值不会产生影响

2:再更新全局变量,将副本的值由自己的工作内存写回到主内存

3:读取全局变量的值(改变后),然后输出

第一种结果:3,2

1.  P1从内存读出a(a=1)

2.  P1使a值+1,然后写入内存

3.  P2从内存读出a(a=2)

4.  P1从内存读出a,然后压入printf栈(预备打印,此后a的值改变不影响printf结果)(a=2)(3.4顺序可换)

5.  P2使a值+1,然后写入内存(a=3)

6.  P2从内存读出a,然后压入printf栈(预备打印,此后a的值改变不影响printf结果)(a=3)

7.  P2 printf显示到屏幕(a=3)

8.  P1 printf显示到屏幕(a=2)

9.  结果 3 2

第二种结果 2,3

1. P1从内存读出a(a=1),然后使a值+1,然后写入内存,然后读出并压入printf栈,显示到屏幕(a=2)

2. P2从内存读出a(a=2),然后使a值+1,然后写入内存,然后读出并压入printf栈,显示到屏幕(a=3)

3. 结果 2 3

第三种结果 3,3

1.  P1从内存读出a(a=1),然后使a值+1,然后写入内存(a=2)

2.  P2从内存读出a(a=2),然后使a值+1,然后写入内存(a=3)

3.  P1从内存读出a,然后压入printf栈,显示到屏幕(a=3)

4.  P2从内存读出a,然后压入printf栈,显示到屏幕(a=3)

5.  结果 3 3

第四种结果 2,2

1.  P1从内存读出a(a=1)

2.  P2从内存读出a(a=1)

3.  P1使a值+1,然后写入内存,然后读出并压入printf栈(a=2)

4.  P2使a值+1,然后写入内存,然后读出并压入printf栈(a=2)

5.  printf显示

6.  结果 2 2

相关文章:

  • 2021-11-29
  • 2021-09-02
  • 2021-11-30
  • 2021-09-19
  • 2021-06-08
  • 2021-08-31
  • 2022-12-23
  • 2022-01-12
猜你喜欢
  • 2021-10-13
  • 2021-07-23
  • 2021-11-10
  • 2021-09-16
  • 2022-12-23
  • 2021-10-24
相关资源
相似解决方案