线程进程与锁

线程,进程与锁是一定要掌握的基础知识点,希望能通过写博客的方式加深印象,并且在以后能够随时补充和回看。

进程

概念

1.什么是进程
进程是可并发执行的程序在某个数据集合上的一次计算活动,也是操作系统进行资源分配和调度的基本单位

2.进程的三种基本状态
运行态:当进程得到处理机,其执行程序正在处理机上运行时的状态称为运行状态。
就绪态:当一个进程已经准备就绪,一旦得到CPU,就可立即运行,这时进程所处的状态称为就绪状态。
阻塞态:若一个进程正等待着某一事件发生(如等待输入输出操作的完成)而暂时停止执行的状态称为等待状态。处于等待状态的进程不具备运行的条件,即使给它CPU,也无法执行。系统中有几个等待进程队列(按等待的事件组成相应的等待队列)。

进程的五种状态:新建-就绪-运行-阻塞-死亡
alt ThreadAndLock-00
就绪->运行:等待cpu调度
运行->阻塞:放弃cpu时间片/IO请求
运行->死亡:run运行完或抛异常

3.进程状态切换过程
运行到等待:等待某事件的发生(如等待I/O完成)
等待到就绪:事件已经发生(如I/O完成)
运行到就绪:时间片到(例如,两节课时间到,下课)或出现更高优先级进程,当前进程被迫让出处理器。
就绪到运行:当处理机空闭时,由调度(分派)程序从就绪进程队列中选择一个进程占用CPU。

4.并发与并行区别?
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

线程

概念

1.什么是线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

2.线程与进程的关系

  • 进程是系统资源分配的基本单位-线程是任务调度执行的基本单位
  • 进程切换开销大-线程间切换开销小
  • 进程间资源和地址空间独立-同一进程下线程共享资源和地址空间
  • 进程崩溃不影响其他进程-线程崩溃整个进程都崩溃(多进程健壮性强)

3.线程间共享的资源

  • 线程间共享方法区
  • 虚拟机栈本地方法栈程序计数器不共享
  • 为什么程序计数器资源不共享:程序计数器不共享是为了线程切换后能恢复到正确的执行位置
  • 为什么虚拟机栈和本地方法栈私有:栈帧用于存储局部变量表、操作数栈、常量池引用等信息,而本地方法栈则为虚拟机使用到的Native方法服务,两者相似。为了保证局部变量不被其他线程访问,所以不共享。
  • 为什么堆和方法区共享:新创建的对象存放在堆中,类信息、常量、静态变量、即时编译器编译后的代码存放在方法区。
  • 多线程不一定提高效率,只是让CPU利用率更高,**如果频繁切换可能效率反而更低(见第7条解释)**。

4.线程间通信方式

  • 全局变量
  • 线程上下文
  • 共享内存
  • 套接字Socket
  • IPC通信
    如何实现线程间通讯:1.通过类变量直接将数据放到主存中 2.通过并发的数据结构来存储数据 3.使用volatile变量或者锁 4.调用atomic类

5.进程间通信方式

  • 管道
  • 有名管道
  • 信号量
  • 共享内存
  • 消息队列
  • 信号
  • 套接字

6.守护线程?
一种后台特殊进程,所有用户的线程都退出了,没有被守护的对象了,也不需要守护线程了,这时守护线程关闭。(JVM退出了它会关闭)
比如GC线程就是守护线程。
用户线程可以转为守护线程:thread.setDaemon(true);

7.线程切换开销
中断处理,多任务处理,用户态切换等原因导致CPU从一个线程切换到另一个线程。
线程上下文切换代价是高昂的,上下文切换的延迟由很多因素决定,平均要50-100ns,而CPU每核心每ns执行十几条指令,切换的过程就花费几百至几千条指令的执行时间。
如果跨核上下文切换,代价更加高昂。

7.线程的几种状态?
alt ThreadAndLock-03

新建(NEW):线程被创建出来但还未启动
就绪(RUNNABLE):调用start()后,线程准备就绪,等到CPU分配资源就可以运行
阻塞(BLOCKED):线程处于活跃状态,等待Monitor监视器锁,等待线程同步锁
等待(WAITING):等待另一个线程执行,如调用了wait(),就要等另一个线程notify()或notifyAll()才唤醒
计时等待(TIMED_WAITING):调用了wait(timeout)或join(timeout)指定了超时时间
终止(TERMINATED):线程执行完毕 终止并释放资源

总结:
alt ThreadAndLock-03

线程池

  • 为什么要用线程池?
    频繁创建和销毁线程费时低效而且浪费内存(线程死亡,相关对象变成垃圾)。线程池尽可能减少了对象创建和销毁的次数,让线程运行完不立即销毁,而是重复使用,从而提高效率。

  • ThreadPoolExecutor是线程池的核心类,构造参数的意义:

    corePoolSize:常驻核心线程数,如果为0且池中无线程执行时自动销毁线程池。如果大于0,线程执行完毕后不会销毁线程,而是进入缓存队列等待再次被运行。这个参数设置过小会频繁创建销毁线程,过大会浪费系统资源。
    
    maximunPoolSize:线程池能创建最大的线程数量。如果常驻核心线程数和缓存队列都已经满了,新的任务进来就会创建新的线程来执行。但是数量不能超过maximunPoolSize,否侧会采取拒绝接受任务策略。
    
    keepAliveTime:线程存活时间,未执行的线程空闲超过该时间则终止。多余线程会销毁直到线程数量达到corePoolSize。如果corePoolSize等于MaximumPoolSize,超过空闲时间也不会销毁任何线程。
    
    unit:存活时间单位,和keepAliveTime配合使用。
    
    workQueue:线程池执行的任务队列(缓存队)列,用来存放等待被执行的任务。
    
    threadFactory:线程工厂,用来创建线程,一般有三种选择策略。(一般用默认即可)
        ArrayBlockingQueue;
        LinkedBlockingQueue;
        SynchronousQueue;
    
    handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种策略为
        ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
        ThreadPoolExecutor.DiscardPolicy:忽略最新任务
        ThreadPoolExecutor.DiscardOldestPolicy:忽略最早的任务(最先加入队列的任务)
        ThreadPoolExecutor.CallerRunsPolicy:把任务交给当前线程执行。
        自定义拒绝策略:新建RejectedExecutionHandler对象然后重写rejectedExecution方法
  • 常见线程池

  • *newCachedThreadPool**:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

    • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

    • 适用大量耗时较少的线程任务

  • *newFixedThreadPool**:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

    • 线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
  • *newSingleThreadExecutor**:单线程串行执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

    • 单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
  • *newScheduleThreadPool**:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

    • 该线程池多用于执行延迟任务或者固定周期的任务。
  • 常见线程池
    线程池执行流程:
    alt ThreadAndLock-04

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核开销。

分类

1.公平锁和非公平锁:公平锁指多个线程按照申请锁的顺序排队来依次获取锁。非公平锁是没有顺序获取锁。(因为公平锁挂起和恢复存在一定开销,所以非公平锁性能好些)(非公平锁获取方式是随机抢占。公平锁和非公平锁就差在 !hasQueuedPredecessors() ,也就是前边没有排队者的话,我就可以获取锁了。tryAcquire方法。)
2.可重入锁:又名递归锁,是指同一个线程在外层的方法获取到了锁,在进入内层方法会自动获取到锁。
3.共享锁S和排它锁X:多个线程可以同时获取一个共享锁,一个共享锁可被多个线程拥有。排它锁也叫独占锁,同一时刻只能被统一线程占用,其他线程需要等待。
4.互斥锁和读写锁:一次只能有一个线程拥有互斥锁,读写锁多个读者可同时读,写必须互斥。写优先于读,如果有写,读必须等待。
5.乐观锁和悲观锁:乐观锁认为读取数据时其他线程不会对数据做修改,不加锁,更新数据时采用尝试更新不断重试的方式。悲观锁认为读取数据时其他线程会对数据做修改,会出问题,所以默认加锁。
6.分段锁:提升并发程序性能的手段之一,粒度更小。将数据分成一段一段的存储(如ConcurrentHashMap的Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。所以说,ConcurrentHashMap在并发情况下,不仅保证了线程安全,而且提高了性能。
7.锁的状态:无锁、偏向锁、轻量级锁、重量级锁:偏向锁是减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。重量级锁,其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程,进行竞争。
无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态 随竞争情况逐渐升级 不可逆 后续会在**概念**部分详细说
alt ThreadAndLock-01
偏向锁:仅有一个线程进入临界区
轻量级锁:多个线程交替进入临界区
重量级锁:多个线程同时进入临界区

8.自旋锁:线程没获得锁时不会被挂起而是空循环,可以减少线程阻塞造成的线程切换的概率。首先,阻塞或唤醒Java线程需要操作系统切换CPU状态来完成,状态切换耗费CPU,可能切换CPU的时间比同步代码块中代码的执行时间都要长,为了很短的同步锁定时间而花费很长的线程切换时间是不值得的,如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
8.自适应自旋锁:自适应是值虚拟机会记录一个锁对象自旋时间和状态,如果之前自旋等待成功获得锁,这次自旋也很大概率成功,会允许持续更长时间的自选等待,后面的自旋会大概率拿到锁。如果一个对象的锁自旋等待很少能成功获取到锁,后续减少自旋次数甚至忽略自旋过程,直接阻塞,避免浪费CPU资源。(简而言之,自旋时间根据之前锁和线程状态动态变化,来减少线程阻塞时间)
11.读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。(如果已占用读锁,其他线程想写需要等读锁释放,其他线程想读可以读;如果已占用写锁,其他线程无论想读想写,都要等写锁释放)

概念

1.从底层角度看,本质上只有一个关键字和两个接口:Synchronized和Lock接口以及ReadWriteLock接口(读写锁)

2.synchronized与reentrantLock简述

synchronized是一个隐式的重入锁,比较笨重,实现方式是锁主存和缓存一致性。现在的JDK版本已经做了很多优化synchronized的措施:自适应的自旋锁、锁粗化、锁消除、轻量级锁等
reentrantLock是一个显式的重入锁,比较灵活,可以扩展为分段锁,实现方式是AQS+双向链表(默认是NonFairSync非公平锁实现,而NonFairSync继承Sync,Sync继承AbstractQueuedSynchronizer,AQS维护volatile变量state作为同步状态)。一个线程可多次获取锁,每次获取都会计数count++,解锁时count--直到变0.
ReentrantLock采用的是独占锁。Semaphore,CountDownLatch等采用的是共享锁,即有多个线程可以同时获取锁。

3.synchronized和Lock区别:

  1、synchronize是java内置关键字,而Lock是一个类。(通过javap看字节码,发现有monitorenter和monitorexit命令,分别对应进入monitor加锁和释放)
  2、synchronize可以作用于变量、方法、代码块,而Lock是显式地指定开始和结束位置。
  3、synchronize不需要手动解锁,当线程抛出异常的时候,会自动释放锁;而Lock则需要手动释放,所以lock.unlock()需要放在finally中去执行。
  4、性能方面,如果竞争不激烈的时候,synchronize和Lock的性能差不多,如果竞争激烈的时候,Lock的效率会比synchronize高。
  5、Lock可以知道是否已经获得锁,synchronize不能知道。Lock扩展了一些其他功能如让等待的锁中断、知道是否获得锁等功能,Lock 可以提高效率。
  6、synchronize是悲观锁的实现,而Lock则是乐观锁的实现,采用的CAS的尝试机制。

4.synchronized和ReenTrantLock区别:

  1、ReenTrantLock可以中断锁的等待,提供了一些高级功能。
  2、ReenTrantLock默认非公平锁,可以设为公平锁,synchronized不行
  3、ReenTrantLock可以绑定多个锁条件。
  4、synchronized是JVM关键字,ReentrantLock是Java API
  5、ReentrantLock只能修饰代码块,synchronized既可以修饰方法也可以修饰代码块
  6、ReentrantLock需要手动加锁和释放锁,synchronized自动释放
  7、ReentrantLock可以知道是否成功获得了锁,synchronized不能

5.锁膨胀:无锁、偏向锁、轻量级锁、重量级锁及升级过程
无锁:每个对象都有一把看不见的锁(内部锁 也叫 Monitor锁) 特点:不断尝试修改资源,失败的线程重试直到成功。
偏向锁:特点:一段同步代码一直被同一个线程访问,锁总是被同一线程多次获得,降低锁的获取代价。只有一个线程执行同步代码块时能提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID(锁标志位)。
在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。
撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

轻量级锁:当前对象持有偏向锁时被另外的线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁:当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。 特点:等待锁的线程都会进入阻塞状态。

6.锁消除:

先说“逃逸分析技术”,该技术在编译期使用,分析对象的动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。  
该技术将确定不会发生逃逸的对象放入栈内存而非堆(故不是所有对象都在堆中)。
为了减少锁的请求和释放操作,“逃逸分析技术”在编译期分析出那些本来不存在竞争却加了锁的代码,让他们的锁失效,从而达到减少锁的请求和释放的目的。

而锁消除就是,发现不存在多线程并发抢占问题的时候,编译后去掉该锁。

7.锁偏向:

先说一下Java对象头,它包括Mark Words、Klass Words两部分,可能还包括数组的长度(如果对象是个数组)。
Klass Word里面存的是一个地址,占32位或64位,是一个指向当前对象所属于的类的地址,可以通过这个地址获取到它的元数据信息。
Mark Word!重点,这里面主要包含对象的Hashcode、年龄分代、锁标志位等,大小为32位或64位。锁状态不同时MarkWord里内容会不同。
锁偏向:当第一个线程请求时,会判断锁的对象头里的ThreadId字段的值,如果为空,则让该线程持有偏向锁,并将ThreadId的值置为当前线程ID。当前线程再次进入时,如果线程ID与ThreadId的值相等,则该线程就不会再重复获取锁了。因为锁的请求与释放是要消耗系统资源的。
如果有其他线程也来请求该锁,则偏向锁就会撤销,然后升级为轻量级锁。如果锁的竞争十分激烈,则轻量级锁又会升级为重量级锁。

对象头:
alt ThreadAndLock-02

8.锁粗化:
在编译期间将相邻的同步代码块合并成一个大同步块。这样做可以减少反复申请和释放同一个锁对象导致的系统开销。
当一个循环中存在加锁操作时,可以将加锁操作提到循环外面执行,一次加锁代替多次加锁,提升性能。

9.volatile:
是Java关键字,功能是保证被修饰的元素:

  • 任何进程读取时都会清空本进程持有的共享变量值,而强制从主存获取(每次访问变量都刷新,每次都能得到最新变量)
  • 任何进程写完毕时都强制将共享变量写回主存
  • 防止指令重排(指令重排作用是JVM单线程程序在不影响运行结果的条件下的优化。多线程指令重排会出问题。)
  • 保证内存可见性(一个线程对共享变量改动,要让其他线程立刻知道)
  • 并不保证操作原子性
    它是怎么避免指令重排的呢?
    通过内存屏障
    内存屏障是通过JVM生成内存屏障指令,在读写操作前后添加内存屏障(解决L1,L2,L3高速缓存与主存速度不一致导致的缓存一致性问题)
    volatile读操作的后面插入一个LoadLoad屏障。
    volatile写操作的后面插入一个StoreLoad屏障。
    它的内存读写过程:
    LoadStore
    LoadLoad
    StoreStore
    StoreLoad
    注:StoreLoad就是触发后续指令中的线程缓存回写到内存; 而LoadLoad会触发线程重新从主存里面读数据进行处理。

应用:
volatile应用于单例模式,防止因指令重排导致单例模式返回一个初始化到一半的对象

public class Singleton {
    /**
     * 双重校验 保证线程安全,提高执行效率,节约内存空间
     */
    private static volatile Singleton instance;  //防止JVM指令重排
    private Singleton(){}
    public static Singleton getInstance(){
        /**
         * 如果instance不加volatile,JVM指令重排会先分配地址再初始化(此时这个地址存在但没值),
         * 所以这里判断不为null为true时有可能对象还未完成初始化,单例可能返回了一个未初始化完的对象。
         */
        if(instance == null){  
            synchronized (Singleton.class){
                if(instance == null){  
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

10.CAS:
全称Compare-And-Swap,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
CAS是CPU的原子指令,底层是汇编语言,Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp。
CAS缺点

  • 循环时间长CPU开销大
  • 只能保证一个共享变量的原子操作
  • 会引发ABA问题(当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存位置V中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作时发现内存中仍然是A,然后线程one操作成功。这个线程one的操作可能出问题。)

11.AQS:
AbstractQueuedSynchronizer,维护一个volatile int state(代表共享资源状态)和一个FIFO线程等待队列。

死锁

1.什么是死锁
两个或多个线程互相持有对方需要的资源,导致一直等待状态,互相等待对方释放资源,如果没有主动释放资源,就会死锁。

2.死锁产生条件
1、存在循环等待
2、存在资源竞争
3、已经获得的资源不会被剥夺
4、请求与保持,一个线程因请求资源被阻塞时,拥有资源的线程的状态不会改变。

3.避免死锁
产生死锁条件中任意一条不满足,就不会产生死锁

4.死锁定位修复
执行程序时,程序没停止,也不继续运行,则是死锁
通过jps获取端口号,再jstack工具可以看到

其他

关于AQS和CAS详细:https://www.jianshu.com/p/2a48778871a9
锁状态升级详解:http://www.jetchen.cn/synchronized-status/
Java并发-volatile内存可见性和指令重排:https://blog.csdn.net/jiyiqinlovexx/article/details/50989328


你自以为的极限,只是别人的起点