java基础巩固-宇宙第一AiYWM:为了维持生计,多高(多线程与高并发)_Part6~整起(打手的自我安全修养之线程安全模块、Synchronized、死锁、CAS、ThreadLocal)

我觉得安全就是从两方面出发喽:

  • 就是我自己的东西我没让改,你别人别给我乱改乱动
    • 成员变量和静态变量是否线程安全?
      • 如果它们没有共享,则线程安全
        • 如果类中没有成员变量,比如dao层类,那么这个类一般是线程安全的
      • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
        • 如果只有读操作,则线程安全
        • 如果有读操作, 则这段代码是临界区,需要考虑线程安全
    • 局部变量是否线程安全?
      • 局部变量是线程安全的,因为每个线程调用testl()方法时局部变量i, 被调用的方法中的局部变量会在每个线程的栈帧内存中被创建多份,因此不存在共享
        在这里插入图片描述
        在这里插入图片描述
      • 但局部变量引用的对象则未必安全
        • 如果该对象没有逃离方法的作用访问,它是线程安全的
        • 如果该对象逃离方法的作用范围(比如发生继承,子类可以访问父类的对象引用,相当于对象发生了逃离,则需要考虑线程安全),需要考虑线程安全
  • 别人的东西我也不能去乱改,做一个不懂礼貌的坏人

当然了,看看大佬对线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

但是呢,如果各家关起门里用各家自己的不向外露出的东西,在如此的法治社会,打手的安全是可以得到一定的保障的。
如果呀一个可变的、共享的东东(这个资源可以被多个线程所持有或者说多个线程都可以访问这个资源),那么就存在线程(打手)安全问题了。

  • 多个线程如果**只是读取共享资源而不会去修改**是不存在线程安全问题的。只有**当至少一个线程修改共享资源而没有任何同步措施时**才会存在线程安全问题。
    在这里插入图片描述

那么下来,最重要的就是,怎样保证线程的安全。共有四个方式,先上图:
在这里插入图片描述

  • 第一个方式:不可变(最简单的方式就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后这些变量就是不可变的)。当我们共享的东西贴上封条,谁都不能不按规矩随意的改变它,那么也能实现一定的线程安全。
    • 用锁可以,但是有上下文切换之类的,影响性能,所以可以使用不可变类
      • 比如,This class is imnutable and thread- safe.这个类是不可改变的和线程安全的。public final class DateTimeFormatter {…}
      • String类也是不可变的
        在这里插入图片描述
        • 像String中的substring这种方法,其实他是当beginIndex>0时先复制一个新数组出来,也就是生成一个副本,然后才会进行数组的更改
          • String中的substring这种方法构造新字符串对象时,会生成新的char[] value,对内容进行复制。这种通过创建副本对象来避免共享的手段称之为[保护性拷贝(defensive copy) ]不共享或者说避免共享了不就线程安全了嘛
      • BigDecimal中的add()方法:
        在这里插入图片描述
      • final:
        在这里插入图片描述
      • 在web阶段学习时,设计Servlet 时为了保证其线程安全,都会有这样的建议,不要为Servlet 设置成员变量,这种没有任何成员变量的类也就是无状态的类是线程安全的。因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为[无状态]
    • 不可变的对象一定是线程安全的。只要一个不可变的对象被正确的构建出来(没有发生this引用逃逸之类的情况),那么这个对象的外部可见状态永远也不会改变,永远也不会看到这个对象在多个线程之中处于不一致的状态。
      在这里插入图片描述
public class ImmutableExample {
	public static void main(String[] args){
		Map<String, Integer> map = new HashMap<>();
		Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
		unmodifiableMap.put("a", hu);
	}
}
//执行结果:报错。因为人家都说不可变了你还想改不给你报个错才怪
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)

  • 第二个方式:就是整天会听到别人念叨的 互斥同步【互斥是实现同步的一种手段罢了,临界区、互斥量、信号量都是常见的互斥实现方式,Java中实现互斥同步的手段主要有synchronized和ReentrantLock两个关键字或者两个锁 呀、加个锁呀…Synchronized+ReentrantLock
    • 其实,Synchronized以及Lock,这两种锁的用法几乎是一样的,都是为了避免临界区的竞态条件发生【保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。】。只不过相比较Synchronized相比较于ReentrantLock又把条件变量做了细分,对应的锁定唤醒方法也换成了await()和signal()方法
      • 如果多个线程对同一个共享资源(也叫临界资源,也叫缓冲区里面的资源)进行访问而不采取同步操作的话(还记得同步操作是什么吗,同步就指的是顺序执行不会交替执行),那么操作的结果是不一致的,也不一定是准确的
        • 一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源(多个线程读共享资源其实也没有问题),在多个线程对共享资源读写操作时发生指令交错,就会出现问题
        • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区【//counter++:++操作既有读也有写,并且counter也是一个共享资源,所以这个++操作就是一个临界区】
        • 竞态条件Race Condition多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
      • 或者说避免临界区的竞态条件发生有多种方法
        • 阻塞式的解决方案: synchronized, Lock
        • 非阻塞式的解决方案:原子变量
          • synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
    • 互斥同步也是一种并发正确性保障手段【互斥解决了并发进程/线程对临界区的使用问题。只要一个进程/线程进入了临界区,其他试图想进入临界区的进程/线程都会被阻塞着,直到第一个进程/线程离开了临界区。】。互斥同步也叫阻塞同步,因为互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题
      在这里插入图片描述
      • 互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施也就是加锁等措施,无论共享数据是否真的会出现竞争都要进行加锁
        在这里插入图片描述
        在这里插入图片描述
      • 同步是指多个线程并发访问共享数据时,并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步,而互斥是实现同步的一种手段(Java中最基本的互斥同步手段就是synchronized关键字),互斥是方法,同步才是互斥的目的****。
        • 虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
          • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
          • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
        • 互斥实现的方式主要有以下几种:
          • 临界区
            • 当多线程相互竞争操作共享变量时,由于运气不好,即在执行过程中发生了上下文切换,我们得到了错误的结果,事实上,每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate)由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
          • 互斥量
          • 信号量:信号量是操作系统提供的一种协调共享资源访问的方法【信号量不仅可以实现临界区的互斥访问控制【通过互斥信号量的方式,就能保证临界区任何时刻只有一个线程在执行,就达到了互斥的效果。】,还可以线程间的事件同步。】,信号量表示资源的数量,对应的变量是一个整型(sem)变量
            • 有两个原子操作【PV 操作的函数是由操作系统管理和实现的,所以操作系统已经使得执行 PV 函数时是具有原子性的】的系统调用函数来控制信号量的【这两个操作是必须成对出现的】,分别是:
              • P 操作:P 操作是用在进入临界区之前,将 sem 减 1,相减后,如果 sem < 0,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞
              • V 操作:V 操作是用在离开临界区之后,将 sem 加 1,相加后,如果 sem <= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞
            • 信号量实现临界区的互斥访问控制:
              • 为每类共享资源设置一个信号量 s,其初值为 1,表示该临界资源未被占用。只要把进入临界区的操作置于 P(s) 和 V(s) 之间,即可实现进程/线程互斥:此时,任何想进入临界区的线程,必先在互斥信号量上执行 P 操作,在完成对临界资源的访问后再执行 V 操作。由于互斥信号量的初始值为 1,故在第一个线程执行 P 操作后 s 值变为 0,表示临界资源为空闲,可分配给该线程,使之进入临界区。若此时又有第二个线程想进入临界区,也应先执行 P 操作,结果使 s 变为负值,这就意味着临界资源已被占用,因此,第二个线程被阻塞。并且,直到第一个线程执行 V 操作,释放临界资源而恢复 s 值为 0 后,才唤醒第二个线程,使之进入临界区,待它完成临界资源的访问后,又执行 V 操作,使 s 恢复到初始值 1。
                在这里插入图片描述
            • 实现线程间的事件同步:
              • 同步的方式是设置一个信号量,其初值为 0。
    • 先泼一盆凉水,Java中的线程与OS中的原生线程是一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized使用就会导致上下文切换,很耗时。(Synchronized会引起线程上下文切换并带来线程调度开销)
      • 或者说,锁带来的问题:
        在这里插入图片描述
    • Java中的每个对象(或者这样说,Java中的每个对象都可以把这个对象当作一个同步锁来用)都有自己的同步锁~加锁本身已经保证了内存可见性
    • 第二种方式中的Synchronized(Java提供的一种原子性内置锁):
      • Java内置的用户或者使用者看不到的锁叫做内部锁或者监视器锁(内置锁是排它锁,就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁)
        • 线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起
          • 阻塞式的解决方案: synchronized,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁],其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
        • 拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源wait系列方法时释放该内置锁
      • Synchronized的作用有哪些:
        • 原子性:确保线程互斥的访问同步代码
        • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
          • 这也是synchronized的一个内存语义,这个内存语义就可以解决共享变量内存可见性问题
            • 进入synchronized块(也就是“加锁”、“获取锁”)的内存语义是**把在synchronized块内(锁内)使用到的变量从线程的私有工作内存中清除**,这样在synchronized块内使用到该变量时就不会从线程的私有工作内存中获取,而是直接从主内存中获取
            • 退出synchronized块(也就是“释放锁时”)的内存语义是**把在synchronized块内对共享变量的修改刷新到主内存**
        • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。
      • synchronized的用法总结起来就是下面三点:
        • 修饰普通方法:虽然synchronized锁是写在方法上,但是跟方法没半毛钱关系,此时锁的是this对象,控制使得多个线程来串行使用这个对象作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
          在这里插入图片描述
          • 构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说
        • 修饰静态方法:作用于当前类,进入同步代码前要获得当前Class类对象的锁,synchronized 关键字加到 **static 静态方法和 synchronized(xxx.class)**代码块上都是是给 Class 类上锁
          • 类对象内存中只有一份
          • synchronized 关键字加到 static 静态方法和 synchronized(类.class) 代码块上都是是给 Class 类上锁
        • 修饰代码块:指定加锁对象,对给定对象加锁,控制使得多个线程来串行使用这个对象,进入同步代码库前要获得给定对象的锁
          在这里插入图片描述
          • 对括号里指定的对象/类加锁:
            • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
              • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
            • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
          • 如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁
          • 尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能
      • synchronized 底层实现原理:
        • synchronized 关键字底层原理属于 JVM 层面:
          在这里插入图片描述
        • synchronized 修饰的方法【如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁】并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
          在这里插入图片描述
          • 不过两者【synchronized 同步语句块的实现和synchronized 修饰的方法(包括静态方法和实例方法)】的 本质都是对对象监视器 monitor 的获取
        • synchronized 同步代码块的实现通过 monitorenter 和 monitorexit 指令(其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置).
          • 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitormonitor对象是OS提供的,存在于每个Java对象的对象头,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因**】的持有权。其内部包含一个计数器【在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。】
            • Monitor(锁):由OS提供,又叫监视器或者管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,这个被上锁的对象的对象头中的Mark Word中就被设置指向Monitor对象的指针
              在这里插入图片描述
              • 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因
              • Monitor的实现过程是这样的:如果有一个新线程过来,那么这个新线程会先检查这个临界区的代码有没有被人上锁呀,所以第一步要先去检查obj对象有没有关联Monitor锁呢,如果已经关联了锁(关联指的就是这个对象的markword有没有一条指向Monitor的指针),那么就会先检查一下这个锁有没有主人(owner有没有指针指向这个Monitor的所有者,Mnitor中的Owner只能有一个主人),那么这个新线程就知道自己此时获取不了锁了。而此时这个新线程不是说啥也不做,而是关联到Monitor(有一个指针从Monitor中的EntryList指向新线程。相当于记录一下,自己下一个要获取锁,正在排队呢,)
                在这里插入图片描述
                在这里插入图片描述
        • 对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁,在执行 monitorexit 指令后将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
        • synchronized 修饰的方法并没有 monitorenter【将lock对象MarkWord 置为Monitor 指针】 指令和 monitorexit 【将lock对象MarkWord 重置,唤醒Monitor的EntryList中的阻塞等待调用的线程们】指令,取得代之的确实是 ACC_SYNCHRONIZED 标识。该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
        • 也可以看看程序员田螺老师关于Synchronized原理的文章
      • 多线程中 synchronized 锁升级的原理
        • 为什么要锁升级:在jdk1.6后Java对synchronized锁进行了升级过程,主要包含偏向锁、轻量级锁和重量级锁,主要是针对对象头MarkWord中的threadid 字段的变化而言。
          • Monitor是OS提供的,如果我们每次进入synchronized都要获取Monitor锁,这多影响性能呀【每次都要上Monitor锁很麻烦】,这不和上下文切换是一样的嘛。所以锁升级就是Monitor的替代品。
        • 在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
      • synchronized 锁升级的过程:在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,锁是 cpu 一个总量级的资源,每次获取锁都要和 cpu 申请,非常消耗性能。
        在这里插入图片描述
        • 在 Java 早期版本中,synchronized 属于重量级锁,效率低下。 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
      • 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗
        在这里插入图片描述
      • synchronized 锁不仅能升级还能降级(不是所有的锁都能降级),具体的触发时机是在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。当锁降级时,主要进行了以下操作:
        • 恢复锁对象的 markword 对象头
        • 重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用
          在这里插入图片描述
          在这里插入图片描述

插播一条新闻:JVM对synchronized的优化有哪些?从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态【锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级】,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。
在这里插入图片描述

  • 锁膨胀:上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
    • 偏向锁:偏心嘛。一句话总结它的作用:为了减少统一线程获取锁的代价,引入偏向锁。在大多数情况下,锁不存在多线程竞争(那偏向锁就是在无竞争时把整个同步都消除掉,连CAS都不用做了)总是由同一线程多次获得,因此如果每次都要竞争锁会付出很多没必要的代价那么此时就是偏向锁。【如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。】
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      • 偏向锁的升级:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致
        • 如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
        • 如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活
          • 如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2) 可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,
          • 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
            在这里插入图片描述
            在这里插入图片描述
      • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
      • 当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
      • 当撤销偏向锁阈值超过40次后, jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
    • 轻量级锁:(在无竞争时使用CAS操作去消除同步使用的互斥量)轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。也就是在没有多线程竞争的前提下减少传统的重量级锁使用OS互斥量产生的性能消耗。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      • 为什么要引入轻量级锁:
        • 轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
      • 轻量级锁什么时候升级为重量级锁
        • 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一- 份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord) ,然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord) 的地址;如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
          在这里插入图片描述
          在这里插入图片描述
      • 调用wait\notify就会将偏向锁或者轻量级锁升级为重量级锁
    • 重量级锁:重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
  • 锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如如果判断在一段代码中堆上的所有数据都不会逃逸出去从而被其他线程访问到,那么就可以把他们当作线程私有的栈上数据对待,那就肯定不需要进行同步加锁啥了。
    在这里插入图片描述
    • 锁消除举个例子:
      public static String concatString(String s1, String s2, String s3){
      	return s1 + s2 + s3;
      }
      JDK1.5之前,编译器会对String的这个拼接方法concatString()进行自动优化,会转化成为StringBuffer对象的连续append()操作:优化完如下:
      public static String concatString(String s1, String s2, String s3){
      	StringBuffer sb = new StringBuffer();
      	sb.append(s1);
      	sb.append(s2);
      	sb.append(s3);
      	return sb.toString();
      }
      //每个append()方法中都有一个同步块,JVM虚拟机会观察变量sb然后也就会很快发现这个sb这个引用变量的动态作用域被限制在concatString()方法内部。也就是说,sb的所有引用永远不会逃逸到concatString()方法之外,其他线程无法访问到他,因此可以进行消除。
      
      
      在这里插入图片描述
      //综下所述,把不安全的线程代码改写成线程安全的代码--改写原来类+main方法中实例化改写后的类
      
      public class AtomicExample{
      	private AtomicInteger cnt = new AtomicInteger();
      
      	public void add(){
      		cnt.incrementAndGet();//也可以用getAndIncrement(),俩方法一样
      	}
      
      	public int get(){
      		return cnt.get();
      	}
      
      	public static void main(String[] args) throws InterruptedException{
      		final int threadSize = 1000;
      		//main方法就改这一句就行
      		AtomicExample atomicExample = new AtomicExample();
      		final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
      		ExecutorService executorService = Executors.newCachedThreadPool();
      		for(int i = 0; i < threadSize; i++){
      			executorService.execute(() -> {
      				atomicExample.add();
      				countDownLatch.countDown		();
      			});
      		}
      		countDownLatch.await();
      		executorService.shutdown();
      		System.out.println(atomicExample.get());
      	}
      }
      执行结果:1000,不管执行多少次,都一直稳定是1000o
      
      在这里插入图片描述
  • 锁粗化:锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围(如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁进行互斥同步操作也会导致性能损耗,所以我们将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,比如上面图片中扩展到第一个append操作之前直到最后一个append操作之后,这样只需要加锁一次就行了),避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。
    在这里插入图片描述
  • 自旋锁与自适应自旋锁
    • 由于 Java 中的线程是与操作系统中的线程 一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程,挂起线程和恢复或者叫唤醒线程的操作都需要转入内核态中完成。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。并且很多应用上共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复并不值得
    • 自旋锁:物理机器有一个以上的处理器时能让两个或者两个以上的线程同时并行执行,我们就让后面请求锁的那个线程执行忙循环(自旋)等待锁的释放,但是不放弃处理器的执行时间看看持有锁的线程是不是就快释放锁了。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。
      • 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销
      • 自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些
        • 使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会忙等待,直到它拿到锁【也就是一直自旋,利用 CPU 周期,直到锁可用】。这里的忙等待可以用 while 循环等待实现不过最好是使用 CPU 提供的 PAUSE 指令来实现忙等待,因为可以减少循环等待时的耗电量
          • 在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU
          • 自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成正比的关系
        • 一般加锁的过程,包含两个步骤:【CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
          • 第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
          • 第二步,将锁设置为当前线程持有
      • 自旋锁==忙等待锁
      • 自旋优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
        在这里插入图片描述
      • 但是自旋锁的优化方式也存在缺点:
        • 如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
        • 自旋锁是当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10 ,可以使用-XX PreB lockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。
          • 由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些 CPU时间白白浪费
            在这里插入图片描述
    • 自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
      在这里插入图片描述
    • 为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
      • 重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
        在这里插入图片描述
        在这里插入图片描述
    • 第二种方式中除了上面的Synchronized,再就是JDK实现的JUC包中的重入锁ReentrantLock(可重入锁,ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer),换句话说ReentrantLock最终还是使用AQS来实现的ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。),ReenTrantLock是Lock接口的一种子实现类(继承自AQS实现的独占锁ReentrantLock时,定义当 status为0表示锁空闲,为1表示锁己经被占用)
      在这里插入图片描述
      在这里插入图片描述
      • Java并发编程之美里面有个用ReentrantLock来实现一个简单的线程安全的list,有时间自取一下。
      • ReetrantLock是一个可重入的独占锁(同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞从而被放入该锁的AQS阻塞队列里面)排它锁),独占锁ReentrantLock的实现是:当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现他自己就是锁的持有者就会把状态值从1变为2(也就是设置可重入次数),而当另一个线程获取锁时发现自己并不是该锁的持有者,这个线程就会被放入AQS阻塞队列后挂起
        • ReentrantLock 意为可重入锁,说起 ReentrantLock 就不得不说 AQS ,因为ReentrantLock这个可重入锁的底层就是「使用 AQS 去实现」的
        • ReentrantLock 的state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的
      • ReentrantLock主要有四个特性
        • ReentrantLock 相对于synchronized它具备如下特点:
          • 可中断,可打断:为了避免死等,被动的避免死等
          • 可以设置超时时间:锁超时,主动的可以放弃等待
          • 可以设置为公平锁,可通过构造方法设置公平或不公平
            • ReentrantLock 默认是不公平的
              在这里插入图片描述
          • 支持多个条件变量
            在这里插入图片描述
            • 条件变量实现原理:
              • await()流程
                在这里插入图片描述
                在这里插入图片描述
              • signal()流程:
                在这里插入图片描述
                在这里插入图片描述
                在这里插入图片描述
        • 等待可中断当持有锁的线程长期不释放锁时正在等待的线程可选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助
          • 不可打断模式:在此模式下,即使它被打断,仍会驻留在AQS队列中,等获得锁后方能继续运行(是继续运行!只是打断标记被设置为true)
        • 一个ReentrantLock对象可同时绑定多个Condition对象
          在这里插入图片描述
        • ReentrantLock有两种模式,一种是公平锁,一种是非公平锁支持公平锁和非公平锁:(ReentrantLock 提供了公平锁和非公平锁的实现),根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁
          在这里插入图片描述
          • 公平锁:ReentrantLock pairLock = new ReentrantLock(true);如果构造函数不传参数则默认是非公平锁(公平模式下等待线程入队列后会严格按照队列顺序去执行)
            在这里插入图片描述
            在这里插入图片描述
          • 非公平锁:ReentrantLock unPairLock = new ReentrantLock(false);(非公平模式下等待线程入队列后有可能会出现插队情况)
            在这里插入图片描述
            • 在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销非公平锁的性能更高
              在这里插入图片描述
            • 非公平锁实现原理:
              在这里插入图片描述
              • 没有竞争时:
                在这里插入图片描述
              • 有一个竞争时
                在这里插入图片描述
                源码如下:
                在这里插入图片描述
                在这里插入图片描述
                他这里面你看代码,当前线程肯定是得先处理addWaiter的逻辑,如下:
                在这里插入图片描述
                然后当前线程在进入acquireQueued的逻辑【-1表示当前节点有责任唤醒后继节点】
                在这里插入图片描述
                在这里插入图片描述
                在这里插入图片描述
        • 可重入ReentrantLock 与synchronized 一样,都支持可重入
          • 当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程**再次获取它自己己经获取的锁如果不被阻塞,那么我们说该锁是可重入的**(也就是只要该线程获取了该锁,那么可以有限次数地进入被该锁锁住的代码)
            在这里插入图片描述
          • 可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0说明该锁没有被任何线程占用。一个线程获取了该锁时计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1,当计数器值为0时,锁里面的线程标示被重置为 null。这时候被阻塞的线程会被唤醒来竞争获取该锁。
            在这里插入图片描述
            java.util.concurrent.locks包下的ReadWriteLock接口,常见的方法挑几个瞅瞅:
            在这里插入图片描述
      • 获取锁:
        • void lock()。当一个线程调用该方法时说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为1,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加1后返回。如果该锁己经被其他线程持有,则调用该方法的线程会被放入AQS 队列后阻塞挂起
        • void locklnterruptibly:该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的 interrupt()方法,则当前线程会抛出InterruptedException异常然后返回。
        • boolean trylock():尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁井返回 true,否则返回fallse 。该方法不会引起当前线程阻塞。tryLock()使用的是非公平策略
        • boolean trylock(long timeout, TimeUnit unit ):与tryLock ()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返 false
      • 释放锁:
        在这里插入图片描述
        • void unlock():尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程持有的AQS状态值减1,如果减去 后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
...
public void lock(){
	sync.lock();//ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync ,这个锁是一个非公平锁或者公平锁
}
...
/**
*sync的子类NonfairSync,也就是非公平锁
*/
final void lock() { 
	//(1)CAS设置状态值,,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1, CAS成功则表示当前线程获取到了锁
	if (compareAndSetState(O, 1)){
		setExclusiveOwnerThread(Thread.currentThread()); //setExclusiveOwnerThread设置该锁持有者是当前线程
	} else{ 
	//(2)调用AQS的acquire方法
		acquire(1);//如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1,
	}
}

public final void acquire(int arg) { 
	//(3)调用ReentrantLock重写的tryAcquire方法。AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化。所以这里会调用ReentrantLock重写的tryAcquire方法。
	if(!tryAcquire(arg) && 
		//tryAcquiref返回false会把当前线程放入AQS阻塞队列
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
		selfinterrupt();
	}
}

/**
*非公平锁的代码
*/
protected final boolean tryAcquire(int acquires) { 
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) { 
	final Thread current= Thread.currentThread() ; 
	int c = getState() ; 
	//(4)当前AQS状态值为0。查看当前锁的状态值是否为0,为0说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后返回true。如果当前状态值不为0说明该锁已经被某个线程持有
	if (c == 0) { 
		if(compareAndSetState(O , acquires)) { 
			setExclusiveOwnerThread(current);
			return true; 
		}//(5)当前线程是该锁持有者。查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true 这里需要注意,nextc<0说明可重入次数溢出了。 如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。
	} else if (current== getExclusiveOwnerThread()) { 
			int nextc = c + acquires; 
			if (nextc < 0)//overflow 
				throw new Error (Maximum lock count exceeded"); 
			setState(nextc); 
			return true; 
	}//(6) 
	return false;
}

/**
*公平锁是怎么实现公平的。公平锁的话只需要看FairSync重写的Acquire方法。
*/
protected final boolean tryAcquire(int acquires) { 
	final Thread current = Thread.currentThread(); 
	int c = getState(); 
	//(7)当前AQS状态值为0
	if (c == 0) { 
	//(8)公平性策略,在设置CAS前添加了hasQueuedPredecessors 方法,该方法是实现公平性的核心代码
		if (1hasQueuedPredecessors() && 
			compareAndSetState(O , acquires)) { 
			setExclusiveOwnerThread(current); 
			return true; 
		}
	}
	//(9)当前线程是该锁持有者
	else if (current == getExclusiveOwnerThread()) { 
		int nextc = c + acquires; 
		if (nextc < 0) 
			throw new Error("Maximum lock count exceeded");
			setState(nextc); 
			return true;
		}//(10)
		return true;
	}
}

/**
*如果当前线程节点有前驱节点则返回住时, 如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。其中如果h==t则说明当前队列为空,直接返回false;如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列。之前enq函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后,那么返回true,如果h!=t并且s!=null和s.thread != Thread.cunentThread()则说明队列里面的第一个素不是当前线程,那么返true
*/
public final boolean hasQueuedPredecessors() { 
	Node t = tail; //Read fields in reverse initial zation order 
	Node h = head; 
	Node s; 
	return h != t && 
		((s = h.next) ==null || s.thread != Thread.currentThread()) ;
}


public class lockExample{
	private Lock lock = new ReentrantLock();
	
	public void func(){
		lock.lock();
		try{
			for(int i = 0; i < 10; i++{
				System.out.print(i + "");
			}
		} finally {
			lock.unlock();//确保在finally中释放锁,从而避免发生死锁。
		}
	}
}

public static void main(String[] args){
	LockExample lockExample = new LockExample();
	ExecutorService executorService = Executors.newCachedThreadPool();
	executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
//执行结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

那这两种方式有啥区别呢,做两个方面的比较瞅一瞅

  • 🕴synchronized 和 Lock 有什么区别
    • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
    • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
    • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
      在这里插入图片描述
      细看一下这个Lock接口:
      在这里插入图片描述
      在这里插入图片描述
  • 这里面有个java.util.concurrent.locks.ReentrantReadWriteLock
    • ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能
    • 因为这个原因才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 🕴synchronized 和 ReentrantLock相同点和不同点是什么
    • 相同点:两者都是可重入锁
      • 可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁【自己可以再次获取自己的内部锁】,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁
        • 比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
      • 两个至少都是锁嘛,都保证了可见性和互斥性,也就是两个都用于控制多线程对共享对象的访问。
    • 不同点:
      • synchronized 依赖于 JVM【synchronized是Java中的关键字,是JVM级别的锁】 而 ReentrantLock 依赖于 API【ReentrantLock 就是Lock接口下的一个实现类,是API层面的锁
        • synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的;ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成
          • synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,也就说明Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放,因为 JVM 会确保锁的释放。ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。
      • ReentrantLock 比 synchronized 增加了一些高级功能(但是除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized),主要来说主要有三点
        • ReentrantLock等待可中断(可响应中断):通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
        • ReentrantLock可以指定是公平锁还是非公平锁:而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
          • (设计者将synchronized设计为非公平锁可能会有这两点考虑:
            • 当持有锁的线程释放锁时,该线程会执行以下两个重要操作,先将锁的持有者 owner 属性赋值为 null然后唤醒等待链表中的一个线程(假定继承者),如果在这个过程中或者说间隙中有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁)。
            • 当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
        • 可实现 选择性通知(锁可以绑定多个条件):ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知
          • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
            • synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式
            • Lock提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制
            • ConditionObject实现了Condition接口,给AQS提供条件变量的支持 。
              • Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如 Condition可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
                在这里插入图片描述
  • 🕴synchronized 和 volatile 的区别是什么
    • volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即 保证了变量的可见性synchronized 解决的事执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且, synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证操作的内存可见性同时也使得这个锁的线程的所有操作都 happens-before 于随后获得这个锁的线程的操作
      • 当线程写入了volatile值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存)
      • 读取volatile值时就相当于进入同步块 (先清空本地内存变量值,再从主内存获取最新值)
    • volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
    • volatile 仅能使用在变量级别;synchronized 则可以使用在 变量、方法和类级别的
      • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好
    • volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性
    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞
    • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化
  • 再反过头来看看:线程同步(不同步就不安全了,线程同步是来维护线程安全的一种方式):
    • 线程同步的形成条件:队列+锁
      在这里插入图片描述
  • 死锁(互相持有对方的资源,这里也有死锁的东西线程死锁的补充):在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是**两个或多个进程无限期的阻塞、相互等待**的一种状态

另外,死锁说完了以后,加锁的几种情况,具体到代码中时:感觉有以下几点

  • 假如咱们写了一个狗类,里面写了两个方法A()和B(),这两个方法分别用synchronized(或者Lock模板圈加锁也一样,先加锁,再在try catch中写咱们的主要代码,再在finally中解锁)。
    • 此时写的测试类中,在main方法中测试一下,咱们就用之前说的创造线程的三种方法先搞两个线程出来,然后分别在两个线程中分别调用狗类中的两个方法,此时就可以说***“狗类中的A()方法和B()方法用的是同一个锁(就是狗这个由狗那个模板类实例化出来的对象对应的锁),A()方法和B()方法谁先拿到锁谁先执行”***
      • 中间要是谁遇到拦路虎,来个延时啥的,谁不就执行的慢了嘛 。比如此时咱们给狗类中的A()方法中加一句(B()方法中没加哦)这样的延时代码,那么A()方法就会后被执行,B()
        先被执行。
TimeUnit.SECOND.sleep(...);
  • 此时在狗类中再加一个C()方法,而这个方法没有被synchronized或者Lock模板圈修饰,同样在测试类的main方法中创造一个线程去调用这个C()方法。此时C方法是先于A、B方法执行的。兜里放个锁很累的哟。
  • 此时来个猫类,里面有个D()方法,然后在测试类的main()方法中创建一个线程掉这个D方法,此时D方法和A、B方法就不是持有同一把锁对象喽,因为不是同一个对象调这些被synchronized或者lock模板圈修饰的同步方法的,所以就可以说他们不是持有同一把锁(锁是对象才有的哦)。此时D方法先执行还是A、B方法先执行就看谁那里有延时之类的拦路虎喽。
  • 倘若给狗类中的A()方法和猫类中的D()方法加上static(加个static说明这个被static修饰的方法或者类在类一加载的时候就有了),而这两个类同出一个class模板类中,即
XXX dog = new Dog();
XXX cat = new Cat();

那么此时这两个方法哪怕分别被dog和cat两个调用而不是被同一个对象调用,这俩方法持有的锁都是一个锁,就是Class模板类那把锁而不是哪个对象的锁

  • 第三种方式下面这种非阻塞同步。非阻塞同步类似一种乐观并发的策略,比如CAS

除了上面第一种和第二种两种不可变和互斥同步,还有就是**第三种方式:下面这种非阻塞同步。**

  • 非阻塞同步:基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据那操作就成功,如果有争用并产生了冲突那就采取补救措施呗,但是过程中并不需要把线程挂起。
    • 使用乐观并发策略需要操作和冲突检测两个步骤具备原子性,这里我们靠硬件来完成,硬件保证需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:
      • 测试并设置
      • 获取并增加
      • 交换
      • 比较并交换CAS
        在这里插入图片描述
      • 加载链接/条件存储
  • 为什么有了上面那种第二种方式,两种锁,还要第三种方式呢。
    • 这是因为呀有时候咱们明知道是只读操作时多个线程同时调用不会存在线程安全问题,但是不得不给这个只读操作对应的方法上加个synchronized关键字,原因是咱们要靠synchronized 来实现 value的内存可见性。这样一比较为了靠锁来实现变量的内存可见性而让我们背上了锁引来的上下文切换的问题(**当一个线程没有获取到锁时这个线程会被阻塞挂起,这会导致线程上下文的切换和重新调度开销**,虽然Java 提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读->改->写等操作之间的原子性问题),还有没有更好的方法呀**不用锁用能实现 value的内存可见性**---------使用java.util.concurrent.atomic.AtomicInteger(JUC包下的原子性操作类),看下面:
      在这里插入图片描述
  • J.U.C并发包提供了三个类,这些类中的方法都是原子性的,所以可以调用这些方法来进行咱们的操作,既安全又高效
    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
  • java.util.concurrent.atomic.AtomicInteger:AtomicInteger (在内部使用非阻塞 CAS算法实现的原子性操作类 AtomicLong)类主要利用 CAS和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
 // 更新操作时提供“比较并替换”的作用
  private static final Unsafe unsafe = Unsafe.getUnsafe();

  private static final long valueOffset;

  static {
      try{
          valueOffset = unsafe.objectFieldOffset(AutomicInteger.class.getDeclaredField("value"));
      }catch(Exception ex){
          throw new Error(ex);
      }
  }

  private volatile int value;

在这里插入图片描述

  • java.util.concurrent.atomic.AtomicInteger 中有两个方法比较重要
    在这里插入图片描述
    • public final int getAndDecrement()
    • public final int incrementAndGet()
private AtomicInteger cnt = new AtomicInteger();

//getAndIncrement()和incrementAndGet() 实现的是同一个作用,就是实现number++,底层用的是CAS
public void add(){
	cnt.incrementAndGet();
}

public final int incrementAndGer(){
//getAndIncrement()方法的底层还是调用的是unsafe这个类的getAndAddInt(XX1, XX2, XX3)实现number++操作,那他是怎么实现的呢,咱看一下getAndAddInt(....)方法的三个形式参数代表什么不就行了
//XX1:代表哪个对象呀,一般常用this,就代表当前对象
//XX2:代表XX1对应对象的内存地址为XX2的那个地方
//XX3:每次加多少,你看例子程序中的var4=1,那就相当于期望值var5,var5加var4的话不就是var5+1嘛
//也不就对应上了咱们CAS(就是那个方法的使用规则嘛),当期望值为var5时,那就更新值为update,当前update=var5+var4=var5+1,不就实现了number++这个操作了嘛。只不过多了XX1和XX2两个形式参数嘛,指明的更具体更底层一点,不就是指到哪个对象的哪个内存地址上了嘛,没个啥。重要的就是compareAndSet(int expect, int update) 里面当前值=expect时,把这个当前值更新为update这个值
	return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}



/**
*
*var1代表对象内存地址
*var2代表该字段相对对象内存地址的偏移
*var4代表操作需要加的数值,这里为1
*
*可以看到getAndAddInt()在一个循环中进行,发生冲突的做法是不断的进行重试
*/
public final int getAndAddInt(Object var1, long var2, int var4){
	int var5;
	do{
			var5 = this.getIntVolatile(var1, var2);//得到旧的预期值
	} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//通过调用compareAndSwapInt()来进行CAS比较,如果该字段内存地址中的值等于var5,那么就更新内存地址为var1+var2的变量为var4+var5
	return var5;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • AtomicLong内部是使用非阻塞CAS算法实现的原子性操作类,那这个CAS是个啥呀。
    在这里插入图片描述
    • 🕴CAS(Compare and swap,即比较并交换,虽然是比较并交换,听着像两个操作,但是这CAS确实是原子性操作,它是一条 CPU 同步原语(通过处理器的指令来保证操作的原子性),是一种硬件对并发的支持(CAS通过硬件保证了比较-更新操作的原子性),针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。),CAS 是一种**无锁的非阻塞算法**的实现
      在这里插入图片描述
      • CAS是一种无锁的非阻塞算法的实现
        在这里插入图片描述
      • CAS的特点:捡田螺的小男孩老师关于CAS乐观锁解决并发问题的一次实践
        在这里插入图片描述
      • CAS 并发原语体现在 Java 语言中的 sum.misc.Unsafe 类(Unsafe类是在JDK的rt.jar包中的)中的各个方法(JDK中的Unsafe类提供了一系列的compareAndSwap*方法)。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。
        在这里插入图片描述
        • Unsafe类主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
          • 但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
        • Unsafe类是在JDK的rt.jar包中的。使用 Unsafe 这个类的话可以通过下面两种方法获取:
          • 利用反射获得 Unsafe 类中已经实例化完成的单例对象 theUnsafe
            在这里插入图片描述
          • 从getUnsafe方法的使用限制条件出发,通过 Java 命令行命令-Xbootclasspath/a把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取 Unsafe 实例:java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
        • Unsafe 类实现功能:
          • 内存操作:在 Java 中是不允许直接对内存进行操作的,对象内存的分配和回收都是由 JVM 自己实现的。但是在 Unsafe 中,提供的下列接口可以直接进行内存操作
            在这里插入图片描述
            在这里插入图片描述
            • 通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存的释放。
            • 使用堆外内存的原因有两点:
              • 对垃圾回收停顿STW的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响
              • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存
            • DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放
          • 内存屏障:
            • 编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而 指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况
            • 在硬件层面上,内存屏障是 CPU 为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能
              在这里插入图片描述
            • 在 Java 8 中引入了一种锁的新机制——StampedLock,StampedLock可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。为了解决这个问题,StampedLock 的 validate 方法会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障
              在这里插入图片描述
          • 对象操作:
            • Unsafe 提供了全部 8 种基础数据类型以及Object的put和get方法,并且所有的put方法都可以越过访问权限,直接修改内存中的数据。基础数据类型是直接操作的属性值(value),而Object的操作则是基于引用值(reference value)。**除了对象属性的普通读写外,Unsafe 还提供了 volatile 读写和有序写入方法。**volatile读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和Object类型
            • 使用 Unsafe 的 allocateInstance 方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:
              在这里插入图片描述
              • 非常规的实例化方式:指的就是Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用
          • 数组操作:
            在这里插入图片描述
            • 这两个与数据操作相关的方法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(可以实现对 Integer 数组中每个元素的原子性操作)中有典型的应用,通过 Unsafe 的 arrayBaseOffset 、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale 。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作
          • CAS 操作:
            • 什么是 CAS? CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg
              在这里插入图片描述
              在这里插入图片描述
          • 线程调度:Unsafe 类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度
            在这里插入图片描述
            • Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS),就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupport 的 park 、unpark 方法实际是调用 Unsafe 的 park 、unpark 方式实现的
              在这里插入图片描述
          • Class 操作:Unsafe 对Class的相关操作主要包括类加载和静态变量的操作方法
            • 静态属性读取相关的方法
              在这里插入图片描述
            • 使用defineClass方法允许程序在运行时动态地创建一个类
              在这里插入图片描述
            • Unsafe 还提供了一个defineAnonymousClass方法【public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);】:使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 Unsafe 的defineAnonymousClass方法。
          • 系统信息:包含两个获取系统相关信息的方法
            在这里插入图片描述
        • Unsafe类几个主要的方法:【Unsafe类中的方法都是native方法【本地方法使用 native 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 本地代码】,使用 JNI的方式访问本地C++实现库。】
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
          • sun.misc.Unsafe 部分源码
            在这里插入图片描述
    • CAS 包含了4个操作数:如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect(当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,如果相同就认为其他线程没有进行修改,而将新值写入内存
      • 需要读写的内存值 V(也就是对象的内存位置)
      • 对象中的变量的偏移值
      • 旧的预期值 A
        • 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值这个过程是原子的。)
      • 要修改的更新值 B
        在这里插入图片描述
        在这里插入图片描述
    • CAS的缺陷或者说CAS中出现的问题:
      • ABA问题:假如线程1使用 CAS 修改初始值为A的变量X,那么线程1会首先去获取当前变量X的值A,然后使用CAS操作尝试修改变量X的值A为B,即使使用CAS操作成功了,也是有问题的,这是因为有可能在线程1获取变量X的值A后,在执行CAS前,线程2使用 CAS修改了变量X的值为B,然后又使用CAS修改X变量的值为A,所以虽然线程1执行CAS时变量X的值是A ,但是这个A己经不是线程1获取时的A了。这就是ABA问题(ABA 问题:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用版本号来解决这个问题。)
        • 并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
          • 可以通过java.util.concurrent.atomic.AtomicStampedReference (一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。(JDK中的AtomicStampedReference类每个变量的状态值都配备了一个时间戳,从而避免了ABA问题))解决ABA问题
            在这里插入图片描述
      • 循环时间长:多时候,CAS思想体现是 有个自旋次数的,就是为了避开这个耗时问题
        • 自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销
          • CPU开销比较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。
      • 只能保证一个变量的原子操作。很
        • CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
        • 可以通过这两个方式解决这个问题:
          • 使用互斥锁来保证原子性
          • 将多个变量封装成对象,通过java.util.concurrent.atomic.AtomicReference 来保证原子性

在这里插入图片描述
除了上面三种,还有**第四种方式,那就是不使用同步**,如下:

  • 不可变
  • 互斥同步
  • 非阻塞同步
  • 不用同步时:要保证线程安全并不是一定就要进行同步,同步只是保证共享数据争用时的正确性的一种手段,如果一个方法本来就不涉及共享数据,那他自然就无须任何同步措施去保证正确性。而且有一些代码天生就是线程安全的
    • 可重入代码:所有的可重入的代码都是线程安全的
      • 判断代码是否具备可重入性的一个原则:如果一个方法的返回结果是可预测的只要输入了相同的数据就都能返回相同的结果,那这个方法就满足可重入性的要求,当然也就是线程安全的
        在这里插入图片描述
    • 线程本地存储:共享数据的代码能保证在同一个线程中执行可以通过java.lang.ThreadLocal类来实现线程本地存储的功能
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • java.lang.ThreadLocal :线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到 线程隔离的作用,避免了线程安全问题。【//创建一个ThreadLocal变量:static ThreadLocal<String> localVariable = new ThreadLocal<>();】
    在这里插入图片描述
    • ThreadLocal类实现了每一个线程都有自己的专属本地变量,或者说主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,ThreadLocal这个盒子中可以存储每个线程的私有数据,从而避免了线程安全问题
      • 如果你创建了一个ThreadLocal变量,那么访问这个ThreadLocal变量的每个线程都会有这个ThreadLocal变量的本地副本,这也是ThreadLocal变量名的由来。可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
    • ThreadLocal
      public class ThreadLocal<T> {
      	//每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
          private final int threadLocalHashCode = nextHashCode();
      
          private static AtomicInteger nextHashCode = new AtomicInteger();
      
          private static final int HASH_INCREMENT = 0x61c88647;
      
          private static int nextHashCode() {
              return nextHashCode.getAndAdd(HASH_INCREMENT);
          }
      
          static class ThreadLocalMap {
              ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                  table = new Entry[INITIAL_CAPACITY];
                  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      
                  table[i] = new Entry(firstKey, firstValue);
                  size = 1;
                  setThreshold(INITIAL_CAPACITY);
              }
          }
      }
      
    • ThreadLocal的实现原理:先看看ThreadLocal相关类图
      在这里插入图片描述
      //Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法
      public class Thread implements Runnable {
          //......
          //与此线程有关的ThreadLocal值。由ThreadLocal类维护
          ThreadLocal.ThreadLocalMap threadLocals = null;
      
          //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
          ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
          //......
      }
      
      • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
        • 【ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用。Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型)。】
          • 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离
      • Thread类(Thread 类中有 threadLocals和inheritableThreadLocals,他们都是threadLocalMap类型的变量,设计为Map结构就是因为每个线程可以通过双列集合Map关联多个ThreadLocal变量了。,threadLocalMap是一个定制化的HashMap。在默认情况下, 每个线程中的这两个变量都为 null ,只有当前线程第一次调用ThreadLocal的set方法或者get方法时才会创建它们俩)有一个类型为ThreadLocal的ThreadLocalMap的实例变量threadLocals,即 每个线程Thread都有一个属于自己的ThreadLocalMapThread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量,ThreadLocalMap是ThreadLocal的静态内部类。)
        在这里插入图片描述
        • 每个线程的本地变量不是存放在 ThreadLocal 里面而是存放在调用线程的threadLocals变量里面,也就是说相当于 ThreadLocal类型的本地变量存放在具体线程内存空间中
        • ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的 get方法时(),再从当前线程的 threadLocals 里面将其拿出来使用
        • 如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的 threadLocals 里面 ,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的remove 方法 ,从当前线程 threadLocals里面删除该本地变量(不删除可能会造成内存溢出)
          • 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象
      • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值【每个Thread中都具备一个ThreadLocalMap(Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量),而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。】。),每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
        在这里插入图片描述
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //......
        }
        
        • 比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。
      • 同一个 ThreadLocal 变量在父线程中被设置值后在子线程是获取不到的。因为在子线程thread里面调用get方法时当前线程是thread 线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回 null 。
        • InheritableThreadLocal继承自ThreadLocal。其提供了一个特性,就是让子线程可 以访问在父线程中设置的本地变量
    • ThreadLocal的应用场景:
      • 数据库连接池
      • 会话管理中使用
    • 为什么会有ThreadLocal,因为当多线程访问同一个共享变量时很容易出现并发问题(特别是多个线程需要对同一个共享变量进行写入时),所以为了保证线程安全一般使用者在访问共享变量时需要进行适当的同步:同步的措施一般有:
      • 加锁
      • 使用ThreadLocal,这种方式是当创建一个变量后,每个线程对其进行访问时访问的是自己线程的变量
    • 如何使用 ThreadLocal
      //官方给了一个例子:
      //这个类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。 
      //例如,下面的类生成每个线程本地的唯一标识符。 线程的ID在第一次调用ThreadId.get()时被分配,并在后续调用中保持不变。 
      
      import java.util.concurrent.atomic.AtomicInteger;
      //只要线程存活并且ThreadLocal实例可以访问,每个线程都保存对其线程局部变量副本的隐式引用; 线程消失后,线程本地实例的所有副本都将被垃圾收集(除非存在对这些副本的其他引用)。
       public class ThreadId {
           // Atomic integer containing the next thread ID to be assigned
           private static final AtomicInteger nextId = new AtomicInteger(0);
      
           // Thread local variable containing each thread's ID
           private static final ThreadLocal<Integer> threadId =
               new ThreadLocal<Integer>() {
                   @Override protected Integer initialValue() {
                       return nextId.getAndIncrement();
               }
           };
      
           // Returns the current thread's unique ID, assigning it if necessary
           public static int get() {
               return threadId.get();
           }
       }  
      
      
      • ThreadLocal 类在 Java 8 中扩展,使用一个新的方法withInitial(),将 Supplier 功能接口作为参数。
        // Integer不是线程安全的,所以每个线程都要有自己独立的副本
        private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> new ThreadLocal<Integer>;
        
        
      • 或者最简单的:
        //创建一个ThreadLocal变量
        static ThreadLocal<String> localVariable = new ThreadLocal<>();
        
        在这里插入图片描述
  • ThreadLocal的set、get、remove 方法的实现逻辑:
    • set():
      在这里插入图片描述

      public void set(T value){
      	//(1)获取当前线程
      	Thread t = Thread.currentThread();
      	//(2)将当前线程作为 key ,去查找对应的线程变量,找到则设置
      	ThreadLocalMap map = getMap(t);//使用当前线程作为参数 调用getMap(t)方法
      	if(map != null){//如果 getMap(t)的返回值不为空,则把 value 值设置到 threadLocals 中,也就是把当前
      值放入当前线程的内存变量threadLocals中,key 就是当前 threadLocal 实例对象引用,value 是通过 set 方法传递的值
      		map.set(this, value);
      	}else{
      		(3)如果getMap(t)返回空值则说明是第一次调 set 方法,这时创建当前线程的threadLocals量(第一次调用就创建当前线程对应的HashMap)
      		createMap(t, value);
      	}
      }
      

      在这里插入图片描述
      在这里插入图片描述

      • 既然是Map结构,那么ThreadLocalMap当然也要实现自己的hash算法来解决散列表数组冲突问题。ThreadLocalMap中hash算法很简单,这里i【int i = key.threadLocalHashCode & (len-1);】就是当前 key 在散列表中对应的数组下标位置。ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647
    • get():

      public T get() { 
      	//(4)获取当前线程
      	Thread t = Thread.currentThread(); 
      	//(5)获取当前线程的threadLocals变量
      	ThreadLocalMap map= getMap(t) ; 
      	//(6)如果threadLocals不为null ,则返回对应本地变量的值
      	if(map != null) {
      		ThreadLocalMap.Entry e = map.getEntry(this) ; 
      		if(e != null) ( 
      			@SuppressWarnings (”unchecked”) 
      			T result= (T)e.value; 
      			return result; 
      		}
      	}
      	//(7)threadLocals 则初始化当前线程的 threadLocals 成员变量
      	return setInitialValue() ;
      }
      

      在这里插入图片描述

    • remove():

      public void remove() { 
      	ThreadLocalMap m = getMap(Thread.currentThread()) ; 
      	if(m != null) {//如果当前线程的 threadLocals变量不为空,删除当前线程中指定的ThreadLocal 实例的本地变量
      		m.remove(this);
      	}
      }
      
  • ThreadLocal 内存泄露问题:ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用(弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存),弱引用比较容易被回收。因此,ThreadLocal(ThreadLocalMap的Key)被就有可能被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。
    • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会 清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
      在这里插入图片描述
      在这里插入图片描述
      上面出现了个AtomicInteger,引出了…Atomic 原子类,点点这里看一看

巨人的肩膀:
深入理解Java虚拟机
狂神说视频
低并发编程
SpringForAll老师关于并发编程有三大核心问题:分工问题、同步问题、互斥问题的简单介绍

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值