多线程入门:轻松掌握编程中的“分身术”

目录

进程

线程

并发

并行

多线程的实现方式

继承Thread类的方式进行实现

​编辑

实现Runnable接口的方式进行实现

实现Callable接口的方式进行实现

Thread类的方法

设置线程的名字及指定线程休眠的时间

线程的优先级

设置为守护线程

出让线程/礼让线程

插入线程

线程的生命周期

线程的安全问题

同步代码块

同步方法

Lock锁(手动上锁,手动释放锁)

等待唤醒机制

线程的状态

线程池

原理

线程池的创建

自定义线程池


        你有没有过这样的经历:一边煲着电话粥,一边刷着抖音,还不忘在电脑上处理一些紧急的工作文件?如果只能做一件事,那效率简直低到令人发指!但幸好,我们人类可以通过大脑的快速切换来同时处理多件事情,虽然可能没有那么完美,但至少能让我们在忙碌的生活中不至于手忙脚乱。

        在编程的世界里,也有类似“分身术”的存在,那就是多线程。想象一下,如果你的程序只能像单线程那样一次做一件事,那用户体验得多糟糕啊!比如,一个简单的网页加载,如果只能等待图片全部加载完成后再显示文字内容,那用户可能早就被漫长的等待给吓跑了。但有了多线程,程序就可以同时做多件事情,比如一边加载图片,一边渲染文字,甚至还能在后台处理一些数据,大大提高了效率,让用户体验更加流畅。

进程

定义:操作系统进行资源分配调度的基本单位。它是程序的一次动态执行过程,包含程序代码、数据、运行时的上下文信息(如寄存器状态、堆栈等)以及操作系统为进程分配的资源(如内存空间、文件句柄等)。

比喻:可以比喻为一个“公司”,公司有自己的办公场地(内存空间)、设备(文件句柄)和其他资源。公司可以同时开展多个业务项目。

简单理解:应用软件中互相独立可以同时运行的功能

应用场景:

  • 软件中耗时操作,拷贝,迁移大文件,加载大量的资源文件
  • 所有的聊天软件
  • 所有的后台服务器

线程

定义:线程是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。它与进程的主要区别在于线程不拥有系统资源,线程之间共享进程的资源。线程可以被看作是进程中的一个“子任务”,多个线程可以并发执行,提高程序的效率。

比喻:可以比喻为公司中的“员工”,员工共享公司的资源,但每个员工可以独立完成自己的任务。多个员工可以同时工作,提高公司的效率。

并发

在同一时刻,有多个指令在单个CPU上交替执行。

并行

在同一时刻,有多个指令在多个CPU上同时执行

描述CPU内部结构和性能的一个具体参数

2核4线程

4核8线程

8核16线程

16核32线程

核心:

  • CPU的核心是实际的物理处理单元,每个核心可以独立执行程序指令。

  • 核心数量越多,CPU同时处理任务的能力越强。

在多核CPU中,每个核心可以运行多个线程,从而提高CPU的运行效率。

并发和并行有可能同时都在发生。

多线程的实现方式

继承Thread类的方式进行实现

重写 Thread 类中的 run() 方法是为了定义线程的具体任务逻辑。这是实现多线程并发执行的关键步骤。

public class MyThread extends Thread {
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"helloworld");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
            多线程的第一种启动方式
            1.自己定义一个类继承Thread
            2.重写run方法
            3.创建子类的对象,并启动线程
         */
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

实现Runnable接口的方式进行实现

public class MyRun implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取当前正在执行的主线程对象,因为Runnable没有继承Thread,
            //所以不能直接调用其方法,先调用它的对象。
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"helloworld");
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        /*
            自己定义一个类实现Runnable接口
            重写里面的run方法
            创建自己的对象
            创建一个Thread类的对象,并开启线程
         */
        //创建MyRun的对象
        //表示多线程要执行的任务
        MyRun mr = new MyRun();
        //创建线程对象
        //把任务传递给线程
        //将Runnable对象传递给Thread
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");
        //开启线程
        t1.start();
        t2.start();
    }
}

实现Callable接口的方式进行实现

Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //求1~100之间的和
        int sum = 0;
        for (int i = 1; i < 100; i++) {
            sum += i;
        }
        return sum;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
        多线程的第三种实现方式:
        1.创建一个类MyCallable实现Callable接口
        2.重写call(是有返回值的,表示多线程运行的结果)
        3.创建MyCallable的对象(表示多线程要执行的任务)
        4.创建FutureTask的对象(作用管理多线程运行的结果)
        5.创建Thread类的对象,并开启(表示多线程)
         */

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable callable = new MyCallable();
        //创建FutureTask的对象(作用管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(callable);
        //创建Thread类的对象
        Thread thread = new Thread(ft);
        //开启
        thread.start();
        //获取结果
        Integer result = ft.get();
        System.out.println(result);
    }
}

Thread类的方法

设置线程的名字及指定线程休眠的时间

String getName()        返回此线程的名字
void setName(String name)设置线程的名字(构造方法也可以设置名字)
细节:
    1、如果我们没有给线程设置名字,线程也是有默认的名字的
       格式:Thread-X(X序号,从0开始的)
    2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
static Thread currentThread()       获取当前线程对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
static void sleep(long time)        让线程休眠指定的时间,单位为毫秒
细节:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2、方法的参数:就表示睡眠的时间,单位毫秒
1 秒=1000毫秒
3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码

没有给线程设置名字,默认的名字Thread-X

public class MyThread extends Thread{
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"@"+i);
            //父类没有抛出异常,子类只能自己try
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(getName()+"@"+i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建子类的线程对象
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        //启动线程
        mt1.start();
        mt2.start();

        
    }
}

利用构造方法设置名字

在MyThread类里面添加构造方法,在实现类就可以设置线程的名字了

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }
MyThread mt1 = new MyThread("飞机");
MyThread mt2 = new MyThread("坦克");

Thread.sleep() 方法声明了 InterruptedException,这意味着调用该方法时,必须处理这个异常。

  • MyThread 类中的 run() 方法

    • run() 方法没有声明抛出 InterruptedException,因此在内部调用 Thread.sleep() 时,必须手动捕获并处理异常。

  • ThreadDemo 类中的 main 方法

    • main 方法声明了抛出 InterruptedException,因此可以直接将异常向上抛出,而不需要手动捕获。

线程的优先级

public class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
            setPriority(int newPriority)    设置线程的优先级
            final int getpriority()         获取线程的优先级
         */
        //创建线程要执行的参数对象
        MyThread mr = new MyThread();
        //创建线程对象
        Thread t1 = new Thread(mr,"飞机");
        Thread t2 = new Thread(mr,"坦克");
        //优先级越高,抢到CPU的概率就越高(但只是概率)
        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();
    }
}

Java 中线程优先级的范围是从 1 到 10,其中:

  • 1 是最低优先级。

  • 10 是最高优先级。

  • 默认优先级 是 5。

Java 提供了以下常量来表示线程优先级:

  • Thread.MIN_PRIORITY:最低优先级,值为 1。

  • Thread.NORM_PRIORITY:默认优先级,值为 5。

  • Thread.MAX_PRIORITY:最高优先级,值为 10。

设置为守护线程

public class MyThread1 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}
public class MyThread2 extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        /*
            final void setDaemon(boolean on)    设置为守护线程
            细节:
                当其他线程完毕之后,守护线程会陆续结束
            通俗来说:
                当女神线程结束了,那么备胎也没有存在的必要了
            应用场景:QQ聊天+传输文件同时进行
            设置传输的文件是守护进程,聊天窗口是非守护,聊天窗口关闭后,传输的文件会慢慢停止传
            当聊天窗口关闭了,传输文件就关闭了
         */

        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");
        //把t2设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start();

    }
}

出让线程/礼让线程

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + " " + i);
            Thread.yield();
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
            public static void yield()  出让线程/礼让线程
         */
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("飞机");
        t2.setName("坦克");
        //执行的结果尽可能的均匀
        t1.start();
        t2.start();
    }
}

插入线程

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + " " + i);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        /*
            public final void join  插入线程/插队线程
         */

        MyThread t = new MyThread();
        t.setName("土豆");
        t.start();
        //表示把t这个线程,插入到当前线程之前
        //t:土豆
        //当前线程:mian线程
        t.join();
        for(int i = 0;i<10;i++){
            System.out.println("main线程"+i);
        }
    }
}

线程的生命周期

线程的安全问题

(1)数据竞争(Data Race)

当多个线程同时访问和修改同一个共享变量时,可能会导致数据不一致。

(2)不可重复读(Non-Repeatable Read)

当一个线程读取共享变量的值时,另一个线程可能同时修改了该变量的值,导致读取的结果不一致。

(3)死锁(Deadlock)

当多个线程互相等待对方持有的锁时,会导致线程无法继续执行,形成死锁。

同步代码块

把操作共享数据的代码锁起来

格式:

synchronized(锁){
    操作共享数据的代码;
}

特点1:锁默认打开,有一个线程进去了,锁自动关闭。

特点2:里面的代码全部执行完毕,线程出来,锁自动打开。

细节:

  1. 写在循环里面(不能写在循环外面,因为一旦窗口1抢到了CPU的执行权,锁就会开启,直到结束循环,才会打开)
  2. 锁的唯一性(确保线程1进来,其他线程就进不来),锁可以是当前类的字节码对象,class文件,在同一个文件里只有唯一的.class文件。

代码展示:

public class MyThread extends Thread {
    //表示这个类所有的对象,都共享ticket数据
    static int ticket = 0;
    //锁对象,一定要是唯一的。
    static Object obj = new Object();
    public void run() {
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (ticket < 100) {
                    ticket++;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(getName() + "买了"+ticket+"张票!!!");
                }else{
                    break;
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
            需求:
                某电影目前正在上映国产大片,共有100张门票,而它有3个窗口卖票,设计一个程序模拟该电影院卖票
         */

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

同步方法

就是把synchronized关键字加到方法上。

格式:

修饰符 synchronized 返回值类型 方法名(方法参数){
        ......
}

特点:

  1. 同步方法是锁住方法里面的所有的代码
  2. 锁对象不能自己指定,非静态:this,静态:当前类的字节文件对象

如果你的代码是单线程,只需要使用StringBuilder,若是多线程则用StringBuffer。

代码展示:

public class MyRunnable implements Runnable {
    //此时的ticket不需要加static,因为只会创建一个Runnable对象
    int ticket = 0;
    @Override
    public void run() {
        while (true) {
            //1.循环
            //2.同步代码块(同步方法)
            //3.判断共享数据是否到了末尾,如果到了末尾
            //4.判断共享数据是否到了末尾,如果没有到末尾
            if(method()) break;
        }
    }
    public synchronized boolean method(){
        synchronized (MyRunnable.class) {
            if (ticket == 100) {
                return true;
            }else{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(Thread.currentThread().getName()+"卖了"+ticket+"张票!!!");
            }
        }
        return false;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //利用同步方法完成
        MyRunnable mr = new MyRunnable();//作为一个参数,让线程去执行
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

Lock锁(手动上锁,手动释放锁)

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

提供了获得锁和释放锁的方法

void lock();获取锁
void unlock();释放锁

Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock来实现实例化,

ReentrantLock的构造方法:ReentrantLock();创建一个ReentrantLock的示例。

锁的本质:当其他线程运行到上锁的代码时,若发现锁没解开,就会让出CPU执行权。

代码展示:

public class MyThread extends Thread {
    static int ticket = 0;
    static Lock lock = new ReentrantLock();
    public void run(){
        while(true){
            lock.lock();
            try {
                if(ticket == 1000){
                    break;
                }else{
                    Thread.sleep(1);
                    ticket++;
                    System.out.println(getName()+"卖了"+ticket+"张票!!!");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

等待唤醒机制

常见方法

void wait();当前线程等待,直到被其他线程唤醒
void notify();随机唤醒单个线程
void notifyAll();唤醒所有线程

案例:生产者和消费者,制作面条,生产者生产一碗面条,消费者就吃一碗,直到面条没有。

核心思想:利用桌子来控制线程的执行。

消费者

  1. I判断桌子上是否有食物
  2. 如果没有就等待
  3. 如果有就开吃
  4. 吃完之后,唤醒厨师继续做

生产者

  1. 判断桌子上是否有食物
  2. 有:等待
  3. 没有:制作食物
  4. 把食物放在桌子上
  5. 叫醒等待的消费者开吃

代码展示

public class Cook extends Thread {
    public void run(){
        /*
            1.循环
            2.同步代码块
            3.判断共享数据是否到了末尾
            4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */
        while(true){
            synchronized(Desk.lock){
                if(Desk.count == 0){
                    break;
                }else {
                    if (Desk.foodFlag == 1) {
                        //如果桌子上有面条,就需要等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //如果桌子上没有面条,就制作
                        System.out.println("厨师做了一碗面");
                        Desk.foodFlag = 1;
                        Desk.lock.notify();
                    }
                }
            }
        }
    }
}
public class Foodie extends Thread {
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.foodFlag == 0){
                        //如果桌面上没有面条,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //总数-1
                        Desk.count--;
                        //如果有面条,就开吃
                        System.out.println("吃货在吃面条,还能再吃"+Desk.count+"碗!");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}
public class Desk {
    /*
        控制生产者和消费者的执行
     */
    //是否有面条
    public static int foodFlag = 0;
    //总个数
    public static int count = 10;
    //锁对象
    public static Object lock = new Object();
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
        需求:完成生产者和消费者(等待唤醒机制)的代码实现线程轮流交替执行的效果
         */

        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        //开启线程
        c.start();
        f.start();
    }
}

线程的状态

线程的状态(NEW)->创建线程对象

就绪状态(RUUNABLE)->start方法

阻塞状态(BLOCKED)->无法获得锁对象

等待状态(WAITING)->wait方法

计时等待(TIME_WAITING)->sleep方法

结束状态(TERMINATED)->全部代码运行完毕

线程池

线程池的核心思想是复用线程,避免频繁创建和销毁线程的开销。

线程池主要由以下几部分组成:

  • 线程池(ThreadPoolExecutor):管理线程的创建、执行和销毁。

  • 任务队列(BlockingQueue):存储待执行的任务。

  • 线程工厂(ThreadFactory):用于创建线程。

  • 拒绝策略(RejectedExecutionHandler):当任务无法被线程池处理时的处理策略。

原理

  1. 创建一个池子,池子里是空的。
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的池子,直接复用已有的线程即可。
  3. 但是如果提交任务时,池子没有空闲线程,也无法创建新的线程,任务就会排队等待。

线程池的创建

Executors:线程池的工具类通过方法调用返回不同类型的线程池对象

public static ExecutorService newCachedThreadPool()	创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool (int nThreads) 创建有上限的线程池
         

代码展示:

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {      
        //1.获取线程池对象
        //创建一个没有上限的线程池
        ExecutorService pool1 = Executors.newCachedThreadPool();
        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        pool1.shutdown();
    }
}

自定义线程池

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        /*
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
        (核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
        参数一:核心线程数量	不能小于0
        参数二:最大线程数	不能小于0,最大数量 >=核心线程数量
        参数三:空闲线程最大存活时间	不能小于0
        参数四:时间单位	用TimeUnit指定
        参数五:任务队列	不能为null
        参数六:创建线程工厂	不能为null
        参数七:任务的拒绝策略	不能为null
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程数量
                6,//最大线程数
                60,//空闲线程最大存活时间
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<Runnable>(3),//任务队列
                Executors.defaultThreadFactory(),//创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
        );
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值