多线程与高并发 2022版学习

一、JUC java并发工具包

1、什么是进程,什么是线程?二者有什么关系?什么是并发,什么是并行?二者有什么关系?

答:进程就是后台运行的一个程序(和操作系统有关), 操作系统进行资源分配的基本单位。
当点击了一个操作系统的可执行文件,就会把数据加载到内存中,操作系统会给它分配资源,需要执行时加载到cpu,程序未运行时存在硬盘上,运行时再加 载到内存中。
进程中包含线程,线程就是进程中的一个基本执行单元。线程在进程内部,是调度执行的的基本单位。多个线程共享同一个进程的中的所有资源;
一个程序中,不同的执行路径就是一个线程。
并发是多个线程去同一时刻去访问同一个资源,高并发是很多个线程同一时刻去争抢一个资源,秒杀 购票。
并行多个事情同时去做。
多线程:一个程序内部不同任务的来回切换。

2、谈谈对volatile关键字的理解

答: volatile是java虚拟机提供的轻量级的同步机制。

重点: 1、保证线程之间的可见性
2、不保证原子性
3、禁止指令重排(有序性)
3、JMM – java 内存模型

可见性:主内存的数据修改,一个线程修改了自己工作空间的数据,并且已经写回给了主内存。此时要马上通知到其他线程,其他线程需要重新拿到最新的值,这就是所谓的可见性。(主内存和线程工作内存的可见性),主要有一个线程修改了主内存中的值(共享变量都在主内存中存储),只要是加了volatile关键字的属性,其他线程在读取时就能读取到最新的值。

关于同步的一些规范:

● 1、线程解锁前,必须将工作空间中的共享变量值刷新到主内存中。
● 2、线程加锁前,必须读取主内存中的最新值到自己的工作内存中。
● 3、线程加锁解锁必须是同一把锁。

JMM的三大特性

● 1、可见性
● 2、原子性
● 3、有序性

4、JAVA程序运行原理分析

在这里插入图片描述

4.1 class文件内容
● 包含java程序执行的字节码
● 严格按照格式(虚拟机规范),紧凑排列在class文件中的二进制流
● 文件开头有一个0xcafebabe(16进制)特殊的一个标志

4.2 jvm运行时数据区
java文件 -> class文件 -> jvm运行时数据区
Java程序加载到虚拟机的内存区域的流程。

4.2.1 线程共享部分
class信息存储在方法区中,存储着整个类。
根据类创建的对象存储在堆内存中,垃圾回收说的就是堆内存。

4.2.1.1 方法区
jvm主要在方法区中存储加载的类信息,常量,静态变量,以及编译后的代码等数据。
方法区这是虚拟机规范中的一个逻辑区域,具体的实现根据不同的虚拟机来实现。

4.2.1.2 堆内存
jvm主要在堆内存中存放创建的对象。
jvm在启动时,堆内存创建,存放创建的对象,主要分为老年代和新生代。
垃圾回收器主要就是管理堆内存,堆内存如果满了就会出现OutOfMemroyError.
在这里插入图片描述
4.2.2 线程独占部分
4.2.2.1 虚拟机栈
每个线程在虚拟机栈中会有一个私人空间,线程栈会由多个栈帧组成,一个线程会执行一个或者多个方法,一个方法对应一个栈帧。
站栈帧内容包括:局部变量表,操作数栈,动态链接,方法返回地址,附加信息等。
栈内存默认最大是1M,超出则会StackOverflowError.

4.2.2.2 本地方法栈

4.2.2.3 程序计数器
线程独占:每个线程都有它独立的空间,随线程的生命周期而创建和销毁。
线程共享:所有的线程均可以访问这块内存数据,随虚拟机或者GC而创建和销毁。

5、线程状态

5.1 6个状态定义
● new 尚未启动的线程的状态
● Runable 可运行线程的线程状态,等待CPU的调度。(不代表线程一定启动了,有俩种情况 1cpu正在执行线程,2线程处于一个随时可以被执行的状态)
● Blocked 线程阻塞等待锁的一个状态,比如在同步代码块或者同步方法中被阻塞。
● Waiting 等待线程的状态 比如:Object.wait / Thread.join / LockSupport.park ,线程当前不执行,等待其他线程唤醒。
● Timed Wating 具有指定时间的等待线程的线程状态。比如:Thread.sleep 等。
● Terminated 终止线程的线程状态 ,线程正常完成执行或者出现异常。

6、线程中止

6.1 不正确的线程终止 - stop
stop : 强制中止线程,并且清除锁的信息,但是可能会导致线程安全问题,JDK不建议使用。不会保证加了锁的数据的一致性,破坏了线程安全。

6.2 正确的线程中止 - interrupt
interrupt: 优雅的中止线程。
如果目标线程是正在调用wait(),join(),sleep()等方法时被阻塞,那么Interrupt就会生效,该线程的中断状态将会被清除,抛出InterruptedException异常,最终达到中止线程的目的。
如果目标线程是被I/0或者NIO中的channel所阻塞,同样I/O操作会被中断,或者返回异常值,最终达到中止线程的目的。
如果以上条件都不具备,那么使用interrupt,就会直接中止线程。
重点:调用interrupt不会改变线程的状态,也不会直接中断线程,而是打一个interrupt可中断标记;
默认为false ,调用interrupt( ) 变为true.假如线程正在睡眠或者阻塞,调用interrupt( )方法就会抛出异常并且重置interrupt状态为false.(中断擦除)

6.3 正确的线程中止 - 标志位
在代码逻辑中,增加一个判断,使用标志位来中止线程。

二、java内存模型

1.1 Java内存模型 和 jvm运行时数据区
1、这俩个完全不相干的俩个产品。java代码在不同的虚拟机执行效果基本一致,是因为各个厂商在开发虚拟机时遵守了java虚拟机规范。
2、每一个语言都会有自己的语言规范。java -> java语言规范 ,比如描述一个类怎么写,一个接口怎么写。
java 语言规范 提出了 java内存模型,描述java语言的特性,-- 描述java语言的。
java 虚拟机规范 提出了 jvm运行时数据区,描述虚拟机jvm在内存中应该有哪样的特点,比如 内存中有 方法区,堆内存 虚拟机栈等。-- 描述虚拟机的。
java 内存模型 实际上是描述多线程程序在执行时候的一些规则,约束多线程,仅限只是提出规则,而是由jvm去解决。

1.2 多线程中的问题
● 1、所见非所得
● 2、无法使用肉眼去检测程序的准确性
● 3、不同的运行平台会有不同的表现
● 4、错误很难重新
java 内存模型 实际上是描述多线程程序在执行时候的一些规则,约束多线程,仅限只是提出规则,具体解决问题,而是由jvm去解决。

1.3 可见性
一个线程修改了数据,另一个线程不能马上感知到。
cpu指令重排
as-if-serial : 指令重排保证单个线程的执行语义,也就是说单个线程指令重排后执行的效果是一致的。

1.4 JIT编译器 (在执行时编译)
解释执行: 也就是我们所说的脚本,在执行时由语言的解释器将脚本一条条翻译成机器可识别的指令。
编译执行:将我们编写的程序,直接编译成机器可识别的指令码。JIT编译。
java 既是脚本语言 也是 编译语言。
JIT编译会进行很多的性能优化,过激优化会导致线程的可见性问题。
javac -> 执行前编译 不会做性能优化。
jit -> 执行时编译 性能优化。

1.5 volatile关键字
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
1.5.1java内存模型规定:
对于volatial关键字修饰的变量,其他线程后续对这个变量读同步(可以及时感知到这个变量的变化)
1.5.2 volatile 功能:
1、禁止缓存(对于volatial修饰的变量,会打一个标记 ACC_VOLATILE),不会缓存。
2、对于volatial修饰的变量不会做指令重排。
3、保证线程之间的可见性。
4、禁止指令重排。

创建一个对象在内存中的操作
首先是申请内存,然后初始化成员变量的值,最后将这个地址赋给字段。 单例模式的懒汉式一定要加volatile,禁止指令重排序,在超高并发下会有几率出现重排出现问题。

1.5.3 Share Variables 定义
可以在线程之间共享的内存称为共享内存或者堆内存。
所有实例字段,静态字段,和数组元素都储存在堆内存(广义的堆内存,包括方法区和堆内存)这也就是共享变量。
冲突:如果至少有一个访问是写操作,那么对同一个变量的俩次访问是冲突的。
这些能被多个线程访问的共享变量是内存模型规范的对象。也就是java内存模型。

1.5.4 线程间操作的定义
1、线程间操作:一个线程执行的操作可被其他线程感知或者被其他线程直接影响。
2、java内存模型只描述 线程间操作 不描述线程内操作,线程内操作按照线程内语义执行。
内存条在硬件级别限制了,同一个地址同一时刻只能被一个线程修改。
注意:所有的线程间操作,都存在可见性问题,JMM需要对其进行规范。

1.5.5 子类和父类的关系
1、在创建子类时,会默认先执行父类的默认初始化属性,再执行父类的无参构造器,以此类推执行子类的。
2、子类父类出现同名的变量,各是各的,

1.5.6 对于同步的规则定义
1、对于 volatile 修饰的变量 的写入,与所有其他线程后续对V 的读同步(可见性,可以实时感知到变量的变化)。
2、对于监视器的M 的解锁与所有后续操作对于m的加锁同步。
3、对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同步。

1.5.7 jvm中线程的理解
由于jvm运行程序是由线程来执行的,而每个线程在创建时jvm都会为其创建一个工作内存,(线程独占部分,栈空间),这是每个线程的私有数据空间,而java内存模型中规定所有的变量都储存在主内存中(堆内存),堆内存时内存共享区域,所有线程都可以访问。但是java内存模型规定了线程对于变量的操作(赋值)等必须在堆内存中进行,首先将变量从堆内存拷贝到自己的工作内存中(线程独占部分)然后对变量就进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量名,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

1.5.8 可见性 = 及时通知
线程之间的通信

1.6 原子性
不可分割,完整性,每个线程正在做具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功要么同时失败。
volatile不能保证原子性。

1.7 synchronized 详解
底层实现:

synchronize(obj) { }

1、在obj这个对象的头部,记录加锁的线程信息。如id,如果这是第一个线程 ,这个就叫偏向锁(只是记录线程信息),默认乐观不存在竞争。
2、如果有线程争抢,会升级为自旋锁。默认自旋10次,如果得不到升级为重量级锁,1.6之后不需要设置,jvm提供了自适应自旋 Adapative Self Spinning , 由jvm自己控制。
3、执行时间短(加锁代码消耗时间),线程数少,可以使用自旋。
4、加锁代码复杂,执行时间长,线程数量多,使用系统锁,也就是重量级锁。
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。
在这里插入图片描述

1.7.1自旋锁的原理
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。但是,如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程将被 OS(Operating System) 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。
解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。但是如何去选择自旋时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!JDK在1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋时间不是固定的了,而是由前一次在同一个锁上的自旋时间以及锁拥有的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

1.7.2自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

CAS 理解:
比较旧值,更新新值。java不能直接操作内存,jvm帮我们实现了封装,使用unsafe类。
unsafe 修改一个对象,需要传入一个对象的引用(地址) 以及对象的哪个字段的偏移量 旧值 新值
unsafe.cas(obj,offset,oldValue,newValue)

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
* 使用unsafe实现i++ 保证线程安全
*/
public class unsafeTest {
    private volatile int  i = 0;
    
    // 存储i字段的偏移量
    private static Long valueOffset = null;
    
    // 操作类
    private static Unsafe unsafe = null;
    
    
    static {
        // unsafe = Unsafe.getUnsafe();
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            
            Field iField = unsafeTest.class.getDeclaredField("i");
            // 获取i字段的偏移量
            valueOffset = unsafe.objectFieldOffset(iField);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    // cas 加1 底层也是这么写的 调用的操作系统层面的cas方法
    public void add(){
        for (;;){
            int current = unsafe.getIntVolatile(this,valueOffset);
            // cas有可能会失败  自旋
            boolean status = unsafe.compareAndSwapInt(this, valueOffset, current, current + 1);
            if(status){
                break;
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        unsafeTest unsafeTest = new unsafeTest();
        
        // 开启10个线程  每个线程从1加到10000
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    unsafeTest.add();
                }
            }).start();
        }
        Thread.sleep(6000L);
        System.out.println(unsafeTest.i);
    }
}
synchronized 理解:

// 1、加锁的状态时如何记录的?
//  记录到对象的对象头的markword中
// 2、状态会记录到this对象中吗?
//  会 加锁就是修改对象头的markword里面的tag标记,代表不同的状态。
// 3、如果锁占用,线程会挂起等待,那么在释放锁时,唤醒挂起的线程是如何做到的呢?  

// 当new( ) 一个对象的时候,在内存中是怎么分布的?跟jvm虚拟机有关系,主要是关注hostpot实现。
// 8个字节对齐是指这个对象的字节数务必是8的整数倍。

class T {
    int a;
    int b;
}
// 重点:
// 8个字节的 markword(标志字)  
// 4个字节的 类型指针(通过它找到 T.class)
// 成员变量(具体大小看类型) 8(俩个int)
// 补4个空字节(8个字节对齐)

对象–>对象头 markword (32位或者64位的内存区域)
一个对象在堆内存中的结构,一个是对象头一个对象的实例字段。

锁的概念:
任何线程要执行代码块里的代码需要先获取到这把锁。
脏读:加synchronize和非synchronize可以共同运行。
可重入锁:一个同步方法调用另一个同步方法,一个线程一把锁。

1.8 synchronized 锁升级的过程

不需要和操作系统打交道的锁,称为轻量级锁。
用户空间锁 重量级锁
偏向锁 和 自旋锁 都是在用户空间完成 需要向内核申请
偏向锁:多数的synchronized的代码段,在实际运行中只有一个线程在执行,没有必要设置锁竞争机制,哪个线程先到就偏向哪一个。把当前线程的指针(线程ID)放到markword里面。
偏向锁原理:
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,
    如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何加锁操作,偏向锁的效率高。

在这里插入图片描述

偏向锁好处:
偏向锁是在只有一个线程执行同步代码块时进一步提高性能,适用于一个线程反复获得同一锁的情况。
偏向锁可以提高带有同步但无竞争的程序性能。
轻量级锁(自旋锁、无锁):偏向锁撤销,锁竞争(自旋锁)LockRecord 锁记录,多个线程自旋的方式,将LR指针记录在markword,其他线程进行自旋等待,不停的自旋等待。
重量级锁:这把锁必须向操作系统申请,objectmonitor 。当一个线程在向操作系统申请一把锁的时候,操作系统会把这个线程丢到objectmonitor 里面的一些队列中,凡是自旋等待拿不到锁的线程 都会丢到 objectmonitor waitset队列中。等待CPU调度。这个是不消耗CPU的。

1.8.1 字节码层级
monitorenter (synchronized代码开始)
monitorexit(synchronized执行结束释放)
monitorexit(synchronized执行中发生异常释放)

1.8.2 重入锁
synchronized 是可重入锁。一个同步方法调用另一个同步方法,一个线程一把锁。程序执行中,出现异常,默认锁是会释放的。
重入字数必须记录,因为要涉及解锁几次。不同锁的实现不同。
偏向锁、自旋锁 —> 线程栈 —> LR+1 (先备份自己的markword,重入一次新增一个LR)
重量级锁 — > ObjectMonitor 对象的某个字段值 记录

1.8.3 自旋锁什么时候升级重量级锁?
竞争加剧,有线程 超过10次自旋, -XX: PreBlockSpin 或者自旋线程数超过CPU核数的一半。1.6之后不需要设置,jvm提供了自适应自旋 Adapative Self Spinning , 由jvm自己控制。

1.8.4 为什么有了自旋锁还要有重量级锁?
自旋是消耗CPU资源的,如果的锁的时间长或者自旋线程多,CPU会被大量消耗,这个情况下会升级为重量级锁,重量级锁里面会有很多的队列,有的来做竞争有的来做等待。当一个线程在向操作系统申请一把锁的时候,操作系统会把这个线程丢到objectmonitor 里面的一些队列中,凡是自旋等待拿不到锁的线程 都会丢到 objectmonitor waitset队列中。等待CPU调度。这个是不消耗CPU的。

1.8.5 偏向锁是否比一定比自旋锁效率高?
不一定 , 在明确知道有多线程竞争的情况下 偏向锁肯定会涉及锁撤销(消耗资源) 这个时候应该直接使用自旋锁。jvm启动过程会有很多线程竞争,默认情况启动时不打开自旋锁,过一段时间再打开。

三、Locks – >ReentrantLock

在这里插入图片描述

owner 记录当前是谁获取了这把锁。
count 记录上锁了几次。可重入的次数。
lock 上锁
unlock 释放锁,只有获取到了锁才能进行释放,否则会报错。
在这里插入图片描述

理解图解 lock 原理:
假如有四个线程来争抢锁,过程是这样的,线程首先会判断ReentrantLock内部的count是否等于0,等于0代表没有加锁,如果是等于0这个线程是使用CAS(0.1),将count+1,修改count的值。然后修改owner的值为自己的内存地址,修改owner不需要使用cas,因为已经把加锁的动作在count操作了,说明已经获取到锁了。那么再来一个线程,也是如此,判断count是否为0,如果不是再去判断owner是否为自己,如果不是就会进去到等待队列,如果owner是自己,就会再次修改count的值进行+1。这个就是锁重入。
锁释放的过程是,当一个线程过来解锁时会首先先判断owner是否为自己,如果owner不是当前线程,解锁会报错。如果是自己就会将count进行-1,调用一次unlock就会操作一次,直到把count修改为0,owner修改为null,在解锁完成之后会把等待队列中的线程唤醒,让其他线程进行加锁操作。
ReentrantLock默认是非公平锁,可以在创建时指定是否公平,参数可以控制,非公平是指新来一个一个线程可以和等待队列里的线程一起争抢锁,不分先来后到,所以是非公平的。

package com.example.juc;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;

/**
 * 手写 实现 ReentrantLock
 *
 * @author wzy
 */
public class ReentrantLock implements Lock {

    /**
     * 标记是谁占有了锁
     */
    private volatile Thread owner = null;

    /**
     * 锁重入次数 (加锁次数)
     */
    private final AtomicInteger count = new AtomicInteger(0);

    /**
     * 等待队列
     */
    private final LinkedBlockingDeque<Thread> waitQueue = new LinkedBlockingDeque<>();

    @Override
    public boolean tryLock() {
        /*
         * 判断count是否为0 如果是 CAS抢锁, 如果不是 判断owner是否为当前线程 如果是 就count+1 不是就 进入等待队列
         * 假如有四个线程来争抢锁,过程是这样的,线程首先会判断ReentrantLock内部的count是否等于0,等于0代表没有加锁,如果是等于0这个线程是使用CAS(0.1),
         * 将count+1,修改count的值。然后修改owner的值为自己的内存地址,修改owner不需要使用cas,因为已经把加锁的动作在count操作了,说明已经获取到锁了。
         * 那么再来一个线程,也是如此,判断count是否为0,如果不是再去判断owner是否为自己,如果不是就会进去到等待队列,如果owner是自己,就会再次修改count的值进行+1。这个就是锁重入。
         * 锁释放的过程是,当一个线程过来解锁时会首先先判断owner是否为自己,如果owner不是当前线程,解锁会报错。如果是自己就会将count进行-1,
         * 调用一次unlock就会操作一次,直到把count修改为0,owner修改为null,在解锁完成之后会把等待队列中的线程唤醒,让其他线程进行加锁操作。
         */
        int ct = count.get();
        // 不等于0 说明已经被加锁了
        if (ct != 0) {
            if (owner == Thread.currentThread()) {
                // 重入
                count.set(ct + 1);
                return true;
            } else {
                // 互斥
                return false;
            }
            // cas 抢锁 更新owner
        } else {
            if (count.compareAndSet(ct, 1)) {
                owner = Thread.currentThread();
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    public void lock() {
        // 尝试获取 浅尝辄止
        boolean tryLock = tryLock();

        // 未成功
        if (!tryLock) {
            // 挂起线程 wait notify park unpark suspend resume
            waitQueue.offer(Thread.currentThread());

            // for循环是为了 解决伪唤醒问题
            for (; ; ) {
                // peek() 读队列中的元素 但是不出队列
                Thread head = waitQueue.peek();
                // 队列头部才能抢锁
                if (head == Thread.currentThread()) {
                    if (!tryLock()) {
                        LockSupport.park(); // 伪唤醒问题
                    } else {
                        waitQueue.poll();
                        break;
                    }
                } else {
                    // 伪唤醒
                    LockSupport.park();
                }
            }
        }
    }

    public boolean tryUnLock() {
        /*
         * 判断owner是否为当前线程
         */
        if (owner != Thread.currentThread()) {
            throw new IllegalMonitorStateException();
        } else {
            // count -1
            // 如果count - 1 = 0 ,解锁成功 否则失败
            int ct = count.get();
            int next = ct - 1;
            count.set(next);
            if (next == 0) {
                owner = null;
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    public void unlock() {
        boolean tryUnLock = tryUnLock();
        if(tryUnLock){
            Thread head = waitQueue.peek();
            if(head != null){
                LockSupport.unpark(head);
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }


    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }


    @Override
    public Condition newCondition() {
        return null;
    }
}

semaphore 信号量限流
最多允许多少个线程同时允许。
acquire 获取许可。
多个线程同时运行,最多允许多个线程同时运行,由参数控制,获取不到许可的在阻塞。

四、线程池

在这里插入图片描述

原理:
1、是否达到核心线程池数量,没有达到,创建一个工作线程来执行任务。
2、工作队列是否已满,没有满,则提交新的任务储存在工作队列中。
3、是否达到线程池最大数量?没有到则创建一个工作线程来执行任务。
4、最后执行拒绝策略来处理这个任务。

五、理解线程中的一些方法:

sleep() :当前线程睡眠一段时间,让其他线程运行。
yield() :当一个线程正在运行,调用了yield() 会进入到等待队列中,让出一下CPU 其他线程有可能抢到执行也有可能抢不到。
join(): 当有俩个线程 t1 t2 ,当俩个线程在运行时,t1在运行时调用了t2的join方法,会先执行完t2再执行t1线程。t1程序在等待,注意线程调用自己的join方法没有效果,只能调用其他线程,自己挂起,其他线程执行。

六、线程状态

在这里插入图片描述

NEW :新建状态
runable 运行 :-- 内部分为俩个状态 ready 就绪状态(cpu的等待队列中) running运行状态(正在被cpu执行)
调用yield() 状态会从running跳到ready
teminated :线程结束 ,之后不允许调用start() 开启线程
blocked : 阻塞 同步代码块 没有得到锁的时候 阻塞状态 获得锁 ->ready ->running
waiting : 等待
timewaiting:有时间的等待

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值