• Java 多线程编程


    目录

    1. 进程和线程

    1.1 区别

    1.2 从JVM角度看线程

    2. 多线程实现

    2.1 继承Thread

    2.2 实现Runnable接口

    2.3 实现Callable接口

    3. 多线程常用方法

    4. 线程安全问题

    4.1 同步代码块

    4.2 同步方法

    4.3 Lock锁

    4.3 线程通信

    5. 线程池

    5.1 创建线程池

    5.2 执行Runnable任务

     5.3线程池执行Callable任务

    5.4  线程池工具类

     6. 线程生命周期

    7. 并发和并行


    1. 进程和线程

    进程:进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的整个过程。是操作系统分配资源的基本单位。

    线程:是进程的一个执行单元。也被称为轻量级进程。是处理器任务调度和处理的基本单位。

    1.1 区别

    • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
    • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
    • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
    • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
    • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
    • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
       

    1.2 从JVM角度看线程

    一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈 和 本地方法栈。 

    程序计数器为什么是私有的?

    程序计数器主要有下面两个作用:

    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

    所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

    虚拟机栈和本地方法栈为什么是私有的?

    • 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
    • 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

    所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

    一句话简单了解堆和方法区

    堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    多进程和多线程区别

    多进程:操作系统中同时运行的多个程序

    多线程:在同一个进程中同时运行的多个任务

    2. 多线程实现

    • 继承Thread类,重写run方法,无返回值
    • 实现Runnable接口,重写run方法,无返回值
    • 实现Callable接口,重写call方法,有返回值

    2.1 继承Thread

    1. // Thread类定义:
    2. public class Thread extends Object implements Runnable {}

    可以看出Thread也是实现了Rubnable接口的。 

    1. public class ThreadDemo extends Thread{
    2. private String name;
    3. public ThreadDemo(String name) {
    4. this.name = name;
    5. }
    6. @Override
    7. public void run() {
    8. for (int i = 0; i < 1000; i++) {
    9. System.out.println(this.name+">>>>>>>>>>>"+i);
    10. }
    11. }
    12. }
    13. public class Main {
    14. public static void main(String[] args) {
    15. ThreadDemo t1 = new ThreadDemo("T1");
    16. ThreadDemo t2 = new ThreadDemo("T2");
    17. ThreadDemo t3 = new ThreadDemo("T3");
    18. t1.start();
    19. t2.start();
    20. t3.start();
    21. // t1.run();
    22. // t2.run();
    23. // t3.run();
    24. }
    25. }

     继承Thread,重写run方法,运行start方法,会默认调用run方法。

    2.2 实现Runnable接口

    Thread虽然可以实现多线程,确定显而易见:面向对象的单继承局限性。接口就不一样了,可以多继承。

    1. @FunctionalInterface
    2. public interface Runnable{
    3. public void run();
    4. }

    都需要重写run方法。不过运行方法不一样。

    1. RunnableDemo t1 = new RunnableDemo("T1");
    2. RunnableDemo t2 = new RunnableDemo("T2");
    3. RunnableDemo t3 = new RunnableDemo("T3");
    4. new Thread(t1).start();
    5. new Thread(t2).start();
    6. new Thread(t3).start();

    利用Runnable方案实现更加能体现面向对象思维,有点类似于代理设计模式。

    2.3 实现Callable接口

    使用Runnable接口实现多线程可以解决单继承局限性,但没有的返回值。Callable接口可以有返回值。

    1. @FunctionalIterface
    2. public interface Callable{
    3. public T call() throws Exception;
    4. }
    1. public class CallableDemo implements Callable {
    2. private String name;
    3. public CallableDemo(String name) {
    4. this.name = name;
    5. }
    6. @Override
    7. public Integer call() throws Exception {
    8. int num = 0;
    9. for (int i = 0; i < 10; i++) {
    10. System.out.println(this.name+">>>>>>>>>>"+i);
    11. num++;
    12. }
    13. return num;
    14. }
    15. }
    16. public class Demo3 {
    17. public static void main(String[] args) throws Exception {
    18. CallableDemo t1 = new CallableDemo("t1");
    19. CallableDemo t2 = new CallableDemo("t2");
    20. CallableDemo t3 = new CallableDemo("t3");
    21. FutureTask ft1 = new FutureTask<>(t1);
    22. FutureTask ft2 = new FutureTask<>(t2);
    23. FutureTask ft3 = new FutureTask<>(t3);
    24. Thread thread1 = new Thread(ft1);
    25. thread1.start();
    26. new Thread(ft2).start();
    27. new Thread(ft3).start();
    28. System.out.println(thread1.getName()+"==================="+ft1.get());
    29. System.out.println(ft2.get());
    30. System.out.println(ft3.get());
    31. }
    32. }

    FutureTask类常用方法:

    • import java.util.concurrent.ExecutionException; // 导入ExecutionException异常包
    • public FutureTask(Callable callable) // 构造函数:接收Callable接口实例
    • public FutureTask(Runable runnable,T result) // 构造函数:接收Runnable接口实例,同时指定返回结果类型 
    • public T get() throws InterruptedException,ExecutionException // 取得线程操作返回

    Thread类的一个构造方法:

    • public Thread(FutureTask futuretask) //构造方法:接收FutureTask实例化对象

    Callable接口实现采用泛型技术实现,继承需要重写call方法,再通过FutureTask包装器包装,传入后实例化Thread类实现多线程。
    其中FutureTask类是Runnable接口的子类,所以才可以利用Thread类的start方法启动多线程,读者可以将call方法假设为有返回值的run方法。

    3. 多线程常用方法

    public static void yield()                线程让步

    setPriority()设置线程优先级,10最高,1最低,默认5

    4. 线程安全问题

    线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。

    1. public class Account {
    2. private String id; //卡号
    3. private Double money; //余额
    4. public Account() {
    5. }
    6. public Account(String id, Double money) {
    7. this.id = id;
    8. this.money = money;
    9. }
    10. public void drawMoney(double money) {
    11. //先搞清楚谁来取钱
    12. String name = Thread.currentThread().getName();
    13. if(this.money>=money) {
    14. System.out.println(name+"来取钱,成功取了:"+money);
    15. this.money-=money;
    16. } else {
    17. System.out.println(name+"来取钱,钱不够了!");
    18. }
    19. System.out.println("取钱后余额:"+this.money);
    20. }
    21. }
    22. public class DrawThread extends Thread{
    23. private Account account;
    24. public DrawThread(Account account,String name) {
    25. super(name);
    26. this.account = account;
    27. }
    28. @Override
    29. public void run() {
    30. account.drawMoney(10000);
    31. }
    32. }
    33. public class Demo4 {
    34. public static void main(String[] args) {
    35. Account acc = new Account("ID", 10000.0);
    36. DrawThread dt1 = new DrawThread(acc, "小红");
    37. DrawThread dt2 = new DrawThread(acc, "小明");
    38. dt1.start();
    39. dt2.start();
    40. }
    41. }

    运行结果:

     

     为解决上述问题,可以使用同步思想。最常见的同步思想就是加锁,意思是每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。

    Java提供了三种加锁方式

    • 同步代码块
    • 同步方法
    • Lock锁

    4.1 同步代码块

    作用就是把访问共享数据的代码锁起来,以此保证线程安全。

    1. //锁对象:必须是一个唯一的对象(同一个地址)
    2. synchronized(锁对象){
    3. //...访问共享数据的代码...
    4. }

    只需修改DrawThread类中的代码即可。

    1. public void drawMoney(double money) {
    2. //先搞清楚谁来取钱
    3. String name = Thread.currentThread().getName();
    4. synchronized (this) {
    5. if(this.money>=money) {
    6. System.out.println(name+"来取钱,成功取了:"+money);
    7. this.money-=money;
    8. } else {
    9. System.out.println(name+"来取钱,钱不够了!");
    10. }
    11. System.out.println("取钱后余额:"+this.money);
    12. }
    13. }

    锁对象如何选择?

    • 建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
    • 对于实例方法,建议使用this作为锁对象
    • 对于静态方法,建议把类的字节码(类名.class)当做锁对象

    4.2 同步方法

    其实同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行。

    1. // 同步方法
    2. public synchronized void drawMoney(double money) {
    3. // 先搞清楚是谁来取钱?
    4. String name = Thread.currentThread().getName();
    5. // 1、判断余额是否足够
    6. if(this.money >= money){
    7. System.out.println(name + "来取钱" + money + "成功!");
    8. this.money -= money;
    9. System.out.println(name + "来取钱后,余额剩余:" + this.money);
    10. }else {
    11. System.out.println(name + "来取钱:余额不足~");
    12. }
    13. }

    同步方法也是有锁对象,只不过这个锁对象没有显示的写出来而已。

    • 对于实例方法,锁对象其实是this(也就是方法的调用者)
    • 对于静态方法,锁对象时类的字节码对象(类名.class) 

    4.3 Lock锁

    Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。

    1. 1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
    2. private final Lock lk = new ReentrantLock();
    3. 2.在需要上锁的地方加入下面的代码
    4. lk.lock(); // 加锁
    5. //...中间是被锁住的代码...
    6. lk.unlock(); // 解锁
    1. // 创建了一个锁对象
    2. private final Lock lk = new ReentrantLock();
    3. public void drawMoney(double money) {
    4. // 先搞清楚是谁来取钱?
    5. String name = Thread.currentThread().getName();
    6. try {
    7. lk.lock(); // 加锁
    8. // 1、判断余额是否足够
    9. if(this.money >= money){
    10. System.out.println(name + "来取钱" + money + "成功!");
    11. this.money -= money;
    12. System.out.println(name + "来取钱后,余额剩余:" + this.money);
    13. }else {
    14. System.out.println(name + "来取钱:余额不足~");
    15. }
    16. } catch (Exception e) {
    17. e.printStackTrace();
    18. } finally {
    19. lk.unlock(); // 解锁
    20. }
    21. }
    22. }

    4.3 线程通信

    经典问题就是生产者消费者问题。

    5. 线程池

    线程池就是一个可以复用线程的技术

    假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。

    5.1 创建线程池

    在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。

    1. ExecutorService pool = new ThreadPoolExecutor(
    2. 3, //核心线程数有3个
    3. 5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
    4. 8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
    5. TimeUnit.SECONDS,//时间单位(秒)
    6. new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
    7. Executors.defaultThreadFactory(), //用于创建线程的工厂对象
    8. new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
    9. );

    临时线程什么时候创建?

    新任务提交时,发现核心线程都在忙、任务队列满了、并且还可以创建临时线程,此时会创建临时线程。

    什么时候开始拒绝新的任务? 

    核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。

    5.2 执行Runnable任务

    创建好线程池之后,接下来我们就可以使用线程池执行任务了。线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。下面的execute方法可以用来执行Runnable任务。

     

    1. public class MyRunnable implements Runnable{
    2. @Override
    3. public void run() {
    4. System.out.println(Thread.currentThread().getName()+"===========> 666");
    5. try {
    6. Thread.sleep(5000);
    7. } catch (Exception e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. }
    12. public class Demo5 {
    13. public static void main(String[] args) {
    14. ExecutorService pool = new ThreadPoolExecutor(
    15. 3, //核心线程数有3个
    16. 5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
    17. 8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
    18. TimeUnit.SECONDS,//时间单位(秒)
    19. new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
    20. Executors.defaultThreadFactory(), //用于创建线程的工厂对象
    21. new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
    22. );
    23. MyRunnable target = new MyRunnable();
    24. pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
    25. pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
    26. pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
    27. //下面4个任务在任务队列里排队
    28. pool.execute(target);
    29. pool.execute(target);
    30. pool.execute(target);
    31. pool.execute(target);
    32. //下面2个任务,会被临时线程的创建时机了
    33. pool.execute(target);
    34. pool.execute(target);
    35. // 到了新任务的拒绝时机了!
    36. pool.execute(target);
    37. }
    38. }

     

     5.3线程池执行Callable任务

    执行Callable任务需要用到下面的submit方法

    1. public class MyCallable implements Callable {
    2. private int n;
    3. public MyCallable(int n) {
    4. this.n = n;
    5. }
    6. // 2、重写call方法
    7. @Override
    8. public String call() throws Exception {
    9. // 描述线程的任务,返回线程执行返回后的结果。
    10. // 需求:求1-n的和返回。
    11. int sum = 0;
    12. for (int i = 1; i <= n; i++) {
    13. sum += i;
    14. }
    15. return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
    16. }
    17. }
    18. public class ThreadPoolTest2 {
    19. public static void main(String[] args) throws Exception {
    20. // 1、通过ThreadPoolExecutor创建一个线程池对象。
    21. ExecutorService pool = new ThreadPoolExecutor(
    22. 3,
    23. 5,
    24. 8,
    25. TimeUnit.SECONDS,
    26. new ArrayBlockingQueue<>(4),
    27. Executors.defaultThreadFactory(),
    28. new ThreadPoolExecutor.CallerRunsPolicy());
    29. // 2、使用线程处理Callable任务。
    30. Future f1 = pool.submit(new MyCallable(100));
    31. Future f2 = pool.submit(new MyCallable(200));
    32. Future f3 = pool.submit(new MyCallable(300));
    33. Future f4 = pool.submit(new MyCallable(400));
    34. // 3、执行完Callable任务后,需要获取返回结果。
    35. System.out.println(f1.get());
    36. System.out.println(f2.get());
    37. System.out.println(f3.get());
    38. System.out.println(f4.get());
    39. }
    40. }

     

    5.4  线程池工具类

    方便但是不推荐使用,这是《阿里巴巴Java开发手册》提供的强制规范要求。

     6. 线程生命周期

    • NEW: 新建状态,线程还没有启动
    • RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
    • BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
    • WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
    • TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
    • TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。 

    7. 并发和并行

    并发指一个CPU同时处理多个线程,为了所以线程都能执行到,CPU采用轮询机制为每个线程服务,由于CPU切换速度快且执行快,给我们的感觉就像这些进程在同时执行,这就是并发。

    这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

     

    并行是指同一时刻,多个线程在多个CPU上同时执行。

    就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

    最后一个问题,多线程到底是并发还是并行呢?

    其实多个线程在我们的电脑上执行,并发和并行是同时存在的。

  • 相关阅读:
    tkinter显示图片
    【毕业设计】MPU6050姿态解算 姿态估计 - 物联网 单片机 stm32
    数字孪生软件架构选BS还是CS?不,我们选择CSaaS!
    React 中的 ref 如何操作 dom节点,使输入框获取焦点
    Android gaode高德地图小人运动轨迹动态移动handler实现,不同于官网的平滑移动,可以控制速度、地图缩放、跟随小人移动后续生成视频,类似华为运动
    java计算机毕业设计ssm前途招聘求职网站的设计与实现
    代码随想录算法训练营第六十三天 |84.柱状图中最大的矩形
    VUE3照本宣科——应用实例API与setup
    C++宏函数和内联函数
    2023.11-9 hive数据仓库,概念,架构
  • 原文地址:https://blog.csdn.net/weixin_45734473/article/details/132805164