JAVA中的多线程详解

1.概念

进程(Process):

进程是一个包含自身执行地址的程序,多线程使程序可以同时存在多个执行片段,这些执行片段根据不同的条件和环境同步或者异步工作,由于转换的数独很快,使人感觉上进程像是在同时运行。

现在的计算机基本上都支持多进程操作,比如使用计算机时可以变上网,边听歌。然而计算机上只有一块CPU,并不能同时运行这些进程,CPU实际上是利用不同的时间段去交替执行每个进程。

线程(Thread): 

在一个进程内部可以执行多项任务,进程内部的任务被称为线程,线程是进程中的实体,一个进程可以拥有多个线程。

多线程是指在一个程序中同时执行多个线程,每个线程都有自己独立的执行路径。在多线程中,程序的执行可以同时进行多个任务,从而提高系统的资源利用率和响应性能。

在传统的单线程编程模型中,程序按照顺序执行,一次只处理一个任务。这种方式在某些情况下可能会导致效率低下或者无法满足需求。而多线程通过将任务拆分为多个子任务,并且在不同的线程上同时执行,从而实现并发处理。

对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:

  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。

很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):

  • 进程可以比作是你开的这一把游戏
  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。

带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?

❤1、线程在进程下进行

(单独的英雄角色、野怪、小兵肯定不能运行)

❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束

(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)

❤3、不同的进程数据很难共享

(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)

❤4、同进程下的不同线程之间数据很容易共享

(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)

❤5、进程使用内存地址可以限定使用量

(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)

2.线程的创建

在JAVA语言中,线程也是一种对象,但是并非任何对象都可以称为线程,只有实现了Runnable接口或者继承了Thread类的对象才能成为线程。

2.1 Thread类

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":打了" + i + "个小兵");
        }
    }

    public static void main(String[] args) {
        //创建MyThread对象
        MyThread t1=new  MyThread();
        MyThread t2=new  MyThread();
        MyThread t3=new  MyThread();
//设置线程的名字
        t1.setName("鲁班");
        t2.setName("刘备");
        t3.setName("亚瑟");
//启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}

运行结果:

 2.2 Runnable接口

创建一个类实现 Runnable 接口,并重写 run 方法。本质上来讲,Runnable接口是JAVA接口语言中用以实现线程的接口,任何实现线程功能的类都必须实现这个接口。

虽然可以使用继承Thread类的方式来实现线程,但是在java语言中,只能继承一个类,如果用户定义的类已经继承其他类,就无法再继承Thread类,无法使用线程,所以Runnable接口就这么诞生了,实现Runnable接口和继承Thread类具有相同的效果,通过实现这个接口就可以使用线程。Runnable接口定义了一个run()方法,在实例化一个Thread对象的时候,可以传入一个实现Runnable接口作为参数,Thead类会调用Runnable对象的run()方法,继而执行run()方法中的内容。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {//sleep会发生异常要显示处理
                Thread.sleep(20);//暂停20毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
        }
    }

    public static void main(String[] args) {
        //创建MyRunnable类
        MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
        Thread t1 = new Thread(mr, "张飞");
        Thread t2 = new Thread(mr, "貂蝉");
        Thread t3 = new Thread(mr, "吕布");
//启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}

运行结果:

2.3 实现 Callable 接口

  • 实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。
  • 重写call()方法:Callable接口中的call()方法和run()方法很相似,但是call()可以返回一个值,并且可以抛出检查型异常。
  • 获取结果:使用Future对象对get()方法获取callable任务的结果
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

3.关于线程的一些疑问

3.1 为什么要重写 run 方法?

这是因为默认的run()方法不会做任何事情。

为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法

public class MyThread extends Thread {
  public void run() {
    System.out.println("MyThread running");
  }
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。

 2.1 run 方法和 start 方法有什么区别?

  • run():封装线程执行的代码,直接调用相当于调用普通方法。
  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。

3.1 通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好? 

实现 Runable 接口好,原因有两个:

  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。

 4.控制线程的其他方法

1.sleep

sleep方法用于暂停当前正在执行的线程的执行指定时间(以毫秒为单位)。sleep方法定义在Thread类中,其主要的用途是让当前线程暂停,让其他的线程有机会运行。需要注意的是,sleep 的时候要对异常进行处理。

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程开始执行,即将进入睡眠状态...");
                Thread.sleep(3000); // 暂停3秒
                System.out.println("线程从睡眠状态唤醒");
            } catch (InterruptedException e) {
                System.out.println("线程被中断:" + e.getMessage());
                Thread.currentThread().interrupt(); // 重置中断状态
            }
        });

        thread.start();
    }
}

2.join

等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。

//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
    t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
    e.printStackTrace();
}
t2.start();
t3.start();

3.setDaemon() 

这个是用于将线程设置为守护线程,属于服务类的线程,用于为用户线程提供服务。当所有的用户线程都结束时,守护线程会自动结束。

public class CallerTask{
    public static void main(String[] args) {
        Thread daemonThread =new Thread(()->{
            System.out.println("守护线程启动");
            try{
                //模拟长时间的运行任务
                while (true){
                    Thread.sleep(1000);
                    System.out.println("守护线程正在进行");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("守护线程被中断,即将结束");
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();

        //主线程运行一小段时间后结束
        try{
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}

在这个示例中,主线程启动一个守护线程,并将其设置为守护状态。主线程在运行3秒后结束,此时所有的用户线程都已结束,守护线程中断。

4.yield()

这个是Thread中的静态方法,它可以使当前正在执行的线程让出对CPU的使用权,从而使得其它同优先级的线程有机会被CPU调度执行,这个方法主要用于线程之间的协作,尤其是在需要让出CPU一遍其他线程可以运行时。

class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(YieldExample::printNumbers, "刘备");
        Thread thread2 = new Thread(YieldExample::printNumbers, "关羽");

        thread1.start();
        thread2.start();
    }

    private static void printNumbers() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);

            // 当 i 是偶数时,当前线程暂停执行
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + " 让出控制权...");
                Thread.yield();
            }
        }
    }
}

运行结果: 

从这个结果可以看得出来,即便有时候让出了控制权,其他线程也不一定会执行。

5.线程的生命周期

  •  第一是创建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。
  • 第二是就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait 等方法都可以导致线程阻塞。
  • 第五是死亡状态。如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。

6.Callable,Future和FutureTask

我们讲述了创建线程的 3 种方式,一种是直接继承 Thread,一种是实现 Runnable 接口,另外一种是实现 Callable 接口。

前 2 种方式都有一个缺陷:在执行完任务之后无法获取执行结果。

如果需要获取执行结果,就必须通过共享变量或者线程通信的方式来达到目的,这样使用起来就比较麻烦。

Java 1.5 提供了 Callable、Future、FutureTask,它们可以在任务执行完后得到执行结果,今天我们就来详细的了解一下。

Runnable接口:

  • 没有返回值
  • 没有异常抛出的限制
  • 可以通过Thread类的run()方法执行

Callable接口: 

  • 有返回值,返回值类型为object
  • 可以抛出异常
  • 必须通过ExecutorService来执行,并且返回一个Future对象
// Runnable接口实现
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running");
    }
}

// Callable接口实现
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable is running");
        return 123;
    }
}

// FutureTask使用
public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        // 使用Runnable
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        
        // 使用Callable和FutureTask
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        executor.execute(futureTask);
        
        // 获取Callable任务的结果
        Integer result = futureTask.get(); // 阻塞直到任务完成
        System.out.println("Callable returned: " + result);
        
        executor.shutdown();
    }
}

那怎么使用 Callable 呢?

一般会配合 ExecutorServiceopen in new window(后面在讲线程池的时候会细讲,这里记住就行)来使用。

ExecutorService 是一个接口,位于 java.util.concurrent 包下,它是 Java 线程池框架的核心接口,用来异步执行任务。它提供了一些关键方法用来进行线程管理。

异步计算Future接口:

Future接口,它代表了异步的计算结果。当你将一个任务提交给ExectorService执行时,它返回一个Future对象,这个Future对象运行你查询任务的状态,获取任务结果,取消任务等等。

以下是Future接口的一些关键特性和方法:

查询任务状态:

  • isDone():检查任务是否完成
  • isCanncelled():检查任务是否被取消

获取结果:

  • get():获取任务的结果,如果任务尚未完成,则次方法会阻塞 
  • get(long timeout,TimeUnit unit):获取任务的结果,如果任务在指定的时间内未完成,则会抛出TimeoutException

 取消任务:

cancle(boolean mayInterruptIfRunning):尝试取消任务。如果任务尚未开始,或者已经开始但未完成,则可以取消任务。参数mayInterruptIfRunning分running决定是否运行中断正在运行的任务。

获取任务是否被取消:

ifCancelled():返回任务是否被取消。

获取任务是否成功完成:

isDone():返回任务是否成功完成

也就是说 Future 提供了三种功能:

  • 1)判断任务是否完成;
  • 2)能够中断任务;
  • 3)能够获取任务执行结果。

由于 Future 只是一个接口,如果直接 new 的话,编译器是会有一个 ⚠️ 警告的,它会提醒我们最好使用 FutureTask。

实际上,FutureTask 是 Future 接口的一个唯一实现类,我们在前面的例子中 executorService.submit() 返回的就是 FutureTask,通过 debug 模式可以观察到。

异步计算结果 FutureTask 实现类

我们来看一下 FutureTask 的实现:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask 类实现了 RunnableFuture 接口,我们看一下 RunnableFuture 接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可以看出 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

FutureTask 提供了 2 个构造器:

public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值