并发

并发与并行

并行:同时执行

并发:交替执行,一般需要在单位时间内

线程

创建方式

  1. 实现 Runnable, Runnable规定的方法是run(),无返回值,无法抛出异常

  2. 实现 Callable, Callable规定的方法是call(),任务执行后有返回值,可以抛出异常

  3. 继承 Thread类创建多线程:继承 java.lang.Thread类,重写 Thread类的run()方法,在run()方法中实现运行在线程上的代码,调用 start()方法开启线程。Thread类本质上是实现了 Runnable接口的一个实例,代表一个线程的实例。启动线程的唯方法就是通过 Thread类的 start实例方法。 start()方法是一个 native方法,它将启动一个新线程,并执行run方法

  4. 通过线程池创建线程

进程&线程&协程

进程是资源分配的最小单位&也是程序执行的最小单位

进程&线程的区别:

  1. 线程在操作系统看来也是进程,只不过是共享了数据(虚拟内存、文件系统、信号、信号处理函数)的进程

  2. 创建:

    • 线程通过 pthread_create函数来创建 clone flags创建线程

    • 进程通过fork函数来创建

  3. 安全性

    • 线程共享数据,所以会导致数据安全,必要时需要互斥

    • 进程由于数据代码独立,所以天生进程安全

  4. 成本

    • 进程创建成本较高(需要复制父进程的内容)

    • 线程创建成本较低(只需要在父线程的内存区域内分配栈內存,其他的共享)

  5. 进程的出现是为了并行/并发执行提高系统性能

  6. 线程的出现是为了避免使用进程时的空间浪费和时间性能

  7. 协程的岀现是为了减少线程切换的开销,线程切换需要在用户态和内核态之间切换,而协程完全在用户态切换

轻量级的线程,拥有自己的执行代码块,但是却不需要系统调用来切换,只需要在用户空间切换。

实现:通过寄存器(指令)暂存指令、数据等等,通过状态变量切换代码块的执行

死锁、活锁、饥饿&竞态

线程安全的活跃性问题可以分为 死锁、活锁、饥饿三种,下面逐一说明:

1、活锁状态:

活锁 就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试。

开发中使用的异步消息队列就有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。

解决活锁的方案:方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试

2、死锁状态:

线程在对同一把锁进行竞争的时候,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁。

解决死锁的方案:破坏产生死锁的四个必要条件之一即可 (互斥、请求与保持、不可剥夺、循环等待)

3、饥饿状态:

饥饿,就是线程因无法访问所需资源而无法执行下去的情况,分为两种情况:

  • 一部分程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态

  • 一部分线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行

饥饿状态解决方案:

  • 保证资源充足,很多场景下,资源的稀缺性无法解决

  • 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源

  • 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短

4、线程安全的静态条件问题

同一个程序多线程访问同一个资源,如果对资源的访问顺序敏感,就称存在竞态条件,代码区成为临界区。 大多数并发错误一样,竞态条件不总是会产生问题,还需要不恰当的执行时序。

最常见的竞态条件为:

  • 先检测后执行执行依赖于检测的结果-->检测结果依赖于多个线程的执行时序-->多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。一种可能的解决办法就是:在一个线程修改访问一个状态时,要防止其他线程访问修改,也就是加锁机制,保证原子性。

  • 延迟初始化(典型为单例)

通信的方法

  1. 线程同步,while循环,忙等待/sychonized、wait/notify/锁的使用,condition、countdownlunch

  2. 共享变量,threadlocal/voliate/本地队列等等

线程安全

cas、原子类、syn、lock、park(底层,UNSAFE类的操作)、aqs

线程的状态

  1. 新建(new):新创建了一个线程对象。

  2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(blocked):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    • (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

    • (二). 同步阻塞:运行(running)的线程在获取对象的同步锁或处理IO请求时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

    • (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  5. 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程池 *

线程池参数

  • corePoolSize:核心线程数,如果核心线程达到这个数量,任务会放入阻塞队列

  • maximumPoolSize:最大线程数,当阻塞队列满了后,会创建新线程执行任务

  • keepAliveTime:非核心线程的空闲存活时间。还有个参数是时间单位。

  • workQueue:任务阻塞队列,常用ArrayBlockingQueue、LinkedBlockingQueue

  • threadFactory:创建线程的工厂,

  • RejectedExecutionHandler:任务拒接策略

    • 中止策略:抛出异常

    • 丢弃策略:丢弃任务

    • 抛弃最旧策略:丢弃任务队列中最靠前的任务

    • 调用方执行:由调用的线程去执行任务

执行流程

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

使用

  • 创建方法

    • Executors,该方法创建的是一些预定义的线程池,不过不建议使用,有三种容易出现oom,fixed、single是因为任务队列是无界队列,catch是因为线程池是无界的

    • ThreadPoolExecutor,自定义线程池

  • 提交任务

    • execute() 提交不需要返回值的任务。

    • submit() 提交需要返回值的任务。线程池会返回一个future 类型的对象,通过这个future 对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

  • 关闭线程池(采用中断的方法,所以任务一般添加线程中断的判断)

    • shutdown:中断所有没有执行任务的线程

    • shutdownNow:尝试停止正在执行任务的线程

几种状态

  • RUNNING能接受新提交的任务,并且也能处理阻塞队列中的任务

  • SHUTDOWN关闭状态,不再接受新提交的任务.但却可以继续处理阻塞队列中已经保存的任务

  • STOP不能接收新任务,也不处理队列中的任务,会中断正在处理的线程工

  • DIDYING所有的任务已经终止, wokercount=0

  • TERMINATED在 terminated()方法执行后进入此状态

合理配置

CPU 密集型任务应配置尽可能小的线程,如配置Ncpu+1 个线程的线程池。由于IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。或使用公式 CPU核数/(1-阻塞系数),其中阻塞系数是0.8~0.9

当然也需要综合考虑任务优先级,执行时间、以及对其他资源的依赖性考虑

Executor的线程池

  • newFixedThreadPool()方法:该方法返回一个固定线程池数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲的线程,则立即执行。若没有,则新的任务会被暂存到一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • newSingleThreadExecutor()方法:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到线程池,任务会被保存到一个任务队列里中,待线程空闲,按先入先出的顺序执行队列中的任务。

  • newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的数量不确定,但若有空闲可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完成后,将返回线程池进行复用。1分钟没有任务,则回收线程

    • return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

  • newSingleThreadSchduledExecutor() 方法:该方法返回一个ScheduledExecutorService对象,线程池的大小为1,ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。

  • newScheduledThreadPool()方法:该方法返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。

任务结束后是否回收线程

通过源码可以看到,实际是一个worder一直在运行,他被存储在一个works的set中,worder执行完自己的任务后,会尝试获取新任务,当获取的任务为空是,会释放线程(processWorkerExit);getTask在两种情况下会返回空,一种是SHUTDOWN或stop了队列为空;另一个种是工作线程数大于最大线程数,或工作线程的空闲时长符合条件&&工作线程大于1,并且队列为空

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    decrementWorkerCount();
    return null;
}
if ((wc > maximumPoolSize || (timed && timedOut))
    && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;
    continue;
}                     

概念:安全点(Safepoint):程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)

安全区域(Safe Region):当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程;

synchronized

类锁和对象锁

  • 类锁作用于class文件,对象锁作用于对象,

  • 修饰方法,修饰静态方法锁作用于class,修饰普通方法锁作用于对象

对象头中的markword关键字

锁状态

25bit

4bit

1big是否是偏向锁

2bit锁标志位

无锁状态

对象的hashcode

对象分代年龄

0

01

偏向锁

  1. 判断是否为可偏向状态- Markward中锁标志是否为‘01′,是否偏向锁是否为′1

  2. 如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤5,否则进入步骤3

  3. 通过CAS操作竞争锁,如果竞争成功,则将 Markward中线程ID设置为当前线程lD,然后执行5;竞争失败,则执行4

  4. CAS获取偏向锁失败表示有竟争。当达到 safepoint时获得偏向锁的线程被挂起,先检查原有持有锁的线程状态,如果是未活动或已退出同步代码块,则释放锁,唤醒线程;如果还在执行同步代码块,则偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块

  5. 执行同步代码

轻量级锁

  1. 进行加锁操作时,jvm会判断是否已经是重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象 Markword复制到该锁记录中

  2. 复制成功之后,jwm使用CAS操作将对象头 Markword更新为指向锁记录的指针,并将锁记录里的 owner指针指向对象头的 Markward。如果成功,则执行3,否则执行5

  3. 更新成功,则当前线程持有该对象锁,并且对象 Markward锁标志设置为00,即表示此对象处于轻量级锁状态

  4. 更新失败,jvm先检查对象 Markward是否指向当前线程栈帧中的锁记录,如果是则执5,否则执行6

  5. 表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分( Displaced Mark Word)为null,并指向 Mark Word的锁对象,起到一个重入计数器的作用

  6. 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁

重量级锁

  1. 分配一个 Objectmonito对象,把 Mark word锁标志置为10,然后 Mark Word存储指向Objectmonitor对象的指针。 Objectmonitor对象有两个队列和一个指针,每个需要获取锁的线程都包装成 Objectwaiter对象

  2. 多个线程同时执行同一段同步代码时, Objectwaiter先进入 Entry List队列,当某个线程获取到对象的 monitor以后进入 Owner区域,并把 monitor中的 lowner变量设置为当前线程同时 monito中的计数器 count+1;

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

TODO:https://github.com/farmerjohngit/myblog/issues/12

锁比较

优点

缺点

适应场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法仅存在纳秒级别的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步块的场景

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程使用自旋会消耗CPU

追求响应时间。同步块执行速度非常快

重量级锁

线程竞争不使用自旋,不会消耗CPU

线程阻塞,响应时间缓慢

追求吞吐量。同步块执行时间较长。

偏向锁的撤销:

JDK15默认关闭偏向锁优化,如果要开启可以使用XX:+UseBiasedLocking,但使用偏向锁相关的参数都会触发deprecate警告

原因

  1. 偏向锁导致synchronization子系统的代码复杂度过高,并且影响到了其他子系统,导致难以维护、升级

  2. 在现在的jdk中,偏向锁带来的加锁时性能提升从整体上看并没有带来过多收益(撤销锁的成本过高 需要等待全局安全点,再暂停线程做锁撤销,也就是说安全点会触发stw(stop the world))

  3. 我个人理解是说原子指令成本变化(我理解是降低),导致自旋锁需要的原子指令次数变少(或者cas操作变少 个人理解),所以自旋锁成本下降,故偏向锁的带来的优势就更小了。维持偏向锁的机会成本(opportunity cost)过高,所以不如废弃

修饰

  • 修饰静态方法锁定的事类

  • 修饰非静态方法锁定的是对象,不同对象调用synchronized方法不会阻塞

lock

lock和synchronized对比

  • lock是接口,synchronized是关键字

  • synchronized异常会自动释放锁,lock需要手动释放

  • lock可以使用线程中断,可公平、非公平;synchronized非公平

  • lock可以尝试获取锁,知道锁有没有获取成功

  • synchronized适合少量同步,lock适合大量同步

  • synchronized 独占式,lock公平锁与非公平锁

Sychronized和 Reentrantlock的区别

  1. sychronized是一个关键字, Reentrantlock是一个类

  2. sychronized会自动的加锁与释放锁, Reentrantlock需要程序员手动加锁与释放锁

  3. synchronized的底层是JWM层面的锁, Reentrantlock是API层面的锁

  4. sychronized是非公平锁, Reentrantlock可以选择公平锁或非公平锁

  5. synchronized锁的是对象,锁信息保存在对象头中, Reentrantlock通过代码中int类型的 state标诉来标识锁的状态

  6. sychronizedll底层有一个锁升级的过程

ReentrantLock

可重入锁:一个线程申请到锁之后,它可以继续申请锁使用。synchronized也是一种可重入锁。

ReentrantLock 在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

公平锁和非公平锁主要体现在lock的时候

  • 公平锁在lock的时候会看aqs队列中是否有线程,如果有,则加入队列排队,如果没有则抢锁

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() && // 如果当前队列中有线程,则返回false,让线程进入aqs队列
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 可重入的判断
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

  • 非公平锁在在lock的时候直接抢锁

    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) { // 非公平直接抢锁
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 可重入的判断
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

  • 公平与非公平只体现在获取锁的时候,如果线程已经进入aqs的队列,则都是排队获取锁;锁释放后唤醒排在最前面的线程

trylock()和lock()方法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回 false

  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

ReentrantReadWriteLock

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

读锁共享,写锁独占

AQS *

队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int 成员变量表示同步状态,通过内置的FIFO 队列来完成资源获取线程的排队工作。

模版方法

定义算法的基本骨架,将部分方法的实现延迟到子类。最常见的是Spring框架里的各种Template。

使用

常用方法:

  • getState():获取当前同步状态。

  • setState(int newState):设置当前同步状态。

  • compareAndSetState(int expect,int update):使用CAS 设置当前状态,该方法能够保证状态设置的原子性。

  • acquire:独占式获取同步状态,失败进入同步队列

  • release:独占式释放同步状态,成功会唤醒同步队列第一个线程

可重写方法:

  • tryAcquire(shared):独占(共享)式获取同步状态,成功true,失败false

  • tryRelease(shared):独占(共享)式释放同步状态,成功true,失败false

  • isHeldExclusively:是否独占

源码

node节点

线程的两种模式:

  • SHARED(shared):表示线程以共享的模式等待锁(如ReadLock)

  • EXCLUSIVE(exclusive):表示线程以互斥的模式等待锁(如ReetrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁

线程节点的状态,wait status

  • CANCELLED(cancelled):值为1,表示线程的获锁请求已经“取消”【取消】

  • SIGNAL(signal信号):值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我【唤醒】

  • CONDITION(condition条件):值为-2,表示线程等待某一个条件(Condition)被满足【阻塞】

  • PROPAGATE(propagate传播):值为-3,当线程处在“SHARED”模式时,该字段才会被使用上【共享】

  • 初始化Node 对象时,默认为0

head 和 tail:

AQS 还拥有首节点(head)和尾节点(tail)两个引用,一个指向队列头节点,而另一个指向队列尾节点。 注意:因为首节点head 是不保存线程信息的节点,仅仅是因为数据结构设计上的需要,在数据结构上,这种做法往往叫做“空头节点链表”。对应的就有“非空头结点链表”

独占式同步状态的获取:

  • 调用自定义同步器实现的tryAcquire(int arg)方法,该方法需要保证线程安全的获取同步状态。

    • 如果同步状态获取失败(tryAcquire 返回false)

      • 则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)

      • 通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部

      • 最后调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程(调用parkAndCheckInterrupt()方法),而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

    • 成功,正常执行

同步状态的释放和获取

同步状态释放:

  • 释放同步状态 tryRelease(int arg)【重写的方法】

  • 唤醒后继节点 unparkSuccessor(Node node)

共享式同步状态的获取:

多了一步传播状态的判断,当前置节点被唤醒后,会判断他后面的节点是不是共享式节点,如果是会释放。被释放的节点会继续判断,直到遇到独占式节点,这就是所谓的传播。

condition

核心方法:

  • await方法就是将线程的锁释放,构建新节点,放到等待队列的尾部

  • single方法就是将等待队列中的线程移到同步队列中,并唤醒该线程,尝试获取锁

结构:

一个同步队列+多个等待队列。

其他

ReentrantLock:对state进行递增和递减

公平锁:获取同步状态的时候添加了一步判断是否有前驱节点

读写锁:高16位表示读状态,低16位表示写状态

写状态就为c&0x0000FFFF,读状态为c >>> 16(无符号位补0右移16位)。当写状态增加1状态变为c+1,当读状态增加1时,状态编码就是c+(1 <<< 16)

死锁

  1. 首先需要将死锁发生的是个必要条件讲出来:

    1. 互斥条件:同一时间只能有一个线程获取资源。

    2. 不可剥夺条件:一个线程已经占有的资源,在释放之前不会被其它线程抢占

    3. 请求和保持条件:线程等待过程中不会释放已占有的资源

    4. 循环等待条件:多个线程互相等待对方释放资源

  2. 死锁预防,那么就是需要破坏这四个必要条件

    1. 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。

    2. 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。(使用尝试获取锁的方法)

    3. 打破请求保持条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

    4. 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。(申请锁的顺序保证一致)

    避免死锁常见的算法有有序资源分配法、银行家算法。

有序资源分配法是操作系统中预防死锁的一种算法,这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。 系统要求申请进程:

1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;

2、在申请不同类资源时,必须按各类设备的编号依次申请。 例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1; 若采用动态分配有可能形成环路条件,造成死锁。 采用有序资源分配法:R1的编号为1,R2的编号为2; PA:申请次序应是:R1,R2 PB:申请次序应是:R1,R2 这样就破坏了环路条件,避免了死锁的发生。

wait和sleep的区别

  1. 所属类:首先,这两个方法来自不同的类分别是 Thread和 Object,wat是 object的方法,seep是 Thread的方法

  2. 作用范围: sleep方法没有释放锁,只是休眠,而wait释放了锁,使得其他线程可以使用冋步控制块或方法

  3. 使用范围:wait, notify和 notify只能在同步控制方法或者同步控制块里面使用,而 sleep可以在任何地方使用

  4. 异常范围:slep必须捕获异常,而wait, notify和 notify不需要捕获异常

notify&notifyall的区别

  1. 锁池和等待池的概念

    1. 锁池:假设线程A已经拥有了某个对象(注意不是类)的锁,而其它的线程想要调用这个对象的某个 synchronized方法(或者 synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

    2. 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在 synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁,同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的 notifyallo方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的 notify0)方法,么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

  2. 如果线程调用了对象的wait方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁

  3. 当有线程调用了对象的 notifyall方法(唤醒所有wait线程)或notify方法(只随机唤醒个wai线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify后只要一个线程会由等待池进入锁池,而 notify会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

  4. 所谓唤醒线程,另—种解释可以说是将线程由等待池移动到锁池,η otifyal调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而not只会唤醒一个线程。

ThreadLocal *

结构

  • 自定义的threadlocalmap,每个线程都持有一个map,key为ThreadLocal,value为值

  • 采用开放寻址法

    • 插入:根据ThreadLocal的hashcode计算在entry数组中的位置。如果没有,直接存储;如果有,则依次next查找是否有空的位置,如果找到尾部,还是没有,从头部开始查找,直到查找到为null,插入结束。

    • 查找:同理,查找停止是在查找到的元素为null的情况

    • 删除:如果匹配到要删除的元素,会对元素后的节点重新hash,避免在查找的时候,误认为后面没有元素了

引用

  • 强引用:new对象

  • 软引用:即将发生内存泄漏的时候

  • 弱引用:gc的时候,threadlocal

  • 虚引用:随时都有可能被gc

内存泄漏

引用关系:

由于ThreadLocalMap 的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。(说白了,就是为了降低内存泄漏的机率)也就是说当指向threadlocal的变量为null时,这时候没有任何的强引用指向threadlocal,threadlocal作为弱引用,当gc垃圾回收的时候会被回收掉。 ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应key 就会导致内存泄漏,而不是因为弱引用。

总结:

  •  JVM 利用设置ThreadLocalMap 的Key 为弱引用,来降低内存泄露。

  • JVM 利用调用remove(显示调用)、get、set(隐式调用,也就是不一定每次都会触发) 方法的时候,回收弱引用。

  • 当ThreadLocal 存储很多Key 为null 的Entry 的时候,而不再去调用remove、get、set 方法,那么将导致内存泄漏。

  • 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value 可能造成累积的情况。

并发工具 *

ReadWriteLock 和 StampedLock

StampedLock可以说是对ReadWriteLock的优化,主要是在读操作上,虽然读锁时共享似的,但也避免不了申请锁消耗的时间。

StampedLock优化读模式,基于假设,假设大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改(stamp时间戳),然后通过 validate 方法确认是否进 入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获 取读锁。

CyclicBarrier 和 CountDownLatch

它们的行为有一定相似度,区别主要在于:

  • CountDownLatch 是不可以重置的,所以无法重用,CyclicBarrier 没 有这种限制,可以重用。

  • CountDownLatch 的 基 本 操 作 组 合 是 countDown/await, 调 用 await 的线程阻塞等待 countDown 足够的次数,不管你是在一个线程还是多个线程里 countDown,只要次数足够即可。 CyclicBarrier 的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续 进行任务,并自动进行重置。

  • CountDownLatch 目的是让一个线程等待其他 N 个线程达到某个条 件后,自己再去做某个事。而 CyclicBarrier 的目的是让 N 多 线程互相等待直到所有的都达到某个状态,然后这 N 个线程再继续执 行各自后续。

volatile

volatile原理 *

《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile 可见性

内存屏障确保了修改数据时强制将数据立即写入主存,并且另其他cpu中对应的缓存行失效

写内存屏障( Store Memory Barrier)可以促使处理器将当前 istore buffer(存储缓存)的值写回主存。读内存屏障( Load Memory Barrier)可以促使处理器处理 invalidate queue(失效队列)。进而避免由于 Store Buffer和 Invalidate Queue的非实时性带来的问题。

volatile 禁止指令重排序

总结起来就是:

当第二个操作是volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile 写之前的操作不会被编译器重排序到volatile 写之后。(写之前,所有的操做已完成)

当第一个操作是volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile 读之后的操作不会被编译器重排序到volatile 读之前。(读最新的)

当第一个操作是volatile 写,第二个操作是volatile 读时,不能重排序。(读取上一个操做写的最新值)

volatile 的内存屏障

在Java 中对于volatile 修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题。

volatile 写

storestore 屏障:对于这样的语句store1; storestore; store2,在store2 及后续写入操作执行前,保证store1 的写入操作对其它处理器可见。(也就是说如果出现storestore 屏障,那么store1 指令一定会在store2 之前执行,CPU 不会store1与store2 进行重排序)

storeload 屏障:对于这样的语句store1; storeload; load2,在load2 及后续所有读取操作执行前,保证store1 的写入对所有处理器可见。(也就是说如果出现storeload 屏障,那么store1 指令一定会在load2 之前执行,CPU 不会对store1 与load2 进行重排序)

volatile 读

在每个volatile 读操作的后面插入一个LoadLoad 屏障。在每个volatile 读操作的后面插入一个loadstore 屏障。

loadload 屏障:对于这样的语句load1; loadload; load2,在load2 及后续读取操作要读取的数据被访问前,保证load1 要读取的数据被读取完毕。(也就是说,如果出现loadload 屏障,那么load1 指令一定会在load2 之前执行,CPU不会对load1 与load2 进行重排序)

loadstore 屏障:对于这样的语句load1; loadstore; store2,在store2 及后续写入操作被刷出前,保证load1 要读取的数据被读取完毕。(也就是说,如果出现loadstore 屏障,那么load1 指令一定会在store2 之前执行,CPU 不会对load1 与store2 进行重排序)

happens-Before 规则

先行发生原则( Happens-before)是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是ava内存,模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,那么操作A产生的影响能够被操作B观察到口诀:如果两个操作之间具有 happen- before关系,那么前一个操作的结果就会对后面的一个操作可见。是ava内存模型中定义的两个操作之间的偏序关系。

帮助理解jmm

1)程序顺序规则:前面的操作先于后面的操作。

2)锁规则:对于同一个锁,unlock先于lock。

3)volatile 变量规则:对一个volatile 域的写先于后续对这个volatile 域的读。

4)传递性:如果A 先于 B,且B 先于 C,那么 A 先于 C。

5)start()规则:线程的start操作先于线程中的任意动作

6)join()规则:A join B操作,B的结果对A可见。

7 )线程中断规则:线程的中断 先于 中断线程的代码检测到中断事件的发生。

8)对象终结规则:一个对象的初始化的完成先于finalize()方法

CAS

ABA的问题

  1. 有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成

  2. 首先线程1、获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新( Compareandswap),成功将变量的值从A更新成B

  3. 更新完毕之后,怡好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。

  4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后有更新成了B。但是线程并不知道,该值已经有了A->B→>A这个过程,这也就是我们常说的ABA问题。

  5. 可以通过加版本号或者加时间戳解决,或者保证单向递增或者递减就不会存在此类问题。