java多线程

进程、线程、协程、程序

说说线程、程序、进程的基本概念,以及之间的关系

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,就是说程序是静态的代码。

进程就是一个程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。

线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程,但是同一个进程的线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程或者在各线程之间进行切换,代价比进程切换要小。

关系:

系统运行一个程序即是一个进程从创建,运行到消亡的过程,所以说进程就是一个执行中的程序,在程序在执行时,将会被操作系统载入内存中,线程是进程划分成的更小的执行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程的基本状态

新建态,就绪态,运行态,阻塞态,终止态。

 

Callable、Future、FutureTask介绍与关系

Callable是Runnable封装的异步运算任务

Future用来保存Callable异步运算结果

FutureTask封装Future的实体类

  1. Callable与Runnable的区别

    定义的方法不同,call()和run()

    call()有返回值,run()没有返回值

    call()可以抛出异常,run()不能

线程的生命周期,什么是僵死进程

 

僵死进程是指子进程退出时,父进程并未对其发出sigchld信号进行适当处理,导致子进程停留在僵死状态等待父进程收尸。该状态的子进程就是僵死进程。

如何实现进程安全

  1. 互斥同步

    临界区:Synchorized、ReentrantLock

    信号量:semaphore

    互斥量:mutex

  2. 非阻塞同步

    CAS(Compare And Swap)

  3. 无同步方案

    可重入代码

    使用ThreadLocal类包装共享变量

    线程本地存储

如何在线程之间共享数据

将数据抽象成一个类,并将数据的操作作为这个类的方法

  1. 将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized

  2. 将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable 对象调用外部类的这些方法。

线程池

线程池

优点:

  1. 使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用

  2. 可以根据系统的承受能力,调整线程池中工作线程的数量,防止线程过多导致系统崩溃。

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

风险:

  1. 死锁

  2. 资源不足

  3. 并发错误

  4. 线程泄漏

  5. 请求过载

创建线程池

public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//允许最大线程数
                          long keepAliveTime,//当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务执行队列
                          ThreadFactory threadFactory,//线程工厂,用于存放新建出来的线程
                          RejectedExecutionHandler handler//拒绝策略
                         ) {
    
}
​

实现原理

线程池处理流程:

  1. 判断线程池中的核心线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下一步。

  2. 线程池判断工作队列是否已满,如果工作队列没满,则将新提交的任务存储在这个任务队列中。如果队列满了,则进入下一步。

  3. 判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已满,则交给饱和策略来处理任务

     

拒绝策略:

ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出RejectedExecutionException 异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选择重试或者放弃提交等策略。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。

ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险。

ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它。

创建线程池的几种方式

  1. newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小.

  2. newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

  3. newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序 按照任务的提交顺序执行。

  4. newSingleThreadScheduleExecutor: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行.

  5. newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行, 线城池数量不固定.

  6. newWorkStealingPool: 创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的CPU个数.

Java线程池如何合理的设置大小

任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池

1、CPU密集型

尽量使用较小的线程池,一般Cpu核心数+1

因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销

2、IO密集型

方法一:可以使用较大的线程池,一般CPU核心数 * 2IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间

方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比+ 1) CPU数目

3、混合型

可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定

为什么不推荐使用Executors创建线程池,而是要自己手动调用ThreadPollExecutor来实现线程池

这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

多线程锁机制,安全机制

互斥同步锁(悲观锁)

Synchorized,ReentrantLock

以互斥手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一时刻只能有一个或者一组线程访问同一资源。

Synchorized是由语言级别实现的互斥同步锁,简单但机制笨拙,是由JVM实现的。jdk6之后性能提升,与ReentrantLock性能相差无几。

ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和Synchorized相比更加灵活。等待可中断,公平锁以及绑定多个条件。

互斥同步锁都是可重入锁,可以保证不会死锁,但是因为涉及到核心态和用户态的切换,性能消耗大。之后有了一些优化:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。

非阻塞同步锁(乐观锁)

原子类(CAS)

非同步锁也叫乐观锁,对资源用版本号进行控制,可以先进行资源的修改,然后根据版本号是否与拿到数据时相同,若相同则修改,若不同,则重新取值进行相关计算,会一直重试直到成功。实现方式依赖处理器的机器指令:CAS(Compare And Swap)。CMPXCHG

JUC中提供了几个Atomic类以及每个类上的原子操作就是乐观锁机制。不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。非阻塞锁是不可重入的,否则会造成死锁。

无同步方案

  1. 可重入代码

    在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源

  2. ThreadLocal、Volaitile

    线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理

  3. 线程本地存储

    如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计

Java的并发和并行

并发:是指两个或多个事件在同一时间间隔发生在一台处理器上同时处理多个任务。

并行:是指两个或多个事件在同一时刻发生,在多台处理器上同时处理多个任务

如何提高并发量

  1. HTML静态化

  2. 图片服务器分离

  3. 数据库集群,分表分库

  4. 缓存

  5. 镜像

  6. 负载均衡

  7. CDN加速技术

如何解决的用户高并发访问

分布式部署,负载均衡就是将负载(工作任务、访问请求等)进行平衡、分摊到多个操作单元(服务器、组件等)上进行执行,是解决高性能,单点故障(高可用,如果你是单机版网络,一旦服务器挂掉了,那么用户就无法请求了,但对于集群来说,一台服务器挂掉了,负载均衡器会把用户的请求发送给其他的服务器进行处理),扩展性(这里主要是指水平伸缩)的终极解决方案。

  1. nginx的负载均衡配置中默认是采用轮询的方式,这种方式中,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除,但存在各个服务器的session共享问题。

  2. 另外一种方式是ip_hash:每个请求按访问的ip的hash结果分配,如果访问的IP是固定的,那么在正常情况下,该用户的请求都会分配到后台的同一台服务器去处理,但是如果用户每次请求的IP都不同呢?所以这种方式也同1的方式一样都存在这么一个问题:session在各个服务器上的共享问题。

  3. 如果集群中的服务器的性能不一,可以通过配置各个服务器的权值来实现资源利用率的最大化,即性能好的优先选择。

    解决服务器共享session问题:使用redis来共享各个服务器的session,并同时通过redis来缓存一些常用的资源,加快用户获得请求资源的速度(个人比较喜欢redis,当然你们也可以使用memcache来实现,不过,memcache不能做到持久化,这样这台服务器一挂掉,那么所有的资源也都没有了......)。

锁优化

减少锁的时间

减少锁的粒度

多线程常用类及关键字

阻塞队列(ArrayBlockingQueue)

原理:使用一个可重入锁和这个锁生成的两个条件对象进行并发控制。

是一个带有长度的阻塞队列,初始化的时候必须要指定队列长度,且长度不能修改。

增加

add方法内部调用offer方法,如果队列满了,抛出IllegalStateException异常,否则返回true,offer方法如果队列满了,返回false,否则返回true

add方法和offer方法不会阻塞线程,put方法如果队列满了会阻塞线程,直到有线程消费了队列里的数据才有可能被唤醒。

这3个方法内部都会使用可重入锁保证原子性。

删除

poll方法对于队列为空的情况,返回null,否则返回队列头部元素。

remove方法取的元素是基于对象的下标值,删除成功返回true,否则返回false。

poll方法和remove方法不会阻塞线程。

take方法对于队列为空的情况,会阻塞并挂起当前线程,直到有数据加入到队列中。

这3个方法内部都会调用notFull.signal方法通知正在等待队列满情况下的阻塞线程。

volatile关键字

一旦一个共享变量被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值会立刻加载到共享内存中。对其他的线程立即可见。

    例如,有线程1和线程2,二者共享变量stop,线程1将主存中的stop加载到自己工作内存的缓存后,线程2对stop进行修改,由于有volatile修饰,所以线程1中的stop会立即失效,并从内存中获得更改后的stop

  2. 禁止进行指令重排序

voilatile无法保证对变量的操作原子性。

volatile能保证原子性和有序性

原理和机制

加入volatile和没加入volatile所生成的汇编代码相差一个lock前缀指令。

lock前缀指令实际上相当于一个内存屏障。有三个功能:

  1. 确保指令重排序的时候不会将lock之前的放在后面或者之后的放在前面

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

  3. 如果是写操作,会导致其他cpu的工作内存中的缓存失效

volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

JVM内存屏障插入策略:

每个volatile写操作的前面插入一个StoreStore屏障;

在每个volatile写操作的后面插入一个StoreLoad屏障;

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

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

volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,影响效率,而volatile关键字在某些情况下优于synchronized,但是并不能替代他,因为volatile无法保证操作的原子性。

使用volatile必须具备两个条件:

  1. 对变量的写操作不依赖于当前值

  2. 该变量没有包含在具有其他变量的不变式中

适用场景:

  1. 状态标记量

  2. double check

wait和notify

notify():唤醒在此对象监视器上等待的单个线程

notifyAll():唤醒在此对象监视器上等待的所有线程

wait():导致当前的线程等待,直到其他线程调用此对象的notify()或notifyAll()方法

  1. 都不属于Thread类,而是属于Object类

  2. 调用这些方法时一定是对竞争资源进行加锁,不加锁会报IllegalMonitorStateException 异常

  3. 当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。

sleep和wait区别

  1. sleep方法是属于Thread类,而wait方法是属于Object类。

  2. sleep方法导致线程暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态仍然保持者,当指定时间到了之后会立即恢复运行状态。

  3. 在调用sleep方法的过程中,线程不会释放对象锁

  4. 调用wati方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify或notifyAll方法后本线程才进入线程锁定池准备获取对象所进入运行状态。

start和run区别

  1. start方法来启动线程真正实现了多线程的运行,这是无需等待run方法体代码执行完毕,可以直接继续执行下面的代码

  2. 通过调用Thread类的start方法来启动一个线程,这时此线程是处于就绪状态,并没运行。

  3. 方法run相当于线程的方法体,包含了执行的线程的内容,线程就进入运行态。

ReentrantLock与synchronized

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。

  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。

两者的共同点:

  1. 都是用来协调多线程对共享对象、变量的访问

  2. 都是可重入锁,同一线程可以多次获得同一个锁

  3. 都保证了可见性和互斥性

两者的不同点:

  1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁

  2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性

  3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的

  4. ReentrantLock 可以实现公平锁

  5. ReentrantLock 通过 Condition 可以绑定多个条件

  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略

  7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。

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

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

  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

tryLock,lock,lockInterruptibly的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false

  2. lock 能获得锁就返回 true,不能的话一直等待获得锁

  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常

什么是CAS(比较并交换-乐观锁机制-锁自旋)

CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,

CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

ABA问题

比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,并修改内存中的值,然后 one仍然被阻塞,这时候线程three读取到B值,并将值又改成了A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。

部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

什么是AQS(抽象的队列同步器)

AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。

ReentrantLock 实现原理(公平锁和非公平锁)

ReentrantLock 就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)来实现的。

是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。

ReentrantLock 分为公平锁非公平锁,可以通过构造方法来指定具体类型:

public ReentrantLock() {
        sync = new NonfairSync();
    }
​
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

默认情况下是非公平锁,非公平锁的效率和吞吐量都高于公平锁。由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制。所以也就能解释非公平锁的效率会被公平锁更高。

公平锁:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
​
        final void lock() {
            acquire(1);
        }
​
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    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;
        }
    }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

第一步是尝试获取锁(tryAcquire(arg)),首先会判断 AQS 中的 state 是否等于 0,0 表示目前没有其他线程获得锁,当前线程就可以尝试获取锁。

注意:尝试之前会利用 hasQueuedPredecessors() 方法来判断 AQS 的队列中中是否有其他线程,如果有则不会尝试获取锁(这是公平锁特有的情况)。

如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(setExclusiveOwnerThread(current))。

如果 state 大于 0 时,说明锁已经被获取了,则需要判断获取锁的线程是否为当前线程(ReentrantLock 支持重入),是则需要将 state + 1,并将值更新。

写入队列

如果 tryAcquire(arg) 获取锁失败,则需要用 addWaiter(Node.EXCLUSIVE) 将当前线程写入队列中。

写入之前需要将当前线程包装为一个 Node 对象(addWaiter(Node.EXCLUSIVE))。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

将数据包装为一个Node结点,先判断队列是否为空,如果不为空,则将新节点通过CAS操作加入队列,插入成功后直接返回该节点,否则调用enq来写入。

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

自旋加上 CAS 保证一定能写入队列。

写入队列之后需要将当前线程挂起(利用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

首先会根据 node.predecessor() 获取到上一个节点是否为头节点,如果是则尝试获取一次锁,获取成功就万事大吉了。

如果不是头节点,或者获取锁失败,则会根据上一个节点的 waitStatus 状态来处理(shouldParkAfterFailedAcquire(p, node))。

waitStatus 用于记录当前节点的状态,如节点取消、节点等待等。

shouldParkAfterFailedAcquire(p, node) 返回当前线程是否需要挂起,如果需要则调用 parkAndCheckInterrupt():

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

他是利用 LockSupport 的 part 方法来挂起当前线程的,直到被唤醒。

非公平锁获取锁

公平锁与非公平锁的差异主要在获取锁:

公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队

而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

还要一个重要的区别是在尝试获取锁时tryAcquire(arg),非公平锁是不需要判断队列中是否还有其他线程,也是直接尝试获取锁:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //没有!hasQueuedPredecessors()判断,不需要判断队列里是否有其他线程
                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;
        }

释放锁

公平锁和非公平锁的释放流程都是一样的:

//ReentrantLock类中的方法
public void unlock() {
        sync.release(1);
    }
//AbstractQueuedSychronizer中的方法
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //唤醒被挂起的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

//ReentrantLock类中实现AbstractQueuedSychronizer中的tryRelrase方法
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

首先会判断当前线程是否为获得锁的线程,由于是重入锁所以需要将 state 减到 0 才认为完全释放锁。

释放之后需要调用 unparkSuccessor(h) 来唤醒被挂起的线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值