第 1 章 Java多线程基础(随笔)

1. 概念

进程:在操作系统中运行的程序就是进程,例如QQ,微信等软件。

线程:线程是程序执行的基本单位,也可以说是执行程序的一次执行过程,它是一个动态的概念。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

通常线程有以下特点:

1)一个线程只能属于一个进程,但是一个进程可以有多个线程。

2)通常main()为主线程,为系统的入口,用于执行整个程序。

3)在同一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器与操作系统紧密相关,先后顺序不能人为干预。

4)同一份资源在操作时,会存在资源抢夺问题,需要加入并发控制。

5)线程会带来额外开销,如CPU调度时间,并发控制开销。

6)每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2. 线程的生命周期

线程的生命周期通常有以下5种状态:

  • 新建状态

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

 一个完整的线程生命周期如下图:

3. 创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口(推荐使用);
  • 通过继承 Thread 类本身;
  • 通过  和 Future 创建线程。
2.1 实现Runnable 接口

实现Runnable 接口启动线程逻辑:实现Runnable 接口,重写run()方法,执行线程需要传入Runnable 接口实现类,然后调用start()方法启动线程。

来看代码例子:

public class Demo implements Runnable{

    //重写run方法
    @Override
    public void run(){
        for(int num=1;num<=6;num++){
                System.out.println("第"+num+"次Running Test");
            }
    }
    public static void main(String[] args){
       
       // Demo t=new Demo();  //创建Runnable接口的实现类对象
       // Thread a=new Thread(t); //创建线程对象,通过线程对象开启线程
       // a.start();

        new Thread(new Demo()).start(); //传入接口实现类Demo类的对象,然后调用Thread的start()方法启动线程

        for(int num=0;num<2;num++){
            System.out.println("main方法线程测试");
        }
    }
}

结果:
main方法线程测试
main方法线程测试
第1次Running Test
第2次Running Test
第3次Running Test
第4次Running Test
第5次Running Test
第6次Running Test

需要注意的是,调用run方法并不会启动线程,只是单纯的调用run方法,而调用start方法会调用start0方法,start0底层C++代码逻辑也会调用run方法同时启动线程,因此,多线程场景通常调用start()方法来启动线程,这样子线程和主线程由CPU调度并行交替执行。调用start方法并不意味着线程立即执行,具体什么时候执行由CPU调度决定

2.2 继承Thread类

继承Thread类启动线程逻辑:继承Thread类,重写run()方法,调用start()方法启动线程。
来看代码例子:

public class Demo extends Thread{

    @Override
    public void run(){
        for(int num=1;num<=6;num++){
                System.out.println("第"+num+"次Running Test");
            }
    }
    public static void main(String[] args){
        Demo t=new Demo();  //实例化Demo对象
        t.start();          //调用start方法启动线程

        for(int num=0;num<2;num++){
            System.out.println("main方法线程测试");
        }
    }
}

结果:
main方法线程测试
main方法线程测试
第1次Running Test
第2次Running Test
第3次Running Test
第4次Running Test
第5次Running Test
第6次Running Test

对比以上2种方式,推荐使用实现Runnable接口的方式来创建线程,这主要是因为Java中类只能单继承,而接口可以多继承,这样可以避免单继承的局限性,方便同一个对象被多个线程使用。

例如:

Students s=new Students(); //一份资源

//多个代理
new Thread(s, name:"xiaozhang").start 
new Thread(s, name:"xiaowang").start 
new Thread(s, name:"xiaoli").start 

2.3 通过 Callable 和 Future 创建线程
  • 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

来看代码例子:

public class Demo implements Callable<Integer> { //Callable接口有返回值

    //重写call方法
    @Override
    public Integer call() throws Exception {
        int num;
        for (num = 1; num <= 6; num++) {
            System.out.println("第" + num + "次Running Test");
        }
        return num;
    }
    public static void main(String[] args){
        Demo ctt = new Demo();
        FutureTask<Integer> ft = new FutureTask<>(ctt); //将Demo对象作为参数传入futureTask类
        new Thread(ft).start(); //创建线程对象,通过线程对象开启线程

        for(int num=0;num<2;num++){
            System.out.println("main方法线程测试");
        }
    }
}

callable与runnable区别:

1)最大的区别,runnable没有返回值,而实现callable接口的任务线程能返回执行结果
2)callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是runnable接口实现类中run方法的异常必须在内部处理,不能对外抛出。

多线程练习:龟兔赛跑

需求分析:

1)同一赛道龟兔从同一起点出发

2)兔子速度快,乌龟速度慢,兔子中途睡觉,乌龟不休息

3)先跑到终点者赢得比赛

public class Demo implements Runnable{
    private int Step1=0;
    private int Step2=0;
    public static  boolean flag = true;

    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if(Thread.currentThread().getName().equals("turtle")){
                System.out.println("比赛开始,"+Thread.currentThread().getName()+"跑了"+Step1++ +"步");
                if(Step1>100){
                    flag=false;
                    System.out.println("比赛结束,恭喜"+Thread.currentThread().getName()+"赢得比赛");
                    break;
                }
            }

            if(Thread.currentThread().getName().equals("rabbit")){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Step2+=2;
                System.out.println("比赛开始,"+Thread.currentThread().getName()+"跑了"+Step2 +"步");
                if(Step2>100){
                    flag=false;
                    System.out.println("比赛结束,恭喜"+Thread.currentThread().getName()+"赢得比赛");
                    break;
                }
            }

        }
    }


    public static void main(String[] args) {
        Demo race=new Demo();
        Thread t1=new Thread(race,"turtle");
        Thread t2=new Thread(race,"rabbit");
        t1.start();
        t2.start();

    }
}

结果:
比赛开始,turtle跑了0步
比赛开始,rabbit跑了0步
比赛开始,turtle跑了1步
比赛开始,rabbit跑了2步
...
比赛开始,turtle跑了98步
比赛开始,rabbit跑了64步
比赛开始,turtle跑了99步
比赛开始,turtle跑了100步
比赛开始,rabbit跑了66步
比赛结束,恭喜turtle赢得比赛

4. 线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,大概率比低优先级的线程优先获得CPU资源。但是,线程优先级不能保证线程执行的顺序,非常依赖于操作系统调度。

Java中可以通过 setPriority(int newPriority) 来设置新的优先级,通过 getPriority() 来获取线程的优先级。

来看代码例子:

public class TestPriority {
    static AtomicLong minTimes = new AtomicLong(0);
    static AtomicLong normTimes = new AtomicLong(0);
    static AtomicLong maxTimes = new AtomicLong(0);

    public static void main(String[] args) {
        List<MyThread> minThreadList = new ArrayList<>();
        List<MyThread> normThreadList = new ArrayList<>();
        List<MyThread> maxThreadList = new ArrayList<>();

        int count = 1000;
        for (int i = 0; i < count; i++) {
            MyThread myThread = new MyThread("min----" + i);
            myThread.setPriority(Thread.MIN_PRIORITY);  //设置线程优先级为1
            minThreadList.add(myThread);
        }
        for (int i = 0; i < count; i++) {
            MyThread myThread = new MyThread("norm---" + i);
            myThread.setPriority(Thread.NORM_PRIORITY);  //设置线程优先级为5
            normThreadList.add(myThread);
        }
        for (int i = 0; i < count; i++) {
            MyThread myThread = new MyThread("max----" + i);
            myThread.setPriority(Thread.MAX_PRIORITY);  //设置线程优先级为10
            maxThreadList.add(myThread);
        }

        for (int i = 0; i < count; i++) {
            maxThreadList.get(i).start();
            normThreadList.get(i).start();
            minThreadList.get(i).start();
        }

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

        System.out.println("maxPriority 统计:" + maxTimes.get());
        System.out.println("normPriority 统计:" + normTimes.get());
        System.out.println("minPriority 统计:" + minTimes.get());
        System.out.println("普通优先级与最高优先级相差时间:" + (normTimes.get() - maxTimes.get()) + "ms");
        System.out.println("最低优先级与普通优先级相差时间:" + (minTimes.get() - normTimes.get()) + "ms");

    }

    static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " priority: " + this.getPriority());
            switch (this.getPriority()) {
                case Thread.MAX_PRIORITY :
                    maxTimes.getAndAdd(System.currentTimeMillis());
                    break;
                case Thread.NORM_PRIORITY :
                    normTimes.getAndAdd(System.currentTimeMillis());
                    break;
                case Thread.MIN_PRIORITY :
                    minTimes.getAndAdd(System.currentTimeMillis());
                    break;
                default:
                    break;
            }
        }
    }
}

结果:
min----13 priority: 1
norm---15 priority: 5
min----4 priority: 1
min----9 priority: 1
min----5 priority: 1
min----10 priority: 1
max----8 priority: 10
min----1 priority: 1
...
...
...
max----994 priority: 10
norm---996 priority: 5
min----999 priority: 1
maxPriority 统计:1699858043845831
normPriority 统计:1699858043846434
minPriority 统计:1699858043846707
普通优先级与最高优先级相差时间:603ms
最低优先级与普通优先级相差时间:273ms

从结果上看,优先级高的线程和优先级低的线程是交替执行的,这说明了:优先级高的线程不代表一定比优先级低的线程优先执行。也可以换另一种说法:代码执行顺序跟线程的优先级无关。看看第二部分的结果,我们可以发现最高优先级的 1000 个线程执行时间戳之和最小,而最低优先级的 1000 个线程执行时间戳之和最大,因此可以得知:一批高优先级的线程会比一批低优先级的线程优先执行,即高优先级的线程大概率比低优先的线程优先获得 CPU 资源

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值