菜鸟玩多线程同步↓↓↓
微信图片_20210529212042.gif

老鸟玩多线程同步↓↓↓
微信图片_20210529212049.gif

1. JAVA创建线程的几种方式

创建线程有这么几种方式:

  • 继承Thread类
  • 实现 Runnable 接口
  • 使用线程池创建
  • 实现 Callable 接口

下面详细讲:

  1. 继承Thread

    1. public class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. //TODO
    5. }
    6. public static void main(String[] args) {
    7. MyThread myThread = new MyThread();
    8. myThread.start();
    9. }
    10. }
  2. 实现 Runnable 接口

    1. public class MyThread{
    2. //实现Runnable接口
    3. private static class MyRunnable implements Runnable{
    4. @Override
    5. public void run() {
    6. //TODO
    7. }
    8. }
    9. public static void main(String[] args) {
    10. MyRunnable myRunnable = new MyRunnable();
    11. Thread thread = new Thread(myRunnable);
    12. thread.start();
    13. }
    14. }
  3. 使用线程池创建

    1. Callable+线程池+Future 方式 ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

public class MyCreateThread1 { //实现Runnable接口 private static class MyRunnable implements Runnable{ @Override public void run() { //TODO } }

  1. public static void main(String[] args) {
  2. MyRunnable myRunnable = new MyRunnable();
  3. ExecutorService executorService = Executors.newCachedThreadPool();
  4. executorService.execute(myRunnable);
  5. }

}

  1. 4. 实现 Callable 接口
  2. 1. Callable+线程池+Future 方式
  3. ```java
  4. import java.util.concurrent.*;
  5. public class MyCreateThread2 {
  6. //实现 Callable 接口
  7. private static class MyCallable implements Callable<String> {
  8. @Override
  9. public String call() throws Exception {
  10. return "success";
  11. }
  12. }
  13. public static void main(String[] args) throws ExecutionException, InterruptedException {
  14. MyCallable myCallable = new MyCallable();
  15. ExecutorService executorService = Executors.newCachedThreadPool();
  16. Future<String> future = executorService.submit(myCallable);
  17. System.out.println(future.get());
  18. }
  19. }

b. Callable+Thread+FutureTask方式

  1. import java.util.concurrent.*;
  2. public class MyCreateThread3 {
  3. //实现 Callable 接口
  4. private static class MyCallable implements Callable<String> {
  5. @Override
  6. public String call() throws Exception {
  7. return "success";
  8. }
  9. }
  10. public static void main(String[] args) throws ExecutionException, InterruptedException {
  11. MyCallable myCallable = new MyCallable();
  12. FutureTask futureTask = new FutureTask<>(myCallable);
  13. Thread thread = new Thread(futureTask);
  14. thread.start();
  15. System.out.println(futureTask.get());
  16. }
  17. }

**面试高频** -> 进程和线程的区别
一般答案:进程就是一个程序运行起来的状态,线程是一个进程中的不同的执行路径。
专业角答案:进程是OS分配资源的基本单位,线程是执行调度的基本单位。分配资源最重要的是:独立的内存空间,线程调度执行(线程共享进程的内存空间,没有自己独立的内存空间)

2. 线程的声明周期

image.png

线程的状态:

  1. NEW: 程序刚刚创建还没启动的状态。
  2. READY:线程已准备好的状态,等待系统的线程调度器执行。
  3. RUNNING:线程正在运行的状态。
  4. WAITING:等待被唤醒的状态
  5. TIME WAITING:隔一段时间之后唤醒
  6. BLOCKED: 被阻塞的状态,正在等待锁。
  7. TERMINATED:线程结束状态。

3. 线程的interrupt

  1. **interrupt()**

设置打断标志(不要被名字骗了),这个方法的作用是设置线程的的打断标志。如果线程处于wait、sleep的状态时, 会走异常处理。如果异常处理了。则打断状态标记会被复位。

  1. import java.util.concurrent.TimeUnit;
  2. /**
  3. * @Author:壹心科技BCF项目组 wangfan
  4. * @Date:Created in 2021/1/20 下午3:32
  5. * @Project:epec
  6. * @Description:TODO
  7. * @Modified By:wangfan
  8. * @Version: V1.0
  9. */
  10. public class TestThreadInterrupt {
  11. public static void main(String[] args) throws InterruptedException {
  12. Thread thread = new Thread(() -> {
  13. try {
  14. TimeUnit.SECONDS.sleep(3);
  15. } catch (InterruptedException e) {
  16. System.out.println("2、线程被打断..");
  17. e.printStackTrace();
  18. }
  19. },"测试线程");
  20. thread.start(); //启动线程
  21. //查看线程的打断状态并打印
  22. boolean interrupted = thread.isInterrupted();
  23. System.out.println("1、"+thread.getName()+"的打断状态为:"+interrupted);
  24. //睡眠1秒后设置打断状态
  25. TimeUnit.SECONDS.sleep(1);
  26. thread.interrupt();
  27. //睡眠1秒后查看线程的打断状态并打印
  28. TimeUnit.SECONDS.sleep(1);
  29. interrupted = thread.isInterrupted();
  30. System.out.println("3、"+thread.getName()+"的打断状态为:"+interrupted);
  31. }
  32. }

结果:

1、测试线程的打断状态为:false 2、线程被打断.. java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at com.mashibing.juc.c_000.TestThreadInterrupt.lambda$main$0(TestThreadInterrupt.java:19) at java.lang.Thread.run(Thread.java:748) 3、测试线程的打断状态为:false

  1. **isInterrupt()**

查看线程是否设置了打断状态

  1. **static interrupted()**

查看当前线程是否被打断,这里需要注意的是,**当前线程!** 。请深刻理解什么是当前线程。

  1. public class TestThreadInterrupt {
  2. public static void main(String[] args) throws InterruptedException {
  3. System.out.println(Thread.interrupted());
  4. }
  5. }

结果:

false

4. 如何优雅的结束一个正在运行的线程

  1. 采用volatile类型信号量中断 ```java import java.util.concurrent.TimeUnit;

/**

  • 采用volatile类型信号量中断。 */ public class HowStopThread {

    /**

    • 实现Runnable */ private static class MyRunnable implements Runnable{ public static volatile boolean running = true; @Override public void run() {

      1. while (running){
      2. }
      3. System.out.println("线程中断");

      } }

      public static void main(String[] args) throws InterruptedException { new Thread(new MyRunnable()).start(); TimeUnit.SECONDS.sleep(1); MyRunnable.running = false; } } ```

  1. 使用interrupt中断线程 ```java import java.util.concurrent.TimeUnit;

/**

  • 使用interrupt中断线程 */ public class HowStopThread {

    public static void main(String[] args) throws InterruptedException {

    1. Thread thread = new Thread(()->{
    2. while (!Thread.interrupted()){
    3. }
    4. System.out.println("线程结束");
    5. });
    6. thread.start();
    7. TimeUnit.SECONDS.sleep(1);
    8. thread.interrupt();

    } } ```

5. 并发编程3大特性

编发编程的3大特性分别是:

  • **可见性**(visibility)
  • **有序性**(ordering)
  • **原子性**(atomicity)

    5.1 可见性

    5.2 什么是线程间不可见?

    java内存模型?(Java Memory Model,简称JMM)规定每个线程有独立的工作内存,他们的工作方式是从主内存将变量读取到自己的工作内存,然后在工作内存中进行逻辑或者自述运算再把变量写回到主内存中。正常情况下各线程的工作内存之间是相互隔离的、不可见的。
  1. 所有的变量都存储在主内存中
  2. 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  3. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  4. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递需要通过主内存来完成

2. 线程 - 图4

  1. //线程间不可见的证明
  2. public class ThreadVisibilityProblem {
  3. //是否运行
  4. boolean running = true;
  5. // volatile boolean running = true;
  6. //开始
  7. void run(){
  8. System.out.println("running");
  9. while (running){
  10. //线程阻塞...
  11. }
  12. System.out.println("stop");
  13. }
  14. //停止
  15. void stop(){
  16. this.running = false;
  17. }
  18. public static void main(String[] args) throws InterruptedException {
  19. ThreadVisibilityProblem tvp = new ThreadVisibilityProblem();
  20. new Thread(tvp::run,"thread1").start();
  21. TimeUnit.SECONDS.sleep(1);
  22. new Thread(tvp::stop,"thread2").start();
  23. }
  24. }

5.3 为什么要线程间不可见?

5.4 线程间不可见的优缺点

5.5 线程间不可见的解决方式

共享变量可见性实现的原理:
线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:

  1. 把工作内存1中更新过的共享变量刷新到主内存中
  2. 将主内存中最新的共享变量的值更新到工作内存2中

5.5.1 synchronized实现可见性以及原子性

JMM关于synchronized的两条规定:

  1. 线程解锁前(退出synchronized代码块之前),必须把共享变量的最新值刷新到主内存中,也就是说线程退出synchronized代码块值后,主内存中保存的共享变量的值已经是最新的了
  2. 线程加锁时(进入synchronized代码块之后),将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)

两者结合:线程解锁前对共享变量的修改在下次加锁时对其他线程可见

根据以上推出线程执行互斥代码的过程:

  1. 获得互斥锁(进入synchronized代码块)
  2. 清空工作内存
  3. 从主内存拷贝变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁(退出synchronized代码块)

什么是指令重排序?
代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化(有些代码翻译成机器指令之后,如果进行一个重排序,那可能重排序之后的顺序更加符合CPU执行的特点,这样就可以最大限度发挥CPU的性能)

  1. 编译器优化的重排序(编译器)
  2. 指令级并行重排序(处理器优化)
  3. 内存系统的重排序(处理器优化)

什么是as-if-serial语义?
无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)

  1. int num1 = 1;
  2. int num2 = 2;
  3. int sum = num1 + num2;

单线程来说:第1、2行的顺序可以重排,但第3行不能

5.5.2 volatile实现可见性

volatile特性:

  1. 能够保证volatile变量的可见性
  2. 不能保证volatile变量复合操作的原子性

如何实现内存可见性?
深入来说:通过加入内存屏障和禁止重排序优化来实现的

  1. 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  2. 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

线程写volatile变量的过程:
1.改变线程工作内存中volatile变量副本的值
2.将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到线程的工作内存中
2.从工作内存中读取volatile变量的副本

3. 线程不安全

  1. //线程不安全的证明
  2. public class UnsafeThreadProblem {
  3. //示例值
  4. private static int i = 0;
  5. //可以容纳10个线程的线程数组
  6. private static Thread[] threadArray = new Thread[10];
  7. //示例值+1
  8. static void increament(){
  9. i++;
  10. }
  11. public static void main(String[] args) throws InterruptedException {
  12. //填充线程数组, 每个线程把i的值累加10W次
  13. for (int i = 0; i < 10; i++){
  14. threadArray[i] = new Thread(()->{
  15. for (int j = 0; j < 100000; j++){
  16. increament();
  17. }
  18. });
  19. }
  20. //启动线程
  21. for (int i = 0; i < 10; i++){
  22. threadArray[i].start();
  23. }
  24. //加入当前线程
  25. for (int i = 0; i < 10; i++){
  26. threadArray[i].join();
  27. }
  28. //打印最后的i的值
  29. System.out.println(i);
  30. }
  31. }

6. 在CPU层面线程是如何切换的

7. 线程是如何调度的

线程调度策略