java多线程

本文深入探讨了多线程的必要性和实现方式,详细解析了进程与线程的概念及其关系,介绍了多线程的创建方法,包括继承Thread类、实现Runnable接口和Callable接口,并对比了它们的区别。同时,阐述了线程的状态变化和Thread类的常用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一:为什么要学多线程

了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也不是很多(看具体项目),虽然代码中很少会自己去创建线程,但是实际环境中每行代码却都是并行执行的,同一时刻大量请求同一个接口,并发可能会产生一些问题,所以也需要掌握一定的并发知识

二:进程与线程

  1. 进程
    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

  2. 线程
    线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。

线程是一条可以执行的路径。多线程就是同时有多条执行路径在同时(并行)执行。

  1. 进程与线程的关系
    一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。,亦即执行处理机调度的基本单位。 进程和线程的关系:
    一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。

处理机分给线程,即真正在处理机上运行的是线程。

线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答,此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)

  1. 为什么要使用多线程
    多线程可以提高程序的效率。

实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。

三:多线程的实现方式

编程的方式:
顺序:代码从上而下按照固定的顺序执行,只有上一件事情执行完毕,才能执行下一件事。就像物理电路中的串行,假如有十件事情,一个人来完成,这个人必须先做第一件事情,然后再做第二件事情,最后做第十件事情,按照顺序做。
并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是同时的)
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。

 多线程创建方式
(1)、继承Thread类,并重写其run()方法。
(2)、实现Runnable接口,并实现其run()方法。
(3)、实现Callable接口,并实现其call()方法。

1. 继承java.lang.Thread, 重写run()方法

public class Main {
    public static void main(String[] args) {
        new MyThread().start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

Thread 类介绍:

package java.lang;
public class Thread implements Runnable {
	// 构造方法
	public Thread(Runnable target);
	public Thread(Runnable target, String name);
	
	public synchronized void start();
}

Runnable 接口介绍

package java.lang;

@FunctionalInterface
public interface Runnable {
    pubic abstract void run();
}

2.实现java.lang.Runnable接口,重写run()方法,然后使用Thread类来包装

public class Main {
    public static void main(String[] args) {
    	 // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

三种方式比较:

Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制

Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行,当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

四:线程的状态

创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
运行(running)状态: 执行run()方法
阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

五:Thread常用方法

public class Thread implements Runnable {
    // 线程名字
    private volatile String name;
    // 线程优先级(1~10)
    private int priority;
    // 守护线程
    private boolean daemon = false;
    // 线程id
    private long tid;
    // 线程组
    private ThreadGroup group;
    
    // 预定义3个优先级
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
    
    
    // 构造函数
    public Thread();
    public Thread(String name);
    public Thread(Runnable target);
    public Thread(Runnable target, String name);
    // 线程组
    public Thread(ThreadGroup group, Runnable target);
    
    
    // 返回当前正在执行线程对象的引用
    public static native Thread currentThread();
    
    // 启动一个新线程
    public synchronized void start();
    // 线程的方法体,和启动线程没毛关系
    public void run();
    
    // 让线程睡眠一会,由活跃状态改为挂起状态
    public static native void sleep(long millis) throws InterruptedException;
    public static void sleep(long millis, int nanos) throws InterruptedException;
    
    // 打断线程 中断线程 用于停止线程
    // 调用该方法时并不需要获取Thread实例的锁。无论何时,任何线程都可以调用其它线程的interruptf方法
    public void interrupt();
    public boolean isInterrupted()
    
    // 线程是否处于活动状态
    public final native boolean isAlive();
    
    // 交出CPU的使用权,从运行状态改为挂起状态
    public static native void yield();
    
    public final void join() throws InterruptedException
    public final synchronized void join(long millis)
    public final synchronized void join(long millis, int nanos) throws InterruptedException
    
    
    // 设置线程优先级
    public final void setPriority(int newPriority);
    // 设置是否守护线程
    public final void setDaemon(boolean on);
    // 线程id
    public long getId() { return this.tid; }
    
    
    // 线程状态
    public enum State {
        // new 创建
        NEW,

        // runnable 就绪
        RUNNABLE,

        // blocked 阻塞
        BLOCKED,

        // waiting 等待
        WAITING,

        // timed_waiting
        TIMED_WAITING,

        // terminated 结束
        TERMINATED;
    }
}
public static void main(String[] args) {
    // main方法就是一个主线程

    // 获取当前正在运行的线程
    Thread thread = Thread.currentThread();
    // 线程名字
    String name = thread.getName();
    // 线程id
    long id = thread.getId();
    // 线程优先级
    int priority = thread.getPriority();
    // 是否存活
    boolean alive = thread.isAlive();
    // 是否守护线程
    boolean daemon = thread.isDaemon();

    // Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
    System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
}

1. start() 与 run()

public static void main(String[] args) throws Exception {
   new Thread(()-> {
       for (int i = 0; i < 5; i++) {
           System.out.println(Thread.currentThread().getName() + " " + i);
           try { Thread.sleep(200); } catch (InterruptedException e) { }
       }
   }, "Thread-A").start();

   new Thread(()-> {
       for (int j = 0; j < 5; j++) {
           System.out.println(Thread.currentThread().getName() + " " + j);
           try { Thread.sleep(200); } catch (InterruptedException e) { }
       }
   }, "Thread-B").start();
}

start(): 启动一个线程,线程之间是没有顺序的,是按CPU分配的时间片来回切换的。
在这里插入图片描述

public static void main(String[] args) throws Exception {
    new Thread(()-> {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            try { Thread.sleep(200); } catch (InterruptedException e) { }
        }
    }, "Thread-A").run();

    new Thread(()-> {
        for (int j = 0; j < 5; j++) {
            System.out.println(Thread.currentThread().getName() + " " + j);
            try { Thread.sleep(200); } catch (InterruptedException e) { }
        }
    }, "Thread-B").run();
}

在这里插入图片描述
run(): 调用线程的run方法,就是普通的方法调用,虽然将代码封装到两个线程体中,可以看到线程中打印的线程名字都是main主线程,run()方法用于封装线程的代码,具体要启动一个线程来运行线程体中的代码(run()方法)还是通过start()方法来实现,调用run()方法就是一种顺序编程不是并发编程。
有些面试官经常问一些启动一个线程是用start()方法还是run()方法,为了面试而面试。
start()方法
当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体(真正的逻辑)。

2. sleep() 与 interrupt()

sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去

3. wait() 与 notify()
wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写,Object类是所有类的超类,因此在程序中可以通过this或者super来调用this.wait(), super.wait()

wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis)
notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态
notifyAll(): 唤醒所有的wait对象
注意:

Object.wait()和Object.notify()和Object.notifyall()必须写在synchronized方法内部或者synchronized块内部
让哪个对象等待wait就去通知notify哪个对象,不要让A对象等待,结果却去通知B对象,要操作同一个对象

4. sleep() 与 wait()
Thread.sleep(long millis): 睡眠时不会释放锁
Thread.wait(long millis): 睡眠时会释放锁

5. wait() 与 interrupt()
wait(): 方法的作用是释放锁,加入到等待队列,当调用interrupt()方法后,线程必须先获取到锁后,然后才抛出异常InterruptedException 。注意: 在获取锁之前是不会抛出异常的,只有在获取锁之后才会抛异常

所有能抛出InterruptedException的方法都可以通过interrupt()来取消的

5.join()
让当前线程加入父线程,加入后父线程会一直wait,直到子线程执行完毕后父线程才能执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

将某个线程加入到当前线程中来,一般某个线程和当前线程依赖关系比较强,必须先等待某个线程执行完毕才能执行当前线程。一般在run()方法内使用

6. yield()
交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到,

7. setDaemon(boolean on)
线程分两种:
用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。
守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程

————————————————
版权声明:本文为优快云博主「vbirdbest」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/vbirdbest/article/details/81282163
————————————————
版权声明:本文为优快云博主「山鬼谣弋痕夕」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_30363263/article/details/80732156

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值