多线程笔记

111 阅读29分钟

为什么需要学习和使用多线程

一个简单的例子,在实现一些耗时的任务的时候(例如执行耗时5s的方法5次),如果让单线程去执行,就需要5*5 = 25s ,如果让5个线程去并发执行任务,只需要5秒的时间

基础知识

并行和并发

操作系统中的任务调度器会把CPU时间片分给不同的程序进行使用。

  • 单核情况下,CPU会在线程之间快速切换,微观串行,宏观并行,线程会轮流使用CPU,相当于一个人在一个时刻只能做一件事,这种情况称为并发
  • 多核情况下,CPU会同时执行多个任务,相当于多个人同时在做多件事,这种情况称为并行

Java线程

创建线程的方式

1.new Thread

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
    @Override
    // run 方法内实现了要执行的任务
    public void run() {
        log.debug("hello");
    }
};
t1.setName("t1");
t1.start();
log.debug("running");

2.使用 Runnable 配合 Thread

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

3.FutureTask 配合 Thread

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

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

4.小结

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

栈帧

线程每次执行到一个方法,都会生成一个栈帧,执行完方法后就会释放掉,大致流程如下图所示 image.png

启动线程的方式

1.run()

代码

@Slf4j
public class Test3 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @SneakyThrows
            @Override
            public void run() {
                TimeUnit.SECONDS.sleep(5);
                log.debug(Thread.currentThread().getName());
            }
        };
        t1.run();
        log.debug("do other things ...");
    }
}

运行结果

可以看到需要等待run()方法体里面的内容执行完才可以往下执行,并且执行run()的是main线程

12:51:47.369 [main] DEBUG test.Test3 - main
12:51:47.371 [main] DEBUG test.Test3 - do other things ...

2.start()

代码

@Slf4j
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @SneakyThrows
            @Override
            public void run() {
                TimeUnit.SECONDS.sleep(5);
                log.debug(Thread.currentThread().getName());
            }
        };
        t1.start();
        log.debug("do other things ...");
    }
}

运行结果 可以看到执行run()的是t1线程

12:50:17.239 [main] DEBUG test.Test4 - do other things ...
12:50:22.246 [t1] DEBUG test.Test4 - t1

3.总结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

sleep

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

yield

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

线程优先级

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

代码

public static void main(String[] args) {
    Runnable task1 = () -> {
        int count = 0;
        for (; ; ) {
            System.out.println("---->1 " + count++);
        }
    };
    Runnable task2 = () -> {
        int count = 0;
        for (; ; ) {
            //Thread.yield();
            System.out.println(" ---->2 " + count++);
        }
    };
    Thread t1 = new Thread(task1, "t1");
    Thread t2 = new Thread(task2, "t2");
    t1.setPriority(Thread.MIN_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
}

运行结果 启动代码,稍作等待,然后停止,可以看到线程2的值增长的比线程1的大,证明了优先级是有作用的

  • 第一次运行的结果
---->1 212101
---->1 212102
---->1 212103
---->1 212104
---->1 212105
 ---->2 256924
 ---->2 256925
 ---->2 256926
 ---->2 256927
 ---->2 256928
  • 第二次运行的结果
---->1 108961
---->1 108962
---->1 108963
---->1 108964
---->2 186360
---->2 186361
---->2 186362
---->2 186363
---->2 186364
---->2 186365

join 方法详解

join用于解决需要等待异步线程的返回结果

单线程的join

首先查看这段代码,我们预期的r应该是10,但是实际得到的结果为0。因为主线程不会等待t1线程执行完,就直接输出了r,所以控制台输出的r为0

static int r = 0;

public static void main(String[] args) throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        try {
            sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("结束");
        r = 10;
    });
    t1.start();
    //t1.join();
    log.debug("结果为:{}", r);
    log.debug("结束");
}

运行效果

16:35:14.438 [main] DEBUG test.Test10Join - 开始
16:35:14.478 [Thread-0] DEBUG test.Test10Join - 开始
16:35:14.479 [Thread-0] DEBUG test.Test10Join - 结束
16:35:14.478 [main] DEBUG test.Test10Join - 结果为:0
16:35:14.479 [main] DEBUG test.Test10Join - 结束

那么应该如何修改呢?

这个时候就可以用t1.join();,这个方法会让主线程等待t1线程执行完再往下走,取消掉//t1.join();的注释,运行代码,可以看到已经能够拿到r的值了。通过观察时间也可以观察到确实是主线程会等待t1线程执行完

16:49:53.152 [main] DEBUG test.Test10Join - 开始
16:49:53.193 [Thread-0] DEBUG test.Test10Join - 开始
16:49:53.195 [Thread-0] DEBUG test.Test10Join - 结束
16:49:53.195 [main] DEBUG test.Test10Join - 结果为:10
16:49:53.196 [main] DEBUG test.Test10Join - 结束

多线程的join

对于多线程来说,join的结果是以执行时间最长的线程为主。例如下面的例子,无论是t1.join()在前还是在后,运行的花费的时间都是2s,因为要等待最长时间的那个线程执行完毕

static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

运行结果

17:01:43.026 [main] DEBUG test.Test11Join - r1: 10 r2: 20 cost: 2003

有时效的 join

  • 顾名思义,就是在join的时候会给这个线程一个最长等待时间,如果超过最长等待时间还没等到任务执行完成,那么就会放弃等待这个线程,直接往下执行。
  • 如果在这个最长等待时间之内完成任务就可以正常执行
  • 如果在最长等待时间之内都没有完成任务,正如下面的代码,等待t1执行1500ms,但是t1执行完需要2000ms,所以主线程就不会拿到r1 = 10的值,因此输出的结果为0

代码

static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

运行结果

17:08:31.335 [main] DEBUG test.Test12Join - r1: 0 r2: 0 cost: 1513

interrupt 方法详解

打断 sleep,wait,join 的线程

这几个方法都会让线程进入阻塞状态 打断 sleep 的线程, 会清空打断状态,以 sleep 为例 代码

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t1");
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
    // 打断状态: {}false
    log.info(" 打断状态: {}" , t1.isInterrupted());
}

运行结果

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at test.Test13Interrupt.lambda$main$0(Test13Interrupt.java:14)
	at java.lang.Thread.run(Thread.java:748)
17:21:16.723 [main] INFO test.Test13Interrupt -  打断状态: false

打断正常运行的线程

打断正常运行的线程, 不会清空打断状态

代码

public static void main(String[] args) throws Exception {
    Thread t2 = new Thread(() -> {
        while (true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if (interrupted) {
                System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true
                break;
            }
        }
    }, "t2");
    t2.start();
    Thread.sleep(500);
    t2.interrupt();
}

运行结果

打断状态: {}true

打断park线程

  • 打断 park 线程, 不会清空打断状态

代码

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("park...");
        LockSupport.park();
        log.debug("unPark...");
        log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    t1.interrupt();
}

运行结果

17:33:30.712 [t1] DEBUG test.Test15Interrupt - park...
17:33:31.722 [t1] DEBUG test.Test15Interrupt - unPark...
17:33:31.722 [t1] DEBUG test.Test15Interrupt - 打断状态:true
  • 如果打断标记已经是 true, 则park会失效

可以看到当第一次park被打断之后,LockSupport.park()不再生效,会继续执行下面的输出代码,证明了上面的park会失效

代码

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            log.debug("park...");
            LockSupport.park();
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }
    });
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    t1.interrupt();

}

运行结果

17:36:45.832 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.836 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - park...
17:36:46.837 [Thread-0] DEBUG test.Test16Interrupt - 打断状态:true

Process finished with exit code 0

主线程与守护线程

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

代码

public static void main(String[] args) throws InterruptedException {
    log.debug("开始运行1...");
    Thread t1 = new Thread(() -> {
        log.debug("开始运行2...");
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("运行结束2...");
    }, "daemon");
    // 设置该线程为守护线程
    t1.setDaemon(true);
    t1.start();
    sleep(1000);
    log.debug("运行结束1...");
}

运行结果

17:46:08.213 [main] DEBUG daemon.TestDaemon - 开始运行1...
17:46:08.250 [daemon] DEBUG daemon.TestDaemon - 开始运行2...
17:46:09.256 [main] DEBUG daemon.TestDaemon - 运行结束1...

线程状态

操作系统层面

image.png

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从 【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

Java API层面

根据 Thread.State 枚举,分为六种状态

image.png

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束

通过代码来解析java中线程不同的状态

代码

public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running");
            }
        };

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while (true) {

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        t3.start();


        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (Test01.class) {
                    try {
                        TimeUnit.SECONDS.sleep(100000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {

            @Override
            public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (Test01.class) {
                    try {
                        TimeUnit.SECONDS.sleep(10000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

        Thread.sleep(1000);
        System.out.println("t1.getState() = " + t1.getState());
        System.out.println("t2.getState() = " + t2.getState());
        System.out.println("t3.getState() = " + t3.getState());
        System.out.println("t4.getState() = " + t4.getState());
        System.out.println("t5.getState() = " + t5.getState());
        System.out.println("t6.getState() = " + t6.getState());

    }
}

运行结果

18:07:04.821 [t3] DEBUG state.Test01 - running
t1.getState() = NEW
t2.getState() = RUNNABLE
t3.getState() = TERMINATED
t4.getState() = TIMED_WAITING
t5.getState() = WAITING
t6.getState() = BLOCKED

共享模型之管程

本章内容

  1. 共享问题
  2. synchronized
  3. 线程安全分析
  4. Monitor
  5. wait/notify
  6. 线程状态转换
  7. 活跃性
  8. Lock

共享带来的问题

截取例子

image.png

image.png

image.png

image.png

Java 的体现

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

代码

public class Test {
    static int counter = 0;
    //static修饰,则元素是属于类本身的,不属于对象,与类一起加载一次,只有一个
    static final Object room = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                //synchronized (room) {
                    counter++;
                //}
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                //synchronized (room) {
                    counter--;
                //}
            }
        }, "t2");


        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

运行结果

// 第一次
-586

// 第二次
-1590

// 第三次
369

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析

  • 对于i++
getstatic i // 获取静态变量i的值 
iconst_1 // 准备常量1 
iadd // 自增 
putstatic i // 将修改后的值存入静态变量i
  • 对于i--
getstatic i // 获取静态变量i的值 
iconst_1 // 准备常量1 
isub // 自减 
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image.png

1.如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

image.png

2.但多线程下这 8 行代码可能交错运行:

  • 出现负数的情况:

image.png

  • 出现正数的情况:

image.png

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0; 
static void increment() // 临界区 
{ 
    counter++;
} 

static void decrement() // 临界区
{ 
    counter--; 
}

竞态条件 Race Condition

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

synchronized 解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

对于:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,解决方案如下,加上synchronize,只有获得了room才可以执行counter--。另外一个没有获得room的线程就需要等待

代码

static int counter = 0;
//static修饰,则元素是属于类本身的,不属于对象,与类一起加载一次,只有一个
static final Object room = new Object();

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter++;
            }
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter--;
            }
        }
    }, "t2");


    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter);
}

运行结果

// 每次都是0
0

image.png

你可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

用图来表示

image.png

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

面向对象改进

把需要保护的共享变量放入一个类

class Room {
    int value = 0;

    public void increment() {
        synchronized (this) {
            value++;
        }
    }

    public void decrement() {
        synchronized (this) {
            value--;
        }
    }

    public int get() {
        synchronized (this) {
            return value;
        }
    }
}

然后进行改进

代码

public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}", room.get());
    }
}

运行结果

22:21:17.001 [main] DEBUG syn.Test01 - count: 0

方法上的 synchronized

  • 普通方法上的锁其实锁的是this,也就是当前类的实例对象
class Test{ 
    public synchronized void test() {
    
    } 
} 
// 等价于 
class Test{ 
public void test() { 
        synchronized(this) {

        }
    } 
}
  • 静态方法上的锁的是Class
class Test{ 
    public synchronized static void test() { 
    
    }
} 
// 等价于 
class Test{ 
    public static void test() {
        synchronized(Test.class) { 

        }
    } 
}

不加 synchronized 的方法

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

所谓的“线程八锁”

其实就是考察 synchronized 锁住的是哪个对象

情况1:锁的是普通方法,相当于锁住this,有可能出现12 或者 21

public class TestSyn01 {
    public static void main(String[] args) {
        /*
         * 锁住的是Number类的对象,也就是n1
         */
        Number n1 = new Number();
        new Thread(n1::a).start();
        new Thread(n1::b).start();
    }

}


@Slf4j
class Number {
    public synchronized void a() {
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

情况2:1s1再2 或者2 1s后1

public class TestSyn02 {
    public static void main(String[] args) {
        Number01 n1 = new Number01();
        new Thread(n1::a).start();
        new Thread(n1::b).start();
    }
}

@Slf4j(topic = "c.Number")
class Number01 {
    public synchronized void a() {
        try {
            sleep(1000);
            log.debug("1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void b() {
        log.debug("2");
    }
}

情况3:3 1s1 2 或者3 2 1s1 或者23 1s1

public class TestSyn03 {
    public static void main(String[] args) {
        Number03 n1 = new Number03();
        new Thread(n1::a).start();
        new Thread(n1::b).start();
        new Thread(n1::c).start();
    }
}

@Slf4j
class Number03 {
    public synchronized void a() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public void c() {
        log.debug("3");
    }
}

情况4:2 1s1(锁的不是同一个对象,互不影响)

public class TestSyn04 {
    public static void main(String[] args) {
        Number04 n1 = new Number04();
        Number04 n2 = new Number04();
        new Thread(n2::b).start();
        new Thread(n1::a).start();
    }


}

@Slf4j(topic = "c.Number")
class Number04 {
    public synchronized void a() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

情况5:2 1s1 (锁的不是同一个东西,一个锁的是this,一个是Class)

public class TestSyn05 {
    public static void main(String[] args) {
        Number05 n1 = new Number05();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();

    }
}

@Slf4j(topic = "c.Number")
class Number05 {
    public static synchronized void a() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

情况6:1s1 2 或者 2 1s1(锁的都是Class)

public class TestSyn06 {
    public static void main(String[] args) {
        Number06 n1 = new Number06();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

@Slf4j(topic = "c.Number")
class Number06 {
    public static synchronized void a() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }
}

情况7: 2 1s1 (锁的不是同一个东西)

public class TestSyn07 {

    public static void main(String[] args) {
        Number07 n1 = new Number07();
        Number07 n2 = new Number07();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
    }

}
@Slf4j(topic = "c.Number")
class Number07{
    @SneakyThrows
    public static synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}

情况8: 2 1s 1 或者 1s1 2(锁的是Class)

public class TestSyn08 {
    public static void main(String[] args) {
        Number08 n1 = new Number08();
        Number08 n2 = new Number08();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n2.b();
        }).start();
    }
}

@Slf4j
class Number08 {
    public static synchronized void a() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }
}

Monitor 原理

Monitor 被翻译为监视器管程

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

Monitor 结构如下

image.png

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意 synchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器,不遵从以上规则

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1() {
    int i = 10;
    i++; 
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

如图

image.png

局部变量的引用稍有不同

先看一个成员变量的例子:同时操作一个在外部的list,可能导致出现ArrayIndexOutOfBoundsException,因为可能list没有放元素进去就调用remove

栈帧示意图

image.png

代码

public class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }

    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }

}

运行结果

Exception in thread "Thread0" Exception in thread "Thread1" java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.add(ArrayList.java:459)
	at safe.ThreadUnsafe.method2(ThreadUnsafe.java:34)
	at safe.ThreadUnsafe.method1(ThreadUnsafe.java:27)
	at safe.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:19)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.ArrayList.remove(ArrayList.java:501)
	at safe.ThreadUnsafe.method3(ThreadUnsafe.java:38)
	at safe.ThreadUnsafe.method1(ThreadUnsafe.java:28)
	at safe.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:19)
	at java.lang.Thread.run(Thread.java:748)

对上面的例子进行改进

ArrayList<String> list = new ArrayList<>();放到method1()里面进行初始化,这样无论是哪个线程进来都拥有了属于自己的list,然后list再调用method2method3,同步执行代码就不会报错了,也就是先执行完2再执行3

栈帧示意图

image.png

代码

public class ThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadSafe test = new ThreadSafe();
        //ThreadSafe test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }


    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }

}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

运行结果

// 无论运行多少次,都不会报错

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafeSubClass extends ThreadSafe{ 
@Override 
public void method3(ArrayList list) { 
        new Thread(() -> { 
            list.remove(0);
        }).start(); 
    } 
}

问题所在

正常流程是调用完了add然后再调用remove,但是如果调用的是new Thread可能会导致remove方法在add之前调用,导致出现IndexOutOfBoundsException异常

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable(); 
new Thread(()->{ 
    table.put("key", "value1"); 
}).start(); 

new Thread(()->{ 
    table.put("key", "value2"); 
}).start();
  • 它们的每个方法是原子的

  • 但注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable(); // 线程1,线程2 
if( table.get("key") == null) 
{ 
    table.put("key", value); 
}

通过观察下面这个图,可以看到这个是非线程安全的,最终v1把v2给干掉了

image.png

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

代码

  1. 首先定义一个字符串,调用substring方法,查看方法是如何实现的
String str = "abc";
String substring = str.substring(0, 1);
  1. 首先是做了一些判断,最终调用到new String(value, beginIndex,subLen) 方法
public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}
  1. 进入new String(value, beginIndex,subLen)方法查看
public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}
  1. 方法这么长,但是用于确保线程安全的地方是this.value = Arrays.copyOfRange(value, offset, offset+count),这个方直接拷贝一份原字符串,所以在多线程同时操作String类型的情况下,每个线程拿到的都是新拷贝出来的字符串,所以不会存在线程安全问题

卖票练习

代码

@Slf4j
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        // List<Integer> sellCount = new ArrayList<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    //synchronized
    public int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

运行结果

// 正常情况
11:27:39.134 [main] DEBUG ticket.ExerciseSell - selled count:2000
11:27:39.137 [main] DEBUG ticket.ExerciseSell - remainder count:0

// 出现线程安全问题的情况
11:42:44.842 [main] DEBUG ticket.ExerciseSell - selled count:2004
11:42:44.845 [main] DEBUG ticket.ExerciseSell - remainder count:0

这里采用的是Vector,如果采用ArrayList呢?

运行结果如下,会出现异常

Exception in thread "main" java.lang.NullPointerException
	at ticket.ExerciseSell.lambda$main$2(ExerciseSell.java:39)
	at java.util.stream.ReferencePipeline$4$1.accept(ReferencePipeline.java:210)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.IntPipeline.reduce(IntPipeline.java:456)
	at java.util.stream.IntPipeline.sum(IntPipeline.java:414)
	at ticket.ExerciseSell.main(ExerciseSell.java:39)

尝试分析一下问题出现的原因

对比ArrayListVertoradd方法

  • ArrayList - add
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
  • Vector - add
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

可以看到主要的区别是在方法上面加了synchronized关键字,加在普通的方法上,锁的是this,对应上面的例子来说,thissellCount

ArrayList去操作add可能会出现一个多个线程同时操作elementData[size++] = e,原本size为0,2个线程同时size++然后size就变成了2,然后同时给size 为 2的下标赋值,导致size为1的地方是null,所以抛出NPE

对于上述例子,正确的修改方式应该是改成下面这样:

  • 用Vector代替ArrayList
  • 给方法加上synchronized关键字,实现ticketWindow 的互斥修改
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);
        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        //List<Integer> sellCount = new ArrayList<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }
        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        log.debug("selled count:{}", sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

转账练习

代码

@Slf4j
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void transfer(Account target, int amount) {
        //synchronized (Account.class) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
        //}
    }
}

运行结果

// 结果可能大于2000
12:07:35.540 [main] DEBUG ticket.ExerciseTransfer - total:2782
12:14:11.472 [main] DEBUG ticket.ExerciseTransfer - total:4328
12:14:16.994 [main] DEBUG ticket.ExerciseTransfer - total:22524
// 结果可能小于2000
12:14:29.274 [main] DEBUG ticket.ExerciseTransfer - total:554

问题出现的原因是没有对共享变量同时进行保护,因此只需要加上Account.class的锁即可

小故事

故事角色

  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样, 即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女 晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因 此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是 自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那 么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦 掉,升级为挂书包的方式。

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老 家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老 王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

wait notify

小故事 - 为什么需要 wait

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

image.png

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开, 其它人可以由老王随机安排进屋
  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

image.png

  • 小南于是可以离开休息室,重新进入竞争锁的队列

image.png

API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

代码

@Slf4j
public class Test01 {
    final static Object obj = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行1....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码1....");
            }
        }).start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行2....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码2....");
            }
        }).start();
        // 主线程两秒后执行
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
             //obj.notifyAll(); // 唤醒obj上所有等待线程
        }

    }
}

运行结果

// notify()的运行结果 唤醒的不一定是1,也可能是2,随机事件
12:32:28.817 [Thread-0] DEBUG notify.Test01 - 执行1....
12:32:28.820 [Thread-1] DEBUG notify.Test01 - 执行2....
12:32:30.826 [main] DEBUG notify.Test01 - 唤醒 obj 上其它线程
12:32:30.826 [Thread-0] DEBUG notify.Test01 - 其它代码1....

// notifyAll()的运行结果
12:33:11.321 [Thread-0] DEBUG notify.Test01 - 执行1....
12:33:11.323 [Thread-1] DEBUG notify.Test01 - 执行2....
12:33:13.331 [main] DEBUG notify.Test01 - 唤醒 obj 上其它线程
12:33:13.331 [Thread-1] DEBUG notify.Test01 - 其它代码2....
12:33:13.331 [Thread-0] DEBUG notify.Test01 - 其它代码1....
  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

wait notify 的正确姿势

sleep(long n)wait(long n)的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态是 TIMED_WAITING

step 1

代码

@Slf4j
public class Test02 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        try {
            sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            hasCigarette = true;
            log.debug("烟到了噢!");
        }, "送烟的").start();

    }
}

运行结果

其他人会被小南阻塞住,无法执行任务

14:19:39.085 [小南] DEBUG notify.Test02 - 有烟没?[false]
14:19:39.087 [小南] DEBUG notify.Test02 - 没烟,先歇会!
14:19:40.096 [送烟的] DEBUG notify.Test02 - 烟到了噢!
14:19:41.092 [小南] DEBUG notify.Test02 - 有烟没?[true]
14:19:41.092 [小南] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
14:19:41.092 [其它人] DEBUG notify.Test02 - 可以开始干活了
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

step2

代码

@Slf4j
public class Test03 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(() -> {
            synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }
}

运行结果

14:28:11.949 [小南] DEBUG notify.Test03 - 有烟没?[false]
14:28:11.952 [小南] DEBUG notify.Test03 - 没烟,先歇会!
14:28:13.955 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:15.963 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:17.977 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:19.986 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:21.995 [其它人] DEBUG notify.Test03 - 可以开始干活了
14:28:21.995 [小南] DEBUG notify.Test03 - 有烟没?[false]
14:28:21.995 [送烟的] DEBUG notify.Test03 - 烟到了噢!

如果小南只等待2s,但是其他线程也需要等待,但是等送烟的来了之后已经超过2s了,会导致小南的任务无法继续执行

step3

代码

@Slf4j
public class Test04 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();
    }

}

运行结果

14:32:16.904 [小南] DEBUG notify.Test04 - 有烟没?[false]
14:32:16.907 [小南] DEBUG notify.Test04 - 没烟,先歇会!
14:32:16.908 [小女] DEBUG notify.Test04 - 外卖送到没?[false]
14:32:16.908 [小女] DEBUG notify.Test04 - 没外卖,先歇会!
14:32:17.908 [送外卖的] DEBUG notify.Test04 - 外卖到了噢!
14:32:17.908 [小女] DEBUG notify.Test04 - 外卖送到没?[true]
14:32:17.908 [小女] DEBUG notify.Test04 - 可以开始干活了
14:32:17.908 [小南] DEBUG notify.Test04 - 没烟,先歇会!

Park & Unpark

基本用法

// 暂停当前线程 
LockSupport.park(); 

// 恢复某个线程的运行 
LockSupport.unpark(暂停线程对象)

情况1

@Slf4j
public class Test01 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        },"t1");
        t1.start();
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

运行结果

14:35:47.340 [t1] DEBUG park.Test01 - start...
14:35:48.348 [t1] DEBUG park.Test01 - park...
14:35:49.344 [main] DEBUG park.Test01 - unpark...
14:35:49.344 [t1] DEBUG park.Test01 - resume...

47s park住,然后等了2s之后 执行unpark,t1才可以往下执行

情况2

@Slf4j
public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();
        sleep(1000);
        log.debug("unpark...");
        LockSupport.unpark(t1);

    }
}

运行结果

14:45:10.847 [t1] DEBUG park.Test02 - start...
14:45:11.857 [main] DEBUG park.Test02 - unpark...
14:45:12.863 [t1] DEBUG park.Test02 - park...
14:45:12.863 [t1] DEBUG park.Test02 - resume...

问题:为什么调用了park会马上resume呢?

可以引入干粮这个概念

  • 干粮默认为0
  • 调用unpark会让干粮+1
  • 调用park会检查干粮并且让干粮-1,如果干粮为1就继续执行,否则就park住

多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低 解决方法是准备多个房间(多个对象锁)

例如

@Slf4j
public class Test01 {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }
}
@Slf4j
class BigRoom {
    @SneakyThrows
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }
    @SneakyThrows
    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);

        }
    }
}

运行结果

22:44:11.239 [小南] DEBUG lock.BigRoom - study 1 小时
22:44:12.249 [小女] DEBUG lock.BigRoom - sleeping 2 小时

在只有一把锁的情况下,需要灯带stydu结束之后才可以进行下一个任务

改进

public class Test02 {
    public static void main(String[] args) {
        BigRoom02 bigRoom = new BigRoom02();
        new Thread(() -> {
            bigRoom.sleep();
        }, "小南").start();
        new Thread(() -> {
            bigRoom.study();
        }, "小女").start();

    }
}

@Slf4j
class BigRoom02 {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();

    @SneakyThrows
    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            TimeUnit.SECONDS.sleep(2);
        }
    }

    @SneakyThrows
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

运行结果

22:46:05.646 [小南] DEBUG lock.BigRoom02 - sleeping 2 小时
22:46:05.646 [小女] DEBUG lock.BigRoom02 - study 1 小时

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1线程获得A对象锁,接下来想获取B对象的锁

t2线程获得B对象锁,接下来想获取A对象的锁

例子如下:

代码

@Slf4j
public class Test01 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();

    }
}

运行结果

22:52:21.896 [t1] DEBUG multiple_lock.Test01 - lock A
22:52:21.896 [t2] DEBUG multiple_lock.Test01 - lock B

检测死锁的工具

jconsole

可以在CMD输入jconsole,然后选择正在执行的类,点击连接

image.png

然后点击线程,点击检测死锁

image.png

然后就可以看到发生死锁的线程详情了

image.png

jps

image.png

image.png

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

image.png

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待