线程进程与锁

佳境Shmily 2020-02-11 11:20:08
技术

线程进程与锁

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

进程

概念

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

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

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

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

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

线程

概念

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

2.线程与进程的关系

3.线程间共享的资源

4.线程间通信方式

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

线程池

协程

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

分类

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关键字,功能是保证被修饰的元素:

应用:
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缺点

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