Java 多线程 & 线程模型

2022-06-19  本文已影响0人  bowen_wu

多线程

概述

Thread 线程 => 是操作系统能够进行运算调度的最小单位。大部分的情况下,它被包含在进程中,是进程中的实际运作单位

生命周期

java.lang.Thread.State Thread Life Cycle

多线程

线程难的原因:需要看着同一份代码,想象不同的人在疯狂的以乱序执行它 => 多个线程同时访问同一个共享变量时,由于变量不是原子的,以致于过程是乱序的,不知道什么时候会发生不正常的,有可能是正常的,有可能是不正常的

// 最终结果不是 1000
public class Test {
    private static int j = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(++j);
            }).start();
        }
    }
}
result

Java 为多线程提供了语言级的支持

多线程适用场景

多线程问题

  1. 正确性
    • 安全 => 竞争条件 & 死锁
    • 协同 => 同时、随机执行的线程,如何让他们协同工作?
  2. 效率 & 易用性
    • 执行的越快越好
    • 用起来越不容易出错越好

java.lang.Object

为什么 Java 中所有对象都可以成为锁?

合理的使用 wait + notify 就可以达到调度不同线程的目的 => wait() 之后 notify 进程才会一直走下去,如果先 notifywait
那么进程将会一直等待 notify

synchronized(obj) {
    while(condition does no hold) {
        obj.wait();
    }
}

synchronized(obj) {
    obj.notify();
}

线程不安全

线程不安全的表现

数据错误

数据错误 => 不是原子操作

死锁

synchronized => 同步 => 锁 => 同一个时刻只能有一个线程拿到同一把锁 => 在 Java 中任何一个对象都可以当成锁🔐

public class Test {
    // Thread1 和 Thread2 使用了不同的顺序来获得资源锁
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread1().start();
        new Thread2().start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock2) {
                    System.out.println("");
                }
            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock2) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock1) {
                    System.out.println("");
                }
            }
        }
    }
}
死锁宏观表现

排查死锁问题

  1. jps => 查看当前所有的 Java 进程
  2. 找到死锁所运行的进程 ID
  3. jstack <java process id> > <file> => 将死锁的进程栈信息保存到指定文件
  4. 分析文件 | 排查调用栈,看每个 Thread 在哪个方法处停滞了

预防死锁产生的原则

所有的线程都按照相同的顺序获得资源的锁🔐

线程安全

实现线程安全的基本手段

synchronized

缺点

JUC 包

Java 的协同机制一定要和锁(monitor | 监视器 | 管程)一起来工作

Lock & Condition

private static Lock LOCK = new ReentrantLock();

public static void Main() {
    LOCK.lock();
    LOCK.lock();
    LOCK.lock();
    
    # lock 了几次就要 unlock 几次,不然其他线程获取不到锁
    LOCK.unlock();
    LOCK.unlock();
    LOCK.unlock();
}

CountDownLatch

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {
  public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
      int finalI = i;
      new Thread(() -> {
        int second = new Random().nextInt(10);
        try {
          Thread.sleep(second * 1000);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
        System.out.println("线程" + finalI + "活干完了");
        latch.countDown();
      }).start();
    }

    latch.await();
    System.out.println("我是老板,所有工人的活都干完了");
  }
}

CyclicBarrier

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Main {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                int second = new Random().nextInt(10);
                try {
                    Thread.sleep(second * 1000);
                    System.out.println("线程" + finalI + "活干完了");
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等其他人到了才能继续");
            }).start();
        }
    }
}

Semaphore

BlockingQueue & BlockingDeque

线程池 ThreadPoolExecutor

参数

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

Rejected Execution Handler Policy

RejectedExecutionHandler

ExecutorService

一个多线程执行器框架,最常用的实现是线程池。屏蔽了线程的细节,提供了并发执行任务机制

Future

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPoll = Executors.newFixedThreadPool(10);
        Future<?> future = threadPoll.submit(() -> {
            try {
                Thread.sleep(10000);
                System.out.println("我结束工作了");
                return "Future Result";
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println("我把工作提交了");

        // 获取 future 结果
        System.out.println(future.get());
        System.out.println("我提交的工作做完了");
    }
}

创建多线程方法

调度线程方法

Runnable & Callable

基于 Runnable 的问题,创造了 Callable,Callable 就是 Runnable 的高级版本,Callable 解决了 Runnable 的问题

Java Memory Modal

JMM => Java 线程模型 => 方法中的局部变量是私有的,其他都是公有的

当两个线程执行时,方法栈是私有的,方法栈中有局部变量,局部变量也是私有的,当在线程中 new Object() 的时候,每个线程都会在 Heap 中生成一个 Object,方法栈中的局部变量指向 Heap 中的 Object,两个线程会生成两个 Object

由于 CPU 和 Main Memory 交换是很慢的,CPU 进行一次内存寻址大约是 1 微秒,大约相当于 CPU 的 1000 - 3000 个时钟周期。所以 Java 模型允许每一个线程自己有一份私有的副本,CPU
和私有的副本进行读写。Java 线程模型定期的把每个线程的私有变量同步给 Main Memory

上述导致的问题:

  1. 线程A更新一个公有变量 globalVariable,线程B感知不到,需要等到线程A同步至 Main Memory,并且线程B的副本同步 Main Memory,才能感知到 globalVariable 的更新
  2. 在现代 CPU 中,如果两条指令没有依赖关系,编译器可以进行一个指令重排

volatile

volatile 关键字声明的变量在多线程中,所有的线程写该变量的时候都会直接写入 Main Memory,对于共享变量所做的修改会立刻写回 Main Memory。当读该变量时,会立刻从 Main Memory 中读取刷新至当前 CPU 的副本中,再从副本中读取

  1. 可见性,并非原子性
  1. 禁止指令重排 => 编译器和处理器都可能对指令进行重排,以提高效率,提高程序执行的速度,在多线程中会导致问题 => 可以保证自己的读写前后插入屏障,使得不会发生指令重排
  2. 有同步的时候无需 volatile => synchronized | Lock | AtomicInteger => 既保证可见性又保证原子性

知识点

  1. 如何查看一个数据是否是线程安全的?
    进入方法后,搜索 thread => <strong>Note that this implementation is not synchronized.</strong> => 线程不安全
  2. 除非特意提及线程安全,不然都是线程不安全的
  3. Collections.synchronized 方法可以将一些线程不安全的类 | 方法变为 synchronized
  4. monitor => 在 Java 世界中,使用 monitor 代表 synchronized 锁住的对象
  5. Runnable 中的 run
    • 没有返回值
    • 没有声明抛出的异常
  6. 线程是一种很昂贵的资源,不能频繁的创建它。在 IO 密集型应用中,创建线程数量最大值约为 CPU 数量 + 1
  7. Object.wait()Thread.sleep() 有什么区别?
    • Object.wait() => 持有锁之后调用锁的 wait 方法后 sleep,之后放弃锁,等待被 notify
    • Thread.sleep() => sleep 的时候持有锁,sleep 可以不持有锁,当不持有锁的时候 sleep,不会影响其他线程工作
  8. 创建线程池里有 new Thread => TODO: 找到相应代码
  9. TODO
上一篇下一篇

猜你喜欢

热点阅读