一、进程与线程

进程:一个应用程序
线程:一个进程中的执行场景/执行单元

对于Java程序来说,当在terminal中输入 java HelloWorld 后,会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法;同时再启动一个垃圾回收线程负责看护、回收垃圾。也就是说,现在的Java程序至少有两个线程并发,一个是垃圾回收线程、一个是执行main方法的主线程

在Java中,同一个Java程序中的两个不同的线程共享堆内存和方法区内存,但不共享栈内存——一个线程一个栈

二、Thread类

使用:编写一个类,继承 Thread 类,并重写 run 方法。
重点: start() 方法的作用是启动一个分支栈,在JVM中开辟一个新的栈空间。
start() 的任务完成之后,瞬间就结束了。启动成功之后,子线程会自动调用run方法(由JVM线程调度机制调度运行),并且run方法在分支栈底部 (压栈),main方法在主栈的栈底部,run和mian方法是平级的。

所以,若是仅调用run()方法,不调用start()方法,并不会创建子线程,这时 run() 方法与 start() 方法是同步的

  1. /**
  2. * 实现线程的第一种方式:
  3. * 编写一个类,直接继承 {@link Thread},重写run方法
  4. */
  5. public class ThreadTest {
  6. public static void main(String[] args) {
  7. // 这里是main方法,这里的代码运行在主线程中
  8. // 新建一个子线程
  9. MyThread myThread = new MyThread();
  10. // 启动线程
  11. // start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间
  12. // 这段代码认为完成之后,瞬间就结束了。
  13. // 启动成功之后,线程会自动调用run方法,并且run方法在分支栈底部(压栈)
  14. // main方法在主栈的栈底部,run和main方法是平级的。
  15. myThread.start();
  16. // 这里的代码还是运行在主线程中
  17. for (int i = 0; i < 1000; i++) {
  18. System.out.println("主线程---->" + i);
  19. }
  20. }
  21. }
  22. class MyThread extends Thread {
  23. @Override
  24. public void run() {
  25. // 编写程序,这段程序运行在分支线程中
  26. for (int i = 0; i < 1000; i++) {
  27. System.out.println("分支线程---->" + i);
  28. }
  29. }
  30. }

三、Runable接口

使用:编写一个类实现 java.lang.Runable 接口
建议使用该方式: 因为使用实现接口的方式,不耽误继承其他类。

  1. public class RunnableTest {
  2. public static void main(String[] args) {
  3. // final MyRunnable myRunnable = new MyRunnable();
  4. // final Thread t = new Thread(myRunnable);
  5. // 采用匿名内部类的方式。
  6. final Thread t = new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. for (int i = 0; i < 1000; i++) {
  10. System.out.println("子线程--->" + i);
  11. }
  12. }
  13. });
  14. t.start();
  15. for (int i = 0; i < 1000; i++) {
  16. System.out.println("主线程--->" + i);
  17. }
  18. }
  19. }
  20. class MyRunnable implements Runnable {
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 1000; i++) {
  24. System.out.println("子线程--->" + i);
  25. }
  26. }
  27. }

3.1 sleep() 方法

static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠 (暂停执行,进入阻塞状态,放弃占有CPU时间片,让给其他线程使用)

3.1.1 关于sleep()方法的面试题:

问题:这行代码会让线程t进入休眠状态码?
答:不会,因为sleep()是静态方法,和对象无关,所以这段代码等价于 => Thread.sleep(1000*5),也就是说会让当前线程也就是main线程休眠5s。

3.1.2 唤醒休眠中的线程

void interrupt() 中断线程。会让sleep()方法抛异常,导致捕获sleep抛出异常的try块结束,从而唤醒线程

  1. public class ThreadTest {
  2. public static void main(String[] args) {
  3. final MyThread t = new MyThread();
  4. t.start();
  5. // 当使用interrupt方法,会使得sleep()方法抛异常,导致捕获sleep抛出异常的try块
  6. // 结束,从而唤醒线程.
  7. t.interrupt();
  8. }
  9. }
  10. class MyThread extends Thread {
  11. @Override
  12. public void run() {
  13. try {
  14. Thread.sleep(365L * 24 * 60 * 60 * 1000);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. doOther();
  19. }
  20. public void doOther() {
  21. // 编写程序,这段程序运行在分支线程中
  22. for (int i = 0; i < 1000; i++) {
  23. String threadName = Thread.currentThread().getName();
  24. System.out.println(threadName + "---->" + i);
  25. }
  26. }
  27. }

3.1.3 优雅地终止一个线程

  1. class MyThread extends Thread {
  2. private boolean run = true;
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 1000; i++) {
  6. if (run) {
  7. doOther(i);
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. } else {
  14. // 终止当前线程
  15. // 在这里可以做一些善后工作
  16. System.out.println(Thread.currentThread().getName() + "终止了");
  17. return;
  18. }
  19. }
  20. }
  21. public void doOther(int i) {
  22. // 编写程序,这段程序运行在分支线程中
  23. System.out.println(Thread.currentThread().getName() + "do task " + i);
  24. }
  25. /**
  26. * 优雅地关闭一个线程
  27. */
  28. public void gracefulTerminate() {
  29. run = false;
  30. }

四、线程的生命周期

  • 新建状态:刚new出来的线程对象
  • 就绪状态:当处于新建状态的线程对象调用start()方法后,进入就绪状态,表示当前的线程具有抢夺cpu时间片的权利。当抢到cpu时间片之后,就开始执行run()方法。
  • 运行状态:run()方法的开始执行,标志着这个线程进入了运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片。

    线程由运行状态到就绪状态之间的转换是靠JVM的调度来实现的。

  • 阻塞状态:当处于运行状态中的线程,遇到阻塞事件时(例如接收用户键盘输入、sleep方法、IO事件等),会自动放弃当前占有的CPU时间片,进入阻塞状态。当阻塞状态解除后(需要的资源等到了),会再次回到就绪状态。

  • 死亡状态:当run()方法执行结束之后,线程进入死亡状态。

五、线程的调度

5.1 线程调度模型

  • 抢占式调度模型

    Java采用抢占式线程调度模型,哪个线程的优先级较高,抢到的CPU时间片的概率高一些。

  • 均分式调度模型

平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。

5.2 线程调度方法

5.2.1 线程优先级

Java中提供了一些方法与线程调度有关

void setPriority(int newPriority) 实例方法,设置线程的优先级 int getPriority() 实例方法,获取线程的优先级

Java提供的线程优先级常量

image.png static int MAX_PRIORITY 线程可以具有的最高优先级,为10 static int MIN_PRIORITY 线程可以具有的最低优先级,为1 static int NORM_PRIORITY 分配给线程的默认优先级,为5

5.2.2 线程让位

static void yield() 让位方法,暂停当前正在执行的线程对象,并执行其他线程 yield()方法不是阻塞方法,让当前线程让出CPU执行权,让给其他线程使用。 yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

5.2.3 合并线程

void join() 实例方法。让当前线程进入阻塞状态,等待实例线程执行完毕后,当前线程才可以继续执行。

  1. /**
  2. * 合并线程
  3. */
  4. public static void joinTest() throws InterruptedException {
  5. final MyThread t = new MyThread();
  6. t.start();
  7. for (int i = 0; i < 100; i++) {
  8. Thread.sleep(50);
  9. System.out.println(Thread.currentThread().getName() + " do task " + i);
  10. }
  11. // 让main线程进入阻塞,t线程执行,直到t线程执行结束,main线程才可以继续执行
  12. t.join();
  13. System.out.println("所有线程执行结束!");
  14. }

六、线程安全 ‼️

6.1 并发安全问题

image.png
问题:什么时候数据在多线程并发的环境下会存在安全问题呢?

满足以下的三个条件,就会存在线程安全问题:

  • 条件一:多线程并发
  • 条件二:有共享数据
  • 条件三:共享数据有修改的行为

解决方法线程同步(让线程排队执行!) 线程同步会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。

6.2 线程同步

  • 异步编程模型(并发)

线程t1和线程t2,各自执行自己的任务,t1不管t2,t2不管t1,谁也不需要等谁。
这种编程模型称为异步编程模型。

  • 同步编程模型(排队)

线程t1和线程t2,在线程t2执行的时候,必须等待t1执行完毕,两个线程之间发生了等待 关系,这就是同步编程模型。

6.2.1 synchronized (排他锁)

1、临界区

synchronized() {
// 线程同步代码块 (也叫临界区)
synchronized后面小括号中传的这个数据必须是多线程共享的数据
() 中写啥?
假设t1、t2、t3、t4、t5有5个线程,
你只希望t1、t2、t3排队,t4、t5不排队,
那么就一定要在()中写一个t1、t2、t3共享的对象
而这个对象对于t4、t5来说不是共享的
}

  1. public void withDraw(double money) throws Exception {
  2. // 以下代码必须排队执行,不能并发
  3. // 当前账户对象是共享的,所以传入this
  4. synchronized (this) {
  5. // synchronized("abc") { // "abc" 在字符串在常量池中,会让所有线程排队
  6. // synchronized(lock) // lock 若为实例变量,也可
  7. double before = this.getBalance();
  8. double after = before - money;
  9. if (after < 0) {
  10. throw new Exception("取款失败,余额不足");
  11. }
  12. this.setBalance(after);
  13. }
  14. }

在Java语言中,任何对象都有一把对象锁,以上代码的执行原理为:

  1. 假设 t1 和 t2 线程并发,开始执行以下代码的时候,肯定有一个先一个后;
  2. 假设 t1 先执行了,遇到了 synchronized ,这时,自动找后面共享对象的对象锁;
  3. 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中,t1一直都占有这把锁,直到同步代码块执行结束,这把锁才会被释放。
  4. 假设在 t1 占有锁的期间,t2 也遇到了synchronized关键字,也会尝试去占有后面共享对象的对象锁,但这把锁已经被t1占有,此时 t2 只能在同步代码块外等待 t1 的结束 (实际上 t2 此时会进入锁池<_lock poll_>并释放占有的CPU时间片)。
  5. 直到 t1 执行结束,释放了对象锁,此时t2终于等到了这把锁,然后 t2 占有这把锁,进入同步代码块执行程序。
    2、synchronized 用在实例方法上,一定锁的是 this
    缺点:这种方式不灵活,synchronized 出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低,所以这种方式不常用。
    优点:代码简洁,如果共享的对象就是 this 并且需要同步的代码块就是整个方法体,建议使用这种方式。
    1. public synchronized void withDraw(double money) throws Exception {
    2. // 以下代码必须排队执行,不能并发
    3. // 当前账户对象是共享的,所以传入this
    4. synchronized (this) {
    5. // synchronized("abc") { // "abc" 在字符串在常量池中,会让所有线程排队
    6. // synchronized(lock) // lock 若为实例变量,也可
    7. double before = this.getBalance();
    8. double after = before - money;
    9. if (after < 0) {
    10. throw new Exception("取款失败,余额不足");
    11. }
    12. this.setBalance(after);
    13. }
    14. }
    3、synchronized使用在静态方法上,找类锁
    对象锁:1个对象1把锁。
    类锁:该类的所有对象,共享1把类锁。

    6.2.2 哪些类型会有线程安全问题

    Java中的三大变量:
  • 实例变量:在堆中
  • 静态变量:在方法区中
  • 局部变量:在栈中

以上三大变量中:
局部变量永远都不会存在线程安全问题,因为局部变量不共享(在栈中)。
还有,常量也不会存在线程安全问题,因为常量不可修改。

是否线程安全
StringBuilder
StringBuffer
ArrayList
Vector
HashMapHashSet
HashTable

6.3 死锁

下面的代码要会写!!!

  1. /**
  2. * 死锁
  3. * synchronized在开发中最好不要嵌套使用
  4. */
  5. public class DeadLockTest {
  6. public static void main(String[] args) {
  7. Object o1 = new Object();
  8. Object o2 = new Object();
  9. // 两个线程共享o1和o2
  10. Thread t1 = new MyThread1(o1, o2);
  11. Thread t2 = new MyThread1(o1, o2);
  12. t1.start();
  13. t2.start();
  14. }
  15. }
  16. class MyThread1 extends Thread {
  17. Object o1;
  18. Object o2;
  19. public MyThread1(Object o1, Object o2) {
  20. this.o1 = o1;
  21. this.o2 = o2;
  22. }
  23. public void run() {
  24. synchronized (o1) {
  25. try {
  26. Thread.sleep(1000);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. synchronized (o2) {
  31. }
  32. }
  33. }
  34. }
  35. class MyThread2 extends Thread {
  36. Object o1;
  37. Object o2;
  38. public MyThread2(Object o1, Object o2) {
  39. this.o1 = o1;
  40. this.o2 = o2;
  41. }
  42. public void run() {
  43. synchronized (o2) {
  44. try {
  45. Thread.sleep(1000);
  46. } catch (InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. synchronized (o1) {
  50. }
  51. }
  52. }
  53. }

6.4 如何在开发中解决线程同步问题

是一上来就选择线程同步吗?synchronized
不是, synchronized 会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差,在不得已的情况下再选择线程同步机制。

第一种方案:

尽量使用局部变量代替实例变量和静态变量。

第二种方案:

如果必须使用实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。

第三种方案:

如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized。

七、守护线程 (Daemon Thread)

Java语言中线程分为两大类:

  • 用户线程
  • 守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程就是一个典型的守护线程。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

  1. Thread t = new MyThread();
  2. t.setDaemon(true); // 启动线程前,将线程设置为守护线程。
  3. t.start();

八、定时器

九、wait和notify

十、Future模式

10.1 Future普通版本

JDK内部有一个 Future 接口,该接口除了提供 get() 方法来获得真实数据外,还提供了一组辅助方法,比如:

boolean cancel() 如果等太久,可以直接取消这个任务 boolean isCancelled() 任务是不是已经被取消了 boolean isDone() 任务是不是已经完成了 V get() 有2个get()方法,不带参数的表示无穷等待,或者你可以只等待给定时间

  1. /**
  2. * jdk中的Future
  3. */
  4. public class FutureTest {
  5. public static void main(String[] args) throws ExecutionException, InterruptedException {
  6. // 定义一个线程池
  7. final ExecutorService executor = Executors.newFixedThreadPool(1);
  8. // 执行FutureTask, 相当于上例中的 client.request() 发送请求
  9. Future<String> future = executor.submit(new Callable<String>() {
  10. @Override
  11. public String call() throws Exception {
  12. final RealData realData = new RealData("hello future");
  13. return realData.getResult();
  14. }
  15. });
  16. System.out.println("request finished, preparing data...");
  17. try {
  18. System.out.println("do other work...");
  19. Thread.sleep(4 * 1000);
  20. System.out.println("other work done.");
  21. } catch (InterruptedException ignored) {
  22. }
  23. // 如果此时call()方法没有执行完成,则依然会等待
  24. System.out.println("data = " + future.get());
  25. }
  26. }

10.2 Future高阶版本

Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行,如果执行调用 get() 方法或者 isDone() 方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。
为了解决这个问题,JDK对Future模式又进行了加强,创建了一个 CompletableFuture , 它的最大作用就是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。

  1. /**
  2. * Future高阶版本 ----- {@link java.util.concurrent.CompletableFuture}
  3. */
  4. public class CompletableFutureTest {
  5. public static void main(String[] args) throws InterruptedException {
  6. // 创建异步执行任务
  7. System.out.println("start creating task...");
  8. CompletableFuture.supplyAsync(CompletableFutureTest::getPrice)
  9. .thenAccept(result -> {
  10. System.out.println("price: " + result);
  11. }) // 当出现异常时,会自动回调exceptionally()里面的方法
  12. .exceptionally(e -> {
  13. e.printStackTrace();
  14. return null;
  15. });
  16. System.out.println("task creating done.");
  17. System.out.println("do other work...");
  18. Thread.sleep(2000);
  19. System.out.println("other work done...");
  20. }
  21. static Double getPrice() {
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException ignored) {
  25. }
  26. // 模拟一个异常
  27. if (Math.random() < 0.3) {
  28. throw new RuntimeException("Error occurred when get price");
  29. }
  30. return Math.random() * 20;
  31. }
  32. }