尚硅谷 java 学习 (b 站版)Day21、多线程

第八章:多线程



8-1 基本概念:程序、进程、线程 (了解即可)


程序(program):一段静态代码,静态对象。为了完成特定任务、用某种语言编写的一组指令的集合

进程(process):是程序的一次执行过程,或是在运行的一个程序

线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)

并行与并发:

并行:多个 CPU 同时执行多个任务

并发:一个 CPU 同时执行多个任务

并发:一个 CPU 同时执行多个任务 比如:秒杀、多个人做同一件事



8-2 线程的创建和使用


方式一、继承于 Thread 类

1、创建一个继承于 Thread 类的子类

2、重写 Thread 类的run()

3、创建 Thread 类的子类对象

4、通过此对象调用 start()

我们不能直接调用对象.run()来启动线程

不可以让已经 start 的线程再去执行一个新的线程;我们需要重新创建一个线程的对象

用代码来演示一下过程

/**
 * 多线程的创建,方式一:继承于 Thread 类
 * @author Jackson_kcw
 * @create 2025-02-22
 */
public class ThreadTest {
    public static void main(String[] args) {
        //3、创建 Thread类的子类的对象
        MyThread myThread = new MyThread();
        //4、通过次对象调用 start() :启动当前线程、调用当前线程的 run()
        myThread.start();
    }

}
//1、创建一个继承于 Thread 类的子类
class MyThread extends Thread{
    //2、重写 Thread 类的 run()
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {//输出 100以内的偶数
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

Thread 其中的方法

CleanShot 2025-02-22 at 16.37.14

yield():释放当前 CPU 的执行权

join():在线程 a 中调用线程 b 的 join 方法,此时线程 a 进入阻塞状态,直到线程 b 完全执行完之后,线程 a 才会结束阻塞状态

stop(): 强制结束当前线程

sleep(long millitime):让当前线程“睡眠”指定的 millitime 毫秒,在指定的时间内,当前线程为阻塞状态

线程的优先级:

1、优先级分类

MAX_PRIORITY: 10

MIN_PRIORITY:1

NORM_PRIORITY:5

2、

getPriority():获取线程的优先级

setPriority(int p):设置线程优先级

注:高优先级只是从概率上来说更可能会先执行,但不意味着绝对


方式二、创建多线程的方式二:实现 Runnable 接口

1、创建一个实现了 Runnable 接口的类

2、实现类去实现 Runnable 的抽象方法:run()

3、创建实现类的对象

4、将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象

5、通过 Thread 类的对象调用 start()

代码例子:

package com.kcw.java1;

/**创建多线程的方式二:实现 Runnable 接口
 * @author Jackson_kcw
 * @create 2025-02-23
 */
public class ThreadTest1 {
    public static void main(String[] args) {
        //3、创建实现类的对象
        MThread thread = new MThread();
        //4、将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
        Thread t1=new Thread(thread);
        //5、通过 Thread 类的对象调用 start():启动线程;调用当前线程的 run()
        t1.start();
    }
}
//1、创建一个类实现了 Runnable 接口的方法
class MThread  implements Runnable{
    //2、实现类去实现 Runnable 中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

创建线程的两种方式的比较

开发中:优先选择:实现 Runnable 接口的方式

原因:1、实现的方式没有类的单继承性的局限性

​ 2、实现的方式更适合来处理多个线程共有数据的情况

联系:Thread 也是实现 Runnable

相同点:两者都需要重写 run 方法,将线程要执行的逻辑声明在 run()中



8-3 线程的生命周期


完整生命周期的几种状态

CleanShot 2025-02-23 at 14.13.00

线程生命周期

CleanShot 2025-02-23 at 14.21.02



8-4 线程的同步


线程安全问题:当某个线程在运行过程中,另一个线程进入运行状态,会影响到共享数据;如卖票 会出现负数情况

如何解决线程安全问题:当一个线程 a操作共享数据数据的时候,其他的线程不能参与进来,直到线程 a操作完共享数据,其他线程才可以开始操作共享数据,即使线程 a 出现阻塞,也不能改变

在 Java 中,我们通过同步机制,解决线程的安全问题

方式一:同步代码块

synchronized(同步监视器){

​ //需要被同步的代码

}

说明:1、操作共享数据的代码即为 需要被同步的代码 ---->不能包含代码多了,也不能包少了

​ 2、共享数据:多个线程共同操作的变量。比如卖票问题里面的 ticket

​ 3、同步监视器:俗称锁。任何一个类的对象都可以充当锁

要求:多个线程必须要共用一把锁

补充:在实现 Runnable接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器

在继承 Thread 类创建多线程的方式中,慎用使用 this 充当同步监视器,可以考虑使用当前类充当同步监视器

方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步

1、同步方法仍然涉及到同步监视器,只是不需要我们显式声明

2、非静态的同步方法,同步监视器是:this

​ 静态的同步方法,同步监视器是:当前类本身

同步方法的格式如下代码:show()方法的格式

package java1;

/**
 * @author Jackson_kcw
 * @Time 2025-02-23
 */
public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w = new Window4();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.start();
        t2.start();
        t3.start();

    }
}
class Window4 implements Runnable{
    private static int ticket=100;
    @Override
    public void run() {
        while(true){

            show();
        }
    }
    public synchronized void  show(){//同步方法  如果这个是继承 Thread 会创建几个对象,需要加 static 将此方法转为静态
        if(ticket>0){
            System.out.println(Thread.currentThread().getName()+"卖票:"+ticket);
            ticket--;
        }

    }
}

同步的方式,解决了线程的安全;但是在操作同步代码时,只能一个线程参与,其他线程等待,效率会低一点

补充:之前的单例模式中的懒汉解决线程安全问题

mac 电脑中 ide 包裹代码块的快捷键为:option+command+T

/**使用同步机制将单例模式里面的懒汉式改写成线程安全的
 * @author Jackson_kcw
 * @Time 2025-02-23
 */
public class BankTest {


}
class Bank{
    private Bank(){

    }
    private static Bank instance=null;
    public  synchronized Bank getInstance(){
        //方式一:效率不高
//        synchronized (Bank.class) {
//            if(instance==null){
//                instance=new Bank();
//                return instance;
//            }
//            else return instance;
//        }

        //方式二:效率更高
        if(instance==null){
            synchronized (Bank.class) {
            if(instance==null){
                instance=new Bank();
                 }
            }
        }
        return instance;
    }

}


死锁问题

CleanShot 2025-02-23 at 16.11.00

我们使用同步时,要避免死锁

解决线程安全问题的方式三:Lock 锁-------JDK5 新增的

例如:

package Lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Jackson_kcw
 * @Time 2025-02-23  PM4:52
 */
public class LockTest {
    public static void main(String[] args) {
        Window window=new Window();
        Thread thread1=new Thread(window);
        Thread thread2=new Thread(window);
        Thread thread3=new Thread(window);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}
class Window implements Runnable{
    private int ticket=100;
    //1、实例化 ReentrantLock
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while(true){
    //2、调用锁定方法 lock
            lock.lock();
            try {
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+"卖票:"+ticket);
                    ticket--;
                }
                else break;
            } finally {
                //3、调用解锁方法 unlock()
                lock.unlock();
            }

        }

    }
}

面试题:synchronized 于 Lock 的异同

相同:两者都是解决线程安全的方法

不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监察器

​ Lock 需要手动的启动同步(lock()),同时结束同步也需要手动的实现解锁( unlock() )

优先使用的顺序

Lock–>同步代码块(已经进入了方法体,分配了相应资源)–>同步方法(在方法体之外)

如何解决线程安全问题?有哪几种方式

答:1、使用 synchronized 关键字,用于代码块或方法(即同步代码块、同步方法)

​ 2、使用 ReentrantLock(可重入锁),lock()与 unlock()手动设置锁和解除锁



8-5 线程的通信


线程通信涉及到三个方法:

wait():使线程进入阻塞状态,并释放同步监视器

notify():唤醒被 wait 的一个线程,如果有多个线程被 wait,就唤醒优先级高的

notifyAll():唤醒所有 wait 的线程

以下见代码例子:

package Java2Communication;

/**线程通信的例子:使用两个线程打印 1-100.线程一、线程二交替打印
 * @author Jackson_kcw
 * @Time 2025-02-24  PM2:04
 */
public class Communication {
    public static void main(String[] args) {
        Number n1=new Number();
        Thread t1=new Thread(n1);
        Thread t2=new Thread(n1);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class Number implements Runnable{
    private int number=1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                //唤醒 阻塞的线程
                notify();
                if(number<=100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                else break;
            }
        }
    }
}

注意点

1、wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中

2、这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException异常

3、这三个方法都定义在 Java.lang.Object 类中

面试题

sleep()和 wait()的异同:

同:一旦执行方法,都可以使当前的线程进入阻塞状态

异:1、两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()

​ 2、调用的范围要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块

​ 3、关于是否释放同步监视器的问题:如果两个方法都用在同步代码块或同步方法中,sleep 不会释放 锁(同步监视器),但是 wait()会释放锁



8-6 JDK5.0 新增线程创建方式


新增方式一:实现 Callable 接口

CleanShot 2025-02-24 at 14.40.11

创建步骤:

1、创建一个实现 Callable 的实现类

2、实现 call 方法,将此线程需要执行的操作声明在 call()中

3、创建 Callable 接口实现类的对象

4、将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象

5、将 FutureTask 的对象作为参数传递到 Thread类的构造器中,创建 Thread 对象,并调用 start();

6、获取 Callable 中的 call 方法的返回值

在代码中如下:

package Java3;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**创建线程的实现方式三:实现 Callable接口
 * @author Jackson_kcw
 * @Time 2025-02-24  PM2:40
 */
public class ThreadNew {
    public static void main(String[] args) {
        //3、创建 Callable 接口实现类的对象
        NumberThread numberThread = new NumberThread();
        //4、将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象
       FutureTask futureTask= new FutureTask(numberThread);
       //5、将 FutureTask 的对象作为参数传递到 Thread类的构造器中,创建 Thread 对象,并调用 start();
        new Thread(futureTask).start();
        try {
            //6、获取 Callable 中的 call 方法的返回值
            //get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值
            Object sum=futureTask.get();
            System.out.println("总和为:"+sum);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

    }
}
//1、创建一个实现 Callable 的实现类
class NumberThread implements Callable {
    //2、实现 call 方法,将此线程需要执行的操作声明在 call()中
    @Override
    public Object call() throws Exception {
        int sum=0;
        for (int i = 1; i <=100 ; i++) {
            if(i%2==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }
}

二、如何理解实现 Callable 接口的方式创建多线程比实现Runnable接口要强大

1、call()方法可以有返回值

2、call()方法可以抛出异常,被外面的操作捕获,获取异常信息

3、Callable 是支持泛型的


新增方式二:使用线程池

CleanShot 2025-02-24 at 15.18.45

CleanShot 2025-02-24 at 15.32.57


利用线程池方式创建线程的步骤:

1、提供指定线程数量的线程池

2、执行指定的线程的操作。需要提供实现 Runnable 或 Callable 接口的实现类

3、关闭连接池子

具体的代码例子

package Java3;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**创建线程的方式四:使用线程池的方式
 * @author Jackson_kcw
 * @Time 2025-02-24  PM3:31
 */
public class ThreadPool {
    public static void main(String[] args) {
        //1、提供指定线程数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
      
      	
      
      
        //2、执行指定的线程的操作。需要提供实现 Runnable 或 Callable 接口的实现类
        executorService.execute(new NumberThread2());//适合使用于 Runnable
        executorService.submit(new FutureTask(new NumberThread1())); //适合适用于 Callable

        executorService.shutdown();//3、关闭连接池子
    }

}
class NumberThread1  implements  Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName()+"  "+i);
            }
        }
        return null;
    }


}
class NumberThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }

        }
    }
}


面试题:创建多线程的方式?四种

方式一、继承于 Thread 类

方式二、实现 Runnable 接口

方式三:实现 Callable 接口

方式四:使用线程池(实际开发中常用)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值