• 【JAVA进阶】多线程


    📃个人主页个人主页

    🔥系列专栏:JAVASE基础

    前言:

    什么是线程?

    线程(thread)是一个程序内部的一条执行路径。

    我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

    1. public class ThreadTest {
    2. public static void main(String[] args) {
    3. for (int i = 0; i <= 5; i++) {
    4. System.out.println("主线程Main输出"+i);
    5. }
    6. }
    7. }

    程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

    多线程是什么?

    多线程是指从软硬件上实现多条执行流程的技术。

    多线程用在哪里,有什么好处

    再例如:消息通信、淘宝、京东系统都离不开多线程技术。

    一、多线程的创建

    方式一:继承Thread类

    Java是通过java.lang.Thread 类来代表线程的。

    按照面向对象的思想,Thread类应该提供了实现多线程的方式。

    ①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

    1. public class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i <= 5; i++) {
    5. System.out.println("子线程MyThread输出"+i);
    6. }
    7. }
    8. }

    ②创建MyThread类的对象

    ③调用线程对象的start()方法启动线程(启动后还是执行run方法的)

    1. public class ThreadTest {
    2. public static void main(String[] args) {
    3. Thread t=new MyThread();
    4. t.start();
    5. for (int i = 0; i <= 5; i++) {
    6. System.out.println("主线程Main输出"+i);
    7. }
    8. }
    9. }

    方式一优缺点: 

    优点:编码简单

    缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

    1、为什么不直接调用了run方法,而是调用start启动线程。

    直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。 只有调用start方法才是启动一个新的线程执行。

    2、把主线程任务放在子线程之前了。

    这样主线程一直是先跑完的,相当于是一个单线程的效果了。

    方式二:实现Runnable接口 

    ①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

    1. public class MyRunnable implements Runnable{
    2. @Override
    3. public void run() {
    4. for (int i = 0; i <= 5; i++) {
    5. System.out.println("子线程Runnable输出"+i);
    6. }
    7. }
    8. }

    ②创建MyRunnable任务对象

    ③把MyRunnable任务对象交给Thread处理。

    ④调用线程对象的start()方法启动线程

    1. public class ThreadTest2 {
    2. public static void main(String[] args) {
    3. Runnable target=new MyRunnable();
    4. new Thread(target).start();
    5. for (int i = 0; i <= 5; i++) {
    6. System.out.println("主线程出"+i);
    7. }
    8. }
    9. }

    Thread的构造器 

    方式二优缺点:

    优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

    缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

    多线程的实现方案二:实现Runnable接口(匿名内部类形式)

    1. public class ThreadTest3 {
    2. public static void main(String[] args) {
    3. new Thread(()->{
    4. for (int i = 0; i <= 5; i++) {
    5. System.out.println("子线程Runnable输出"+i);
    6. }
    7. }
    8. ).start();
    9. for (int i = 0; i <= 5; i++) {
    10. System.out.println("主线程出"+i);
    11. }
    12. }
    13. }

    方式三:JDK 5.0新增:实现Callable接口 

    1、前2种线程创建方式都存在一个问题:

    • 他们重写的run方法均不能直接返回结果。
    • 不适合需要返回线程执行结果的业务场景。

    2、怎么解决这个问题呢?

    • JDK 5.0提供了Callable和FutureTask来实现。
    • 这种方式的优点是:可以得到线程执行的结果。

    多线程的实现方案三:利用Callable、FutureTask接口实现。

    ①得到任务对象

    • 定义类实现Callable接口,重写call方法,封装要做的事情。
    1. import java.util.concurrent.Callable;
    2. public class MyCallable implements Callable{
    3. private int n=0;
    4. public MyCallable(int n) {
    5. this.n = n;
    6. }
    7. @Override
    8. public String call() throws Exception {
    9. int sum=0;
    10. for (int i = 0; i <=n; i++) {
    11. sum+=i;
    12. }
    13. return "子线程求和:"+sum;
    14. }
    15. }
    • 用FutureTask把Callable对象封装成线程任务对象。
    1. MyCallable myCallable = new MyCallable(101);
    2. FutureTask task = new FutureTask(myCallable);

    ②把线程任务对象交给Thread处理。

    ③调用Thread的start方法启动线程,执行任务

    ④线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

    1. public class ThreadTest4 {
    2. public static void main(String[] args) throws Exception {
    3. MyCallable myCallable = new MyCallable(101);
    4. FutureTask task = new FutureTask(myCallable);
    5. new Thread(task).start();
    6. System.out.println(task.get());
    7. }
    8. }

    FutureTask的API

     方式三优缺点:

    • 优点:
    • 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
    • 可以在线程执行完毕后去获取线程执行的结果。

    • 缺点:编码复杂一点。

    二、Thread的常用方法

    Thread常用API说明

    • Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
    • 至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会在高级篇以及后续需要用到的时候再为大家讲解。 

     注意:

    1、此方法是Thread类的静态方法,可以直接使用Thread类调用。  

    2、这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。

     ·

    1. public class ThreadTest5 {
    2. public static void main(String[] args) {
    3. Thread thread1 = new MyThread("子线程1");//Thread-0
    4. thread1.start();
    5. System.out.println(thread1.getName());
    6. Thread thread2 = new MyThread("子线程2");//Thread-1
    7. thread2.start();
    8. System.out.println(thread2.getName());
    9. Thread thread = Thread.currentThread();//main
    10. thread.setName("主线程");
    11. System.out.println(thread.getName());
    12. for (int i = 0; i <= 5; i++) {
    13. System.out.println(thread.getName()+"输出:"+i);
    14. }
    15. }
    16. }
    1. public class MyThread extends Thread {
    2. public MyThread(String name) {
    3. super(name);
    4. }
    5. public MyThread() {
    6. }
    7. @Override
    8. public void run() {
    9. Thread thread = Thread.currentThread();
    10. for (int i = 0; i <= 5; i++) {
    11. System.out.println("子线程"+thread.getName()+"输出:"+i);
    12. }
    13. }
    14. }

    在Java中,Thread.sleep()方法用于使当前线程暂停执行指定的时间。这个方法通常用于实现多线程之间的协作,例如等待其他线程完成某些操作。

    Thread.sleep()方法接受一个表示毫秒数的参数,表示当前线程应该暂停执行的时间。例如,以下代码将使当前线程暂停5秒钟:

    1. try {
    2. Thread.sleep(5000); // 5000毫秒 = 5秒
    3. } catch (InterruptedException e) {
    4. // 处理中断异常
    5. }

    当线程调用Thread.sleep()方法时,它将被阻塞,直到指定的时间过去。在阻塞期间,线程不会执行任何代码,也不会消耗CPU资源。但是,需要注意的是,Thread.sleep()方法可能会抛出InterruptedException异常,因此需要在调用时捕获该异常。

    在使用Thread.sleep()方法时,需要注意以下几点:

    1. Thread.sleep()方法不会释放任何锁资源,如果当前线程持有锁,则其他线程无法访问被锁定的资源。
    2. Thread.sleep()方法的参数是一个整数,表示毫秒数。如果需要暂停更长的时间,可以考虑使用TimeUnit类来避免计算错误。
    3. 在使用Thread.sleep()方法时,需要注意线程安全问题。如果多个线程同时访问共享资源,可能会导致竞争条件。为了避免这种情况,可以考虑使用synchronized关键字或其他同步机制来确保线程安全。

    在Java中,Thread.join()方法用于等待该线程终止。在调用join()方法时,当前线程将被阻塞,直到该线程终止。这通常用于确保在主线程中执行某些操作之前,其他线程已经完成它们的任务。

    例如,假设有两个线程A和B,线程B必须在线程A完成后才能开始执行。在这种情况下,可以使用join()方法来实现同步:

    1. ThreadA.start();
    2. ThreadA.join(); // 等待ThreadA终止
    3. ThreadB.start();

    这将确保线程B在线程A终止之前不会开始执行。

    需要注意的是,join()方法可能会抛出InterruptedException异常,因此需要在调用时捕获该异常。

    三、线程安全

    线程安全问题是指在多线程环境下,多个线程同时访问和修改共享数据时可能导致的问题。

    取钱模型演示:

    需求:有一对夫妻,他们有一个共同的账户,余额是一千元。

    如果2人同时来取钱,而且2人都要取600元,可能出现什么问题呢?

    1. class BankAccount {
    2. private double balance;
    3. public BankAccount(double balance) {
    4. this.balance = balance;
    5. }
    6. public void withdraw(double amount){
    7. if (balance >= amount) {
    8. System.out.println(Thread.currentThread().getName() + "来取钱" + amount );
    9. balance -= amount;
    10. System.out.println(Thread.currentThread().getName() + "成功取出" + amount + "元,当前余额为:" + balance);
    11. } else {
    12. System.out.println(Thread.currentThread().getName() + "取款失败,余额不足!");
    13. }
    14. }
    15. }
    16. class WithdrawThread extends Thread {
    17. private BankAccount account;
    18. private double amount;
    19. public WithdrawThread(BankAccount account, double amount) {
    20. this.account = account;
    21. this.amount = amount;
    22. }
    23. @Override
    24. public void run() {
    25. account.withdraw(amount);
    26. }
    27. }

    1. public class WithdrawDemo {
    2. public static void main(String[] args) {
    3. BankAccount account = new BankAccount(1000);
    4. WithdrawThread thread1 = new WithdrawThread(account, 600);
    5. WithdrawThread thread2 = new WithdrawThread(account, 600);
    6. thread1.start();
    7. thread2.start();
    8. }
    9. }

    结果:2人都取钱600,银行亏了200。 

    四、线程同步

    同步思想概述

    取钱案例出现问题的原因?

    多个线程同时执行,发现账户都是够钱的。

    如何才能保证线程安全呢?

    让多个线程实现先后依次访问共享资源,这样就解决了安全问题

    线程同步的核心思想

    加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

    方式一:同步代码块

    作用:把出现线程安全问题的核心代码给上锁。

    原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

    在Java中,可以使用synchronized关键字定义一个同步代码块。这个关键字可以与任何对象一起使用,而这个对象就被当作锁对象。当一个线程进入同步代码块,它会锁住这个锁对象,直到它离开这个代码块时才会释放这个锁。在这个线程持有锁的期间,其他任何尝试获取这个锁的线程都会被阻塞,直到锁被释放。

    以下是Java中使用synchronized关键字的一个例子:

    1. public class MyClass {
    2. private Object lock = new Object();
    3. public void myMethod() {
    4. synchronized(lock) {
    5. // 在这里的代码只能由一个线程同时执行
    6. // 对共享数据的访问和操作都在这里进行
    7. }
    8. }
    9. }

     用在上述取钱案例中:

    1. public void withdraw(double amount){
    2. synchronized (this){
    3. if (balance >= amount) {
    4. System.out.println(Thread.currentThread().getName() + "来取钱" + amount );
    5. balance -= amount;
    6. System.out.println(Thread.currentThread().getName() + "成功取出" + amount + "元,当前余额为:" + balance);
    7. } else {
    8. System.out.println(Thread.currentThread().getName() + "取款失败,余额不足!");
    9. }
    10. }
    11. }

    同步代码块的同步锁对象有什么要求?     

    • 对于实例方法建议使用this作为锁对象。
    • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

    方式二:同步方法

    作用:把出现线程安全问题的核心方法给上锁。

    原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

    同步方法是指在多线程编程中,使用synchronized关键字修饰的方法。它可以确保在任何时候只有一个线程可以访问该方法,从而避免多线程并发操作导致的数据不一致和其他问题。

    在Java中,定义同步方法有两种方式:

    1.在方法声明中使用synchronized关键字,例如:

    1. public synchronized void myMethod() {
    2. // 在这里的代码只能由一个线程同时执行
    3. // 对共享数据的访问和操作都在这里进行
    4. }

    2.使用静态synchronized方法,在方法名前加上static关键字,例如:

    1. public static synchronized void myMethod() {
    2. // 在这里的代码只能由一个线程同时执行
    3. // 对共享数据的访问和操作都在这里进行
    4. }

    静态同步方法只能同步静态方法,而不能同步非静态方法。和非静态同步方法一样,静态同步方法也可以用锁来控制多线程的访问,避免并发问题。需要注意的是,静态同步方法的锁对象和实例对象的锁对象是不同的。如果一个类中有多个静态同步方法,它们之间共享的锁对象是同一个,而不同实例对象的锁对象是不同的。

    同步方法底层原理

    • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
    • 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
    • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

    方式三:Lock锁

    Lock锁是Java中用于控制多个线程对共享资源访问的工具。与synchronized方法和语句相比,Lock锁提供了更广泛的锁定操作和更灵活的结构。Lock锁允许完全不同的属性,并且可能支持多个关联的Condition对象。

    Lock锁接口的实现允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁,从而允许使用此类技术。 Lock锁的底层实现有多种方式,例如ReentrantLock和ReentrantReadWriteLock,其中ReentrantLock是可重入的,允许线程在完成任务后继续占用锁,直到锁的变量为0。

    使用Lock锁时,需要注意以下几点:

    1. Lock锁的使用和释放应该手动进行,比synchronized更加灵活。
    2. Lock锁可以中断获取锁和超时获取锁。
    3. Lock锁可以支持公平和非公平锁,其中公平锁按照线程请求锁的顺序分配,而非公平锁则允许其他线程插队。
    4. Lock锁的使用可能会带来额外的责任,例如需要处理死锁等问题。
    5. Lock锁的实现可以提供与隐式监视器锁完全不同的行为和语义,例如保证排序、不可重入使用或死锁检测等。
    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class Counter {
    4. private int count = 0;
    5. private Lock lock = new ReentrantLock();
    6. public void increment() {
    7. lock.lock(); // 获取锁
    8. try {
    9. count++;
    10. } finally {
    11. lock.unlock(); // 释放锁
    12. }
    13. }
    14. public int getCount() {
    15. return count;
    16. }
    17. }

    在这个例子中,我们使用了ReentrantLock,它是一种可重入的互斥锁。我们用它来保护对count的并发访问,这样多个线程就不会同时修改它。我们在increment()方法中获取和释放锁,确保在这个方法中的代码块在任何时候只能由一个线程执行。

  • 相关阅读:
    懒加载
    股票预测和股票分析就用FineBI!
    关于c#:displayname属性
    windows 11 install windows subsystem for android
    领域里的X能力是什么?
    h5+js 移动端监听点击、移动、松开,获取鼠标位置
    【蓝桥杯省赛真题37】Scratch三国演义字数统计 少儿编程scratch编程蓝桥杯省赛真题讲解
    集卡拖车运输最新政策调整来了_箱讯科技
    Docker常见操作
    异质图神经网络(HGNN)常用数据集信息统计(持续更新ing...)
  • 原文地址:https://blog.csdn.net/Javascript_tsj/article/details/133036793