【Java并发编程一】八千字详解多线程

2024-09-11 08:54:26 浏览数 (3)

多线程基础

并发编程是目前很多大公司面试考核的重点内容,为什么并发编程那么重要呢?这还要从CPU的发展讲起,考量一块CPU性能高不高的重要一个因素就是CPU的计算能力,起初,为了提升CPU的计算能力,硬件厂家们尽可能的缩小其内部每个计算单元所占的体积,保证每块CPU上尽可能的装有更多的计算单元,但受限于工艺水平,硬件厂家们发现,当一块CPU内计算单元的数目越多时,它成为残次品的概率也会提高,相应成本也会提高,为了解决这一问题,硬件厂家们发明了现在常见的多核CPU,及一个CPU上存在多个核心。为了充分利用多核技术,逐渐发展起来了多进程和多线程。下面就让我来带大家深入理解多线程的奥秘吧。

1.线程和进程

线程是什么?

一个线程就是一个 " 执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行着多份代码.

为啥要有线程?
  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
  • 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
进程和线程的区别?
  1. 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  2. 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  3. 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

虽然多进程也能实现 并发编程 , 但是线程比进程更轻量 .

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后 , 线程虽然比进程轻量 , 但是人们还不满足 , 于是又有了 " 线程池 "(ThreadPool) 和 " 协程 "

(Coroutine)

Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用( 例如 Linux 的 pthread 库 ).

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装

使用jconsole观察线程

在jdk的bin文件夹下,Java为我们提供了一个工具jconsole.exe,启动这个工具我们便可以清晰的观察自己电脑内Java线程的启动和销毁了。

2.创建线程的多种方式

方法一:继承Thread来创建一个线程类。

代码语言:javascript复制
//创建一个线程类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
MyThread t = new MyThread();//创建 MyThread 类的实例
t.start(); // 线程开始运行

方法二: 实现Runnable接口

代码语言:javascript复制
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
//创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行

方法三:使用ExecutorService、Callable、Future实现有返回结果的多线程

代码语言:javascript复制
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
 
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
    InterruptedException {
   System.out.println("----程序开始运行----");
   Date date1 = new Date();
 
   int taskSize = 5;
   // 创建一个线程池
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);
   // 创建多个有返回值的任务
   List<Future> list = new ArrayList<Future>();
   for (int i = 0; i < taskSize; i  ) {
    Callable c = new MyCallable(i   " ");
    // 执行任务并获取Future对象
    Future f = pool.submit(c);
    // System.out.println(">>>"   f.get().toString());
    list.add(f);
   }
   // 关闭线程池
   pool.shutdown();
 
   // 获取所有并发任务的运行结果
   for (Future f : list) {
    // 从Future对象上获取任务的返回值,并输出到控制台
    System.out.println(">>>"   f.get().toString());
   }
 
   Date date2 = new Date();
   System.out.println("----程序结束运行----,程序运行时间【"
       (date2.getTime() - date1.getTime())   "毫秒】");
}
}
 
class MyCallable implements Callable<Object> {
private String taskNum;
 
MyCallable(String taskNum) {
   this.taskNum = taskNum;
}
 
public Object call() throws Exception {
   System.out.println(">>>"   taskNum   "任务启动");
   Date dateTmp1 = new Date();
   Thread.sleep(1000);
   Date dateTmp2 = new Date();
   long time = dateTmp2.getTime() - dateTmp1.getTime();
   System.out.println(">>>"   taskNum   "任务终止");
   return taskNum   "任务返回运行结果,当前任务时间【"   time   "毫秒】";
}
}

tips:

上述的方法一,方法二均可以通过匿名内部类的方式创建线程,还可用lambda表达式来简化

代码语言:javascript复制
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

3.Thread类及其常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。 Thread 类的对象 就是用来描述一个线程执行流的, JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

Thread类的常见构造方法

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用 Runnable 对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用 Runnable 对象创建线程对象,并命名

【了解】 Thread(ThreadGroup group, Runnable target)

线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可

代码语言:javascript复制
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
Thread类的常见属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明
Thread类的常见方法

(1).线程启动(start()方法)

前面我们知道覆写run方法来创建一个线程对象,但run方法只是为了给线程启动提供一个要做的事情清单,不能通过调用run方法来使线程启动。

调用start方法,可以真正在操作系统的底层创建一个线程。

(2).线程中断(interrupt()方法)

interrupt()、interrupted() 和 isInterrupted() 方法是 Java 中用于线程中断的相关方法,它们有着不同的功能和用法。下面我将逐一解释它们的区别。

我们把run方法运行结束叫做线程的中断,通常情况下我们为了保证线程的存在,会在run方法内部自定义设置一个循环条件等于true,让循环能一直存在,我们把这个条件叫做标记值,当标记值被改为false时,循环结束,run方法也会执行完毕。当run方法内的代码运行完毕之后,内核中的线程就会被摧毁,称为线程中断。除了自定义标记值之外,Thread内部还提供了一个boolean类型的变量可以作为标记值(isInterrupted),

  • 清除中断标志:将 isInterrupted 的值设置为 false
  • 设置中断标志:将 isInterrupted 的值 设置为 true

3.2.1 interrupt()

当调用线程的 interrupt() 方法时,如果线程处于阻塞状态,就会抛出 InterruptedException 异常并清除中断标志,接着结束睡眠。

如果线程未处于阻塞状态,仅仅是设置中断标志,并不是真正的中断线程。线程的中断由线程决定,线程可以通过检查中断标志来决定是否中断自己的执行。

代码语言:javascript复制
public class MyThread extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // 线程任务逻辑
            // ...
        }
    }
}

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

    // 等待一段时间后中断线程
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();
}

3.2.2 interrupted()

interrupted() 方法是一个静态方法,用于判断当前线程是否被中断,并返回中断状态,而且在判断中断状态后,还会自动清除中断标志。

  • 如果线程没有被中断,则返回 false;
  • 如果线程被中断,则返回 true;
代码语言:javascript复制
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i  ) {
        System.out.println(Thread.interrupted());
     }
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}

3.2.3 isInterrupted()

isInterrupted() 用于检查当前线程是否被中断,并且不会清除线程的中断状态。

  • 当线程被中断时,返回 true。
  • 当线程没有被中断时,返回 false。
代码语言:javascript复制
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      	for (int i = 0; i < 10; i  ) {
			System.out.println(Thread.currentThread().isInterrupted());
     	}
     }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}

(3).线程等待(join()方法)

java中线程是并发执行的,线程的调度是抢占式的,所以操作系统对于线程调用的顺序的不知道的,我们无法判断哪个线程先结束,为了能够控制线程的结束顺序,因此 java 提供了 join()方法。join方法内可以添加long类型参数,译为等待线程结束,最多等多少毫秒。

在 Java 中,join() 方法是 Thread 类的一个方法,它允许一个线程等待另一个线程执行完毕再执行接下来的步骤。当一个线程调用另一个线程的 join() 方法时,调用线程将被阻塞。

代码语言:javascript复制
public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                // 模拟线程执行耗时的操作
                Thread.sleep(2000);
                System.out.println("子线程执行完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread.start(); // 启动子线程
        thread.join(); // 主线程等待子线程执行完毕

        System.out.println("主线程继续执行");
    }
}

(4).获取线程引用(currentThread()方法)

public static Thread currentThread();

返回当前线程对象的引用

代码语言:javascript复制
public class ThreadDemo {
  public static void main(String[] args) {
    Thread thread = Thread.currentThread();
    System.out.println(thread.getName());
 }
}

(5).线程休眠(sleep()方法)

让线程休眠一段时间,不去参与CPU的竞争,与阻塞状态不同,需要注意的是,因为线程的调度室不可控的,所以,这个方法只能保证实际休眠时间>=参数的设置的休眠时间。

代码语言:javascript复制
public class ThreadDemo {
  public static void main(String[] args) throws InterruptedException {
    System.out.println(System.currentTimeMillis());
    Thread.sleep(3 * 1000);
    System.out.println(System.currentTimeMillis());
 }
}

4.线程的各种状态

线程的状态是一个枚举类型 Thread.State,我们可以通过这段代码得到线程的状态:

代码语言:javascript复制
public class ThreadState {
  public static void main(String[] args) {
    for (Thread.State state : Thread.State.values()) {
      System.out.println(state);
   }
 }
}

Java中线程的状态主要有以下几种:

(1)新建状态(NEW) Thread对象创建了。但是还没有调用start,操作系统内核中的线程还没创建

(2)终止状态(TERMINATED) run方法执行完毕,内核中的线程已经销毁

(3)可运行状态(RUNNABLE) 线程就绪(正在cpu上执行)

(4)超时等待状态(TIMED_WAITING) 由sleep等带有有时间的方法进入的阻塞

(5)等待状态(WAITING) 不带时间的阻塞,线程在等待其他线程的特定操作,如wait / join

(6)阻塞状态(BLOCKED) 由于锁竞争产生的阻塞

5.使用多线程的风险(线程安全问题)

虽然我们现在使用的CPU拥有多个核心,在正常使用计算机时,一个核心上仍然会同时运行多个线程,怎样保持多个线程同时运行呢,其实是通过高频切换来完成的,即一个线程在CPU上运行一会,再切换为另一个线程运行,因为中间切换的时间很短,且CPU执行速率非常快,在宏观层面我们就认为是多个线程在同时运行。如果同时运行的这几个线程在操作同一件事,且这件事不具有原子性就很容易发生线程安全问题。

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令

比如我们之前常看到的 n ,其实是由三步操作组成的:

1. 从内存把数据读到 CPU

2. 进行数据更新

3. 把数据写回到 内存

当两个线程同时执行n 操作时,有可能其中一个线程刚执行完第一步,CPU就切换线程了,此时第二个线程执行n 的操作执行完毕重新写回内存,这时CPU重新切回第一个线程,第二个线程继续之前的执行第二步操作,因为两个线程都是在同一个n的基础上进行了 1操作,最后写会内存中的n只加了一次1,但我们执行了两次n 内存中的正确结果应该是n要加两次1,就会出现bug,想像一下,如果这种情况出现在银行存钱的时候,你和你家人同存钱,却只显示存进去了一份,这会是一件多么严重的bug,为了解决这一问题,Java也给我们提供了很多方法,我会在下篇文章详细介绍如何避免发生线程安全问题。

❤️

0 人点赞