多线程笔记整理(草稿)

进程、线程

概念

  • 进程就是进行中的程序,它是个动态的概念。是系统进行资源分配与调度的基本单位.
  • 线程就是进程中并发执行的一个子任务。
  • 程序计数器和运行栈是线程私有,共享内存空间和父进程资源。
  • ThreadPoolTaskExecutor线程池拿到线程,然后CompletionService completionService = new ExecutorCompletionService(threadPoolTaskExecutor) 得到CompletionService做异步执行
  • Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。completionService.submit(new Callable) 然后completionService.take()拿到Feature, future.get()得到结果

守护线程

  • 当线程只剩下守护线程的时候,JVM就会退出;补充一点如果还有其他的任意一个用户线程还在,JVM就不会退出
  • thread.setDaemon(true)必须在thread.start()之前设置
  • 作用:当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要一个个关闭的麻烦。

CAS

CAS 机制:

  • 适用场景:乐观认为并发不高,不需要阻塞,可以不上锁。
  • 特点:一般使用中常用自旋,不断比较更新,直到成功。
  • 缺点:高并发cpu压力大;ABA问题
  • 关联:在AQS和Atomic相关类中也是使用到了CAS机制。

公平锁与非公平锁

  • 先对锁进行请求的就一定先获取到锁,那么这就是公平锁,反之是非公平锁。
  • 非公平锁机制的效率往往会胜过公平锁的原因是,恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟.
  • 持有锁的时间相对较长或者请求锁的平均间隔较长,应该使用公平锁。因为这些情况下,插队带来的吞吐量提升可能不会出现

AbstractQueuedSynchronizer

获取锁思路

AQS 在获取锁的思路是,先尝试直接获取锁,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,则会失败并将当前线程放在队列中,按照 FIFO 的原则等待锁。

final void lock() { acquire(1);}

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • addWaiter是线程入队之前包装成Node,然后追加到队尾。
  1. 创建好Node后,如果队列不为空,使用cas的方式将Node加入到队列尾。注意,这里只执行了一次修改操作,并且可能因为并发的原因失败。因此修改失败的情况和队列为空的情况,需要进入enq。
  2. enq是个死循环,保证Node一定能插入队列。注意到,当队列为空时,会先为头节点创建一个空的Node,因为头节点代表获取了锁的线程,现在还没有,所以先空着。
  • 调用acquireQueued阻塞线程
  1. 这里先自旋几次,不成功在阻塞
  2. 如果前一个节点正好是head,表示自己排在第一位,可以马上调用tryAcquire尝试。如果获取成功就简单了,直接修改自己为head(这步是实现公平锁的核心,保证释放锁时,由下个排队线程获取锁)。
  3. 自选几次后如果还是不能获取锁就挂起,直到上一个线程release后释放锁并打断该线程。继续自旋,此时就可以tryAcquire成功,然后进行处理了。

参考:https://www.jianshu.com/p/fe027772e156

释放锁思路

唤醒后继节点,后继节点为nul则需要跳过该节点从tail节点开始。这是因为:

为何后继节点为null: 存在超时、被中断的情况
为何不是从node.next开始: 在于node.next仍然可能会存在null或者取消了,所以采用tail回溯办法找第一个可用的线程。最后调用LockSupport的unpark(Thread thread)方法唤醒该线程

独占锁

ReetrantLock 有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁

共享锁

CountDownLatch

读写锁

ReentrantReadWriteLock、StampedLock

共享和独占的相同和不同点

与 AQS 的独占功能一样,共享锁是否可以被获取的判断为空方法,交由子类去实现。
与 AQS 的独占功能不同,当锁被头节点获取后,独占功能是只有头节点获取锁,其余节点的线程继续沉睡,等待锁被释放后,才会唤醒下一个节点的线程,而共享功能是只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒 AQS 队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能

自旋??

condition实现原理

  1. 首先,线程1调用lock.lock()时,由于此时锁并没有被其它线程占用,因此线程1直接获得锁并不会进入AQS同步队列中进行等待。
  2. 在线程1执行期间,线程2调用lock.lock()时由于锁已经被线程1占用,因此,线程2进入AQS同步队列中进行等待。
  3. 在线程1中执行condition.await()方法后,线程1释放锁并进入条件队列Condition中等待signal信号的到来
  4. 线程2,因为线程1释放锁的关系,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
  5. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是加入到了AQS等待队列中去了
  6. 待线程2执行完成之后并调用lock.unlock()释放锁之后,会唤醒此时在AQS队列中的头结点.所以线程1开始争夺锁(由于此时只有线程1在AQS队列中,因此没人与其争夺),如果获得锁继续执行。
    直到线程1释放锁整个过程执行完毕。
    可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作
    原文:https://blog.csdn.net/u010412719/article/details/52089561

acquire和acquireInterruptibly的区别

在acquire中,如果park操作被中断,那么只是记录了interrupted状态,然后继续进入循环判断是否可以acquire或者阻塞。而在acquireInterruptibly中,一旦被中断,那么就立即抛出InterruptedException异常

更多参见:https://www.infoq.cn/article/java8-abstractqueuedsynchronizer

ReetrantLock

  • 默认非公平锁.可通过 其构造器设置为公平锁。
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • 公平的锁原理是:老老实实的开始就走 AQS 的流程排队获取锁

  • 非公平锁原理是:在 lock 的时候先直接 cas 修改一次 state 变量(尝试获取锁),成功就返回,不成功再走 AQS 的流程。

  • 公平锁和非公平锁区别:


    多线程笔记整理(草稿)_第1张图片
    image.png
  • ReetrantLock支持对同一线程重加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁

  • ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等

  • Condition能够精细的控制多线程的休眠与唤醒。

  • 对于一个锁,我们可以为多个线程间建立不同的Condition

  • AbstractQueuedSynchronizer又称为队列同步器,内部通过一个int类型的成员变量state来控制同步状态.内部类Node线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列.

动态代理

  • newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

并发辅助类

CountDownLatch

它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能

CyclicBarrier

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

Semaphore

字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

线程安全容器

ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList

并发队列

ArrayBlockingQueue、ConcurrentLinkedQueue、ConcurrentLinkedQueue、SynchronousQueue、PriorityBlockingQueue

多线程笔记整理(草稿)_第2张图片
image.png

参考: https://blog.csdn.net/vernonzheng/article/details/8247564

ArrayBlockingQueue

有界队列,基于数组实现的阻塞队列

LinkedBlockingQueue

其实也是有界队列,但是不设置大小时就时Integer.MAX_VALUE,内部是基于链表实现的

  • ArrayBlockingQueue 与 LinkedBlockingQueue 对比
    ArrayBlockingQueue 实现简单,表现稳定,添加和删除使用同一个锁,通常性能不如后者
    LinkedBlockingQueue 添加和删除两把锁是分开的,所以竞争会小一些

PriorityBlockingQueue

具有优先级的阻塞队列,无界队列

ConcurrentLinkedQueue

无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常

SynchronousQueue

比较奇葩,内部容量为零,适用于元素数量少的场景,尤其特别适合做交换数据用,内部使用 队列来实现公平性的调度,使用栈来实现非公平的调度,在Java6时使用CAS代替了替换了原来的锁逻辑,它是Executors.newCachedThreadPool()的默认队列。
参见:https://blog.csdn.net/yanyan19880509/article/details/52562039

Executor框架

线程池

参考:https://www.jianshu.com/p/ade771d2c9c0

ExecutorService

CompletionService

你可能感兴趣的:(多线程笔记整理(草稿))