多线程编程的基础用法

前言

在学习Java中的多线程编程之前,需要对进程、线程的基本知识有初步的了解,不懂的同学可以看我之前总结的博客

初始多线程

深入理解多线程

多线程与锁

在Java标准库中,提供了一个Thread类,用来表示/操作线程

1、创建线程的方式

第一种方式: 自定义一个类,继承Thread类,重写run方法

//创建子类,继承父类,重写run方法
class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("hello,thread");
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        //这里调用了start才是真正在系统中创建了线程,然后开始执行run操作
        t.start();
    }
}

在这里插入图片描述

run方法描述了线程内部需要执行哪些代码,run方法中的逻辑,是在新创建出来的线程中,被执行的代码。

new MyThread()并不是真正的创建线程,当调用start()方法后,才会在操作系统中创建一个线程,并且执行run操作,在调用start()方法之前,系统中是没有创建出线程的

第二种方式: 创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例

//Runnable 就是在描述一个“任务”
class MyRunnable implements Runnable{
    @Override
    public void run(){
        System.out.println("hello,thread");
    }
}


public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

在这里插入图片描述

通过Runnable来描述任务的内容,进一步再把描述好的任务交给Thread实例

第三种方式: 使用匿名内部类的方式创建

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello,thread");
            }
        };
        t.start();
    }
}

在这里插入图片描述

创建一个匿名内部类,继承自Thread类,重写run方法,同时再new出这个匿名内部类的实例

第四种方式: 创建Runnable匿名内部类的实例,作为参数传给Thread

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread");
            }
        });
    }
}

在这里插入图片描述

new的Runnable,针对这个创建的匿名内部类,同时new出Runnable实例传给Thread的构造方法

和第三种方式中的代码对比,通常认为第四种方式的写法更好一点,它能够做到让线程和线程执行的任务,更好的进行解耦。Runnable单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,Runnable本身并不关心Runnable里面的代码也不关心

第五种方式: 使用lambda表达式,是第四种方式的延伸

public class Demo6 {
    public static void main(String[] args) {
        //利用lambda表达式创建线程
        Thread t = new Thread(()->{
            System.out.println("hello thread");
        });
        t.start();
    }
}

在这里插入图片描述

2、通过代码比较多线程的优势

都说多线程快,那我们就简单证明一下,串行执行两个变量从0增加到10亿和并发执行两个变量从0增加到10亿,看看哪一个更快

串行执行:

public class Demo7 {
    private static final long count = 10_0000_0000;

    public static void serial() {
        //记录程序执行时间
        long begin = System.currentTimeMillis();
        long a = 0;
        for(int i = 0; i < count; ++i) {
            a++;
        }

        long b = 0;
        for(int i = 0; i < count; ++i) {
            b++;
        }
        //记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("serial()消耗时间: " + (end - begin) + "ms");
    }

    public static void main(String[] args) throws InterruptedException {
        serial();
    }
}

在这里插入图片描述

执行5次,消耗的时间大致都在750ms上下

并发执行:

public class Demo7 {
    private static final long count = 10_0000_0000;
    
    public static void concurrency() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
           long a = 0;
           for(int i = 0; i <count; ++i) {
               a++;
           }
        });
        t1.start();

        Thread t2 = new Thread(()->{
           long b = 0;
           for(int i = 0; i < count; ++i) {
               b++;
           }
        });
        t2.start();
        //让main线程等待t1和t2执行完了再记录结束时间
        t1.join();//让main线程等待t1执行结束
        t2.join();//让main线程等待t2执行结束
        long end = System.currentTimeMillis();
        System.out.println("concurrency()消耗时间: " + (end - begin) + "ms");
    }

    public static void main(String[] args) throws InterruptedException {
        concurrency();
    }
}

在这里插入图片描述

执行5次,消耗的时间大致都在460ms上下

并发执行的效率提升了将近50%,但是并不是说一个线程600多ms,两个线程就400多ms。这两个线程在底层到底是并行还是并发,是不确定的,真正并行执行的时候,效率才会提升

多线程不是万能的,不是用了多线程,效率就一定高,还得看具体的应用场景

3、Thread类常见的构造方法和属性

常见的构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

常见的属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复

  • 名称是各种调试工具用到

  • 状态表示线程当前所处的一个情况

  • 优先级高的线程理论上来说更容易被调度到

  • 关于后台线程,创建的是前台线程,main执行完毕后,进程也不能退出,得等到线程执行完毕后,整个进程才结束,如果是后台线程,main执行完毕后,整个进程就直接退出,线程会被强行终止。需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行

  • 是否存活,操作系统中对应的线程是否正在运行。Thread对象的生命周期和内核中对应的线程的生命周期并不完全一致,创建出线程对象之后,在调用start之前,系统中是没有对应的线程的。在run方法执行完毕后,系统中的线程就被销毁了,但线程这个对象可能还在

  • 线程的中断问题,中断也就是让一个线程停下来

通过以下代码,来打印线程的各种属性:

public class Demo32 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还 活着");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {
        }
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
    }
}

在这里插入图片描述

4、启动线程

启动线程就是线程实例调用start()方法,前面已经使用过,不多赘述,这里主要讲一下start和run的区别

start和run的区别

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        
        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
两个线程并发,交替打印

如果将t.start()换成t.run(),则会出现不一样的结果

在这里插入图片描述

结果只会打印"hello thread"

run方法只是一个普通的方法,在main线程中调用run,其实并没有创建的新的线程
这个循环仍然是在main线程中执行
既然是在一个线程中执行,代码就得从前到后按顺序执行
先运行第一个循环,再运行第二个循环,但一个循环会一直进行下去

调用 start 方法, 才真的在操作系统的底层创建出一个线程

5、中断线程

线程停下来的关键,就是让线程对应的run方法执行完毕(还有一个特殊的线程,那就是main线程,对于main来说,main方法执行完毕后,线程就完了)

1.)可以手动的设置一个标志位(自己创建的变量,boolean),来控制线程是否要执行结束

public class Demo10 {
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        //把isQuit设为true,t线程的循环就结束了,再进一步执行run,线程就结束了
        Thread.sleep(5000);
        isQuit = true;
        System.out.println("线程t终止");
    }
}

注意:main线程和t线程在同一个进程地址空间,因此,main线程修改的isQuit和t线程判定的isQuit是同一个值

2.)上述代码不够严谨,更好的做法是使用Thread中内置的一个标志位来进行判定
可以通过Thread.interrupted()和Thread.currentThread.isinterrupted()获得这个标志位
前者是一个静态方法,后者是一个实例方法
推荐使用后者,因为一个代码中的线程可能有很多个,随时哪个线程都可能会终止。
Thread.interrupted()判定的标志位是Thread的static成员(一个程序中只有一个标志位)
Thread.currentThread.isinterrupted()判定的标志位是Thread的普通成员,每个线程实例都有自己的标志位

public class Demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //当触发异常后,立即退出循环
                    break;
                } finally {
                    System.out.println("这是收尾工作");
                }
            }
        });
        t.start();


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

        }

        //在主线程中调用 interrupt 方法,来中断这个线程
        //t.interrupt()的意思就是让t中断!!
        //如果调用这个方法,可能会产生两种情况
        //1.如果t线程处在就绪状态,就是设置线程的标志位为true
        //2.如果t线程处在阻塞状态(sleep休眠),就会触发一个interruptedException,此时设置标志位就不能起到及时唤醒的作用
        t.interrupt();
    }
}

在这里插入图片描述

6、等待线程

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。因此,我们需要一个方法明确等待线程的结束。

public class Demo12 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           for(int i = 0; i < 5; ++i) {
               System.out.println("hello,thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();

        //在主线程中可以使用一个等待操作,来等待t线程的执行结束
        try {
            t.join(5000);//最多等待5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main等待完毕");
    }
}

在这里插入图片描述

首先,调用t.join()这个方法的线程是main线程,针对t这个线程对象调用的,此时就是让main等待t
调用join之后,main线程就会进入阻塞状态(暂时无法在cpu上执行),直到t线程执行完毕,main线程才会被唤醒,继续向下执行。
通过线程等待,一定程度上的干预了线程的执行顺序

join()方法默认情况下,是死等(不见不散),如果t线程一直不执行完毕,main线程就一直等下去
如果给join()方法添加参数,例如join(5000),表示main线程最多等5秒,如果在5秒之内,t线程执行完毕,main线程就不会再等。如果t线程5秒之后还在执行,main线程最多等待5秒就不再等待了

7、获取当前线程的引用

public static Thread currentThread()就能够获取到当前线程的引用(Thread实例的引用)。
哪个线程调用的这个方法,就获取到的是哪个线程的实例

public class Demo13 {
    public static void main(String[] args) {
            Thread t1 = new Thread(){
            @Override
            public void run() {
                //获取当前线程的实例
                //这个代码是通过继承Thread的方式来创建线程
                //此时run方法中,直接通过this,拿到的就是当前的Thread的实例
                System.out.println(Thread.currentThread().getName());
                System.out.println(this.getName());
            }
        };
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //此处的this不是指向Thread,而是指向Runnable,而Runnable只是一个单纯的任务,没有name属性的
                //System.out.println(this.getName());
                //要想拿到线程的名字,只能通过Thread.currentThread().getName()
                //lambda表达式效果同Rannable
                System.out.println(Thread.currentThread().getName());
            }
        });
        t2.start();

        //这个线程是在main线程中调用的,因此拿到的是main这个线程的实例
        System.out.println(Thread.currentThread().getName());
    }
}

在这里插入图片描述

注意:通过Runnable去构造Thread对象,在run方法中,this指向的是Runnable,而不是Thread对象。使用lambda去构造Thread对象和Runnable是一样的

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值