Java详解---多线程

目录

一、线程的创建方式

1.继承Thread类

实现步骤

代码演示

实现一个方法:

 实现多个方法:

优点

缺点

2.实现Runnable接口

实现步骤

代码演示

优点

缺点

3.实现Callable接口

实现步骤

代码演示

优点

缺点

总结 

 二、线程的执行原理

 三、线程生命周期

 四、并发

示例

语法:

场景示例

​编辑 

 同步

 同步处理(解决示例中售票问题)

1.继承Thread类创建线程

2.实现Runnable接口创建线程

Synchronized如果放在对象方法上 

休眠

线程间的通讯

线程的优先级

加入线程

让出线程

守护线程

死锁

 单例&多例

wait()&sleep()¬ify()¬ifyAll()

1. 方法所属

2. 方法的作用

(1) wait()

(2) notify()

(3) notifyAll()

(4) sleep()

总结:


多线程是并发编程中的一种重要实现方式,在讲解多线程之前先了解一下线程和进程。

在操作系统理论中,进程(Process)是资源分配的独立单位,线程(Thread)是资源调度的独立单位;以现实中的场景为例子,就好比是QQ和里面的每一个聊天框,QQ是得到系统独立的资源分配,类进程机制,同时里面每个聊天框互不干扰,独立进行任务,类线程机制。

一、线程的创建方式

在Java中,创建线程主要有三种方式:通过继承Thread类、实现Runnable接口,以及实现Callable接口。这三种方式各有特点和适用场景,下面逐一演示。

1.继承Thread

这是最直接的创建线程的方式。通过继承Thread类并重写run()方法,可以定义线程的执行逻辑。

实现步骤

  1. 定义一个类继承Thread

  2. 重写run()方法:在run()方法中定义线程的具体任务。

  3. 创建线程对象并启动:通过调用start()方法启动线程。

代码演示

实现一个方法:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程正在运行:" + Thread.currentThread().getName());
    }
}

//测试(懒得重写一个Test类new MyThread测试了,就在本类中写Main方法凑活看)
public class Main {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread1.start();  // 启动线程
        myThread2.start();  // 启动线程
    }
}

注:在 run() 方法中调用 Thread.currentThread().getName(),因为当前执行代码的线程是新启动的 myThread 线程,而在代码里并没有显式地给这个线程设置名称,所以 Java 会给它分配一个默认名称。默认情况下,线程的名称格式为 Thread-X,这里的 X 是一个整数,从 0 开始依次递增。

得到结果:

 实现多个方法:

当然,重写Run方法来实现线程需要的功能并不意味着只能实现一个方法一个功能,还可以用下面的方法实现多个方法:

class MyThread extends Thread {
    // 自定义方法 1,用于执行功能 1
    private void function1() {
        System.out.println("执行功能 1");
    }

    // 自定义方法 2,用于执行功能 2
    private void function2() {
        System.out.println("执行功能 2");
    }

    @Override
    public void run() {
        function1();
        function2();
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
优点
  • 实现简单,直接继承Thread类并重写run()方法即可。

缺点

  • 单继承限制:Java不支持类的多继承,如果一个类已经继承了其他类,就不能再继承Thread类。

  • 功能局限性Thread类本身已经包含了线程的实现逻辑,继承Thread类可能会限制线程类的扩展性。

2.实现Runnable接口

这是更推荐的方式,因为它避免了单继承的限制(一个类可以同时实现多个接口),并且可以更好地分离线程的执行逻辑和线程的控制。

实现步骤

  1. 定义一个类实现Runnable接口

  2. 实现run()方法:在run()方法中定义线程的任务。

  3. 创建Thread对象并传入Runnable实例:通过Thread类的构造函数将Runnable实例传递给线程。

  4. 启动线程:调用Thread对象的start()方法。

代码演示

public class DownLoad implements Runnable {

    private String name;

    public DownLoad(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(name+"下载了"+i+"%");
        }
    }
}

测试TestMain:

public class ThreadTest {

    public static void main(String[] args) {
        //创建线程对象
        Thread t = new Thread(new DownLoad("Thread1"));
        Thread t1 = new Thread(new DownLoad("Thread2"));
        t.start();
        t1.start();
    }
}

注:上面Main函数中可以这种格式创建线程对象是因为:Thread 类提供了多个构造方法,其中有一个构造方法的签名如下: public Thread(Runnable target, String name)

得到结果:

由于创建线程本质上还是Thread类,所以可以看到用继承Runnable接口的方式创建线程在Main方法中的实现中是new Thread,并在 new出的Thread放入实现了Runnable接口的DownLoad类;

在继承Thread类创建线程的方法中,Main函数中可以直接new这个继承了Thread类的子类,这就涉及子类可以继承父类的属性了。

优点
  • 避免单继承限制:可以与任何其他类组合使用。

  • 更好的扩展性Runnable接口的实现类可以专注于任务逻辑,而线程的控制由Thread类负责。

缺点
  • 代码稍显复杂:需要额外创建一个Thread对象来启动线程(上面解释过了,为什么实现Runnable接口会比继承Thread类的方法更加复杂)。

3.实现Callable接口

Callable接口是Runnable接口的增强版本,它允许线程执行完成后返回一个结果,并且可以抛出异常。Callable接口通常与FutureExecutorService一起使用,适用于需要线程返回结果的场景。

实现步骤

  1. 定义一个类实现Callable接口

  2. 实现call()方法:在call()方法中定义线程的任务,并返回一个结果。

  3. 使用ExecutorService提交任务:通过ExecutorServicesubmit()方法提交Callable任务。

  4. 获取结果:通过Future对象获取线程的执行结果。

代码演示

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("线程正在运行:" + Thread.currentThread().getName());
        return 42;  // 返回一个结果(适用于有线程有返回值的情况)
    }
}

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(new MyCallable());  // 提交Callable任务
        Integer result = future.get();  // 获取线程的执行结果
        System.out.println("线程返回的结果:" + result);
        executor.shutdown();  // 关闭线程池
    }
}

注:Thread.currentThread().getName()语句会输出当前线程的名称;由于使用的是单线程线程池,默认的线程名称格式为 pool-X-thread-Y,这里 X 表示线程池的编号(通常从 1 开始),Y 表示线程的编号(在单线程线程池中通常为 1),所以输出为 pool-1-thread-1。 

【注意:这里返回格式和是否使用线程池创建或是直接使用 Thread 类创建有关。】

得到结果:

优点
  • 返回结果:线程执行完成后可以返回一个结果。

  • 异常处理call()方法可以抛出异常,便于处理线程中的错误。

  • 线程池支持:通常与ExecutorService结合使用,可以更好地管理线程资源。

缺点
  • 复杂度较高:需要使用ExecutorServiceFuture来管理线程和结果,代码相对复杂。

  • 适用场景有限:适用于需要线程返回结果的场景,对于简单的线程任务可能过于复杂。

总结 

方式优点缺点适用场景
继承Thread实现简单,直接使用Thread单继承限制,功能有限简单的线程任务,不需要返回结果
实现Runnable接口避免单继承限制,更好的扩展性需要额外创建Thread对象大多数并发任务,不需要返回结果
实现Callable接口支持返回结果和异常处理,与线程池结合使用代码复杂,需要使用ExecutorServiceFuture需要线程返回结果的复杂任务

 二、线程的执行原理

线程的并发执行通过多个线程不断的切换CPU的资源,这个速度非常快,我们感知不到,我们能感知到的就是三个线程在并发的执行。

如下,CPU在三个线程中快速切换。

 三、线程生命周期

1.新建: 线程被new出来

2.准备就绪:线程具有执行的资格,即线程调用了start(),没有执行的权利

3.运行:具备执行的资格和具备执行的权利(获取到CPU)

4.阻塞:没有执行的资格和执行权利

5.销毁: 线程的对象变成垃圾,释放资源。

 四、并发

示例

互联网的项目中存在着大量的并发的案例,如卖火车票,电商网站。

范例:火车站有100张票,4个窗口同时买票。

分析:4个窗口是4个线程同时在运行,100票是4个线程的共享资源。

锁的概念在并发编程中应用极为广泛,是解决多线程并发访问共享资源时数据不一致和竞态条件等问题的关键技术。

语法:

synchronized(锁对象){

//操作共享资源的代码

}

场景示例

此时的厕所作为共享资源就会被上锁。

 同步

同步代码加在什么地方?

1.代码被多个线程访问

2.代码中有共享的数据

3.共享数据被多条语句操作。

 同步处理(解决示例中售票问题)

1.继承Thread类创建线程
public class SaleTicketThread extends Thread {

    private String name;

    /**
     * 定义共享的数据100张票
     */
    static int tickets = 100; //注意这里需要设置static

    //创建一个锁对象,这个对象是多个线程对象共享的数据
    static Object obj = new Object();


    public SaleTicketThread(String name) {
        this.name = name;


    }

    @Override
    public void run() {

        //卖票是持续的
        while (true){

            synchronized (obj){
                if(tickets > 0){
                    System.out.println(name+"卖出座位是"+(tickets--)+"号");
                }else{
                    break;
                }
            }

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        System.out.println(name+"卖票结束");
    }
}

TestMain测试:

public class ThreadTest {

    public static void main(String[] args) {
        SaleTicketThread t1 = new SaleTicketThread("窗口1");
        SaleTicketThread t2 = new SaleTicketThread("窗口2");
        SaleTicketThread t3 = new SaleTicketThread("窗口3");
        SaleTicketThread t4 = new SaleTicketThread("窗口4");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
2.实现Runnable接口创建线程
public class SaleTicket implements Runnable {

    /**
     * 多个线程共享的100张票
     */
    int tickets = 100; //注意实现Runnable接口实现同步不用再对共享资源设置static(因为Main中只需要创建一个买票的类)

    //创建一个锁对象,这个对象是多个线程对象共享的数据
    Object obj = new Object();

    @Override
    public void run() {
        //卖票是持续的
        while (true){
            synchronized (obj){
                if(tickets > 0){
                    System.out.println(Thread.currentThread().getName()+"卖出座位是"+(tickets--)+"号");
                }else{
                    break;
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        System.out.println(Thread.currentThread().getName()+"卖票结束");
    }


}

TestMain测试:

public class ThreadTest {

    public static void main(String[] args) {
        //创建一个卖票的对象
        SaleTicket st = new SaleTicket();

        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");
        Thread t4 = new Thread(st, "窗口4");

        t1.start();
        t2.start();
        t3.start();
        t4.start();


    }
}

Synchronized如果放在对象方法上 

在 Java 中,当 synchronized 关键字放在对象方法上时,该方法会成为同步方法,这意味着同一时间只能有一个线程访问该方法 。

Synchronized如果在类方法上那么锁对象就是类的类对象。

休眠(从这里开始,后面的先了解,后续并发中再详细介绍)

在同步代码块中休眠,线程不会释放锁且不会让出CPU,可能导致其他线程等待锁而阻塞,降低并发效率;而在非同步代码块中休眠,线程仅暂停执行,不会影响其他线程,但也不会让出CPU资源。

线程间的通讯

 生产者生成水果,如果水果没有被买走那么就不生产处于等待状态,如果水果被消费者买走这时候消费者会通知生产者告诉他我们已经把水果买走了请生产,消费者同理,如果水果已经生产出来那么就买走,买走之后再通知生产者水果已经没了请生产。

注意:

1.线程间的通信共享数据一定要有同步代码块synchronized

2.一定要有waitnotify,而且二者一定是成对出现。

3.生产者和消费者的线程实现一定是在whiletrue)里面。

线程的优先级

我们可以通过public final void setPriority(int newPriority)

来设置线程的优先级,但是优先级并不是绝对的,只是先对来说比其他的线程得到CPU的资源机会多一些。(当然这个优先级也只是相对的优先级,毕竟在时间片轮转中总会切走CPU的)

加入线程

join线程会抢先拿到CPU来执行线程,然后其他的线程再来执行

加入线程必须要在先执行的线程的start下面来执行。

让出线程

当前的线程从运行阶段回到就绪阶段,目的是把CPU的资源让给其他的线程。

守护线程

守护线程会随着主线程的结束而结束。

死锁

 单例&多例

new出的对象都对应着不同的地址,这就叫多例模式;

单例模式:控制new出的对象是一个。

单例模式的好处:

  1. 在单例模式中,活动的单例只有一个实例,如果单例类的所有实例化得到的都是相同的一个实例,这样就防止了其它对象对自己的实例化,确保所有的对象都能访问一个实例。
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上具有相应的伸缩性。
  3. 提供了对唯一实例的受控访问。
  4. 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁对象时单例模式无疑可以提高系统性能。
  5. 允许可变数目的实例。
  6. 避免了对共享资源的多重占用。

wait()&sleep()&notify()&notifyAll()

1. 方法所属

  • wait()notify()notifyAll():这些方法属于Object类,是所有Java对象的内置方法。

  • sleep():属于Thread类,用于线程控制。

2. 方法的作用

(1) wait()
  • 作用:使当前线程进入等待状态,并释放当前对象的锁。线程会暂停执行,直到被其他线程唤醒(通过notify()notifyAll())。

  • 使用场景:常用于线程间的协作,例如生产者-消费者模型中,消费者线程等待生产者线程的通知。

  • 示例

    synchronized (obj) {
        obj.wait();  // 当前线程等待,释放obj的锁
    }
  • 注意:只能在同步代码块或同步方法中使用,否则会抛出IllegalMonitorStateException

(2) notify()
  • 作用:唤醒一个正在等待当前对象锁的线程(随机选择一个线程)。

  • 使用场景:用于通知等待的线程继续执行,例如生产者线程通知消费者线程。

  • 示例

    synchronized (obj) {
        obj.notify();  // 唤醒一个等待obj锁的线程
    }
  • 注意:只能在同步代码块或同步方法中使用。

(3) notifyAll()
  • 作用:唤醒所有正在等待当前对象锁的线程。

  • 使用场景:当多个线程等待同一个对象锁时,需要唤醒所有线程,例如在某些复杂的线程协作场景中。

  • 示例

    synchronized (obj) {
        obj.notifyAll();  // 唤醒所有等待obj锁的线程
    }
  • 注意:只能在同步代码块或同步方法中使用。

(4) sleep()
  • 作用:使当前线程暂停执行指定的时间,不释放锁。

  • 使用场景:用于控制线程的执行节奏,例如在轮询任务中避免频繁占用CPU。

  • 示例

    try {
        Thread.sleep(1000);  // 当前线程暂停1秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  • 注意

    • sleep()不会释放锁。

    • 需要捕获InterruptedException异常。

总结:

方法所属类作用是否释放锁是否需要捕获异常使用场景
wait()Object使线程等待并释放锁线程协作,同步代码块中
notify()Object唤醒一个等待的线程线程协作,同步代码块中
notifyAll()Object唤醒所有等待的线程线程协作,同步代码块中
sleep()Thread暂停线程,不释放锁控制线程节奏,任何地方
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值