一、Synchronized同步锁回顾
1. 锁介绍
分类:
-
乐观锁:以其他的方式实现了线程安全,实际无锁的操作。
-
悲观锁:真正意义的锁。
在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。
Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
2. synchronized介绍
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)。加锁范围中的代码出现异常,自动解锁。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区(工作内存|高速缓存)。
-
synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性,一个线程的操作一旦开始,就不会被其他线程干扰,只能当前线程执行完,其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
-
修饰实例方法,非静态方法(对象锁) 需要在类实例化后,再进行调用。
-
修饰静态方法(类锁)静态方法属于类级别的方法,静态方法可以类不实例化就使用。
-
修饰代码块(对象锁、类锁)。
-
3. 修饰实例方法
锁类型:使用synchronized修饰实例方法时为对象锁。
锁范围:锁的范围是加锁的方法。
锁生效:必须为同一个对象调用该方法该锁才有作用。
public class Demo08 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//5个线程使用的为同一个对象,该对象锁生效
Demo06 demo06 = new Demo06();
System.out.println(demo06);
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
demo06.test();
//输出当前执行线程的名字
System.out.println(Thread.currentThread().getName());
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//实例方法加锁,锁的范围是加锁的方法,必须为同一个对象调用该方法该锁才会生效
public synchronized void test(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
错误情况:
public class Test02 {
static int a = 0;
public static void main(String[] args) {
//这样才是正确的
/*Test02 test02 = new Test02();*/
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
/* 每一个线程都会创建一个新对象,不同对象调用加锁的方法,锁失效 */
//五个线程会创建五个对象,相当于五个锁对象,所以锁不会生效
Test02 test02 = new Test02();
//五个子线程都调用test()方法
test02.test();
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}
//实例方法加锁,锁的范围是加锁的方法,必须为同一个对象调用该方法该锁才会生效
public synchronized void test(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
4. 修饰静态方法
锁类型:使用synchronized修饰静态方法时为类锁。
锁范围:锁的范围是加锁的方法。
锁生效:该类所有的对象调用加锁方法,锁都生效 。
public class Test01 {
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
/* 方式一 */
/*Test01.test();*/
/* 方式二 */
//不管创建几个对象,使用的都是类锁,锁都生效
Test01 test01 = new Test01();
test01.test();
//输出当前执行线程的名字
System.out.println(Thread.currentThread().getName());
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Test01.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//静态方法加锁,锁的范围是加锁的方法,该类所有的对象调用加锁方法,锁都生效
public synchronized static void test() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
5. 修饰代码块
语法:
synchronized(锁){
// 内容
}
锁代码块是非常重要的地方。添加锁的类型是Object类型。
运行过程:
多线程执行时,每个线程执行到这个代码块时首先会判断是否有其他线程持有这个锁,如果没有,执行synchronized代码块。如果已经有其他线程持有锁,必须等待线程释放锁。当一个线程执行完成synchronized代码块时会自动释放所持有的锁。
5.1 锁为固定值
当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待。
由于值固定不变,所有的对象调用加锁的代码块,都会争夺锁资源,属于类锁。
public class Test01 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
Test01 test01 = new Test01();
System.out.println(test01);
test01.test();
}
}).start();
}
try {
Thread.sleep(3000);
System.out.println(Test01.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有
public void test(){
synchronized ("随意的名字"){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
5.2 锁为不同内容
每个线程中的synchronized锁不相同时,相当于没有加锁。
因为没有需要竞争锁的线程,线程执行到synchronized时,直接获取锁,进入到代码块。
public class Test01 {
static int a = 0;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Test01 test01 = new Test01();
System.out.println(test01);
test01.test(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000);
System.out.println(Test01.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有
public void test(String name){
synchronized (name){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
5.3 锁为this
当锁为this时,需要看线程中是否为同一个对象调用的包含synchronized所在的方法。这种写法也是比较常见的写法。
5.3.1 同一个对象调用加锁方法时:
如果是同一个对象调用synchronized所在方法时,this代表的都是一个对象。this就相当于固定值。所以可以保证结果正确性,属于对象锁。
public class Test01 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
Test01 demo06 = new Test01();
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
demo06.test();
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Test01.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为this时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,this就相当于调用加锁代码块的对象,同一个对象调用,锁生效
//谁调用的test()方法,this就指向谁
synchronized (this){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
5.3.2 不同对象调用加锁方法时:
如果不是同一个对象调用synchronized所在方法时,this所代表的对象就不同。相当于锁为不同内容时,锁失效。
public class Test02 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
Test02 demo06 = new Test02();
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
demo06.test();
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Test02.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为this时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,this就相当于调用加锁代码块的对象,同一个对象调用,锁生效
//谁调用的test()方法,this就指向谁
synchronized (this){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
5.4 锁为class
锁为Class时,是一个标准的类锁,所有的对象调用加锁的代码块都生效。
public class Test01 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
Test01 test01= new Test01();
test01.test();
}
}).start();
}
try {
Thread.sleep(3000);
System.out.println(Test01.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为类时,所有对象执行加锁代码块,锁都生效
synchronized (Test01.class){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
6. 对象锁和类锁(面试题)
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用,锁生效。
当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。
体现在:
多个对象使用时,锁生效,使用类锁。
同一对象使用时,锁生效,使用对象锁。
7. 什么是可重入锁(面试题)
某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。
synchronized为可重入锁。
7.1 代码演示
test1()、test2()中都使用的同一把锁。在执行test1()时没有人持有”锁”,所以进入到test1()中的synchronized,输出test1,调用test2()方法,因为是同一个线程,且synchronized是可重入锁,所以允许继续执行,就会进入到test2(),执行synchronized,输出test2。
反之,如果synchronized是不可重入锁的话,在执行到test2的synchronized时会阻塞。因为test1()中synchronized已经持有锁,且没有释放锁。
public class Test {
public static void main(String[] args) {
Test test = new Test();
new Thread(new Runnable() {
@Override
public void run() {
test.test1();
}
}).start();
}
public void test1(){
synchronized (this){
System.out.println("test1");
test2();
}
}
public void test2(){
synchronized (this){
System.out.println("test2");
}
}
}
7.2 可重入锁底层原理
可重入锁底层原理特别简单,就是计数器。
当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到这个锁时,如果是可重入锁就对持有锁数量再次加1(如果是不可重入锁,发现持有锁为1了,就不允许多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。
二、生命周期图回顾
线程生命周期从新建到死亡共包含五种状态:
新建状态、就绪状态、运行状态、阻塞状态、死亡状态
2.1 新建状态
当实例化Thread对象后,线程就处于新建状态. 这时线程并没有执行。
2.2 就绪状态
只要在代码中启动了线程,就会从新建状态,变为就绪状态。
thread.start();
就绪状态属于一种临时状态。处于就绪状态的线程会去抢占CPU,只要抢占成功就会切换到运行状态,失去了cpu执行权,回到就绪状态。
2.3 运行状态
运行状态就是开始执行线程的功能。具体就是执行run()方法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run()执行开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("run()执行结束");
}
});
thread.start();
在代码执行过程中分为三种情况:
1. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态。
2. 如果调用yield()方法或失去CPU执行权限会切换为就绪状态。
3. 如果run()方法成功执行完成,或出现问题或被停止(中断)会切换为死亡状态。
2.4 阻塞状态
阻塞状态时,线程停止执行。让出CPU资源。
处于阻塞状态的线程需要根据情况进行判断是否转换为就绪状态:
1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态。
2. 如果是因为wait()变为阻塞状态,需要调用notify()或notifyAll()手动切换为就绪状态。
3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态。
4. (已过时)如果是因为suspend()暂停的线程,需要通过resume()激活线程。
2.5 死亡状态
死亡状态即线程执行结束。
三、线程中相关方法回顾
1. stop()介绍(已过时)
1.1 stop()介绍(已过时)
1.2 stop()弃用的原因
stop()本身就是不安全的,强制停止一个线程,可能导致破坏线程内容。导致错误的结果。同时程序还没有任务异常。
推荐使用interrupt()停止一个长时间wait的线程。
stop()太绝对了,什么情况下都能停,并没有任何的提示信息,可能导致混乱结果。
1.3 stop()代码演示
1.3.1 停止并结束阻塞状态的线程
public class StopMethod {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {//就绪状态变为可运行状态时,运行run()方法
while(true){
System.out.println("run()执行开始");
System.out.println("run()执行结束");
}
}
});
//启动线程
thread.start();
try {
Thread.sleep(1000);//主线程阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
//结束处于运行状态的线程
thread.stop();
}
}
2. interrupt()介绍
interrupt()作为stop()的替代方法。可以实现中断线程,抛出异常,程序继续执行。
interrupt()只能中断当前线程状态带有InterruptedException异常的线程,当程序执行过程中,如果被强制中断会出现Interrupted异常。
interrupt() 负责打断处于阻塞状态的线程。防止出现死锁或长时间wait(等待)。
2.1 停止并结束阻塞状态的线程
-
Thread类中的sleep()方法,sleep():不会释放锁。使用时没有锁的要求。
public static native void sleep(long millis) throws InterruptedException;
-
代码实现:
public class SleepMethod {
/*
* interrupt():
* 中断阻塞状态的线程,抛出中断异常,线程继续执行。
* 运行状态不可以中断。
* 注意:
* 该方法只能中断声明了InterruptedException异常状态的线程
* */
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run()开始执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("run()执行结束");
}
});
//启动线程
thread.start();
try {
Thread.sleep(2000);//主线程阻塞状态
//抛出这个InterruptedException这个异常才能关闭
} catch (InterruptedException e) {
e.printStackTrace();
}
//结束处于阻塞状态的线程.打断阻塞中的程序
thread.interrupt();
System.out.println("主方法");
}
}
2.2 停止并结束运行状态的线程
运行状态没有抛出InterruptException 所以不能中断。
public class SleepMethod {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("run()开始执行");
/*try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println("run()执行结束");
}
}
});
//启动线程
thread.start();
try {
Thread.sleep(1000);//主线程阻塞状态
//抛出这个InterruptedException这个异常才能关闭
} catch (InterruptedException e) {
e.printStackTrace();
}
//结束处于阻塞状态的线程.打断阻塞中的程序
thread.interrupt();
System.out.println("主方法");
}
}
3. suspend()和resume()
3.1 suspend()介绍(已过时)
suspend()可以挂起、暂停线程,让线程处于阻塞状态,是一个实例方法,已过时。
挂起时,不会释放锁
3.2 resume()介绍(已过时)
resume()可以让suspend()的线程唤醒,变成就绪状态,已过时。
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("run()开始执行");
System.out.println("run()执行结束");
}
}
});
try {
//启动线程
thread.start();
Thread.sleep(1000);//主线程阻塞状态
thread.suspend();//挂起线程
Thread.sleep(2000);
thread.interrupt();//唤醒线程
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.resume();
//结束处于运行状态的线程
thread.interrupt();
}
3.4 被弃用的原因
死锁:当一个线程持有锁,因为各种原因,不释放锁。其他线程又想拿到这个锁,但拿不到,这时这个锁就称为死锁。
由于这两个已经是过时方法,容易产生死锁,目前已经很少使用了。
官方解释:
已弃用
此方法已被弃用,因为它本质上容易死锁。 如果目标线程在挂起时保护关键系统资源的监视器上持有锁,则在目标线程恢复之前,没有线程可以访问该资源。 如果将恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。 这种死锁通常表现为“冻结”进程。
解释说明:
如果线程A持有锁(假设锁叫做L),对线程A做了suspend,让线程A挂起。在线程A没有resume之前,线程B无论如何也是无法获得锁的,也就出现了死锁。因为suspend时没有释放锁。
3.5 代码演示:死锁
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test1(Thread.currentThread().getName());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test1(Thread.currentThread().getName());
}
});
thread1.start();
Thread.sleep(500); //主线程休眠1秒
//线程挂起 -> 线程阻塞状态
thread1.suspend();
Thread.sleep(500);
thread2.start();
//thread.interrupt(); suspend()方法没有声明InterruptedException,不能被中断,不释放锁
//thread2启动执行需要锁,所以不执行
}
/*
* 调用方法前加锁
* 方法执行结束后解锁
* */
public synchronized static void test1(String name) {
for (int i = 0; i < 100; i++) {
try {
System.out.println(i + " :" +name);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
四、线程通信回顾
1. 什么是线程通信
需要多个线程配合完成一件事情,如何让多个线程能够合理的切换就是线程通信需要考虑的问题,重点在于配合。
2. 生产者消费者模式
生产者和消费者模式为最经典的线程通信案例:
1. 需求:
1. 商品具有库存数。
2. 如果商品的库存满了,可以让用户进行购买/消费这个商品。
3. 如果库存为0,商品需要进货,补充库存,库存满了之后才能继续消费。
2. 实现思路:
1. 提供成员变量,代表库存数。
2. 如果库存为10,执行消费的线程减少该变量的值。
3. 如果库存为0. 执行生产的线程增加该变量的值。
3. 代码实现:
public class Test {
//库存
static int count = 0;
static final String Lock = "锁";
public static void main(String[] args) {
//生产者线程
Thread producter = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Lock) {
while (true) {
if (count == 10){
break;
}
count++;
try {
Thread.sleep(300);
System.out.println("生产了: "+count);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10){
break;
}
}
}
}
}
});
//消费者线程
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Lock) {
while (true) {
if (count == 0){
break;
}
count--;
try {
Thread.sleep(300);
System.out.println("消费了: "+(10-count));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 0){
break;
}
}
}
}
}
});
producter.start();
consumer.start();
}
}
该方式的缺点:
-
因为两个线程一直处于运行状态和就绪状态。
-
两个线程一直在运行和就绪状态之间切换。 抢到锁的就可以执行,而没有抢到锁的线程一直在抢锁,所以对系统性能损耗较大,不推荐使用这种方式。
3. 线程通信的几种方式(面试题)
-
wait()和notify() | notifyAll() 方式
-
join()方式
-
Condition 方式
-
...
4. wait()和notify() | notifyAll()
4.1 介绍
wait() 是Object中的方法。调用wait()后会让线程从运行状态变为阻塞状态。
在Object类中提供了wait()的重载方法 。
wait()方法会让线程变为阻塞,阻塞的同时会释放锁。所以wait()必须要求被等待的线程持有锁,调用wait()后会把锁释放,其他线程竞争获取锁。当其他线程竞争获取到锁以后,如果达到某个条件后可以通过notify()唤醒,如果有多个wait的线程,系统判断唤醒其中一个。如果多个处于wait的线程可以使用notifyAll全部唤醒。唤醒后线程处于就绪状态。
需要注意的是:一个线程唤醒其他线程时,要求当前线程必须持有锁
最简易结论:
1. 使用wait()和notify() | notifyAll()要求必须有锁。
2. wait()、notify()、notifyAll() 都是放入锁的代码中。
3. wait()和notify() | notifyAll() 配合使用。
4.2 代码演示
这种实现方式比while轮询方式优点在于,当执行完自己任务后,当前线程处于阻塞状态,在没有notify之前,一直是阻塞状态,不会去竞争锁。
当其他线程唤醒了wait线程时会立即让自己wait,整体没有过多性能损耗。
public class TestPlus {
//库存
static int count = 0;
static final String Lock = "锁";
public static void main(String[] args) {
//生产者线程
Thread producter = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Lock) {
while (true) {
if (count == 10){
try {
Lock.notify();
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
count++;
try {
Thread.sleep(300);
System.out.println("生产了: "+count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
//消费者线程
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Lock) {
while (true) {
if (count == 0){
Lock.notify();
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
count--;
try {
Thread.sleep(300);
System.out.println("消费了: "+(10-count));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
producter.start();
consumer.start();
}
}
4.3 wait()和sleep()区别(常见面试题)
-
所属类不同
wait(long) 是Object中方法
sleep(long)是Thread的方法
-
唤醒机制不同
wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()
sleep()是到指定时间自动唤醒
-
锁机制不同
wait(long)释放锁
sleep(long)只是让线程休眠,不会释放锁
-
使用位置不同
wait()必须持有对象锁
sleep()可以使用在任意地方
-
方法类型不同
wait()是实例方法
sleep()是静态方法
5. join()
5.1 介绍
join() 把线程加入到另一个线程中。在哪个线程内调用join(),就会把对应的线程加入到当前线程中。
join()后,会让当前线程挂起,变成阻塞状态,直到新加入的线程执行完成。当前线程才会继续执行。
public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread() {
@Override
public void run() {
System.out.println("我是子线程");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
try {
thread1.start();
//thread1线程加入主线程,主线程挂起,thread1执行完主线程继续执行
thread1.join();
System.out.println("我是主线程");
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、 JUC中的locks包
1. locks包介绍
java.util.concurrent.locks:JUC中对锁支持的工具包 。
六、JUC的锁机制
1. AQS
1.1 介绍
AQS全名AbstractQueuedSynchronizer,是并发容器JUC(java.util.concurrent)下locks包内的一个类。
它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
1.2 工作原理
AQS的核心思想为如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是使用队列实现的锁,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个int state成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
AQS使用CAS对该同步状态进行原子操作实现对其值的修改,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁。
2. 锁机制介绍
JUC中锁的底层使用的就是AQS
-
ReentrantLock:Lock接口的实现类,可重入锁。相当于synchronized同步锁。
-
ReentrantReadWriteLock:ReadWriteLock接口的实现类。类中包含两个静态内部类,ReadLock读锁、WriteLock写锁。
-
Condition:是一个接口,都是通过lock.newCondition()实例化。属于wait和notify的替代品。提供了await()、signal()、singnalAll()与之对应。
-
LockSupport:和Thread中suspend()和resume()相似。
3. 锁机制详解
3.1 ReentrantLock重入锁
ReentrantLock是JUC中对重入锁的标准实现。作用相当于synchronized。
加锁和解锁过程都需要由程序员手动控制,使用很灵活。
提供了2种类型的构造方法。
1. ReentrantLock():创建非公平锁的重入锁。
2. ReentrantLock(boolean):创建创建锁。取值为true表示公平锁,取值为false表示非公平锁。
公平锁:多线程操作共一个资源时,严格按照顺序执行。进行排队
非公平锁:多线程在等待时,可以竞争,谁竞争成功,谁获取锁。
非公平锁的效率要高于公平锁。ReentrantLock默认就是非公平锁。
语法:
创建:
ReentrantLock rk = new ReentrantLock();
加锁:
//无返回值 阻塞代码
rk.lock();
//有返回值 不会阻塞代码
boolean b = rk.tryLock()
解锁:
rk.unLock();
注意:
1. ReentrantLock出现异常时,不会自动解锁
2. 多线程的情况下,一个线程出现异常,并没有释放锁,其他线程也获取不到锁,容易出现死锁
3. 建议把解锁方法finally{}代码块中
4. synchronized加锁与释放锁不需要手动的设置,遇到异常时,会自动的解锁
3.1.1 正确情况
public class Test {
static int a = 0;
public static void main(String[] args) {
//true,false是否是公平锁
ReentrantLock rl = new ReentrantLock(false);
for (int i = 0; i < 5; i++) {
new Thread() {
@Override
public void run() {
rl.lock();//加锁
test();
//重入锁
/*rl.lock();
test();
rl.unlock();*/
rl.unlock();//解锁
}
}.start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a);
}
public static void test() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
3.1.2 错误情况
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock rl = new ReentrantLock(false);
new Thread() {
@Override
public void run() {
rl.lock();//加锁
test(Thread.currentThread().getName());
int i = 1/0; //出现异常,不释放锁
rl.unlock();//解锁
}
}.start();
new Thread() {
@Override
public void run() {
rl.lock();//加锁
test(Thread.currentThread().getName());
rl.unlock();//解锁
}
}.start();
}
public static void test(String name) {
System.out.println(name);
}
}
注意:
避免死锁,需要将解锁放到finally{}中
3.2 Condition等待 | 唤醒
wait和notify是针对synchronized的,Condition是针对Lock的
语法:
创建:
ReentrantLock rk = new ReentrantLock();
Condition condition = rk.newCondition();
线程等待:
condition.await();
唤醒一个线程|唤醒所有线程
condition.signal(); //唤醒一个线程
condition.signalAll(); //唤醒所有线程
3.2.1 代码演示
public class Test {
public static void main(String[] args) {
//1.创建重入锁
ReentrantLock rl = new ReentrantLock();
//2.通过锁对象创建唤醒对象
Condition condition = rl.newCondition();
new Thread(){
@Override
public void run() {
//加锁
try {
rl.lock();
System.out.println("白日依山尽");
condition.await();
System.out.println("欲穷千里目");
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rl.unlock();
}
}
}.start();
new Thread(){
@Override
public void run() {
try {
//加锁
rl.lock();
System.out.println("黄河入海流");
condition.signal();
condition.await();
System.out.println("更上一层楼");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rl.unlock();
}
}
}.start();
}
}
3.3 ReadWriteLock读写锁
ReadWriteLock为接口,实现类为ReentrantReadWriteLock
ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁
WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他写的线程等待,避免死锁。
注意:
读写锁,实际含义为是否能有多个线程同时获取
语法:
创建:
ReentrantReadWriteLock rk = new ReentrantReadWriteLock();
读锁:
//获取读锁
ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
//加锁
readLock.lock();
boolean b = readLock.tryLock();
//解锁
readLock.unlock();
写锁:
//获取写锁
ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
//加锁
writeLock.lock();
boolean b = writeLock.tryLock();
//解锁
writeLock.unlock();
3.3.1 读锁(共享锁)演示
public class ReadTest {
public static void main(String[] args) {
ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
new Thread() {
@Override
public void run() {
//加读锁
readLock.lock();
while (true){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//解锁
//这里不会执行,上面有循环
//readLock.unlock();
}
}.start();
new Thread() {
@Override
public void run() {
//加读锁
readLock.lock();
/*while (true){*/
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*}*/
//解锁
readLock.unlock();
}
}.start();
}
}
3.3.2 写锁(独占锁)演示
public class Test {
public static void main(String[] args) {
//获取读写锁对象
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
writeLock.lock();
test(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
writeLock.lock();
test(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}).start();
}
public static void test(String name) {
for (; ; ) {
try {
System.out.println(name);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.4 LockSupport 暂停 | 恢复
LockSupport是Lock中实现线程暂停和线程恢复。suspend()和resume()是synchronized中的暂停和恢复。
注意:暂停恢复不会释放锁,避免死锁问题
语法:
暂停:
LockSupport.park();
恢复:
LockSupport.unpark(t1);
3.4.1 代码演示
public class Test {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("快上车,要车开车了");
//暂停线程
LockSupport.park();
System.out.println("车开了");
}
});
thread.start();
Thread.sleep(5000);
//五秒后唤醒线程
LockSupport.unpark(thread);
}
}
3.5 synchronized和lock的区别(面试题)
-
类型不同
synchronized是关键字。修饰方法,修饰代码块
Lock是接口
-
加锁和解锁机制不同
synchronized是自动加锁和解锁,程序员不需要控制。
Lock必须由程序员控制加锁和解锁过程,解锁时,需要注意出现异常不会自动解锁
-
异常机制
synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。
Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。
-
Lock功能更强大
Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字,所以无法判断。
-
Lock性能更优
如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。
-
线程通信方式不同
synchronized 使用wait()和notify()线程通信。
Lock使用Condition的await()和signal()通信。
-
暂停和恢复方式不同
synchronized 使用suspend()和resume()暂停和恢复,这俩方法过时了。
Lock使用LockSupport中park()和unpark()暂停和恢复,这俩方法没有过时。
七、JUC中的Tools
1. Tools介绍
Tools也是JUC中的工具类,其中包含了CountDownLatch、CyclicBarrier、Semaphore
2. CountDownLatch计数器
在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join() | 主线程休眠实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类。底层基于AQS。
CountDown是计数递减的意思,Latch是门闩的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等Latch数量递减到0的时候,结束阻塞,执行后续操作。
创建:
CountDownLatch cdl= new CountDownLatch(数字);
线程等待:
//当前线程等待,直到到Latch计数到零,或者被interrupt
cdl.await():
计数器递减:
//减少Latch的计数,如果计数达到零,释放等待的线程
cdl.countDown( ):
2.1 代码实现:
public class Test {
int a = 0;
public static void main(String[] args) {
//计数器初始值为5
CountDownLatch countDownLatch = new CountDownLatch(5);
ReentrantLock rl = new ReentrantLock();
Test test = new Test();
/*//创建带有10个核心线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);*/
//用线程池执行5次test任务
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//上锁
rl.lock();
test.test();
//调用countDown()方法,计数器-1
countDownLatch.countDown();
System.out.println(test.a);
//解锁
rl.unlock();
}
}).start();
}
try {
countDownLatch.await(); //计数器值不为0阻塞,为0 恢复执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.a+"===");
}
public void test() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
3. CyclicBarrier回环屏障
CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为0之后,再调用countDown()、await()将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。
await()方法表示当前线程执行时计数器值不为0则等待。如果计数器为0则继续执行。每次await()之后计算器会减少一次。当减少到0下次await从初始值重新递减。
3.1 代码实现
假设多个任务都有三个阶段组成,多个线程分别指向一个任务,必须保证每个任务的一个阶段结束后,才进入下一个阶段。此时使用CyclicBarrier正合适
public class Test {
public static void main(String[] args) {
//回环屏障(相当于两个线程轮流使用这个2,2变成0后会重新变成2,变成0线程继续执行)
CyclicBarrier cb = new CyclicBarrier(2);
//裁判线程
new Thread() {
@Override
public void run() {
try {
System.out.println("1.裁判:比赛开始");
//每次调用await()方法容量都-1
cb.await(); //2-1 = 1 线程阻塞
System.out.println("4.裁判:吹哨");
cb.await(); //1-1 = 0 代码继续执行
System.out.println("5.裁判:出示红牌");
cb.await(); //2-1 = 1 阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}.start();
//球员线程
new Thread() {
@Override
public void run() {
try {
System.out.println("2.球员:开始行动");
cb.await(); // 1-1 = 0 代码继续执行
System.out.println("3.球员:球员犯规");
cb.await(); //2-1 = 1 线程阻塞
System.out.println("6.球员:接受红牌");
cb.await(); //1-1=0 代码继续执行
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}.start();
}
}
4. Semaphore 信号量
CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是可加可减的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。
语法:
创建:
Semaphore sp= new Semaphore(数字);
获取信号量的值:
int i = sp.availablePermits();
增加信号量:
//信号量+1
sp.release();
//信号量+n
sp.release(n);
减少信号量:
sp.acquire(); //信号量-1,无返回值
sp.tryAcquire(); //信号量-1,有返回值
sp.acquire(n); //信号量-n,无返回值
sp.tryAcquire(n); //信号量-n,有返回值
4.1 代码实现:
public class Test {
public static void main(String[] args) throws InterruptedException {
//信号量默认值是10
Semaphore sp = new Semaphore(10);
//availablePermits()获取当前信号量
//带try的有返回值
System.out.println(sp.availablePermits());
sp.release();//信号量+1
System.out.println(sp.availablePermits());
sp.release(10); //信号量+10
System.out.println(sp.availablePermits());
sp.acquire();//信号量-1
System.out.println(sp.availablePermits());
boolean b = sp.tryAcquire(15);
System.out.println(b+"-"+sp.availablePermits());
/*
* acquire(n):信号量不足 <0,阻塞等待
* sp.tryAcquire(n):信号量不足 <0,返回false,继续执行
* */
sp.acquire(5);
}
}
八、并发集合类
1. 介绍
并发集合类:主要是提供线程安全的集合。
比如:
1. ArrayList对应的并发类是CopyOnWriteArrayList
2. HashSet对应的并发类是 CopyOnWriteArraySet
3. HashMap对应的并发类是ConcurrentHashMap
这些类的方法API和之前学习的ArrayList、HashSet、HashMap的API是相同的,所以重在实现原理上,而不是API的使用上。
2. CopyOnWriteArrayList
2.1 ArrayList
ArrayList是最常用的集合之一,大小不固定,可以随着元素的增多可以自动扩容。
储存的数据为有序,可重复. 底层实现是基于数组,线程不安全。
2.2. CopyOnWriteArrayList
使用方式和ArrayList相同, 当时CopyOnWriteArrayList线程为安全的。
写时复制
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
对于读操作远远多于写操作的应用非常适合,特别在并发情况下,可以提供高性能的并发读取。
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
2.3 CopyOnWriteArrayList重点源码
public class CopyOnWriteArrayList<E> implements List<E>,RandomAccess, Cloneable, java.io.Serializable {
//创建不可改变的对象
final transient Object lock = new Object();
//volatile修饰的Object类型的数组, 保证了数组的可见性,有序性
private transient volatile Object[] array;
//获取元素,根据下标获取元素,支持多线程查询
public E get(int index) {
return elementAt(getArray(), index);
}
//设置数组
final void setArray(Object[] a) {
array = a;
}
//添加元素,写时复制
public boolean add(E e) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//获取数组的长度
int len = es.length;
//复制旧数组,长度+1,创建一个新数组
es = Arrays.copyOf(es, len + 1);
//根据下标,将添加的元素放入
es[len] = e;
//将新数组设置为当前的数组
setArray(es);
return true;
}
}
//修改元素
public E set(int index, E element) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//根据传递的下标,获取数组中的元素
E oldValue = elementAt(es, index);
//数组中该下标存储的元素和修改的元素不一致
if (oldValue != element) {
es = es.clone();
//修改元素
es[index] = element;
}
//将新数组设置为当前的数组
setArray(es);
return oldValue;
}
}
//删除元素
public E remove(int index) {
//加锁
synchronized (lock) {
//获取当前数组
Object[] es = getArray();
//获取数组长度
int len = es.length;
//根据传递的下标,获取数组中的元素
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
//最有一个元素
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
}
3. CopyOnWriteArraySet源码分析
3.1 HashSet
HashSet无序,无下标,元素不可重复的集合,线程不安. 底层实现为(HashMap)
3.2 CopyOnWriteArraySet
它是线程安全的HashSet,CopyOnWriteArraySet则是通过"动态数组(CopyOnWriteArrayList)"实现的,并不是散列表
CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的Set,CopyOnWriteArrayList中允许有重复的元素;但CopyOnWriteArraySet是一个Set集合,所以它不能有重复数据。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
3.3 CopyOnWriteArraySet重点源码
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
//声明CopyOnWriteArrayList
private final CopyOnWriteArrayList<E> al;
//无参构造方法
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
//添加元素
public boolean add(E e) {
return al.addIfAbsent(e);
}
private transient volatile Object[] array;
//获取当前的数组
final Object[] getArray() {
return array;
}
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
/*
* &&逻辑与,只要第一个返回false,直接返回false
* 第一个判断,当前添加的元素是否存在:
* 存在返回这个元素的下标
* 不存在返回-1
*
* 第二个判断,如果不存在添加元素
* 添加成功,true
* 添加失败,false
* */
return indexOfRange(e, snapshot, 0, snapshot.length) < 0
&& addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
//加锁
synchronized (lock) {
//获取数组
Object[] current = getArray();
//获取数组长度
int len = current.length;
//数组发生过修改
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
return false;
if (indexOfRange(e, current, common, len) >= 0)
return false;
}
//数组没有发生修改 数组拷贝,原数组长度+1
Object[] newElements = Arrays.copyOf(current, len + 1);
//添加元素
newElements[len] = e;
//新数组替换原数组
setArray(newElements);
return true;
}
}
}
4. ConcurrentHashMap
4.1 HashMap
HashMap也是使用非常多的集合,线程不安全,以key-value的形式存在。
在HashMap中,底层实现为哈希表,系统会根据hash算法来计算key的存储位置,我们可以通过key快速地存、取value,允许一个key-value为null
1. HashMap JDk1.7以及1.7之前
HashMap 底层是基于数组+链表
组成的
头插
2. HashMap JDk1.8以及1.8之后
HashMap 底层是基于 数组+链表+红黑树
组成的,当 Hash 冲突严重时,在数组上形成的链表会变的越来越长,这样在查询时的效率就会越来越低,达到一定的条件,就会由链表转换为红黑树,提高查询的效率
尾插
4.2 HashTable
HashTable和HashMap的实现原理几乎一样,差别无非是
1. HashTable不允许key和value为null
2. HashTable是线程安全的,但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时,只要有一个线程访问操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差
4.3 ConcurrentHashMap1.7及之前
ConcurrentHashMap采用了非常精妙的"分段锁"策略。
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。
4.4 ConcurrentHashMap1.8及之后
ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用synchronized + CAS,如果没有出现hash冲突,使用CAS直接添加数据,只有出现hash冲突的时候才会使用同步锁添加数据,又提升了效率,它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
4.5 ConcurrentHashMap1.8及之后重点源码
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key-value都为空, 抛出异常
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
int hash = spread(key.hashCode());
/*
* 使用链表保存时,binCount记录结点数;
*/
int binCount = 0;
//循环遍数组
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//判断当前桶是否为空,空的就需要初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//计算 key 的 hash 值,通过(n - 1) & hash计算key存放的位置, 存储的位置为空,使用cas直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//发现是ForwardingNode结点,说明此时table正在扩容,则尝试协助数据迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else { //出现hash冲突,也就是table[i]桶中已经曾经添加了Node节点,加锁,添加数据
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 出现hash冲突,就会找到“相等”的结点,判断是否需要更新value值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
//插入数据
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
//如果当前桶为红黑树,那就要按照红黑树的方式写入数据
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 如果链表中节点个数达到阈值,数组长度大于64,链表转化为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 计数值加1
addCount(1L, binCount);
return null;
}