多线程的概念

  • 进程:程序的基本执行实体(例如电脑中运行的某个软件)
  • 线程:线程是操作系统能够运算调度的最小单位。它被包含在进程中,是进程的实际运作单位。一个进程中有多个线程。
  • 进程和线程的关系:一个进程可以有多个线程,多个线程共享进程的方法区,但是每个线程有自己的程序计数器虚拟机栈本地方法栈
  • 多线程:程序同时执行多个任务;
  • 并发(concurrent)指在java程序中同时运行多个线程;(同一时刻,多个指令在单个CPU交替执行;
  • 并行:同一时刻,多个指令在多个CPU同时执行;
  • 线程安全性
    • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性一致性
    • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

多线程的实现方式

  • 实现方式一:自己定义一个继承自Thread的类,重写run方法;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Thread1 extends Thread{
    @Override
    public void run() {
    for(int i=0;i<10;i++){
    System.out.println(getName() +" hello world!");
    }
    }
    }

    // 测试
    public class MyThread {
    public static void main(String[] args) {
    Thread1 t1 = new Thread1();
    t1.setName("线程1");
    Thread1 t2 = new Thread1();
    t2.setName("线程2");

    t1.start();
    t2.start();

    }
    }
    // 运行结果
    线程2 hello world!
    线程1 hello world!
    线程1 hello world!
    线程1 hello world!
    线程1 hello world!
    线程2 hello world!
    线程2 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
    线程1 hello world!
    线程2 hello world!
  • 实现方式二:

    • 自己定义一个类实现Runnable接口,重写run方法;
    • 创建一个自己类的对象,创建Thread类的对象,并开启线程;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class Thread2 implements Runnable{
    @Override
    public void run() {
    String name = Thread.currentThread().getName();//获取当前线程的name
    for(int i=0;i<10;i++){
    System.out.println(name +" hello world!");
    }
    }
    }

    // 测试
    public class MyThread {
    public static void main(String[] args) {
    Thread2 thread = new Thread2();

    Thread t1 = new Thread(thread);
    Thread t2 = new Thread(thread);
    t1.setName("线程1");
    t2.setName("线程2");


    t1.start();
    t2.start();
    }
    }
  • 实现方式三:

    • 创建一个类实现Callable接口,重写call方法(有返回值,表示多线程的运行结果);
    • 创建自己类的对象;
    • 创建FutureTask对象(用于管理多线程处理的结果);
    • 创建Thread类的对象,并启动线程;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class Thread3 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
    int sum = 0;
    for(int i=0;i<10;i++){
    sum += i;
    }
    return sum;
    }
    }

    // 测试
    public class MyCallable {
    public static void main(String[] args) {
    Thread3 thread = new Thread3();

    FutureTask<Integer> f = new FutureTask<>(thread);

    Thread t1 = new Thread(f);
    Thread t2 = new Thread(f);
    t1.setName("线程1");
    t2.setName("线程2");

    t1.start();
    t2.start();

    int ans = f.get();
    }
    }

多线程的常用方法

1
2
3
4
5
6
7
8
9
public final void setName(String name); // 更改线程的名字
public final String getName(); // 获取线程的名字
public void start(); // 启动线程
public static Thread currentThead(); // 获取正在执行的线程
public final int getPriority(); // 获取当前线程优先级
public final void setPriority(int newPriority); //设置线程的优先级(1~10)
public static void sleep(long millis); // 睡眠指定时间,暂停运行
public final void join(long millis); // 插入线程
public static void yield(); //礼让线程

线程的生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • New:线程被创建,但是还未调用start()方法;
  • Runnable:可运行状态,线程被调用了start()方法,等待运行的状态;
  • Blocked:线程阻塞状态,需要等待锁释放;
  • Waiting:等待状态,该线程需要等待其他线程做出特定动作(通知或终止);
  • time_waiting:超时等待状态,可以在指定时间后自行返回,不用一直等待;
  • terminated:终止状态,表示该线程已经执行完毕。

thread_life1

  • 常用方法:
    • sleep(long):暂停执行线程,但是未释放锁;
    • wait():暂停执行线程并释放锁;wait()方法是Object类的本地方法,因为释放对象锁需要操作对应的对象(Object)。
    • notify():唤醒线程;
    • notifyAll()
  • 可以直接调用run()方法吗?
    • new一个Thread对象,然后调用start()方法,进入就绪状态,分配到时间片后就可以运行了;start()将线程相应的准备工作都做好了,然后自动执行run()方法中的内容。
    • 如果直接调用run()方法,则会把run()当成main线程下的普通方法,并不会在某个线程中去执行,这不是多线程的工作。

thread_life2

线程的安全问题

  • 竞态条件(Race Condition)

    • 多个线程同时对共享资源进行读写操作,导致数据不一致。
    • 解决:同步机制(synchronized),锁(Lock),使用线程安全的数据结构(如ConcurrentHashMap)
    • 同步代码块
      • 锁对象一定要是唯一的,如果每个线程拥有一个不同的锁对象,则锁就失去了意义;
      • synchronized不要写在循环内,不然一个线程抢到执行权后,直到循环结束之前,其他线程没法进入;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //注:这里使用线程的第一种自定义方法
    public class MyThread extends Thread{

    // 加static关键字,表示共享ticket数据
    static int ticket = 0;

    // 锁对象必须是唯一的
    // 加static关键字表示的MyThread创建的所有对象都共享同一个obj
    static Object obj = new Object();

    @Override
    public void run() {
    while(true){
    // 同步代码块
    synchronized (obj){
    if(ticket<100){
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    ticket++;
    System.out.println(getName()+"正在卖第"+ticket+"张票!");
    }else{
    break;
    }
    }
    }
    }
    }
    • 同步方法
      • 将同步代码块抽取出来形成一个方法,为这个方法上锁;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //注:这里使用线程的第二种自定义方法
    public class MyRunnable implements Runnable{
    int ticket = 0;// 不需要static关键字,Runnable对象只创建一次

    @Override
    public void run() {
    while(true){
    if(memthod()) break;
    }
    }

    // 同步方法
    private synchronized boolean memthod() {
    if(ticket<100){
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    ticket++;
    System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票!");
    }else{
    return true;
    }
    return false;
    }
    }
  • StringBulider&StringBuffer

    • StringBufferStringBuilder都是Java中用于处理可变字符串的类,它们的主要区别在于线程安全性和性能。

    • StringBuffer线程安全的,因此可以在多线程环境下使用。它的所有公共方法都是同步的,即在方法内部使用了synchronized关键字来确保线程安全。这种同步机制会带来一定的性能损失,因为多个线程需要竞争同一把锁,这会导致线程阻塞和上下文切换。因此,如果不需要在多线程环境下使用可变字符串,建议使用StringBuilder

    • StringBuilder非线程安全的,因此不能在多线程环境下使用。它的所有公共方法都不是同步的,因此在单线程环境下使用StringBuilder比使用StringBuffer更加高效。

  • 锁(Lock)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class MyThread extends Thread{
    static int ticket = 0;
    static Lock lock = new ReentrantLock();
    @Override
    public void run() {
    while (true) {
    lock.lock(); // 上锁
    try {
    if (ticket < 100) {
    Thread.sleep(100);
    ticket++;
    System.out.println(getName() + "正在卖第" + ticket + "张票!");
    } else {
    break;
    }
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    } finally {
    lock.unlock(); // 释放锁
    // 使用finally关键字,不管怎样都会被执行,防止线程跳出循环后未释放锁
    }
    }
    }
    }
  • 死锁

    • 死锁是指两个或多个线程在互相持有对方所需要的资源时,都在等待对方先释放资源,导致程序无法继续执行的情况。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      public class DeadLockDemo {
      private static Object resource1 = new Object(); // 资源1
      private static Object resource2 = new Object(); // 资源2

      public static void main(String[] args) {
      new Thread(() -> {
      synchronized (resource1) {
      System.out.println(Thread.currentThread().getName() + "获取到了资源:resource1");

      try {
      Thread.sleep(1000);
      } catch (InterruptedException e) {
      throw new RuntimeException(e);
      }
      System.out.println(Thread.currentThread().getName() + "准备获取资源:resource2");
      synchronized (resource2) {
      System.out.println(Thread.currentThread().getName() + "获取到了资源:resource2");
      }
      }
      }, "线程1").start();

      new Thread(() -> {
      synchronized (resource2) {
      System.out.println(Thread.currentThread().getName() + "获取到了资源:resource2");

      try {
      Thread.sleep(1000);
      } catch (InterruptedException e) {
      throw new RuntimeException(e);
      }
      System.out.println(Thread.currentThread().getName() + "准备获取资源:resource1");
      synchronized (resource1) {
      System.out.println(Thread.currentThread().getName() + "获取到了资源:resource1");
      }
      }
      }, "线程2").start();
      }
      }

      运行结果:

      1
      2
      3
      4
      线程1获取到了资源:resource1
      线程2获取到了资源:resource2
      线程1准备获取资源:resource2
      线程2准备获取资源:resource1
    • 避免死锁的方法:

      • 避免使用多个锁:尽量减少线程需要持有的锁的数量,或使用更高级别的同步机制(如ReentrantLock)来避免死锁;
      • 避免使用多个锁的嵌套;
      • 超时等待:线程尝试获取锁时,设置一个超时时间,在等待超过一定时间后,放弃获取锁。
  • 等待唤醒机制

    • 等待唤醒机制是一种用于线程间通信的机制,它允许一个线程等待另一个线程的通知,以便在特定时间点上恢复执行。

    • 常用方法:

      1
      2
      3
      4
      void wait(); // 释放锁,进入等待状态
      void sleep(); // 暂停执行,但是未释放锁
      void notify(); // 唤醒
      void notifyAll();

线程池

  • 之前写多线程的弊端:线程用完之后就消失,浪费资源;

  • 线程池是一种用于管理和重用线程的机制,它允许在应用程序中创建一组线程,并在需要时执行任务,而不需要频繁地创建和销毁线程。

  • 使用线程池的优点:

    • 降低资源消耗
    • 提高响应速度
    • 提高线程的可管理性
  • 线程池核心原理:

    • 创建一个池子,初始是空的;
    • 提交任务时,池子创建一个新的线程对象,任务执行完毕后把线程还给池子,下次创建线程时不需要创建新的线程,直接复用已有的线程;
    • 但是如果提交任务时线程池中没有空闲线程,也无法创建新的线程,任务就会等待;
  • 创建线程池的方式:

    • 通过ThreadPoolExecutor构造函数来创建(推荐)
    • 通过 Executor 框架的工具类 Executors 来创建。
      • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。
      • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。初始大小为0。当有新任务提交时,如果当前线程池中没有线程可用,它会创建一个新的线程来处理该任务。如果在一段时间内(默认为60秒)没有新任务提交,核心线程会超时并被销毁,从而缩小线程池的大小。
      • SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
      • ScheduledThreadPool:该方法返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ThreadPoolDemo {
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();

    // 创建一个没有上限的线程池对象
    // ExecutorService pool = Executors.newCachedThreadPool();

    // 创建一个有上限的线程池对象
    ExecutorService pool = Executors.newFixedThreadPool(3);

    // 提交任务
    pool.submit(new MyThread());
    pool.submit(new MyThread());

    // 销毁线程池
    pool.shutdown();

    }
    }
  • 线程池常见参数

    • corePoolSize:核心线程数(一直存活的线程,即便当前没有任务执行)
    • maximumPoolSize:最大线程数,如果maximumPoolSize > 当前线程 > corePoolSize,且队列任务已满,线程池会创建新线程来执行任务。
    • keepAliveTime:线程保持空闲时间,当线程空闲时间达到keepAliveTime时,线程会自动退出,直到线程数等于核心线程数。
    • workQueue:新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
    1
    2
    3
    4
    5
    6
    7
    8
    public ThreadPoolExecutor(int corePoolSize, // 核心线程数
    int maximumPoolSize, // 最大线程数
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    Executors.defaultThreadFactory(), defaultHandler);
    }
  • ThreadPoolExecutor执行过程

    • 提交任务;
    • 判断是否达到核心线程数,如果没有,创建新线程执行任务;
    • 如果达到核心线程数,判断工作队列是否已满,如果没有,则将任务添加到队列;
    • 如果工作队列是否已满,判断是否达到最大线程数,如果没有,则创建新线程来执行任务;
    • 如果已经达到最大线程数,则根据饱和策略处理(例如抛出异常);

ThreadLocal

  • ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

  • 常用方法:

    1
    2
    3
    public void set(T value); // 存储数据
    public T get(); // 获取数据
    public void remove(); // 删除数据
  • ThreadLocal原理:最终的变量是放在了当前线程的 ThreadLocalMap (底层是HashMap)中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

    ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
    // 将需要存储的值放入到这个哈希表中
    map.set(this, value);
    else
    createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

Java线程内存模型

并发编程中的三个概念

  • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

    1
    2
    3
    x = 10;  // 原子性操作
    x++; // 非原子性操作
    y = x; // 非原子性操作
  • 有序性: 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

  • 可见性

    • 当一个共享变量volatile修饰时,它会保证修改的值会立即更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
    • 通过synchronizedLock也能够保证可见性,synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

volatile关键字

  • volatile关键字可以保证变量的可见性,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

    2)禁止进行指令重排序。

  • volatile关键字不能保证数据的原子性。