01-Java线程基础

nobility 发布于 2022-02-14 05-Java并发编程 1309 次阅读


Java线程基础

实现线程

从Java官方文档中可以查看到,无论是JDK8还是JDK11中对线程的实现方式只有两种,一种是继承Thread类重写run()方法,一种是实现Runnable接口实现run()方法并传递给Thread类实例

Thread源码中也可以看出,无论是哪种方式,最终还是调用Threadrun()方法,所以准确的说,创建线程只有一种方式,就是构造Thread类,而实现线程的执行单元有两种方式

继承Thread类方式

public class MyThread extends Thread{
  @Override
  public void run() {
    System.out.println("继承Thread,复写run()方法");
  }

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

实现Runnable接口方式

public class MyRunnable implements Runnable {
  @Override
  public void run() {
    System.out.println("实现Runnable,实现run()方法");
  }

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

两种方式对比

通常情况下会优先选择实现Runnable接口方式

  • 避免由单继承局限带来的影响

  • 从代码架构考虑线程所执行的任务应该与线程创建动作应该是解耦的

  • 线程创建的操作对于CPU消耗是比较大的,继承Thread类实现的线程,每次只能新建的独立的线程,而实现Runable接口实现线程,可通过线程池之类的工具来降低创建线程的性能损耗(线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类)

两种方式的本质区别,从Thread的源码中可以看出,若使用实现Runnable接口方式,其实是直接执行了run()方法;若使用继承Thread类方式其实是将run()方法进行重写

class Thread implements Runnable {
  //省略...
  private Runnable target;  //用于构造函数接收Runnable接口实例
  //省略...
  @Override
  public void run() {
    if (target != null) {  //判断该线程中是否接收过Runnable接口实例
      target.run();
    }
  }
  //省略...
}

从源码中可以看出,同时使用两种方式实现线程时,实现Runnable接口方式将失效

public static void main(String[] args) {
  new Thread(() -> System.out.println("Runnable中的run()方法")) {
    @Override
    public void run() {
      System.out.println("Thread中的run()方法");
    }
  }.start();
}

其他封装层面的创建线程方式

虽然多线程的实现方式在代码层面的写法有很多,但是本质上都是对实现Runnable接口方式,继承Thread类方式的封装

实现Callable接口方式

public class MyCallable implements Callable<String> {
  @Override
  public String call() {
    System.out.println("当前线程正在执行:" + Thread.currentThread().getName());
    return "Callable中的call()方法的返回值";
  }

  public static void main(String[] args) {
    FutureTask<String> futureTask = new FutureTask<>(new MyCallable());  //使用FutureTask类对Callable实现类进行包装
    new Thread(futureTask).start();  //使用Thread来启动线程
    try {
      String result = futureTask.get();  //使用FutureTask的get()方法来线程获取返回值
      System.out.println(result);
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
}

线程池方式

public static void main(String[] args) {
  ExecutorService executorService = Executors.newCachedThreadPool();  //创建线程池
  for (int i = 0; i < 1000; i++) {
    //向线程池中提交1000个任务
    executorService.submit(() -> System.out.println("当前线程正在执行:" + Thread.currentThread().getName()));
  }
}

定时器方式

public static void main(String[] args) {
  Timer timer = new Timer();  //创建定时器
  timer.scheduleAtFixedRate(new TimerTask() {  //传递定时任务对象
    @Override
    public void run() {
      System.out.println("当前线程正在执行:" + Thread.currentThread().getName());
    }
  }, 0, 1000);  //延时0秒,间隔1秒
}

启动线程

run方法与start方法比较

  • 直接调用run()方法并未开启新的线程,而是在主线程中被当作一个普通的方法进行调用
  • 调用start()方法开启新的线程,由新线程调用run()方法
public static void main(String[] args) {
  Thread thread = new Thread(() -> System.out.println("当前线程正在执行:" + Thread.currentThread().getName()));
  thread.run();  //main
  thread.start();  //Thread-0
}

start方法含义

  • 请求JVM在空闲时启动新线程
    • 调用start方法的顺序并非线程的执行顺序,线程的执行顺序由线程调度器决定
    • 新线程一定由父线程创建
  • 启动后让新线程处于就绪状态,也就是获取到了除CPU之外的其他资源
    • 当获取到CPU资源后进入运行状态
    • 不能重复的调用start方法,若启动多次会抛出IllegalThreadStateException非法线程状态异常

start方法源码分析

  1. 首先检查该线程状态,若状态是已启动状态则会抛出IllegalThreadStateException非法线程状态异常
  2. 加入线程组
  3. 调用sart0()这个本地方法

停止线程

停止线程的原则

使用interrupt()来通知线程进行中断,而不是强制停止,在Java中若想停止线程最多就是告诉该线程该中断了,被告知的线程拥有是否停止线程的决定权;之所以这样设计是因为,要中断的线程通常需要完成一些保存工作再进行停止,若强行停止可能会出现混乱,比如数据丢失等情况

停止线程的相关方法

方法名 描述
boolean isInterrupted() 判断线程中断状态
static bolean interrupted() 判断当前线程中断状态,并消除当前线程的中断标记位
void interrupt() 中断线程

停止线程的正确方式

能够让线程正确停止方式只有以下两种情况

  • run()方法所有代码执行完毕
  • 线程中抛出异常,并未进行异常的捕获

开发多线程时停止线程设计

中断检查

run()方法中添加收到中断信号后进行return返回即可

public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(() -> {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {  //为了让程序运行时间足够长
      if (Thread.currentThread().isInterrupted()) {  //每次循环中检查是否收到中断信号
        System.out.println("收到中断信号,进行保存工作");
        return;  //若收到中断信号该线程的就会执行结束
      }
      if (i % 1000000 == 0) {
        System.out.println(i);
      }
    }
  });

  thread.start();
  Thread.sleep(1000);
  thread.interrupt();  //1秒后传递中断信号
}
中断阻塞
阻塞方法列表

只有下面列表中的方法可以(由于重载方法比较多,将方法的参数省略):将当前线程进入阻塞状态后可响应到中断信号

  • Object.wait()
  • Thread.sleep()
  • Thread.join()
  • java.util.concurrent.BlockingQueue.take()/put()
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.concurrent.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange()
  • java.nio.channels.InterruptibleChannel类的相关方法
  • java.nio.channels.Selector类的相关方法
单次阻塞

run()方法出现阻塞情况的代码,添加对InterruptedException中断异常的捕获,当收到中断信号后就会进入该catch块,在catch块中return返回即可

public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(() -> {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {  //若收到中断信号后会就进入该catch块
      e.printStackTrace();
      System.out.println("收到中断信号,进行保存工作");
      return;  //若没有return该线程并不会结束,除非后面没有代码了
    }

    for (int i = 0; i < Integer.MAX_VALUE; i++) {
      if (i % 1000000 == 0) {
        System.out.println(i);
      }
    }
  });

  thread.start();
  Thread.sleep(1000);
  thread.interrupt();  //1秒后传递中断信号
}
多次阻塞

在迭代中的catch块相当于后面还有代码,之所以会出现这种情况是因为,Java中的Thread.sleep()方法一旦响应中断就会将中断标记位清除,所以即时再次进行中断检查也无济于事

public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(() -> {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
      if (Thread.currentThread().isInterrupted()) return;
      //若在catch块中没有return,即时在此处进行中断检查也无法停止线程
      System.out.println(i);
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        System.out.println("收到中断信号,进行保存工作");
        e.printStackTrace();
        //return;  //若没有return该线程并不会结束
      }
    }
  });

  thread.start();
  Thread.sleep(1000);
  thread.interrupt();  //1秒后传递中断信号
}

开发方法时停止线程的设计

不应该屏蔽中断,而是优先采用传递中断或恢复中断,否则会出现用户在调用时,线程无法向预计那样停止下来,比如下面代码这样

public class MyLib {
  public void myMethod() {
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("myMethod执行完毕");
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
      MyLib myLib = new MyLib();
      while (true) {
        if (Thread.currentThread().isInterrupted()) return;
        //用户中断检查,并return,但无法停止
        myLib.myMethod();
      }
    });

    thread.start();
    Thread.sleep(1000);
    thread.interrupt();  //1秒后传递中断信号
  }
}
传递中断

应该优先选择传递中断:将InterruptedException中断异常的向外抛出,而并非捕获,那么用户在run()方法中调用我们的方法时就会强制要求对该异常强制处理

public class MyLib {
  public void myMethod() throws InterruptedException {
    Thread.sleep(500);
    System.out.println("myMethod执行完毕");
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
      MyLib myLib = new MyLib();
      while (true) {
        try {
          myLib.myMethod();
        } catch (InterruptedException e) {
          e.printStackTrace();
          return;  //用户中断检查,并return
        }
      }
    });

    thread.start();
    Thread.sleep(1000);
    thread.interrupt();  //1秒后传递中断信号
  }
}
恢复中断

其次选择恢复中断:在catch块中再次调用interrupt()方法来恢复Thread.sleep()方法清除的中断标记

public class MyLib {
  public void myMethod() {
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      e.printStackTrace();
      Thread.currentThread().interrupt(); //恢复中断
    }
    System.out.println("myMethod执行完毕");
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
      MyLib myLib = new MyLib();
      while (true) {
        if (Thread.currentThread().isInterrupted()) return;  //用户中断检查,并return
        myLib.myMethod();
      }
    });

    thread.start();
    Thread.sleep(1000);
    thread.interrupt();  //1秒后传递中断信号
  }
}

停止线程的错误方式

使用弃用的线程停止方法

  • 使用suspend()resume()方法并不像stop()方法那样会破坏对象,但是会让一个线程带着锁进行挂起,很容易造成死锁,比如:唤醒该线程的线程需要这把锁就造成了死锁
  • 使用被弃用的stop(),会导致线程运行一般突然停止,无法完成一个基本单位的操作,会造成脏数据
public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
      System.out.println("事务开始");
      System.out.println("账户B:-100");
      try {
        Thread.sleep(500);  //模拟转账延迟
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("账户A:+100");
      System.out.println("事务结束");
    }
  });

  thread.start();
  Thread.sleep(1000);
  thread.stop();  //1秒后强制停止线程
}

volatile标记位方式

使用标记位方式在线程没有被阻塞的情况下是可行的,但是一旦线程进入阻塞状态后,只有线程被吵醒后才会停止线程,比如阻塞队列进入阻塞状态后,该线程就无法停止

public class MyRunnable implements Runnable {
  private volatile boolean flag = false;

  @Override
  public void run() {
    for (int i = 0; i < 5; i++) {  //为了让程序运行时间足够长
      if (flag) return;   //每次循环中检查标记位
      System.out.println(i);  //每次循环休眠20秒
      try {
        Thread.sleep(20000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    MyRunnable myRunnable = new MyRunnable();
    new Thread(myRunnable).start();
    Thread.sleep(1000);
    myRunnable.flag = true;  //1秒后希望线程停止
  }
}

线程的生命周期

Java线程状态

Java中线程共有以下六种状态:

  • New:线程新建状态,创建但还未启动的新线程的状态
  • Runnable:线程可运行状态,包含操作系统中线程两种状态Ready和Running,也就是说无论线程是正在运行还是等待CPU调度都属于该状态
  • Block:线程阻塞状态,Java将线程阻塞状态进一步细分为下面三种状态
    • Blocked:线程被阻塞状态,当线程进入同步代码块或同步方法,并且锁已被占用时线程所处的状态
    • Waiting:线程等待状态,当线程在无限期等待另一个线程的特别动作时的状态
    • Timed Waiting:线程计时等待状态,当线程在有期限等待另一个线程的特别动作时的状态
  • Terminated:线程终止状态,线程执行完毕(包括抛出异常的情况)

Java线程状态转换图

代码中查看Java线程状态

非阻塞状态

public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(() -> {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {  //为了让程序运行时间足够长
      if (Thread.currentThread().isInterrupted()) {  //每次循环中检查是否收到中断信号
        System.out.println("收到中断信号,进行保存工作");
        return;  //若收到中断信号该线程的就会执行结束
      }
      if (i == Integer.MAX_VALUE / 2) {
        System.out.println("线程正在运行的状态:" + Thread.currentThread().getState());  //打印出RUNNABLE的状态
      }
    }
  });
  System.out.println("未调用start()方法的状态:" + thread.getState()); //打印出NEW的状态
  thread.start();
  System.out.println("调用start()方法后的状态:" + thread.getState());  //打印出RUNNABLE的状态
  Thread.sleep(5000);  //等待线程结束,可适当调整时间
  System.out.println(thread.getState());  //打印出TERMINATED状态
}

阻塞状态

public class Main {
  private synchronized void sync() {  //当另一个线程也想进入同步方法时会进入Blocked状态
    try {
      Thread.sleep(1000);  //线程休眠会进入TimedWaiting状态
      wait();  //线程等待会进入Waiting状态
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Main main = new Main();  //为了保证多个线程使用的是同一把锁
    Runnable runnable = () -> main.sync();
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    thread2.start();
    Thread.sleep(5);
    System.out.println("先进入同步方法的线程状态:" + thread1.getState());
    System.out.println("后进入同步方法的线程状态:" + thread2.getState());
    Thread.sleep(1500);
    System.out.println("执行wait()方法后的状态:" + thread1.getState());

    thread1.stop();
    thread2.stop();
  }
}

线程相关方法

线程等待与唤醒机制

注意事项

线程等待和唤醒的方法必须在有synchronized保护的代码块或方法中执行,并且是使用该保护代码块的锁对象进行方法的调用,也就是必须先拥有monitor锁,否则会抛出IllegalMonitorStateException非法monitor锁状态异常;之所以这样设计是因为,若不是在同步代码块中的话,由于线程的随机性可能会产生死锁,比如说两个线程,一个是要唤醒,另一个是要等待,若先等待才能被唤醒,反之就会陷入永久等待,所以该操作就需要安全的进行控制

线程等待和唤醒方法是定义在Object对象上的final native方法,调用等待或唤醒方法的是同一个对象,否则不是同一把锁会无法唤醒,之所以这样设计是因为,这些方法是锁级别的操作,而锁是属于某个对象的(绑定在对象头中),而不是线程中,若将锁定义在线程中则没个线程只能有一把锁,并且也不要使用线程对象的等待方法,这是因为在每个线程执行结束时该线程对象都会都会自动调用notifyAll()方法

线程等待与唤醒

  • wait():调用该方法的线程会处于等待状态,同时释放调用该方法对象的monitor锁
  • notify():随机唤醒一个等待线程
  • notifyAll():唤醒全部等待线程,那个优先级高那个先执行

处于等待状态的线程,只有发生以下情况才会被唤醒

  • 另一个线程调用这个对象的notify()方法,且刚好被唤醒的是该线程
  • 另一个线程调用这个对象的notifyAll()方法
  • 使用了可设置了等待时间的wait()方法,并且已经等待超时(若等待时间设置为0则永久等待)
  • 该线程调用了interrupt()方法
public static void main(String[] args) throws InterruptedException {
  Object lock = new Object();  //用来当锁的对象
  Thread thread = new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "线程开始执行");
    synchronized (lock) {
      try {
        lock.wait();  //当前线程会进入等待状态,同时释放锁
        //lock.wait(500);  //等待指定时间,超时后自动唤醒,同时获取锁
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "线程获取到了锁");
    }
  });
  thread.start();
  Thread.sleep(1000);  //为了让thread线程先执行

  synchronized (lock) {
    lock.notify();  //线程唤醒
    //lock.notifyAll();  //全部线程唤醒
  }
  //thread.interrupt();  //线程中断

  System.out.println(Thread.currentThread().getName() + "线程执行了唤醒操作");
}

生产者消费者模式

使用两个线程或进程对数据操作进行解耦(不局限两个),共用一个块数据,一个负责消费数据,一个负责生产数据,同时解决了两者生产消费速度的不一致问题:当生产者看到仓库满了时不再生产,而是通知消费者消费,当消费者看到仓库空了时不再消费,而是通知生产者生产

自己实现阻塞队列实现生产者消费者模式(未考虑到线程中断)

public class Storage<V> {
  private int maxSize;  //仓库最大容量
  private LinkedList<V> list;  //仓库容器

  public Storage() {
    this(10);  //默认容量是10
  }

  public Storage(int maxSize) {
    this.maxSize = maxSize;
    this.list = new LinkedList<V>();
  }

  public synchronized void put(V element) {
    while (list.size() == maxSize) {  //当仓库满了就进入等待状态
      try {
        wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    list.add(element);
    notify();
  }

  public synchronized V take() {
    while (list.size() == 0) {  //当仓库空了就恢复可运行状态
      try {
        wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    V element = list.poll();
    notify();
    return element;
  }

  public static void main(String[] args) {  //测试代码
    Storage<Integer> storage = new Storage<>();
    Thread producer = new Thread(() -> {
      for (int i = 0; i < 50; i++) {
        System.out.println("Producer生产了一个" + i);
        storage.put(i);
      }
    });
    Thread consumer = new Thread(() -> {
      for (int i = 0; i < 50; i++) {
        try {
          Thread.sleep(500);  //模拟生产消费速度不一致
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println("Consumer消费了一个" + storage.take());
      }
    });
    producer.start();
    consumer.start();
  }
}

使用Java中自带的阻塞队列实现生产者消费者模式(考虑到了线程中断)

public static void main(String[] args) {
  ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
  Thread producer = new Thread(() -> {
    for (int i = 0; i < 50; i++) {
      try {
        System.out.println("Producer生产了一个" + i);
        storage.put(i);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  });
  Thread consumer = new Thread(() -> {
    for (int i = 0; i < 50; i++) {
      try {
        System.out.println("Consumer消费了一个" + storage.take());
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  });
  producer.start();
  consumer.start();
}

线程交替运行

同步代码块空转

该方式会导致若某个线程一直会拿到锁,也无法进行业务的执行,而是在做空操作,效率较低

public static int count;

public static void main(String[] args) {
  Object lock = new Object();
  new Thread(() -> {
    while (count < 100) {
      synchronized (lock) {
        if ((count & 1) == 0) {  //与运算高效判断奇偶数
          System.out.println(Thread.currentThread().getName() + count++);
        }
      }
    }
  }, "偶数").start();
  new Thread(() -> {
    while (count < 100) {
      synchronized (lock) {
        if ((count & 1) == 1) {  //与运算高效判断奇偶数
          System.out.println(Thread.currentThread().getName() + count++);
        }
      }
    }
  }, "奇数").start();
}
交替唤醒与休眠

拿到锁,执行业务代码,唤醒其他线程,自己进入等待状态;不存在空操作,效率高,但是交替执行的两个线程不保证一定谁先谁后

public static int count = 0;
public static int max = 100;

public static void main(String[] args) {
  Object lock = new Object();
  Runnable runnable = () -> {
    synchronized (lock) {
      while (count < max) {
        System.out.println(Thread.currentThread().getName() + count++);
        lock.notify();
        if (count == max) return;  //由于锁在两个线程之间互相传递,需要被唤醒后再次进行判断退出线程,而不是等待
        try {
          lock.wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  };
  new Thread(runnable, "奇数").start();
  new Thread(runnable, "偶数").start();
}

线程join等待

  • 当前线程等待,让出当前线程资源,交由另一个调用该方法的线程执行
    • 无参则会等待另一个线程执行结束后执行
    • 有参则会先让另一个线程执行指定时间后在争抢资源
      • 若另一个线程已经结束但时间还未结束时,当前线程在另一个线程执行结束后也会直接会执行
  • 当前线程等待期间被中断时抛出InterruptedException中断异常,同时消除当前线程的等待状态
public static void main(String[] args) throws InterruptedException {
  Runnable runnable = () -> {
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "执行完毕");
  };
  Thread thread1 = new Thread(runnable);
  thread1.start();
  thread1.join();  //主线程会在该线程执行结束后执行
  //thread1.join(2500);  //主线程会在该线程执行2.5秒后执行
  //thread1.join(10000);  //超过该线程执行时后,会在该线程执行结束后就执行
  System.out.println(Thread.currentThread().getName() + "执行完毕");
}

join()方法的原理是在当前线程执行wait()方法,让当前线程处于等待状态;任何线程执行结束后都,会执行都会使用这个任意线程对象来执行notifyAll()方法,所以join()方法相当于下面代码

public static void main(String[] args) throws InterruptedException {
  Runnable runnable = new Runnable() {
    @Override
    public void run() {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "执行完毕");
      //this.notifyAll();  //自动执行该代码
    }
  };
  Thread thread = new Thread(runnable);
  thread.start();
  synchronized (thread) {  //主线程进入同步代码块,并且使用thread线程对象作为锁
    thread.wait();  //当thread线程执行结束时,会自动执行thread.notifyAll()方法唤醒主线程
  }
  System.out.println(Thread.currentThread().getName() + "执行完毕");
}

线程休眠

  • 线程不再占用CPU资源
  • 休眠期间被中断时不仅抛出InterruptedException中断异常,而且会清除中断标记
  • 不释放synchronizedlock
  • 使用TimeUnit.XXX.sleep()方法可指定休眠时间单位,编码上可减少对时间的换算,并且对于非法参数并不会抛出异常而是直接忽略
public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();  //为了多个线程使用同一把锁
    Runnable runnable = () -> {
      synchronized (lock) {  //可通过注释切换不同的锁进行查看
        //lock.lock();
        System.out.println(Thread.currentThread().getName() + "获得到了锁");
        try {  //锁并不会在休眠中释放掉
          TimeUnit.SECONDS.sleep(1);  //使用SECONDS单位,休眠一秒
           System.out.println(Thread.currentThread().getName() + "线程苏醒");
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          //lock.unlock();  //释放锁应该包裹在finally块中
          System.out.println(Thread.currentThread().getName() + "释放掉了锁");
        }
      }
    };
    //下面启动两个线程
    new Thread(runnable).start();
    new Thread(runnable).start();
  }

线程礼让

让当前线程释放当前CPU时间片,也就是当前线程礼让一次,线程仍处于可运行状态,随后继续按照抢占式继续执行;但是Java标准中规定JVM并不一定保证遵循线程礼让原则,也就是说当前CPU资源不紧张时,并不会一定线程礼让,所以一般开发中不适用线程礼让

线程相关属性

线程ID

每个线程都有唯一的ID,线程ID是没有setter方法的,也就是不能修改的,这是因为ID是给JVM使用的

  • 线程的ID是一个从1开始自增的序列,Main函数就是第一个线程
  • JVM在启动时除啦主线程还会自动启动很多其他辅助线程,由于线程的随机性即时自己创建的一个线程ID也可能不是2
public static void main(String[] args) {
  System.out.println("主线程ID" + Thread.currentThread().getId());
  System.out.println("自定义线程ID" + new Thread().getId());
}

线程名

  • 对于没有指定名字的非主线程会有一个以Thread-开头,从0开始的自增序列结尾的默认名字(主线程名是main)
  • 在源码中该自增序列使用了同步锁,所以默认名不会重名,虽然可以认为设置同名线程
  • 对于Java程序层的线程名是可以在线程启动后进行修改的,对于本地方法层的线程名是只能在构造函数中设置一次的,通过setter方法修改线程名是修改的Java程序层的线程名
public static void main(String[] args) {
  Thread t = new Thread("t");
  t.start();
  System.out.println(t.getName());  //初始线程名
  t.setName("tt");
  System.out.println(t.getName());  //修改后的线程名
}

守护线程

通常用于服务用户线程,守护线程有以下几个特性:

  • 线程类型默认继承父线程,也就是说由守护线程创建出的默认是守护线程,由用户线程创建出的线程默认是用户线程
  • 通常守护线程由JVM自动启动,main方法在运行时JVM还会自动启动很多守护线程
  • 所有非守护线程结束,守护线程自动结束

守护线程与普通线程整体上没有区别,只不过守护线程不会影响JVM的退出,一般也无需将用户线程手动设置为守护线程,因为守护线程的停止是JVM停止就突然停止,类似与stop()方法,无法完成一个基本单位的操作,会造成脏数据

线程优先级

Java中线程优先级分为10级别

  • 默认为5,最大为10,最小为1
  • 线程的优先级也会继承浮现出

不要依赖优先级,这是因为JVM会将优先级映射到操作系统上,对于不同的操作系统优先级级别并不相同,并且优先级可以被操作系统所修改(比如windows中优先级推进器功能会将非常想要执行的线程优先级提高),所以优先级并不可靠

线程异常处理

子线程中捕获异常

public static void main(String[] args) {
  new Thread(() -> {
    try {
      throw new RuntimeException();
    } catch (RuntimeException e) {
      e.printStackTrace();
    }
  }).start();
}

线程异常处理器

Java中异常处理器策略:若有父线程则使用父线程的异常处理器,若没有父线程就会使用全局异常处理器,若我们设置了全局异常处理器就会使用我们的异常处理器,否则就会自行处理也就是将堆栈信息打印

实现Thread.UncaughtExceptionHandler接口的uncaughtException方法,通过setDefaultUncaughtExceptionHandler()方法可以为以下几种范围设置异常处理器

  • 使用Thread类为全局设置异常处理器
  • 使用某个线程对象单独为某个线程设置异常处理器
  • 使用线程池对象为某个线程池设置异常处理器
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
  @Override
  public void uncaughtException(Thread t, Throwable e) {
    Logger logger = Logger.getAnonymousLogger();  //使用jdk的日志打印工具
    logger.log(Level.WARNING, "线程异常", t.getName());
    System.out.println("[" + t.getName() + "] " + e.getMessage());
  }

  public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
    new Thread(() -> {
      throw new RuntimeException("异常测试");
    }).start();
  }
}

多线程带来的问题

线程安全

当多线程访问一个对象时,若无需考虑下面几点(也就是无需做任何额外考虑,就想单线程那样),调用该对象的行为都可以得到正确的结果,则该对象是线程安全的

  • 无需考虑线程在运行时环境下的调度和交替执行
  • 无需考虑额外的同步
  • 无需考虑调用方进行任何其他的协调操作

要是出现下面这几种情况就可能会出现线程安全问题

  • 数据争用:访问共享的变量或资源,由于同时写数据,会造成错误数据
  • 数据捆绑:数据之间存在捆绑关系,由于修改未进行同步,会造成错误数据
  • 竞争条件:依赖时序的操作,由于顺序原因造成的错误,比如写入前就读取了

非原子操作的数据争用

public static int a;

public static void main(String[] args) throws InterruptedException {
  Runnable runnable = () -> {
    //while (a < 10000) a++;  //使用while将无法直接知道a++执行的次数
    for (int i = 0; i < 10000; i++) a++;  //两个线程虽然执行了共执行了a++语句20000次,但a的值最终不一定是20000
  };
  Thread thread1 = new Thread(runnable);
  Thread thread2 = new Thread(runnable);
  thread1.start();
  thread2.start();
  thread1.join();
  thread2.join();
  System.out.println(a);
}

使用下面代码可找出那一步数据写入时出现了数据争用

public static int a;
/*使用原子类,可保证对该数据的操作是原子操作*/
public static AtomicInteger atomicA = new AtomicInteger(0);  //与非原子数据进行对比
public static AtomicInteger errorCount = new AtomicInteger(0);  //记录出现错误的次数
public static boolean[] mark = new boolean[20001];
/*用来让两个线程在某处同时放行的栅栏*/
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

public static void main(String[] args) throws InterruptedException {
  Object lock = new Object();
  mark[0] = true;
  Runnable runnable = () -> {
    for (int i = 0; i < 10000; i++) {
      try {
        /**
         * 保证两个线程同时执行,且都执行完毕a++后再进行标记,
         * 以防出现由于该语句不在同步代码块中,而篡改已进入同步代码块中的线程,使用a值的语句,
         * CyclicBarrier实例的await方法可以做一个栅栏,若线程不够多时不会向下执行
         */
        cyclicBarrier2.reset();  //在等待之前需要重置,以便下次使用重置过了的CyclicBarrier实例
        cyclicBarrier1.await();  //等到两个线程执行到此处才会继续向下执行
        a++;
        cyclicBarrier1.reset();  //在等待之前需要重置
        cyclicBarrier2.await();  //等到两个线程执行到此处才会继续向下执行
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (BrokenBarrierException e) {
        e.printStackTrace();
      }
      atomicA.incrementAndGet();  //参考值原子性的自增
      synchronized (lock) {
        if (mark[a] && mark[a - 1]) {
          /**
           * 由于大部分情况都是未发生冲突的情况,所以大部分情况会让a自增两次,
           * 导致a会越过一项,所以只有前一项和当前项都为true时才是出现冲突,
           * 由于是从0开始进行对比的,所以第一位应该一开始就设置为true,从而保证第一次就发生冲突时就能判断出来
           */
          System.out.println("在[" + a + "]处发生冲突");
          errorCount.incrementAndGet();  //错误次数原子性自增
        }
        mark[a] = true;  //每次赋值成功都将对于位置打上true标记
      }
    }
  };
  Thread thread1 = new Thread(runnable);
  Thread thread2 = new Thread(runnable);
  /*启动两个子线程*/
  thread1.start();
  thread2.start();
  /*让主线程最后执行结果输出语句*/
  thread1.join();
  thread2.join();
  System.out.println("实际次数(" + a + ") + 冲突次数(" + errorCount + ") = 真正次数(" + atomicA + ")");
}

活跃性问题

  • 死锁:两个或多个线程永久阻塞,互相等待对方释放资源
  • 饥饿:一个线程因CPU时间全被其他线程抢走而得不到CPU资源
  • 活锁:死锁的另一种表现,两个或多个线程并未进入阻塞状态,而是互相避让资源让对方先执行

对象发布与初始化

发布:将对象可超过该类范围之外的地方使用,比如下面这几种情况

  • 将一个对象在类是使用public修饰
  • 方法返回一个对象
  • 将对象当作参数传递给一个方法

逸出:对象发布到了不该发布的地方

方法返回私有对象的逸出

返回该私有对象的副本可解决此类线程安全问题

public class ReturnPrivateObject {

  private Map<String, String> map;

  public ReturnPrivateObject() {
    map = new HashMap<>();
    map.put("127.0.0.1", "80");
  }

  public Map<String, String> getMap() {
    return map;  //直接返回该私有对象
  }

  public Map<String, String> getMapImproved() {
    return new HashMap<>(map);  //返回该私有对象的副本
  }

  public static void main(String[] args) {
    ReturnPrivateObject returnPrivateObject = new ReturnPrivateObject();
    Map<String, String> map = returnPrivateObject.getMap();

    System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));
    returnPrivateObject.getMapImproved().remove("127.0.0.1");  //假设是其他线程做的操作
    System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));

    System.out.println("getMap(): " + map.get("127.0.0.1"));
    map.remove("127.0.0.1");  //假设是其他线程做的操作
    System.out.println("getMap(): " + map.get("127.0.0.1"));
  }
}
对象未初始化完就提供外界使用的逸出
构造函数未初始化完就将this赋值
public class Point {
  public static Point point;
  private int x;
  private int y;

  public Point(int x, int y) throws InterruptedException {
    this.x = x;
    point = this;
    Thread.sleep(100);
    this.y = y;
  }

  @Override
  public String toString() {
    return x + "," + y;
  }

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      try {
        new Point(1, 1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();
    /*由于对象初始化在一个线程中使用在另一个线程中,所以导致未初始化完就使用*/
    Thread.sleep(99);  //1,0
    //Thread.sleep(101);  //1,1
    System.out.println(Point.point);
  }
}
构造函数中新建线程
import java.util.HashMap;
import java.util.Map;

public class InitNewThread {
  private Map<String, String> map;

  public InitNewThread() {
    map = new HashMap<>();
    new Thread(() -> {
      try {
        Thread.sleep(100);  //模拟延时初始化
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      map.put("127.0.0.1", "80");  //新建线程中进行初始化
    }).start();
  }

  public Map<String, String> getMap() {
    return map;  //直接返回该对象
  }

  public static void main(String[] args) throws InterruptedException {
    InitNewThread initNewThread = new InitNewThread();
    /*由于对象属性的初始化在另一个线程中,所以导致未初始化完就使用*/
    Thread.sleep(99);
    //Thread.sleep(101);
    System.out.println(initNewThread.getMap().get("127.0.0.1"));
  }
}
监听事件中事件触发时机
@FunctionalInterface
interface Event {  //事件接口
  void onEvent();  //绑定事件方法
}

public class Observer {
  static int count;  //要初始化的属性
  private Event event;

  public void registerEvent(Event event) {
    this.event = event;  //注册事件
  }

  public void trigger() {
    event.onEvent();  //触发事件
  }

  public static void main(String[] args) throws InterruptedException {
    Observer observer = new Observer();
    new Thread(() -> {
      observer.registerEvent(() -> System.out.println(count));  //该该线程注册事件
      try {
        Thread.sleep(100);  //模拟延时初始化
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      count = 100;  //初始化语句
    }).start();
    /*先让注册线程执行注册,count值未初始化好就触发了事件,count返回0,若初始化好就会返回100*/
    Thread.sleep(99);
    //Thread.sleep(101);
    new Thread(() -> {
      observer.trigger();  //该线程中触发事件
    }).start();
  }
}

使用工厂模式可解决此类线程安全问题

@FunctionalInterface
interface Event {  //事件接口
  void onEvent();  //绑定事件方法
}

public class Observer {
  static int count;  //要初始化的属性
  private Event event;

  private Observer() {
  }

  public void registerEvent(Event event) {
    this.event = event;  //注册事件
  }

  public void trigger() {
    event.onEvent();  //触发事件
  }

  public static Observer getInstance(Event event) {  //工厂方法
    Observer observer = new Observer();
    count = 100;  //先进行初始化
    observer.registerEvent(event);  //再注册事件
    return observer;
  }

  public static void main(String[] args) throws InterruptedException {
    Observer observer = Observer.getInstance(() -> System.out.println(count));  //工厂方法将注册和初始化合并
    new Thread(() -> {
      observer.trigger();  //此时事件触发一定会在注册且初始化之后
    }).start();
  }
}

多线程性能问题

上下文切换

上下文切换:主要是发生在线程调度时(当可运行线程数超过CPU数量时就会发生线程调度),操作系统内核在CPU上对进程或线程进行以下活动

  • 挂起一个进程,将该进程在CPU中的状态存储在内存,进程状态就是上下文
  • 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  • 跳转到程序计数器所指向的位置,即跳转到进程被中断时的代码处于,以恢复运行

上下文切换会导致CPU缓存失效,需要重新缓存:程序很大概率会按照一定规律进行数据访问,比如循环语句,CPU会按照一定的算法(LRU算法等)预测下一次要访问的数据进行缓存,从而提高执行效率,一旦发生上下文切换就导致预测失效,就需要重新缓存

频繁抢锁、IO等原因导致频繁阻塞,就会密集型上下文切换

内存同步

为了数据的正确性,同步手段会禁止编译器优化、使CPU内的缓存失效

加油啊!即便没有转生到异世界,也要拿出真本事!!!\(`Δ’)/
最后更新于 2022-02-14