多线程【八股】

什么是进程?什么是线程?

什么是进程?进程就是是系统进行资源分配和调度的基本单位,并且进程是一个动态的概念,这个概念包括了进程的创建、运行、销毁的过程。一个进程可以包括多个线程,线程是更小的单位,线程(thread)是操作系统能够进行运算调度的最小单位。比如我们运行一个java程序,就会产生一个java进程,main函数所在的是一个线程,也被称为主线程。

一个进程可以产生多个线程,多个线程共享进程的堆和方法区,但是每个线程都有自己的程序计数器,虚拟机栈和本地方法栈。所以系统在线程之间的切换就要方便的多,至少比进程方便。

Linux的进程和线程的 堆栈 总结:

进程栈是运行时决定的,跟编译无关。

进程栈大小是比较随机的,不是固定的,但是一定比线程栈大,并且不会超过线程栈的2倍。

线程栈的大小是固定的,默认是8M,可以使用ulinit -a 查看,可以使用ulimit -s 修改

默认情况下,每个线程的栈空间是在进程的堆空间中分配的,一般来说,每个线程都是有自己独立的栈空间,并且为了隔离,线程的栈空间之间会有隔离,隔离地区guardsize一般是4K一旦触碰到隔离地区guardsize,就会报错。

为什么程序计数器是私有的?

程序计数器主要有2个作用,第一个、主要通过字节码解释器来控制程序运行的顺序,依次读取指令,通过这个来控制是顺序执行还是循环还是判断等。第二个、在多线程的情况下,如果线程执行的之后切换回来,需要知道 上次执行到哪儿了,程序计数器就是可以提供这个信息。

一句话总结,程序计数器为了能够让线程知道自己现在执行到哪儿了,和正在执行什么逻辑。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈:每个java程序执行的时候,都会有一个栈,保存引用的常量,本地方法,局部变量等,方法的开始执行到结束,对应一个栈帧在java虚拟机栈中入栈和出栈的过程。

本地方法栈:这个作用和虚拟机栈差不多,区别就是它主要是为了对应的是java程序需要使用到的native的方法的服务,在后续的Hotspot虚拟机中,二者就合二为一了

一句话总结,就是为了线程中的局部变量不被其他线程访问,所以必须私有。

一句话总结堆和方法区

首先,堆和方法区是所有线程共享的资源,然后堆是最大的一块资源,所有的对象都在堆中分配资源,方法区保存已经被加载的类信息,方法,常量,静态变量,即时编译器编译后的代码等等。

并发和并行的区别?

并发:同一时间段内,多个任务都在执行(同一时刻只有一个任务在执行,但是这些任务都在抢占cpu资源保证自己能够执行)

并行:同一时间内,多个任务同时执行(同一时刻,有多个任务在执行)

为什么需要使用多线程?

总体上:

说白了就是比较快,因为现在计算机的硬件性能上来了,多线程比单线程要快,因为线程之间切换的调度比进程之间的切换快得多。

现在的并发量越来越大,多线程高并发是应对目前互联网发展的一种手段。

底层:

对于cpu来说,如果是单核时代,(举个例子)cpu在执行计算的时候,io就是空的,执行io的时候,计算就是空的,如果是多线程,可以保证执行io的时候,计算也可以执行,可以保证cpu的使用效率更多

多核时代,如果还是单线程,那么同一时间只有一个核被使用,如果是多线程,就可以做到同一时刻有多个核被使用。

多线程可能带来的问题

内存泄露,上下文切换,死锁

线程的生命状态?

一定是6种生命状态的一种,分别是NEW RUNNNABLE BLOCKED WAITING TIME_WAITING TERMINATED

状态名 解释
NEW 初始的状态,这个时候线程刚刚被创建,但是还没有被调用,也就是没有使用start方法
RUNNABLE 运行状态,java概念中,准备就绪状态和运行中状态统称为runnable
BLOCKING 阻塞状态,也就是目前线程被阻塞,一般是被锁阻塞,等待锁释放
WAITING 等待状态,正在等待其他的线程做出一些动作才能结束等待,比如通知或者中断
TIME_WAITING 超时等待状态,不同于waiting状态的是,他可以在时间超时之后自动返回。
TERMINATED 终止状态,表示目前线程已经结束运行。
java中,各种状态一定是下面的状态转移图的情况
多线程线程状态的转移

一些注意:

进入waiting状态的线程需要等待其他线程的通知才能结束wait状态,而time_waiting状态多了一个超时时间到自动返回的过程。

线程在进入同步代码块(synchronized代码块或者synchronized方法的时候),同时锁被其他线程掌控未释放的情况下,就会进入阻塞态,直到拿到锁开始执行run方法。

线程在执行完run方法之后就会进入终止状态。

什么是上下文切换?

简单的来说,就是上下文切换是一个概念,因为线程肯定大于cpu的核心数量,所以在每个时刻,cpu的一个核心只能被一个线程执行,在这种情况下,cpu就会使用时间片的方法,轮转分配给每个线程,让每个线程都能够得到执行的机会,这样的一个线程处理一半,然后把cpu时间片给其他线程用的过程就是一个上下文切换。这个过程的时候,线程会通过程序计数器保存自己目前的状态,然后再让时间片,这样下次获取到cpu的时候就可以继续上一次的执行继续执行,这样的上下文切换的过程一般耗费了cpu绝大多数的时间,linux最好的优点就是上下文切换的时间耗时非常的小。

什么是死锁,如何避免?

死锁的概念:死锁就是多个2个线程出现一种情况,全部都在等待一个被别人掌握但是未释放的资源,这种情况就会导致线程的无限期阻塞,无法正常终止,就是死锁。

死锁产生的条件:1. 互斥条件,一个资源任意时刻只有一个线程占用。2. 请求保持,就是一个进程在请求资源的时候,对于已经请求到手的资源不会释放。3. 不剥夺条件,线程已获得的资源不会被其他线程强行剥夺,只有自己释放才能。4. 循坏等待条件,若干进程之间形成了请求资源的首尾相连的环。

如何避免死锁?

破坏四个条件中的任何一条即可

破坏互斥条件:无法,因为互斥也是我们的目的

破坏请求保持:一次性申请所有的资源。

破坏不剥夺条件:占用部分资源申请其他资源的时候,如果申请不到,就放弃自己的资源

破坏循环等待条件:若干进程之间形成头尾相连的顺序申请资源并反向释放资源即可。

sleep()方法和wait()方法的共同点和不同点?

二者最大的区别就是sleep()没有释放锁,但是wait()方法释放了锁。

sleep()方法就是线程自己,进入睡眠状态,过一段时间自动苏醒。wait()方法不会自动苏醒,需要同一个对象的别的线程来notify(),如果是wait(long time)的话,也会到时间自动苏醒。

调用start()方法执行run()方法,为什么不能直接调用run()方法?
new一个线程之后,如果使用start()方法,会将该线程需要执行的一些东西准备好,然后在得到cpu时间片的时候去执行,这个才是多线程的执行方法。

但是如果是直接调用run()方法,他会将run()方法看做是main线程中的一个方法执行,这并不是多线程的执行方法。

Synchronized锁的理解?

早期的时候,这种线程的挂起和唤醒需要借助操作系统的帮助,这种时候,操作系统进行进程的切换需要从用户态转成内核态,这个过程非常的复杂。

在java1.6之后,进行了多种优化,目前的自旋锁、适应性自旋锁、锁消除等减少开销的方法也非常多,效率优化的也非常不错。

Synchronized的三种关键使用方法:

修饰实例方法,作用于当前的实例对象,进入同步方法的时候,需要获取当前对象的实例的锁

synchronize void method(){
    //业务代码
}
  1. 修饰静态方法,如果修饰了一个静态方法,相当于给这个类的所有实例对象全部加锁,因为静态是属于类的,不是属于某一个实例对象的,对于所有的这个类的实例,都是需要获取这个类的class的锁。所以,如果出现一个类的2个实例,其中一个实例A访问同步实例方法,一个实例B访问同步静态方法,这是允许的并且不会造成阻塞。因为获取的锁不一样,一个是实例对象,一个是当前类class对象
synchronized void static methd() {
    //业务代码
}
  1. 修饰代码块,给给定的一块代码进行加锁,同时需要给定锁对象。synchronized(thisObject)其中thisObject是实例对象,表明进入同步代码块之前需要获取给定的对象锁,Synchronized(类.class)表明进入同步代码块之前需要得到一个类的class对象锁。
synchronized(thisObject /.class){
    //同步代码块
}

示例:

双重锁校验的单例模式

class Single{
    private volatitle static Single union;
    
    public Single(){
    }
    
    public static Single getUnion() {
        //先判断是否写过
        if(union==null) {
            //类对象加锁
            synchronized(Single.class){
                if(union==null){
                    union==new Single();
                }
            }
        }
        return union;
    }
}

volatile表明不允许jvm的指令重拍,防止返回未初始化的union,举例子,如下:

union = new Single()这个语句有三个步骤,1. 为union分配内存空间,2. 初始化union, 3. 将union指向分配内存空间。

但是多线程可能出现执行顺序为1 3 2,这样union就是获取得到未初始化的union ,不允许的情况。

构造方法不能使用Synchronized修饰,因为构造方法本身就是线程安全,不存在线程不安全的说法。

Synchronized关键字底层属于jvm层别

1、修饰同步代码块的时候

可以知道的是Synchronized关键字通过moniterenter和monitorexit指令,一个表示同步代码快的开始位置,一个表示同步代码快的结束位置,当执行到monitorenter的时候,程序开始试图获取锁,也就是monitor(对象监视器)的所有权。java中,这些东西是基于cpp编写的,所有的对象都有一个内置的monitor对象,然后只有同步代码块Synchronized才能调用wait以及notify方法,就是因为只有他们才是基于cpp的monitor实现的,如果别的地方调用就会出现非法monitor异常,获取锁的过程需要和程序计数器结合,如果尝试获取锁的时候,程序计数器是0,就获取,如果程序计数器是1,就无法获取,证明这个锁别人在用,获取锁之后会把程序计数器改成1,释放锁之后会把程序计数器改成0。

2、修饰方法的时候

修饰同步方法的时候,调用的是一个flag,叫做ACC_SYNCHRONIZED ,这个标志位表示了这个方法 是一个同步方法,jvm看到了这个标志,之后就会执行相应的同步方法的调用。

小结:

二者的本质都是通过monitor对象监视器的获取,只是方式和方法不同。

为什么要有CPU高速缓存?

一句话总结,就是为了解决cpu处理速度和内存的io速度不匹配的问题,因为内存可以看做外村的缓存,那么缓存就是内存的缓存,都是为了一层一层的提高io的速度。

工作方式:

首先复制一份数据到CPU cache中,当CPU需要数据的时候就直接从CPU cache中读取,运算结束之后才会存到main Memory中,这个过程就实现了缓存和内存的沟通。

但是会出现缓存和内存数据不一致的问题。为了解决内存和缓存的不一致问题,cpu通过制定缓存一致性协议或者其他手段来解决。

JMM内存模型

在之前的jvm中,所有的变量都是在主存中,但是现在改成了每个线程有自己的本地内存,比如寄存器,这种时候就出现一个问题,比如一个变量在一个线程中改变了,另一个变量使用的还是主存中的值,这种时候就会导致这个值的不确定,所以需要使用volatile关键字,volatile关键字除了防止jvm指令重排,还可以实现告诉jvm,这个变量是不稳定的,每次使用都要在主存中读取。

Synchronized关键字和volatile关键字的比较和区别?

首先,二者不是对立的关系,而是相辅相成。

volatile关键字是一种轻量级的同步锁的概念,而Synchronized是一个重量级的同步锁,虽然现在性能优化不错,但是跟volatile比肯定还是volatile比较好。

volatile关键字是为了保证变量的可见性的,就是说在jvm的底层中,使用了volatile关键字的变量会在主存中读取,而不是线程的本地内存,这样就保证了这个数据每个线程读取的时候,操作的时候之后的一致性。但是volatile不保证操作的原子性。Synchronized关键字既保证了可见性,也保证了原子性。

所以volatile关键字主要解决变量在多个线程之间的可见性,而Synchronized关键字解决多个线程之间访问资源的同步性。

ThreadLocal了解吗?

ThreadLocal是一个供给每个线程的单独的一个变量的概念,相当于存储数据的一个盒子,每个线程都有自己的盒子存储自己的数据,可以使用get和set方法对于数据进行更改,如果你创建了一个ThreadLocal变量,那么每个访问这个变量的线程都有这个变量的副本,每个线程操作的也是自己的副本,这样就实现了变量和线程之间的隔离性,保证了数据的安全。

原理:

ThreadLocal是一个存储数据的东西,每一个线程Thread中都有一个ThreadLocalMap的概念,这个可以理解成是一个定制化的HashMap,这个map的key就是当前的线程,value就是想要存储的各种值,如果使用了get和set方法,也会先判断这个map是否实例化,如果没有的话就是会先实例化这个map,然后调用的get和set方法实际上也就是这个map的get和set方法。

ThreadLocal可以理解成是这个map的封装,对外也只是对于这个map的值的变量的进行传递。

ThreadLocal内存泄露是什么?

ThreadLocal的map中的key是一个弱引用,但是value却是强引用,那么在这个ThreadLocal没有进行外部引用的情况下,在垃圾回收的时候,key就有可能会被回收,但是value缺没有被回收,那么这种时候就会出现key是null,value有值的entry,如果不做措施,value无法被回收,造成内存泄露。ThreadLocal考虑到了这种情况,所以他在调用get、set、remove、方法的时候,都会清理掉key是null的entry,那么还是建议使用完ThreadLocal之后手动调用一下remove方法。

拓展一下:

弱引用。就相当于可有可无的东西,那么既然是可有可无的,垃圾回收线程在扫描到这个弱引用的时候,无论内存够还是 不够,都会直接将这个弱引用的内存释放掉,不过垃圾回收线程是一个优先级非常小的线程,不一定能及时发现这些弱引用。

线程池,为什么要用线程池?

线程池,应用的场景非常多,比如数据库连接池,比如http线程池都是这种线程池技术的应用。

最主要的作用就是减少每次获取资源的消耗,提高资源的利用率。

好处如下:

降低资源的消耗:创建线程和销毁线程需要一定的资源的消耗,使用已经创建好的线程并且不用去销毁,这样其实是降低了资源的损耗。

提高相应的速度:每次请求来的时候,不需要等到创建线程的过程,直接就可以立马执行,这个过程就是提高了响应的速度。

提高线程的可管理性:线程是一个稀缺资源,无限制的创建也会影响系统的性能,并且不好管理,通过线程池可以统一的对于线程进行规划,管理,这个过程是可控的,可以统一对于线程进行管控、调优、监控。

Callable和Runnable有什么区别?

Runnable是一直都有的接口,提供了线程的run方法,callable是后来才有的接口,提供了call方法。

Runnable接口没有返回值,也无法抛出异常,Callable有返回值,同时可以抛出异常。Callable就是为了解决Runnable解决不了的问题。

@FunctionInterface
public interface Runnable {
    public abstract void run();
}

@FunctionInterface
public interface Callable<V> {
    V call() throws Exception;
}

从上面可以看到,Callable是返回值,并且也可以抛出异常的。

ps:这里可以看出,接口中也可以有抽象方法,所以可以小小总结下抽象类和接口的区别以及相同。

接口 抽象类

相同
接口和抽象类中都可以有抽象方法。一个类现在继承抽象类或者实现接口的时候,必须实现他们其中的所有抽象方法。

如果一个类实现抽象类或者继承接口的时候,只实现了部分的抽象方法,那么这个类必须是抽象类,因为相当于它其中还有抽象方法,有抽象方法的类必须是抽象类。

抽象类中可以没有抽象方法,也可以包含非抽象方法,但有抽象方法的类一定是抽象类。

一个类,如果实现了抽象类或者接口,然后实现了所有的抽象方法,那么 这个类可以声明成抽象类,也可以声明成普通具体类,所以这就印证了上面这句话。

抽象类和接口都是一个抽象层面的东西,都无法进行实例化(多态的方法除外),需要有具体类来继承或者实现他们的抽象方法。

抽象类中可以有实例属性,属性的默认修饰是public static final,也就是跟着类不能更改的静态最终变量,一般都是抽象到顶层了,如果实在没办法的,必须要有的属性才会在抽象类中顶一个一个属性。接口中也可以有属性变量,同样的也必须默认修饰为public static final。

不同
抽象类中可以有抽象方法和非抽象方法,接口中必须是抽象方法。(后续,接口可以有default方法,但是还是不能有常规的普通方法)

抽象类可以有变量和常量,接口只有常量。

抽象类和普通类更多是一个继承的关系,是一个is A的关系,是一个共性功能,而接口和实现类是一个实现,是一个like A的关系,是一个拓展功能。(可以展开讲到deque stack vector的概念,这就是为什么java中实现栈想要使用deque而不是stack的原因)

抽象类可以有static方法,但是接口不能有static方法。

接口里只能定义静态常量属性,不能定义普通属性;抽象类里既可以定义普通属性, 也可以定义静态常量。

接口不包含构造函数;抽象类包含构造函数,抽象类里的构造函数并不是用于创建对象,而是让其子类调用这些构造函数来完成属于抽象类的初始化操作。

接口不包含初始化块,但抽象类可以包含初始化块

一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口以弥补 Java 单继承的不足。

执行execute()方法和submit()方法的区别是什么?

execute()方法用于处理那些不需要返回值的任务,在这种时候,并不知道任务的执行结果是成功还是失败。

submit()方法用于处理那些需要返回值的任务,在这种时候,他会返回一个Future对象,可以调用Future对象的get()方法来知道任务执行的结果是成功还是失败。get()方法会阻塞当前线程直到拿到结果,而get(long time)则会阻塞当前线程time时间,然后返回,这个时候,可能线程还没有执行完毕。

如何创建线程池?

在开发手册中,要求不允许使用Executors创建,而是使用ThreadPoolExecutor的方法,这样的处理可以强调线程池的运行规则,避免资源的损耗。

Executors的创建方法的弊端:

FixedThreadPool 和 SingleThreadExecutor允许请求的队列长度是Max Integer,有概率造成大量的堆积,造成OutOfMemory

CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为Max Integer,可能有大量的线程,这个也会造成OutOfMemory

ThreadPoolExecutor的创建线程池的方法:

通过构造函数来实现

在这里插入图片描述

通过Executor框架的工具类Executors来实现

可以创建3种ThreadPoolExecutor

第一种、FixedThreadPool:创建一个固定线程数量的线程池,这个池子中,线程数目是固定的,如果有任务过来,如果线程都在忙,没有空闲,任务会放在一个任务队列中,如果有线程闲下来了,就会直接去任务队列中处理。如果任务来的时候有线程是空闲的,那么这个线程就会直接处理当前任务。

第二种、SingleThreadExecutor:这个只会创建一个线程的线程池,在这个池子中,同时只能处理一个任务,如果任务来的时候线程不空闲,也会放在一个任务队列中,然后线程空闲之后按照先进先出的顺序处理任务队列中的任务。

第三种、CachedThreadPool,创建的线程的数目不确定,如果所有线程都在忙,那么有新的任务过来了,没有任务队列的概念,直接创建新的线程进行处理,那么这种情况下使用完的线程会重新放到线程池中复用,如果有空闲的线程也会优先使用空闲的线程。

ThreadPoolExecutor的分析

构造方法

给的构造方法有很多中,都是方法的重载,无非就是有些参数如果你不给,那么就是默认的参数。

image.png

如上图,参数列表如下

corePoolSize:核心线程数量,规定了当前线程池最小的可以同时运行的线程数量

maximumPoolSize:任务队列中,存放的任务已经满了的时候,线程池中最大的可以同时运行的数量

workQueue:任务队列,任务来的时候会先判断是否达到了核心线程数,如果达到了就直接放在任务队列中,如果任务队列满了,那么就会增加线程,处理任务队列,最大达到max

KeepaliveTime:当前的线程数量大于核心线程数量的时候,这些额外的线程如果空闲了,并不会马上销毁,而是等待时间超过了keepAliveTime之后才会销毁。

unit:keepAliveTime的单位,

threadFactory:线程工厂,在executor创建新线程的时候才会用到。这有默认的值。

handler:饱和策略,这个也有默认值

下面根据饱和策略handler进行讲解

一句话总结,就是怎么样处理在运行线程已经最大,同时任务队列满了,这时候还有新任务来。

处理方法一、ThreadPoolExecutor.CallerRunsPolicy:用调用者所在的线程来执行任务,如果当前的线程还在阻塞,那么就等阻塞的执行完毕;

处理方法二、ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionHandler拒绝新来的任务。(这是默认处理方法)

处理方法三、ThreadPoolExecutor.DiscardPolicy:不做任何处理,直接丢弃掉,不管他。

处理方法四、ThreadPoolExecutor.DiscardOldestPolicy:丢弃任务队列中最久没有处理额任务,然后新的加入任务队列。

多线程执行的execute的方法的逻辑

image.png

介绍一下Atomic原子类

原子类的概念就是操作一旦开始,那么就无法分割,也就是说如果这个执行开始了,就无法中断,并且这个线程一旦开始,就无法被其他线程干扰。

并发包JUC( java.util.concurrent )包中的原子类全部都在 java.util.concurrent.atomic 中。

JUC中的原子类分为一下几个类型:

基本类型

AtomicInteger:整数原子类型

AtomicLong:长整数原子类型

AtomicBoolean:布尔原子类型

数组类型

AtomicIntegerArray:整形数组原子类

AtomicLongArray:长整形数组原子类

AtomicReferenceArray:引用数组原子类

引用类型

AtomicReference:引用类型的原子类

AtomicStampedReference:引用类型的原子类,带有版本号的引用类型,该类将整数数值和引用类型放在一起,可以解决原子的更新数据和数据的版本号,可以解决CAS进行原子更新的ABA问题

AtomicMarkableReference:原子更新带有标记位的引用类型

对象属性修改类型

AtomicIntegerFieldUpdater:原子更新整型属性的更新器

AtomicLongrFieldUpdater:原子更新长整型属性的更新器

AtomicReferenceFieldUpdater:原子更新引用类型属性的更新器

CAS机制

这个是和Synchronized概念相对应的,Synchronized概念是悲观锁的概念,也就是直接将资源锁起来,但是这个情况就是性能低下,如果你锁起来了一个资源,那么需要获取到这个资源的所有线程全部都阻塞了。

CAS就是针对这种情况,使用的是乐观锁的概念,也就是有比较然后交换(compare and swap),首先,会有三个值:内存位置V,预期原值A,预期结果B,只有在内存位置的值==预期原值的时候,才会做交换,将内存位置更新成预期的结果。CAS是通过无限循环的过程来比较的,如果以此比较不成功,就自旋等待下次比较。

CAS的问题:

ABA问题,也就是如果针对一个值,从a改成了b,又从b改成了a,这个时候从CAS角度来看,这个值是没有发生改变的,那么针对这个问题的结局方法,就是JUC中有一个AtomicStampReference解决这个问题,说白了就是在每次更改的时候加上版本号的概念,那么无论怎么更改,只要更改了之后,版本号不一样就可以看出这个数据被人修改过。

CAS的自旋消耗:如果多个线程竞争同一个资源,在多次自旋不成功的情况下,就会一致在循环中等待,占用cpu资源浪费性能,解决办法就是破坏掉无限循环,当请求时间或者次数到达一定次数,就直接return。

CAS只能针对一个单变量,不能针对多个变量,那么解决的办法就是要不加锁,要不封装成为对象解决。

AQS原理是什么?

AbstractQueueSynchronizer,这个在java.util.concurrent.lock下面

AQS是用来构建同步器和锁的框架,我们可以利用AQS来构建出各种同步器,比如ReentrantLock,Semaphore等,也可以实现自己需求的同步器。

AQS在讨论并发的时候一般都会聊到,AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置成有效线程,并且将共享资源设置成为锁定状态。如果请求的资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即,暂时获取不到锁的线程会被放到队列中。

PS:

CLH:craig Landin and Hagersten 是一个虚拟的双向队列,虚拟的双向队列也就是不存在队列的实现,仅仅存在结点之间的关联关系。AQS是将每条请求共享资源的线程封装成一个CLH队列的一个节点,通过这个节点实现锁的分配。

原理解析:

首先,是一个双向队列,这个队列中,实现了FIFO,然后拥有锁还是不拥有锁的状态信息使用一个int来表示,这个int使用的有事CAS的方式来修改,保证原子性,同时这个变量也是volatile类型,因为这样可以保证变量的共享。

image.png

public volatile int state; //通过volatile实现变量的共享

状态信息的获取以及更改是通过其中的函数实现,其中更改使用的CAS,使用的Unsafe类的compareAndSwap

AQS的状态分为2中情况,第一种是独占,第二种是共享

独占:1. 公平锁,就是按照队列中的顺序,轮流获得锁 2. 非公平锁,锁一旦公开,就所有的都去抢

共享:多个线程可以同时执行

AQS的底层模板:

同步器的设计一般都是基于模板的,AQS的底层模板设计的原则或者说规则是根据如下方法的:

使用者继承AbstractQueueSynchronizer,并重写制定的方法,重写的方法就是对于state的释放和获取

将AQS组合在自定义的同步组件的实现中,调用模板方法,模板方法会自动调用你重写的方法。

需要重写的方法

public class jicheng extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int arg) {  //独占方法,获取资源,成功返回true,失败返回false
        return super.tryAcquire(arg);
    }

    @Override
    protected boolean tryRelease(int arg) {   //独占方法,释放资源,成功返回true,失败返回false
        return super.tryRelease(arg);
    }

    @Override
    protected int tryAcquireShared(int arg) {  // 共享方法,尝试获取资源 负数表示失败,0表示成功,但是没有剩余资源可用,整数表示成功,并且有可用资源
        return super.tryAcquireShared(arg);
    }

    @Override
    protected boolean tryReleaseShared(int arg) {  // 共享方法,尝试释放资源,true表示成功,false表示失败
        return super.tryReleaseShared(arg);
    }

    @Override
    protected boolean isHeldExclusively() {  //判断改线程是否独占资源,只有用到condition才会实现它
        return super.isHeldExclusively();
    }
}

默认情况下,这些方法都会抛出UNsupportOperationException,并且这些方法的内部实现的必须是线程安全的。

ReentrantLock为例子,初始化state为0,表示未锁定的状态,获取以此就需要state+1,获取多次也是允许的,但是你获取了多次也就是要释放多次,释放一次state-1,一定要将它改成0才是未锁定。

AQS组件总结:

semaphore (信号量):允许多个线程同时访问,Synchronized和reentrantlock都是只允许一个线程

countDownLatch (倒计时器):是一个同步工具类,用来协调多个线程之间的同步,这个工具通常用来控制线程等待,可以实现线程等待固定时间之后再开始执行

cyclicbarrier (循环栅栏):和countDownLatch的功能、使用场景都比较类似,但是他比countDownLatch的功能更加强大。他做的事情就是固定数量的一组线程,陆续到达一个点的时候被阻塞,这个点就是同步点,然后在最后一个到达同步点的时候,大家才开始干活。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GGUOHHUO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值