【8月后端】JAVA多线程(112000字)

    • 1. 多线程环境下的线程安全体现在哪些方面?
    • 2. 创建线程的方式及其区别?
    • 3. 说一下从Java API层面上的6种线程状态
    • 4 final原理
    • 4 ThreadLocal有了解吗?
    • 5. synchronized 和Lock区别
    • 6. as-if-serial与happens-before
  • 【同步的方式】
    • [1] synchronized同步方法
    • [2 synchronized同步代码块
    • [3] 使用volatile实现线程同步
    • [4] 使用 ReentrantLock实现线程同步
    • [5] 使用ThreadLocal实现线程同步
    • [6] 使用LinkedBlockingQueue实现线程同步
    • [7] 使用原子变量实现线程同步
  • 【锁】
    • [1] 公平锁/非公平锁
    • [2] 可重入锁
    • [3] 独享锁/共享锁(互斥锁/读写锁)
    • [4] 乐观锁/悲观锁
    • [5] 分段锁
    • [6] 偏向锁/轻量级锁/重量级锁
    • [7] 自旋锁
    • [8] 可中断锁/不可中断锁/超时时间
    • [9] 显式锁/隐式锁
    • [10] 条件变量
    • [11] AQS
  • 【<锁的属性>】
    • [1] 为什么要加锁之临界区
    • [2] 实现一个锁需要考虑哪些方面?
  • 【<锁升级>】
    • [1] Java对象头
    • [2] Mark word 结构
    • [3] Monitor
    • [4] synchronized锁升级
      • 无锁
      • 偏向锁(可关闭)
      • 轻量级锁
      • 重量级锁
    • [6] synchronized锁对比
  • 【CAS】
    • [1] 什么是CAS?
    • [2] 什么是自旋锁?
    • [3] CAS可能出现的问题有什么?
    • [4] 哪里有用到CAS?
    • [5] CAS 和volatile如何实现无锁并发?
  • 【<死锁>】
    • [1] 什么是死锁
    • [2] 产生死锁的原因?
    • [3] 死锁的产生必须满足的四个必要条件
    • [4] 解决死锁的基本方法
    • [5] 说下对悲观锁和乐观锁的理解?
    • [6] 乐观锁常见的两种实现方式是什么?
  • 【<线程锁死>】
    • [1] 什么是线程锁死
    • [2] 线程锁死分为哪两种
    • [3] 活锁
    • [3] 线程饥饿
    • [5] 线程活性故障总结
  • 【<多线程常用方法>】
    • [1] start 与 run
    • [2] sleep 与 yield
    • [3] sleep 和 wait
    • [4] Daemon
    • [5] join和yield
    • [6] wait() notify() notifyAll()
    • [7] await() signal() signalAll()
    • [8] InterruptedException
    • [9] interrupted()
    • [10] Executor
    • [11] CopyOnWriteArrayList
  • 【ReentrantLock】
    • [1] ReentrantLock简介
    • [2] ReentrantLock方法
    • [3] ReentrantLock实战
    • [4] ReentrantLock原理
    • [5] ReentrantLock如何实现可重入锁?
    • [6] ReentrantLock如何实现公平锁和非公平锁?
    • [7] 条件变量Condition实现原理?
    • [8] 谈谈 synchronized 和 ReenTrantLock 的区别?
  • 【< volatile 关键字专题>】
    • [1] 谈一下你对 volatile 关键字的理解?
    • [2]Volatile如何保证可见性和有序性?
      • 1. 可见性
      • 2.有序性的
    • [3] volatile在什么情况下可以替代锁?
  • 【< synchronized专题>】
    • [1] synchronized 关键字?
    • [2] synchronized 关键字使用场景
    • [3] synchronized 内部字节码指令
    • [4] synchronized如何保证有序性、可见性、原子性?
      • 1. 原子性
      • 2. 可见性
      • 3. 有序性
    • [5] synchronized 关键字锁升级过程?
    • [5] JVM 对 synchronized 的锁优化
    • [8] synchronized 和 volatile 的区别是什么?
  • 【< AQS>】
    • [1]什么是AQS
    • [2] LCK队列源码及其实现
    • [3] 独占式同步状态获取
    • [4] 共享式同步状态获取与释放
    • [5] 独占式超时获取同步状态
    • [5]AQS的思想
  • 【< J.U.C>】
    • [1] AQS
    • [2] ReentrantLock
    • [3] ReentrantReadWriteLock
    • [4] StampedLock
    • [5] Semaphore
    • [6] threadlocal
    • [7] CountDownLatch
    • [8] CyclicBarrier?
    • [9] Atmoic
    • [10] FutureTask
    • [11] ForkJoin
  • 【Java中的线程池】
    • [1] 使用线程池的好处
    • [2] Executor框架
    • [3] ThreadPoolExecutor类的参数字段
    • [4] 线程池的排队策略
    • [6] 拒绝策略
    • [5] 常见的阻塞队列
    • [6] Java提供的四种线程池
    • [7] 手写一个线程池

1. 多线程环境下的线程安全体现在哪些方面?

答:多线程环境下的线程安全主要体现在原子性,可见性与有序性方面。

  1. 原子性是一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。

    对于涉及到共享变量访问的操作,若该操作从执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性。即,其它线程不会“看到”该操作执行了部分的中间结果。

    **举例:**银行转账流程中,A账户减少了100元,那么B账户就会多100元,这两个动作是一个原子操作。我们不会看到A减少了100元,但是B余额保持不变的中间结果。

    Java原子性的实现方式:

    • 利用锁的排他性,保证同一时刻只有一个线程在操作一个共享变量
    • 利用**CAS(Compare And Swap)**保证
    • Java语言规范中,保证了除long和double型以外的任何变量的写操作都是原子操作

    关于原子性,你应该注意的地方:

    • 原子性针对的是多个线程的共享变量,所以对于局部变量来说不存在共享问题,也就无所谓是否是原子操作
    • 单线程环境下讨论是否是原子操作没有意义
    • volatile关键字仅仅能保证变量写操作的原子性,不保证复合操作,比如说读写操作的原子性
  2. 可见性是指一个线程对共享变量的更新对于另外一个线程是否可见的问题。

    **定义:**可见性是指一个线程对于共享变量的更新,对于后续访问该变量的线程是否可见的问题。

    为了阐述可见性问题,我们先来简单介绍处理器缓存的概念。

    现代处理器处理速度远大于主内存的处理速度,所以在主内存和处理器之间加入了寄存器,高速缓存,写缓冲器以及无效化队列等部件来加速内存的读写操作。也就是说,我们的处理器可以和这些部件进行读写操作的交互,这些部件可以称为处理器缓存。

    处理器对内存的读写操作,其实仅仅是与处理器缓存进行了交互。一个处理器的缓存上的内容无法被另外一个处理器读取,所以另外一个处理器必须通过缓存一致性协议来读取的其他处理器缓存中的数据,并且同步到自己的处理器缓存中,这样保证了其余处理器对该变量的更新对于另外处理器是可见的。

    在单处理器中,为什么也会出现可见性的问题呢?

    单处理器中,由于是多线程并发编程,所以会存在线程的上下文切换,线程会将对变量的更新当作上下文存储起来,导致其余线程无法看到该变量的更新。所以单处理器下的多线程并发编程也会出现可见性问题的。

  3. 有序性是指一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题。

    **定义:**有序性是指一个处理器上运行的线程所执行的内存访问操作在另外一个处理器上运行的线程来看是否有序的问题。

    重排序:
    为了提高程序执行的性能,Java编译器在其认为不影响程序正确性的前提下,可能会对源代码顺序进行一定的调整,导致程序运行顺序与源代码顺序不一致。

    重排序是对内存读写操作的一种优化,在单线程环境下不会导致程序的正确性问题,但是多线程环境下可能会影响程序的正确性。

    重排序举例:
    Instance instance = new Instance()都发生了啥?
    具体步骤如下所示三步:

    • 在堆内存上分配对象的内存空间
    • 在堆内存上初始化对象
    • 设置instance指向刚分配的内存地址

    第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。(在多线程下的单例模式中,我们必须通过volatile来禁止指令重排序

什么是重排序?

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?

一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标,而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。

JMM对底层尽量减少约束,使其能够发挥自身优势。

因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。

一般重排序可以分为如下三种:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

这里还得提一个概念,as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

  1. 总结:
  • 原子性是一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。注意,原子操作+原子操作不一定还是原子操作。
  • 可见性是指一个线程对共享变量的更新对于另外一个线程是否可见的问题。
  • 有序性是指一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题。
  • 可以这么认为,原子性 + 可见性 -> 有序性

缘可续

2. 创建线程的方式及其区别?

方法一,继承 Thread类

// 创建线程对象
Thread t = new Thread() {public void run() {// 要执行的任务}
};
// 启动线程
t.start();

例如:

package cn.mycast;import lombok.extern.slf4j.Slf4j;@Slf4j(topic= "c.创建一个线程")
public class 创建一个线程 {public static void main(String[] args)  {Thread t1 =new Thread("t1"){@Overridepublic void run() {log.debug("hello");}};t1.start();}
}

结果

15:33:39.617 c.创建一个线程 [t1] - hello

方法二,实现 Runnable 接口

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程

  • Runnable 可运行的任务(线程要执行的代码)

Runnable runnable = new Runnable() {public void run(){// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

例如:

// 创建任务对象
Runnable task2 = new Runnable() {@Overridepublic void run() {log.debug("hello");}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

输出:

9:19:00 [t2] c.ThreadStarter - hello

Java 8 以后可以使用 lambda 精简代码

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐Thread t2 = new Thread(task2, "t2");
t2.start();

区别:

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了

  • 用 Runnable 更容易与线程池等高级 API 配合

  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活(java不能实现多继承的补偿)

方法三,实现Callable接口

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {log.debug("hello");     return 100});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

方法四:使用线程池创建

3. 说一下从Java API层面上的6种线程状态

  1. 新建(New):这是属于一个已经创建的线程,但是还没有调用start方法启动的线程所处的状态。
  2. 可运行(Runnable):该状态包含两种可能。有可能正在运行,或者正在等待CPU资源。包含了操作系统线程状态种的运行,可运行状态和阻塞状态(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行);
  3. 阻塞(Blocked):阻塞状态,当线程准备进入synchronized同步块或同步方法(排它锁)的时候,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。如果其线程释放了锁就会结束此状态;
  4. 等待(Waiting):该状态的出现是因为调用了方法1。处于该状态下的线程在等待另一个线程 执行一些其余action来将其唤醒。等待其他线程显式唤醒,否则不会再被分配CPU时间片;
  5. 限期等待(Timed Waiting):该状态和上一个状态其实是一样的,调用了方法2是不过其等待的时间是明确的。
  6. 死亡(TERMINATED):消亡状态比较容易理解,那就是线程执行结束了,run方法执行结束表示线程处于消亡状态了。

方法1:

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 LockSupport.unpark(Thread)

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

方法2:

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)

附:线程状态转换

假设有线程 Thread t
情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING
t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

注意是当前线程t 线程对象的监视器上等待

  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

4 final原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwWJRmLC-1596599154311)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714195425381.png)]

4 ThreadLocal有了解吗?

**答:**使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。

ThreadLocal内部实现机制:

  • 每个线程内部都会维护一个类似HashMap的对象,称为ThreadLocalMap,里边会包含若干了Entry(K-V键值对),相应的线程被称为这些Entry的属主线程
  • Entry的Key是一个ThreadLocal实例,Value是一个线程特有对象。Entry的作用是为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系
  • Entry对Key的引用是弱引用;Entry对Value的引用是强引用。

5. synchronized 和Lock区别

https://blog.csdn.net/qq_29373285/article/details/85964460

  1. 在实现上

synchronized是一个关键字,它基于JVM。它有锁升级过程,从偏向锁,轻量级锁,到重量级锁。

Lock是一个接口,它是基于JDK,它实现的主要实现类是ReentrantLock,它的使用也离不开AQS。

  1. 在使用上

synchronized是隐式锁,加锁解锁对使用者是隐藏的,可以作用于方法,代码块和类。

Lock是显示锁,需要手动上锁和释放锁(lock和unlock)

  1. 在功能上
  • Lock和synchronized都是互斥锁且支持可重入

  • Lock支持默认非公平锁,但支持公平锁,synchronized只支持非公平锁

  • lock的condition支持多个条件变量,但是synchronized

  • Lock 可中断,而 synchronized 不行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3idqMrdX-1596599154315)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200725094645345.png)]

6. as-if-serial与happens-before

as-if-serial规则:

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

happens-before:

JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

下面来比较一下as-if-serial和happens-before:

  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

【同步的方式】

https://www.cnblogs.com/Terry-Wu/p/10788663.html

为何要使用同步?

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
从而保证了该变量的唯一性和准确性

[1] synchronized同步方法

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,

内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如:

public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

[2 synchronized同步代码块

即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

代码如:

synchronized(object){ }

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

package com.xhj.thread;/*** 线程同步的运用** @author XIEHEJUN**/public class SynchronizedThread {class Bank {private int account = 100;public int getAccount() {return account;}/*** 用同步方法实现** @param money*/public synchronized void save(int money) {account += money;}/*** 用同步代码块实现** @param money*/public void save1(int money) {synchronized (this) {account += money;}}}class NewThread implements Runnable {private Bank bank;public NewThread(Bank bank) {this.bank = bank;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {// bank.save1(10);bank.save(10);System.out.println(i + "账户余额为:" + bank.getAccount());}}}/*** 建立线程,调用内部类*/public void useThread() {Bank bank = new Bank();NewThread new_thread = new NewThread(bank);System.out.println("线程1");Thread thread1 = new Thread(new_thread);thread1.start();System.out.println("线程2");Thread thread2 = new Thread(new_thread);thread2.start();}public static void main(String[] args) {SynchronizedThread st = new SynchronizedThread();st.useThread();}}
[3] 使用volatile实现线程同步
  • volatile关键字为域变量的访问提供了一种免锁机制,
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

例如: 在上面的例子当中,只需在account前面加上volatile修饰,即可实现线程同步。

代码实例:

//只给出要修改的代码,其余代码与上同class Bank {//需要同步的变量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//这里不再需要synchronizedpublic void save(int money) {account += money;}}

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。

[4] 使用 ReentrantLock实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法有:

  • ReentrantLock() : 创建一个ReentrantLock实例
  • lock() : 获得锁
  • unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

例如: 在上面例子的基础上,改写后的代码为: 代码实例:

//只给出要修改的代码,其余代码与上同class Bank {//需要同步的变量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//这里不再需要synchronizedpublic void save(int money) {account += money;}}

注:关于Lock对象和synchronized关键字的选择:

  • 最好两个都不用,使用一种java.util.concurrent包提供的机制, 能够帮助用户处理所有与锁相关的代码。
  • 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
  • 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
[5] 使用ThreadLocal实现线程同步

如果使用ThreadLocal(局部变量)管理变量,则每一个使用该变量的线程都获得该变量的副本, 副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal 类的常用方法

  • ThreadLocal() : 创建一个线程本地变量
  • get() : 返回此线程局部变量的当前线程副本中的值
  • initialValue() : 返回此线程局部变量的当前线程的"初始值"
  • set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
    在上面例子基础上,修改后的代码为:
//只改Bank类,其余代码与上同public class Bank{//使用ThreadLocal类管理共享变量accountprivate static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue(){return 100;}};public void save(int money){account.set(account.get()+money);}public int getAccount(){return account.get();}}

注:ThreadLocal与同步机制

  • a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
  • b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
[6] 使用LinkedBlockingQueue实现线程同步

前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。 本小节主要是使LinkedBlockingQueue来实现线程的同步 LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~

LinkedBlockingQueue 类常用方法

  • LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
  • put(E e) : 在队尾添加一个元素,如果队列满则阻塞
  • size() : 返回队列中的元素个数
  • take() : 移除并返回队头元素,如果队列空则阻塞

代码实例:
实现商家生产商品和买卖商品的同步

package com.xhj.thread;import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;/*** 用阻塞队列实现线程同步 LinkedBlockingQueue的使用** @author XIEHEJUN**/
public class BlockingSynchronizedThread {/*** 定义一个阻塞队列用来存储生产出来的商品*/private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();/*** 定义生产商品个数*/private static final int size = 10;/*** 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程*/private int flag = 0;private class LinkBlockThread implements Runnable {@Overridepublic void run() {int new_flag = flag++;System.out.println("启动线程 " + new_flag);if (new_flag == 0) {for (int i = 0; i < size; i++) {int b = new Random().nextInt(255);System.out.println("生产商品:" + b + "号");try {queue.put(b);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("仓库中还有商品:" + queue.size() + "个");try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}} else {for (int i = 0; i < size / 2; i++) {try {int n = queue.take();System.out.println("消费者买去了" + n + "号商品");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("仓库中还有商品:" + queue.size() + "个");try {Thread.sleep(100);} catch (Exception e) {// TODO: handle exception}}}}}public static void main(String[] args) {BlockingSynchronizedThread bst = new BlockingSynchronizedThread();LinkBlockThread lbt = bst.new LinkBlockThread();Thread thread1 = new Thread(lbt);Thread thread2 = new Thread(lbt);thread1.start();thread2.start();}}

注:BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:

  • add()方法会抛出异常
  • offer()方法返回false
  • put()方法会阻塞
[7] 使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger类常用方法:

  • AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
  • addAddGet(int dalta) : 以原子方式将给定值与当前值相加
  • get() : 获取当前值

代码实例:
只改Bank类,其余代码与上面第一个例子同

class Bank {private AtomicInteger account = new AtomicInteger(100);public AtomicInteger getAccount() {return account;}public void save(int money) {account.addAndGet(money);}}

补充–原子操作主要有:

  • 对于引用变量和大多数原始变量(long和double除外)的读写操作;
  • 对于所有使用volatile修饰的变量(包括long和double)的读写操作。

【锁】

JAVA锁有哪些种类,以及区别(转)

  1. 实现上

Synchronized

ReentrantLock

CAS

Volatile

2 类型上

  • 公平锁/非公平锁 :
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释

[1] 公平锁/非公平锁
  • 介绍:

公平锁是指多个线程按照申请锁的顺序来获取锁,

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

  • 优缺点:

公平锁可以防止线程饥饿。

非公平锁的优点在于吞吐量比公平锁大,有可能会造成优先级反转或者饥饿现象。

  • 实现:

对于Java ReentrantLock而言,默认是非公平锁。,但是支持公平锁。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

1. 公平调度方式:

按照申请的先后顺序授予资源的独占权。

2. 非公平调度方式:

在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。

如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。

公平调度和非公平调度方式优缺点分析

非公平调度策略:

  • 优点:吞吐率较高,单位时间内可以为更多的申请者调配资源
  • 缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象

公平调度策略:

  • 优点:线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象;适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;
  • 缺点:吞吐率较小

接下来,我们一起来看看JVM对synchronized内部锁的调度方式吧。

JVM对synchronized内部锁的调度

JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.

[2] 可重入锁
  • 介绍:

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

  • 优缺点:

可重入锁的一个好处是可一定程度避免死锁。

  • 实现:

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。

synchronized可重入锁的实现:

之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

synchronized void setA() throws Exception{Thread.sleep(1000);setB();
}synchronized void setB() throws Exception{Thread.sleep(1000);
}
[3] 独享锁/共享锁(互斥锁/读写锁)
  • 介绍:

独享锁是指该锁一次只能被一个线程所持有。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
共享锁是指该锁可被多个线程所持有。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

  • 实现:

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLockSynchronized
读写锁在Java中的具体实现就是ReadWriteLock

对于Synchronized而言,当然是独享锁。

[4] 乐观锁/悲观锁
  • 介绍:

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁适合写操作非常多的场景
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

  • 优缺点:

从悲观锁适合写操作非常多的场景

乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

  • 实现:

悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

[5] 分段锁
  • 介绍:

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

  • 实现:

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

[6] 偏向锁/轻量级锁/重量级锁
  • 介绍:

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 实现:

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

[7] 自旋锁
  • 介绍:

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。

  • 优缺点

好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU造成ABA问题。

  • 实现:

典型的自旋锁实现的例子,可以参考自旋锁的实现

[8] 可中断锁/不可中断锁/超时时间

可中断锁:顾名思义,就是可以相应中断的锁。**不会无限制等待下去,是避免死锁的一种方式。**在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

超时时间:中断是被动的打断,而设置超时时间是主动的打断,可以避免死锁。

[9] 显式锁/隐式锁

synchronized加锁是隐式加锁,使用者不会看到其加锁解锁过程锁升级过程·。

ReentrantLock 加锁和解锁是显示的。如果 ReentrantLock 调用lock方法加锁, unlock 方法解锁,否则会造成死锁。

[10] 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

synchronized 是那些不满足条件的线程都在一间休息室等消息

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤

使用要点:

  • await 前需要获得锁

await 执行后,会释放锁,进入 conditionObject 等待

await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

竞争 lock 锁成功后,从 await 后继续执行

[11] AQS

AQS是AbustactQueuedSynchronizer(队列同步器)的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类

AQS中可重写的方法分为独占式与共享式的

可以直接调用的模板方法有

同步器提供的如下3个方法来访问或修改同步状态。 ·getState():获取当前同步状态。 ·setState(int newState):设置当前同步状态。 ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

2.2 实现

2.2.1 同步队列

同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,获取锁失败那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点

使用CAS将节点插入到尾部,并用tail指向该结点

2.2.2 独占锁的获取和释放流程

获取

  • 调用入口方法acquire(arg)
  • 调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
  • 将当前线程构造成一个Node节点,并利用addWaiter(Node node) 将其加入到同步队列尾部
  • 调用acquireQueued(Node node,int arg)方法,使得该 节点以“死循环”的方式获取同步状态
  • 自旋时,首先判断其前驱节点为头节点且释放&是否成功获取同步状态,两个条件都成立,则将当前线程的节设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待前驱节点释放唤醒自己,之后继续判断。

释放

  • 调用入口方法release(arg)
  • 调用模版方法tryRelease(arg)释放同步状态
  • 利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第五步)

2.2.3 共享锁的获取和释放流程

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态

获取锁

  • 在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态
  • tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0
  • 可以看到,在doAcquireShared(int arg)方法的自 旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

释放锁

  • 调用releaseShared(arg)模版方法释放同步状态
  • 调用模版方法tryReleaseShard(arg)释放同步状态
  • 如果释放成功,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点
  • 与独占式区别在于线程安全释放,通过循环和CAS保证,因为释放同步状态的操作会同时来自多个线程

2.2.4 独占锁和共享锁在实现上的区别

  • 独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
  • 共享锁的同步状态>1,取值由上层同步组件确定
  • 独占锁队列中头节点运行完成后释放它的直接后继节点
  • 共享锁队列中头节点运行完成后释放它后面的所有节点
  • 共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况

2.2.5 重入锁

重入锁指的是当前线程成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中ReentrantLock和synchronized都是可重入锁,synchronized由JVM偏向锁实现可重入锁,ReentrantLock可重入性基于AQS实现。

重入锁的基本原理是判断上次获取锁的线程是否为当前线程(current == getExclusiveOwnerThread()),如果是则可再次进入临界区,如果不是,则阻塞。

 final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//通过AQS获取同步状态int c = getState();//同步状态为0,说明临界区处于无锁状态,if (c == 0) {//修改同步状态,即加锁if (compareAndSetState(0, acquires)) {//将当前线程设置为锁的ownersetExclusiveOwnerThread(current);return true;}}//如果临界区处于锁定状态,且上次获取锁的线程为当前线程else if (current == getExclusiveOwnerThread()) {//则递增同步状态int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。

成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放 同步状态时减少同步状态值

2.2.6 公平锁和非公平锁

对于非公平锁,只要CAS设置 同步状态成功,则表示当前线程获取了锁,而公平锁则不同

 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;}

该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

2.2.7 读写锁

Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁

写锁的获取与释放 写锁是一个独占锁,所以我们看一下ReentrantReadWriteLock中tryAcquire(arg)的实现:

     protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();int w = exclusiveCount(c);if (c != 0) {if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}

上述代码的处理流程已经非常清晰:

  • 获取同步状态,并从中分离出低16为的写锁状态
  • 如果同步状态不为0,说明存在读锁或写锁
  • 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
  • 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
  • 如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入) 将当前线程设置为写锁的获取线程

写锁的释放过程与独占锁基本相同:

     protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;if (free)setExclusiveOwnerThread(null);setState(nextc);return free;}

在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。

读锁的获取与释放

读锁是一个共享锁,获取读锁的步骤如下:

  • 获取当前同步状态
  • 计算高16为读锁状态+1后的值
  • 如果大于能够获取到的读锁的最大值,则抛出异常
  • 如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
  • 如果上述判断都通过,则利用CAS重新设置读锁的同步状态

读锁的释放步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。

三、使用AQS与Lock自定义一个锁

 class Mutex implements Lock {    // 静态内部类,自定义同步器    private static class Sync extends AbstractQueuedSynchronizer {            // 是否处于占用状态            protected boolean isHeldExclusively() {                    return getState() == 1;            }            // 当状态为0的时候获取锁            public boolean tryAcquire(int acquires) {                    if (compareAndSetState(0, 1)) {   setExclusiveOwnerThread(Thread.currentThread()); return true;                    }                    return false;            }            // 释放锁,将状态设置为0            protected boolean tryRelease(int releases) {                    if (getState() == 0) throw new IllegalMonitorStateException();            setExclusiveOwnerThread(null);                    setState(0);                    return true;            }            // 返回一个Condition,每个condition都包含了一个condition队列            Condition newCondition() { return new ConditionObject(); }    }    // 仅需要将操作代理到Sync上即可    private final Sync sync = new Sync();public void lock() { sync.acquire(1); }public boolean tryLock() { return sync.tryAcquire(1); }public void unlock() { sync.release(1); }    public Condition newCondition() { return sync.newCondition(); }public boolean isLocked() { return sync.isHeldExclusively(); }public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }public void lockInterruptibly() throws InterruptedException {            sync.acquireInterruptibly(1);    }public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{ return sync.tryAcquireNanos(1, unit.toNanos(timeout));    } }

流程:

  • 这个自定义类Mutex首先实现了Lock接口,
  • 内部静态类Sync继承了AQS抽象类,并重写了独占式的tryAcquire和tryRelease方法,
  • 接着Mutex实例化Sync内部类,
  • Mutex类重写Lock接口的方法,如lock、tryLock、unlock等方法,具体实现是通过调用Sync类中的重写的方法(tryAcquire)以及模板方法(acquire)等
  • 用户使用Mutex时调用Mutex提供的方法,在Mutex的实现中,调用同步器的模板方法acquire(int args)

【<锁的属性>】

[1] 为什么要加锁之临界区

多个线程访问共享资源时,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

[2] 实现一个锁需要考虑哪些方面?

实现一个锁,主要需要考虑2个问题

  1. 如何线程安全的修改锁状态位?
  2. 得不到锁的线程,如何排队?

【<锁升级>】

[1] Java对象头

我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在上面中我们知道了,synchronized 用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?

在64位的虚拟机中:


在32位的虚拟机中:

https://stackoverflow.com/questions/26357186/what-is-in-java-object-header

[2] Mark word 结构

默认(Normal):hashcode(地址码);age(分代中的年龄);biased—lock(是不是偏向锁),01(加锁状态:表示没有和任何monitor关联)

重量级锁(Normal):ptr_to_heavyweight_moniter(指向锁的地址),10(加锁状态:已经与monitor关联)轻轻量级锁(Normal):ptr_to_lock_record(锁记录的地址),00(加锁状态:轻量级锁)

锁状态 存储内容 标志位
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 11

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1RfvPQD-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714201707252.png)]

[3] Monitor

Monitor 被翻译为监视器管程每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJLqTIub-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714200132363.png)]

  • 刚开始 Monitor 中 Owner 为 null

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入

EntryList BLOCKED (Monitor中只能有一个 Owner)

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲

wait-notify 时会分析

https://www.jianshu.com/p/c3313dcf2c23

① owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
② _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
③ _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
④ _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中

synchronized 必须是进入同一个对象的 monitor 才有上述的效果

不加 synchronized 的对象不会关联监视器,不遵从以上规则

[4] synchronized锁升级
无锁

不通过阻塞的方式来访问并修改资源。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功,CAS就是一个无锁的形式。

偏向锁(可关闭)

偏向锁是指一段同步代码仅仅被一个线程所访问时,那么会给对象加偏向锁。简单来说:第一次使用时,使用CAS将线程ID存储到mark word上,之后测试这个锁是自己的就不需要再竞争了。

加锁

  1. 访问Mark Word中偏向锁的标识,如果锁标志位(biased_lock)是为0,则说明无锁。将对象头的markword当前线程ID,并执行同步代码块。
  2. 如果锁标志位是否为01,为可偏向状态,则测试将Mark Word中线程ID是否指向当前线程 。
    • 如果是,执行同步代码;
    • 如果否,则通过CAS操作竞争偏向锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码块,如果竞争失败,则说明有其他线程在使用,执行撤销操作。

撤销:有竞争时进行撤销,一旦有了竞争就升级为轻量级锁,他会当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,撤销偏向锁的时候会导致stop the word操作。

关闭:有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7a2WeJRU-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715094211946.png)]

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

加锁:这里涉及到一个锁记录的概念,线程在执行同步代码块之前,每个的栈桢都新建一个锁记录的结构,提前将对象的markword复制到锁记录中,官方称为displaced mark word。然后尝试使用CAS(displaced mark word==markword)将对象头的Markword替换为指向锁记录的指针。

  • 如果成功,则执行代码块
  • 如果失败,则使用CAS自旋操作来获取锁

解锁:使用原子操作的CAS将displaced mark word替换回对象头

  • 如果成功,则表示没有竞争发生

    如果失败,则表明当前存在竞争,则升级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VaY9x140-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715100154850.png)]

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。详细见monitor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zy60VhMh-1596599154321)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715100210104.png)]

[6] synchronized锁对比
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢

【CAS】

[1] 什么是CAS?

**CAS,英文全称compare-and-swap,即比较并交换,是一种乐观锁的思想。**CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

工作内存首先会读取当前内存中将要修改的值,即预期值。然后计算结果值,在修改结果值之前将当前工作内存中的值与预期值对比,如果相等则修改并返回ture,不相等,返回false。(自旋状态失败下:从读取预期值开始重复上述步骤。)

cas里面比较E和当前新值相等后,在修改前有被其他线程修改了怎么办? 即这里怎么保证这两步之间的原子性的?

有个lock指令(lock cmpxchg)保证在CPU操作一个格子时其他CPU不能操作它,再底层是lock指令后时候锁定一个北桥电信号,并非总线指令。

https://www.cnblogs.com/yungyu16/p/13200626.html

[2] 什么是自旋锁?

cas是一种乐观锁机制,cas可以不用自旋机制,失败也可以直接返回false。只是一般应用场景下,cas都会带有重试机制(while和for实现空转,不断尝试)。

 //CAS模拟实现public class SimulatedCAS {private int value;public synchronized int get() {return value;}public synchronized int compareAndSwap(int expectedValue, int newValue) {int oldValue = value;if (oldValue == expectedValue) {value = newValue;}return oldValue;}public synchronized boolean compareAndSet(int expectedValue, int newValue) {return (expectedValue == compareAndSwap(expectedValue, newValue));}}
[3] CAS可能出现的问题有什么?
  • ABA 问题:在读取预期值到将其与现在的值比较的在这段时间里,另一个线程将旧的预期值改为其他值,然后又改回 A(即A->B->A)。那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA” 问题。

ABA问题的解决思路就是使用版本号机制,每次更新就把版本号加1,那么原来的A->B->A就会变为A1->B2->A3,根据**版本号(时间戳,布尔值)**二次确认值是否被修改过。

JDK 1.5 以后的AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

**ABA可能产生问题,也可能不产生问题。**https://blog.csdn.net/superfjj/article/details/106465175

  • 循环时间长开销大:自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。

如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一:它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二:它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。

  • 只能保证一个共享变量的原子操作:CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效

但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作(比如i=2,j=a,合并一下ij=2a)。

[4] 哪里有用到CAS?
  • ReenterLock内部的AQS
  • 各种Atomic开头的原子类
  • synchronized中轻量级锁的加锁和解锁都用到了 CAS 操作。
[5] CAS 和volatile如何实现无锁并发?

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思。

因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

【<死锁>】

原文链接:https://blog.csdn.net/hd12370/article/details/82814348

[1] 什么是死锁

死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

[2] 产生死锁的原因?

可归结为如下两点:

a. 竞争资源

系统中的资源可以分为两类:

  1. 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
  2. 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
    产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
    产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

b. 进程间推进顺序非法

若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

[3] 死锁的产生必须满足的四个必要条件

死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于Runnable)。死锁的产生必须满足如下四个必要条件:

  • 资源互斥:一个资源每次只能被一个线程使用。即进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放,
  • 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺,即只能由获得该资源的进程自己来释放(只能是主动释放)。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

总结:死锁只能发生在像 synchronized 的同步代码块中,一个资源只能被一个线程占用。而且占用资源的锁不会因为请求其他资源失败而主动释放当前锁,已经持有的锁也不能被其他进程剥夺。另外还需要形成一个循环等待的结构,否则它申请的锁有可能被释放。

[4] 解决死锁的基本方法
  1. 预防死锁----- 确保系统永远不会进入死锁状态

产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。
使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
c、破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。如图所示:

这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率
————————————————
原文链接:https://blog.csdn.net/guaiguaihenguai/article/details/80303835

  1. 避免死锁----- 在使用前进行判断,只允许不会产生死锁的进程申请资源

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。

死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。
两种避免办法:
1、如果一个进程的请求会导致死锁,则不启动该进程
2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。

银行家算法:首先需要定义状态安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

a、银行家算法的相关数据结构
可利用资源向量Available:用于表示系统里边各种资源剩余的数目。由于系统里边拥有的资源通常都是有很多种(假设有m种),所以,我们用一个有m个元素的数组来表示各种资源。数组元素的初始值为系统里边所配置的该类全部可用资源的数目,其数值随着该类资源的分配与回收动态地改变。
最大需求矩阵Max:用于表示各个进程对各种资源的额最大需求量。进程可能会有很多个(假设为n个),那么,我们就可以用一个nxm的矩阵来表示各个进程多各种资源的最大需求量
分配矩阵Allocation:顾名思义,就是用于表示已经分配给各个进程的各种资源的数目。也是一个nxm的矩阵。
需求矩阵Need:用于表示进程仍然需要的资源数目,用一个nxm的矩阵表示。系统可能没法一下就满足了某个进程的最大需求(通常进程对资源的最大需求也是只它在整个运行周期中需要的资源数目,并不是每一个时刻都需要这么多),于是,为了进程的执行能够向前推进,通常,系统会先分配个进程一部分资源保证进程能够执行起来。那么,进程的最大需求减去已经分配给进程的数目,就得到了进程仍然需要的资源数目了。

银行家算法通过对进程需求、占有和系统拥有资源的实时统计,确保系统在分配给进程资源不会造成死锁才会给与分配。
死锁避免的优点:不需要死锁预防中的抢占和重新运行进程,并且比死锁预防的限制要少。
死锁避免的限制
必须事先声明每个进程请求的最大资源量
考虑的进程必须无关的,也就是说,它们执行的顺序必须没有任何同步要求的限制
分配的资源数目必须是固定的。
在占有资源时,进程不能退出
————————————————
原文链接:https://blog.csdn.net/guaiguaihenguai/article/details/80303835

如何避免死锁的发生?

  • **粗锁法:**使用一个粒度粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
  • 锁排序法:(必须回答出来的点)指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作

在多线程条件下,如何避免死锁?

通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。

  • 使用显式锁中的**ReentrantLock.try(long,TimeUnit)**来申请锁。
  1. 检测死锁

首先为每个进程和每个资源指定一个唯一的号码;

然后建立资源分配表和进程等待表

  1. 解除死锁

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

撤消进程可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
————————————————
原文链接:https://blog.csdn.net/Beyond_2016/article/details/81363361

[5] 说下对悲观锁和乐观锁的理解?
  • 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

  • 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

  • 两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

[6] 乐观锁常见的两种实现方式是什么?

乐观锁一般会使用版本号机制或者 CAS 算法实现。

  • 版本号机制

    一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

  • CAS 算法

    即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数:

1、需要读写的内存值 V

2、进行比较的值 A

3、拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

【<线程锁死>】

[1] 什么是线程锁死

线程锁死是另一种常见的线程活性故障,与线程死锁不可以混为一谈。线程锁死的定义如下:

线程锁死是指等待线程由于唤醒其所需的条件永远无法成立,或者其他线程无法唤醒这个线程而一直处于非运行状态(线程并未终止)导致其任务 一直无法进展。

线程死锁和线程锁死的外部表现是一致的,即故障线程一直处于非运行状态使得其所执行的任务没有进展。但是锁死的产生条件和线程死锁不一样,即使产生死锁的4个必要条件都没有发生,线程锁死仍然可能已经发生。

[2] 线程锁死分为哪两种
  • 信号丢失锁死:

信号丢失锁死是因为没有对应的通知线程来将等待线程唤醒,导致等待线程一直处于等待状态。

典型例子是等待线程在执行Object.wait( )/Condition.await( )前没有对保护条件进行判断,而此时保护条件实际上可能已经成立,此后可能并无其他线程更新相应保护条件涉及的共享变量使其成立并通知等待线程,这就使得等待线程一直处于等待状态,从而使其任务一直无法进展。

  • 嵌套监视器锁死:

嵌套监视器锁死是由于嵌套锁导致等待线程永远无法被唤醒的一种故障。

比如一个线程,只释放了内层锁Y.wait(),但是没有释放外层锁X; 但是通知线程必须先获得外层锁X,才可以通过 Y.notifyAll()来唤醒等待线程,这就导致出现了嵌套等待现象。

[3] 活锁

活锁是一种特殊的线程活性故障。当一个线程一直处于运行状态,但是其所执行的任务却没有任何进展称为活锁。比如,一个线程一直在申请其所需要的资源,但是却无法申请成功。

[3] 线程饥饿

线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法运行的情况。线程调度模式有公平调度和非公平调度两种模式。在线程的非公平调度模式下,就可能出现线程饥饿的情况。

[5] 线程活性故障总结
  • 线程饥饿发生时,如果线程处于可运行状态,也就是其一直在申请资源,那么就会转变为活锁
  • 只要存在一个或多个线程因为获取不到其所需的资源而无法进展就是线程饥饿,所以线程死锁其实也算是线程饥饿

【<多线程常用方法>】

[1] start 与 run

https://www.cnblogs.com/agilestyle/p/11421515.html

start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。

run :run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

[2] sleep 与 yield

sleep()

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

[3] sleep 和 wait
  • **sleep方法:**是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
  • wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
[4] Daemon

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

package cn.mycast;import lombok.extern.slf4j.Slf4j;import static cn.itcast.n2.util.Sleeper.sleep;@Slf4j(topic= "c.守护线程")
public class 守护线程 {public static void main(String[] args) {//方法1Thread t1 = new Thread(()-> {sleep(2);log.debug("hello");});t1.setDaemon(true);t1.start();sleep(1);log.debug("运行结束...");}
}
[5] join和yield

**join 方法:**当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行(即调用此方法的线程必须加入)。

yield 方法:该方法使得线程放弃当前分得的 CPU 时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
所以,该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

[6] wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

注意:

  1. 它们都属于 Object 的一部分,而不属于 Thread。
  2. 只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。
  3. 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁
[7] await() signal() signalAll()

await() signal() signalAll()–多线程的协调。java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

[8] InterruptedException

InterruptedException–中断机制,通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。

try {Thread.sleep(2000);System.out.println("Thread run");} catch (InterruptedException e) {e.printStackTrace();}
[9] interrupted()

interrupted()–中断机制,如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

[10] Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。主要有三种 Executor:

  • CachedThreadPool:一个任务创建一个线程;
  • FixedThreadPool:所有任务只能使用固定大小的线程;
  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
[11] CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,修改完毕后,再原子性时候修改共享访问的变量,让它指向新的对象。更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。

【ReentrantLock】

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

ReentrantLock ,re: 可重新 entrant:进入 Lock:锁 ,所以他就是可冲入锁

[1] ReentrantLock简介

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。相对于 synchronized 它具备如下特点:

  • 可重入():是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
  • 可中断:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量:synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
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(); // 确保释放锁,从而避免发生死锁。}}
}
[2] ReentrantLock方法
  • 加锁

    ReentrantLock 类提供了最基本的加锁和解锁方法:

    public void lock();
    
    class Counter {private static int counter = 0;private static ReentrantLock lock = new ReentrantLock();public static int getCounter() {return counter;}public static void increase() {try {lock.lock();counter++;} finally {lock.unlock();}}private Counter() {};
    }
    

    这个方法保证了线程安全,他和 synchronized 关键字实现了相同的效果:

    class Counter {private static int counter = 0;public static int getCounter() {return counter;}public synchronized static void increase() {counter++;}private Counter() {};
    }
    

    显然,synchronized 关键字的实现更为简洁和清晰,同时,如果 ReentrantLock 忘记调用 unlock 方法将会造成死锁,这是必须要注意的一点

    因此,如果仅仅是想要进行上面代码中这样的加锁和解锁,synchronized 还是最好的选择

  • 可重入

    默认可重入

  • 可中断

    public void lockInterruptibly()  // 可中断锁,调用线程 interrupt 方法,则锁方法抛出 InterruptedException
    
  • 可以设置超时时间

    public boolean tryLock(long timeout, TimeUnit unit) // 尝试获取锁,最多等待 timeout 时长
    
  • 可以设置为公平锁

    使用 synchronized 锁是不保证等待的线程获取到锁的顺序的,这就是非公平锁,除了默认的非公平锁构造方法外,ReentrantLock 还提供了一个带有 boolean 参数的构造方法:

    public ReentrantLock(boolean fair);
    

    如果传入参数为 true,则会创建公平锁,所谓的公平锁,就是保证了先进入等待的线程一定先获取到锁

    可以通过 isFair 方法查询 ReentrantLock 对象是否是公平锁:

    public final boolean isFair();
    
  • 支持多个条件变量

​ 通过 newCondition 方法,可以创建出 Condition 对象

​ Condition 接口提供了如下的方法:

void await();                                // 可被中断的等待
boolean await(long time, TimeUnit unit);    // 最多等待 time 时长的可中断等待
long awaitNanos(long nanosTimeout);         // 最多等待 nanosTimeout 毫秒的可中断等待
boolean awaitUntil(Date deadline);          // 等待直到指定时间的可中断等待
void awaitUninterruptibly();                // 不可中断的等待
void signal();                              // 唤醒一个线程
void signalAll();                           // 唤醒所有等待中的线程

上面的五个等待方法中,除了 awaitUninterruptibly 方法,其他四个都可以被 interrupt 方法中断,而 signal 和 signalAll 方法可以中断上述所有等待方法

但是,signal 和 signalAll 方法只能唤醒通过当前 Condition 对象调用过等待方法的线程

基于上述特性,我们可以精准的控制让某个指定的线程被唤醒,而 Object 的 notify、notifyAll 方法的唤醒则是随机的,同一个 ReentrantLock 每次调用 newCondition 方法都将获得不同的 Condition 对象

  • 查询接口
int getHoldCount();      // 获取当前线程持有该锁的次数
boolean isHeldByCurrentThread();    // 判断当前线程是否持有该锁
boolean isLocked();     // 获取锁状态是否为加锁状态
boolean isFair();       // 当前锁是否是公平锁
Thread getOwner();      // 获取持有锁的线程
boolean hasQueuedThreads();         // 判断当前锁是否有线程在等待
boolean hasQueuedThread(Thread thread); // 判断指定线程是否在等待该锁
int getQueueLength();   // 获取正在等待该锁的线程数量
boolean hasWaiters(Condition condition);    // 判断是否有线程等待在该 Condition 对象上
int getWaitQueueLength(Condition condition);    // 获取等待在该 Condition 对象的线程数
[3] ReentrantLock实战

加锁及其解锁:

class Counter {private static int counter = 0;private static ReentrantLock lock = new ReentrantLock();
public static int getCounter() {return counter;
}public static void increase() {try {lock.lock();counter++;} finally {lock.unlock();}
}private Counter() {};}

https://baijiahao.baidu.com/s?id=1648624077736116382&wfr=spider&for=pc

可重入:可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {method1();
}
public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}
}
public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}
}
public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNjXr1no-1596599154323)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080920156.png)]

可打断

ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090log.debug("启动...");try {lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("等锁的过程中被打断");return;}try {log.debug("获得了锁");} finally {lock.unlock();}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {sleep(1);t1.interrupt();log.debug("执行打断");
} finally {lock.unlock();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQHH7OAp-1596599154324)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080945053.png)]

可超时:

ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {log.debug("启动...");if (!lock.tryLock()) {log.debug("获取立刻失败,返回");return;}try {log.debug("获得了锁");} finally {lock.unlock();}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {sleep(2);
} finally {lock.unlock();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECeWKX59-1596599154325)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080851608.png)]

公平锁:

ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start...");lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}
}, "强行插入").start();
lock.unlock()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrPSxPZW-1596599154325)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080827688.png)]

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

synchronized 是那些不满足条件的线程都在一间休息室等消息

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤

使用要点:

await 前需要获得锁

await 执行后,会释放锁,进入 conditionObject 等待

await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

竞争 lock 锁成功后,从 await 后继续执行

static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {new Thread(() -> {try {lock.lock();while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的烟");} finally {lock.unlock();}}).start();new Thread(() -> {try {lock.lock();while (!hasBreakfast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的早餐");} finally {lock.unlock();}}).start();sleep(1);sendBreakfast();sleep(1);sendCigarette();
}
private static void sendCigarette() {lock.lock();try {log.debug("送烟来了");hasCigrette = true;waitCigaretteQueue.signal();} finally {lock.unlock();}
}
private static void sendBreakfast() {lock.lock();try {log.debug("送早餐来了");hasBreakfast = true;waitbreakfastQueue.signal();} finally {lock.unlock();}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8OFFylM4-1596599154326)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080757372.png)]

[4] ReentrantLock原理

如何实现一个锁?

实现一个锁,主要需要考虑2个问题

  1. 如何线程安全的修改锁状态位?
  2. 得不到锁的线程,如何排队?

带着这2个问题,我们看一下JUC中的ReentrantLock是如何做的?

ReentrantLock类的大部分逻辑,都是其均继承自AQS的内部类Sync实现的

如何线程安全的修改锁状态位?

锁状态位的修改主要通过,内部类Sync实现的,们可以发现线程安全的关键在于:volatile变量和CAS原语的配合使用

    public abstract class AbstractQueuedSynchronizer{//锁状态标志位:volatile变量(多线程间通过此变量判断锁的状态)private volatile int state;protected final int getState() {return state;}protected final void setState(int newState) {state = newState;}}abstract static  Sync extends AbstractQueuedSynchronizer {final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//volatile读,确保了锁状态位的内存可见性int c = getState();//锁还没有被其他线程占用if (c == 0) {//此时,如果多个线程同时进入,CAS操作会确保,只有一个线程修改成功if (compareAndSetState(0, acquires)) {//设置当前线程拥有独占访问权setExclusiveOwnerThread(current);return true;}}//当前线程就是拥有独占访问权的线程,即锁重入else if (current == getExclusiveOwnerThread()) {//重入锁计数+1int nextc = c + acquires;if (nextc < 0) //溢出throw new Error("Maximum lock count exceeded");//只有获取锁的线程,才能进入此段代码,因此只需要一个volatile写操作,确保其内存可见性即可setState(nextc);return true;}return false;}//只有获取锁的线程才会执行此方法,因此只需要volatile读写确保内存可见性即可protected final boolean tryRelease(int releases) {//锁计数器-1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//锁计数器为0,说明锁被释放if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}}

得不到锁的线程,如何排队?

JUC中锁的排队策略,是基于CLH队列的变种实现的。因此,我们先看看啥是CLH队列

如上图所示,获取不到锁的线程,会进入队尾,然后自旋,直到其前驱线程释放锁。

这样做的好处:假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,减少了并发冲突。(ZK的分布式锁,为了避免惊群效应,也使用了类似的方式:获取不到锁的线程只监听前一个节点)

为什么说JUC中的实现是基于CLH的“变种”,因为原始CLH队列,一般用于实现自旋锁。而JUC中的实现,获取不到锁的线程,一般会时而阻塞,时而唤醒。

[5] ReentrantLock如何实现可重入锁?

可重入锁的实现原理:

如果当前锁的状态不为0,表示有线程占有该锁。再判断如果当前线程就是占有这个锁的线程,修改当前线程的同步状态值,同步状态加1,这样就实现了可重入。
每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?释放锁会调用unlock方法,内部有一个tryRelease方法,每释放一次锁同步状态减1,只有当同步状态为0时,锁成功被释放,返回true。

static final class NonfairSync extends Sync {// ...// Sync 继承过来的方法, 方便阅读, 放在此处final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入else if (current == getExclusiveOwnerThread()) {// state++int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// Sync 继承过来的方法, 方便阅读, 放在此处protected final boolean tryRelease(int releases) {// state-- int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 支持锁重入, 只有 state 减为 0, 才释放成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
}
[6] ReentrantLock如何实现公平锁和非公平锁?

总结:公平锁和非公平锁**(默认)**只有两处不同:

非公平:

  1. 调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
  2. 调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程

以上两步都属于插队现象,可以提高系统吞吐量

公平:
1.调用lock()方法时,不进行CAS尝试
2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state

上面通过判断该线程是否是队列的头结点,从而保证公平性

public ReentrantLock() {sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

公平锁的 lock 方法:

static final class FairSync extends Sync {final void lock() {acquire(1);}// AbstractQueuedSynchronizer.acquire(int arg)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待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;}
}

非公平锁的 lock 方法:

static final class NonfairSync extends Sync {final void lock() {// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// AbstractQueuedSynchronizer.acquire(int arg)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
}
/*** Performs non-fair tryLock.  tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
[7] 条件变量Condition实现原理?

Condition的作用和Object.wait()和Object.notify()的作用相同,可以使当前线程阻塞和唤醒。只不过condition需要与reentrantlock配合使用,而wait/notify需要与snychronized配合使用。

通过Lock接口(重入锁实现了这一接口)的new Condition()方法可以生成一个与当前重入锁绑定的Condition实例,每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。

condition常用的方法:

  • await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或signalAll()方法时,线程会重新获得锁并继续执行。或者当其他线程被中断时,也能跳出等待。这和Object.wait()很相似。
  • awaitUninterruptibly()与await()方法基本相同,但他并不会在中断过程中响应中断。
  • signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程。这和Object.notifyAll()很类似。

await 流程

public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;// 第一个等待节点private transient Node firstWaiter;// 最后一个等待节点private transient Node lastWaiter;public ConditionObject() { }// ㈠ 添加一个 Node 至等待队列private Node addConditionWaiter() {Node t = lastWaiter;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 创建一个关联当前线程的新 Node, 添加至队列尾部Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;}// 唤醒 - 将没取消的第一个节点转移至 AQS 队列private void doSignal(Node first) {do {// 已经是尾节点了if ( (firstWaiter = first.nextWaiter) == null) {lastWaiter = null;}first.nextWaiter = null;} while (// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢!transferForSignal(first) &&// 队列还有节点(first = firstWaiter) != null);}// 外部类方法, 方便阅读, 放在此处// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功final boolean transferForSignal(Node node) {// 如果状态已经不是 Node.CONDITION, 说明被取消了if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 加入 AQS 队列尾部Node p = enq(node);int ws = p.waitStatus;if (// 上一个节点被取消ws > 0 ||// 上一个节点不能设置状态为 Node.SIGNAL!compareAndSetWaitStatus(p, ws, Node.SIGNAL) ) {// unpark 取消阻塞, 让线程重新同步状态LockSupport.unpark(node.thread);}return true;}// 全部唤醒 - 等待队列的所有节点转移至 AQS 队列
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);}// ㈡private void unlinkCancelledWaiters() {// ...}// 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);}// 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁public final void signalAll() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignalAll(first);}// 不可打断等待 - 直到被唤醒public final void awaitUninterruptibly() {// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁, 见 ㈣int savedState = fullyRelease(node);boolean interrupted = false;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);// 如果被打断, 仅设置打断状态if (Thread.interrupted())interrupted = true;}// 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列if (acquireQueued(node, savedState) || interrupted)selfInterrupt();}北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090// 外部类方法, 方便阅读, 放在此处// ㈣ 因为某线程可能重入,需要将 state 全部释放final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();if (release(savedState)) {failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}}// 打断模式 - 在退出等待时重新设置打断状态private static final int REINTERRUPT = 1;// 打断模式 - 在退出等待时抛出异常private static final int THROW_IE = -1;// 判断打断模式private int checkInterruptWhileWaiting(Node node) {return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;}// ㈤ 应用打断模式private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {if (interruptMode == THROW_IE)throw new InterruptedException();else if (interruptMode == REINTERRUPT)selfInterrupt();}// 等待 - 直到被唤醒或打断public final void await() throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null) unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}// 等待 - 直到被唤醒或打断或超时public final long awaitNanos(long nanosTimeout) throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);// 获得最后期限final long deadline = System.nanoTime() + nanosTimeout;int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// 已超时, 退出等待队列if (nanosTimeout <= 0L) {transferAfterCancelledWait(node);break;}// park 阻塞一定时间, spinForTimeoutThreshold 为 1000 nsif (nanosTimeout >= spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;nanosTimeout = deadline - System.nanoTime();}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null)unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);return deadline - System.nanoTime();}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean awaitUntil(Date deadline) throws InterruptedException {// ...}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean await(long time, TimeUnit unit) throws InterruptedException {// ...}// 工具方法 省略 ...
}
[8] 谈谈 synchronized 和 ReenTrantLock 的区别?
  1. synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)、性能已不是选择标准。

  2. synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API。synchronized 是依赖于 JVM 实现的,JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

  1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  2. 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
  3. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
  4. 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
  5. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。

答:ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。

synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁**。**

【< volatile 关键字专题>】

[1] 谈一下你对 volatile 关键字的理解?

**答:**volatile 关键字是用来保证有序性和可见性的。

  1. 保证了不同线程对该变量操作的内存可见性;
  2. 禁止指令重排序。

我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样做是为了减少流水线阻塞,提高 CPU 的执行效率。这就需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作、有序性实现的是通过插入内存屏障来保证的。

解析:

  • volatile 可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
  • volatile 仅能保证变量写操作的原子性,不能保证读写操作的原子性。
  • volatile可以禁止指令重排序(通过插入内存屏障),典型案例是在单例模式中使用。

volatile变量的开销:

volatile不会导致线程上下文切换,但是其读取变量的成本较高,因为其每次都需要从高速缓存或者主内存中读取,无法直接从寄存器中读取变量。

[2]Volatile如何保证可见性和有序性?

https://blog.csdn.net/duzhe2905/article/details/106038681?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare

https://blog.csdn.net/qq_35590091/article/details/106986536

1. 可见性

主内存与工作内存

java内存模型规定了所有的变量都存储在主内存。每条线程还有自己的工作内存,线程的工作内存中保存了被改线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量传递均需要通过主内存来完成。当多个线程操作的变量涉及到同一个主内存区域,将可能导致各自的工作线程数据不一致,这样就导致变量同步回主内存的时候可能冲突导致数据丢失。
原文链接:https://blog.csdn.net/y124675160/article/details/78310121

volatile修饰的共享变量进行写操作的时候多出一条带lock前缀的指令,lock前缀的指令在多核处理器下会引发两件事情

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完了不知道什么时候写回内存。而对声明了volatile关键字的变量进行写操作,JVM会向处理器发送一条lock前缀的指令,将这个变量所在的缓存行立即写回系统内存。并且为了保证各个处理器的缓存是一致的,实现了缓存一致性协议,各个处理通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,那么下次对这个数据进行操作,就会重新从系统内存中获取最新的值。对应JMM来说就是:

  1. Lock前缀的指令让线程工作内存中的值写回主内存中;
  2. 通过缓存一致性协议,其他线程如果工作内存中存了该共享变量的值,就会失效;
  3. 其他线程会重新从主内存中获取最新的值;

原文链接:https://blog.csdn.net/y124675160/article/details/78310121

2.有序性的

为了性能优化,JVM会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序性问题。

内次屏障分为以下4类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKTKcESW-1596599154331)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200723232539965.png)]
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,下面是基于保守策略的JMM内存平展插入策略。

  • 在每个volatile写操作的前面插入一个StoreStore屏障,可以保证前面普通的写操作已经对任意处理器可见。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障,可以保证前面普通的写操作已经对任意处理器可见。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障,确保前面的数据先于后面的指令写入工作内存。
  • 在每个volatile读操作的后面插入一个LoadStore屏障,确保前面的数据先于后面的指令写入工作内存。

volatile在写操作前后插入了内存屏障后生成的指令序列,示意图如下:

volatile在读操作后面插入了内存屏障后生成的指令序列示意图如下:

[3] volatile在什么情况下可以替代锁?

volatile是一个轻量级的锁,适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量。可以将多个线程共享的一组状态变量合并成一个对象,用一个volatile变量来引用该对象,从而替代锁。

理解volatile和CAS配合使用原理

https://blog.csdn.net/liaoxiaolin520/article/details/93711623

【< synchronized专题>】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsvo1GMZ-1596599154332)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200518105440632.png)]

[1] synchronized 关键字?

synchronized是Java中的一个关键字,通常用于多线程环境下,在同一时刻只允许有一个线程访问共享变量。它会根据情况,将锁升级为不同状态,如偏向锁(可以关掉),轻量级锁(无锁/自旋锁/自适应锁),重量级锁。重量级锁会调用操作系统层面的minotor,这时候获得不到线程的对象会被阻塞在队列中。这也是经常被称为一个重量级锁原因。

另外它可以保证原子性,可见性和有序性。支持可重入,但不可中断(Lock的tryLock方法是可以被中断的)。

内部锁底层实现:

  • 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
  • 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态

https://www.zhihu.com/question/57794716/answer/606126905

[2] synchronized 关键字使用场景
package synchronizedTest;public class SynchronizedTest {// 作用于方法上(或者静态方法上)public synchronized void test(){System.out.println("synchronized test!!!");}// 作用于代码块内public void testBlock(){synchronized (this) {System.out.println("synchronized test!!!");}}
}
//作用于一个类上
class ClassName {public void method() {synchronized(ClassName.class) {// todo}}}
  1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 因为静态方法是属于类的而不属于对象的 。同样的, synchronized修饰的静态方法锁定的是这个类的所有对象,所有类用它都会有锁的效果。
  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
    同步关键字锁的是对象
[3] synchronized 内部字节码指令

synchronized 内部字节码指令可以保证出现异常时正常解锁。

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {synchronized (lock) {counter++;}
}
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: getstatic #2 // <- lock引用 (synchronized开始)3: dup4: astore_1 // lock引用 -> slot 1 <解锁时候用>5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 <c实现的>6: getstatic #3 // <- i9: iconst_1 // 准备常数 110: iadd // +111: putstatic #3 // -> i 14: aload_1 // <- lock引用   <解锁>15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList16: goto 24
//19-23: Exception table还在检测范围内检测异常,6 16 19如果6-16出现异常们就会到19行。19行到最后可以保证在异常发生时正常解锁19: astore_2 // e -> slot 2 20: aload_1 // <- lock引用21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // <- slot 2 (e)    23: athrow // throw e24: returnException table:  from to target type6 16 19 any19 22 19 anyLineNumberTable:line 8: 0line 9: 6line 10: 14line 11: 24LocalVariableTable:Start Length Slot Name Signature0 25 0 args [Ljava/lang/String;StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class "[Ljava/lang/String;", class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4

方法

[4] synchronized如何保证有序性、可见性、原子性?

https://blog.csdn.net/qq_35590091/article/details/106986641

1. 原子性

​ synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。这两个字节码对应的内存模型的操作是lock(上锁)和unlock(解锁)。因为这两个操作之间运行的都是原子的(这个操作保证了变量为一个线程独占的,也就是说只有获得锁的线程才能够操作被锁定的内存区域),所synchronized也具有原子性。

这两个字节码都需要一个对象来作为锁。因此,

1、如果synchronized修饰的是实例方法,则会传入this作为参数,

2、如果修饰的是静态方法,则会传入class类对象作为参数。

3、如果只是一个同步块,那么锁就是括号里配置的对象。

执行monitorenter字节码时,如果这个对象没有被上锁,或者当前线程已经持有了该锁,那么锁的计数器会+1,而在执行monitorexit字节码时,锁的计数器会-1,当计数器为0时,锁被释放。如果获取对象的锁失败,那么该线程会被阻塞等待,直到之前把这个对象上锁的线程释放这个锁为止。

每个对象都有一个monitor(监视器)与之关联,所谓的上锁,就是获得对象的monitor的独占权(因为只用获得monitor才能访问这个对象)。执行monitorenter字节码的时候,线程就会尝试获得monitor的所有权,也就是尝试获得对象的锁。只有获得了monitor,才能进入同步块,或者执行同步方法。独占对象的本质是独占对象的monitor。

2. 可见性

**lock(上锁时)**清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值;

unlock(解锁):这个操作规定,放开对某个变量的锁的之前,需要把这个变量从缓存更新到主内存。

因此它也具有可见性。

3. 有序性

为什么synchronized无法禁止指令重排,却能保证有序性?因为在一个线程内部,他不管怎么指令重排,他都是as if serial的,也就是说单线程即使重排序之后的运行结果和串行运行的结果是一样的,是类似串行的语义。**而当线程运行到同步块时,会加锁,其他线程无法获得锁,也就是说此时同步块内的方法是单线程的,根据as if serial,可以认为他是有序的。**而指令重排序导致线程不安全是多线程运行的时候,不是单线程运行的时候,因此多线程运行时静止指令重排序也可以实现有序性,这就是volatile。

原子性 + 可见性 -> 有序性,即使内部重排序,也不会有影响,可以说是多线程的serif

[5] synchronized 关键字锁升级过程?

锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程。目的是为了提高获得锁和释放锁的效率。

偏向锁:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁。

对于同一时刻只有一个线程访问时,每次进入锁是检查是否是自己的锁,是则执行,不是则升级

轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。

在少量线程访问同步代码快时,使用CAS操作实现无

【烈日炎炎战后端】JAVA多线程(11.2万字)相关推荐

  1. 【烈日炎炎战后端】JAVA虚拟机(3.6万字)

    JVM(36098字) 1. 说一下 JVM 的主要组成部分? 2. 谈谈对运行时数据区的理解? 3. 谈谈对内存泄漏的理解? 4. JMM是什么? 5. 为什么要学习Jvm? 6. 什么是栈帧? 7 ...

  2. 【烈日炎炎战后端】JAVA集合(1.8万字)

    JAVA集合(18186字) [HashMap问答] [1] HashMap是什么? [2] HashMap的底层是怎样的? [3] HashMap的树化及其链表化机制及其原因? [4] HashMa ...

  3. 【烈日炎炎战后端】JAVA基础(3.4万字)

    JAVA基础(34587 字) 1. 如何理解面向对象? 2. Java和C++的区别? 3. Java面向对象的三大特性? 4. Java中重载和重写 5. Java 中的访问修饰符 6. Java ...

  4. 【烈日炎炎战后端】计算机网络(4.2万字)

    计算机网络(42068字) 2. 输入url(网址)之后到显示网页的过程? 3. 什么是沾包?如何处理? [< TCP专题之三次握手四次挥手>] [1] TCP报文的结构 [2] 解释一下 ...

  5. 【烈日炎炎战后端】SpringMVC(0.5万字)

    SpringMVC 1.谈谈你对 MVC 模式的理解? 2.SpringMVC 的工作原理/执行流程? 3.SpringMVC 的核心组件有哪些? 4.SpringMVC 常用的注解有哪些? 5.@R ...

  6. 【烈日炎炎战后端】Elecsticsearch(1.5万字)

    Elecsticsearch 1. Elecsticsearch介绍 2. Elecsticsearch核心概念 3. Elecsticsearch中的倒排索引 4. Elasticsearch分布式 ...

  7. 【烈日炎炎战后端】设计模式(1.1万字)

    设计模式 1. 你知道那些常用的设计模式? 2. 设计模式的六大原则? 3. 如何理解设计模式的六大原则? 4. 设计模式的分类 5. 请手写一下单例模式? 1. 你知道那些常用的设计模式? 1. 单 ...

  8. 【烈日炎炎战后端 】MyBatis(0.4万字)

    MyBatis 1. 谈谈你对 MyBatis 的理解? 2. MyBaits 的优缺点有哪些? 3. MyBatis 与 Hibernate 有哪些不同? 4.MyBatis 中 #{} 和 ${} ...

  9. 【烈日炎炎战后端】Nginx(0.3万字)

    Nginx 1.什么是Nginx 2.为什么要用Nginx 3.为什么Nginx性能这么高 4.Nginx怎么处理请求的 5.什么是正向代理和反向代理 6.使用"反向代理服务器的优点是什么? ...

  10. 【烈日炎炎战后端】MySQL理论(2.8万字)

    MySQL理论 1. 数据库三大范式 2. char 和 varchar 的区别? 3. Mysql的存储引擎以及区别 4. 一条SQL查询是如何执行的? 5. 什么是回表 6. MySQL是如何解决 ...

最新文章

  1. Solution : Cannot add new node – Rule SQL Server Database Services feature state failed.
  2. C语言 FileStreaming buffer
  3. [Ext JS]5.1.1 分组标题的表格(Grouped Header Grid)与使用技巧
  4. Git add 常见用法
  5. 摸鱼一年半,我终于摸出了一篇顶会论文
  6. carrot2中lingo实现总结(一)
  7. VGA线材说明与鉴别详解
  8. Ubuntu20.04开启night夜间模式保护视力
  9. 麒麟子Cocos Creator实用技巧九:棋牌UI全分辨率适配方案
  10. 一文速学-玩转MySQL中INTERVAL关键字和INTERVAL()函数用法讲解
  11. 【技巧】Markdown 交叉引用
  12. ubuntu下git使用Beyond Compare来做diff和merge方法
  13. Linux 下进程相关的常用命令汇总
  14. Android studio新手:实现最新版QQ登陆界面
  15. Java中单引号和双引号的区别
  16. 蓝桥杯算法训练cowboys-dp-递归-python题解
  17. 命令行将U盘文件系统转换成ntfs(转)
  18. JointJS入门实例01-补充第一篇在JOINTJS元素中使用HTML
  19. 开源软件层出不穷 ISV、集成商成为受益者
  20. Python内置函数(32)——input

热门文章

  1. 浅谈数据挖掘中的关联规则挖掘
  2. android layoutinflater 高度,从LayoutInflater.inflate看View的创建过程
  3. 关于联想硬盘保护卡还原卡同传不能连接无法登录的解决方法
  4. java毕业设计小小银动漫网站源码+lw文档+mybatis+系统+mysql数据库+调试
  5. 2021-06-24
  6. 首份2020信创报告出炉,四大巨头市场格局立现(附全文下载)
  7. ubuntu18.04引导界面、登录界面美化
  8. java程序怎么混淆,使用混淆器,保护你的java程序,混淆java
  9. 计算机核心期刊新排名(八大学报)
  10. 深度学习笔记(一)——M-P模型(神经元模型)