Java中的多线程(上)

目录

一、关于线程

1.What

2.Why

3.When 

二、线程的创建与启动

1. 创建一——Thread类:

2. 创建二——Runnable接口

3. 其他写法 

4. 注意事项

三、jconsole工具 

四、线程的常见属性

五、线程的方法使用——协调控制

1.join()方法

2.Thread.sleep()方法

3.Thread.currentThread()方法

4.Thread.yield()方法

5.线程的停止 

六、小总-并发VS单线程 


一、关于线程

1.What

什么是线程 —— 一个执行流就是一个线程

之前我们写的代码都只有一个main主线程

在操作系统职责概述这一篇里我们详细总结了进程与线程的概念:

 (具体进程线程的概念理解点链接:操作系统的职责概述_笨笨在努力的博客-CSDN博客

相应的,在Java中,也有进程和线程的操作,基本概念都是一样的,进程是系统分配资源的最小单位,线程是进行调度的最小单位,在Java中,主要也是多线程的开发,基本很少有多进程的开发

2.Why

多线程实现的就是并发编程,主要还是为了提高代码的执行效率,或是充分利用资源

首先, "并发编程" 成为 "刚需".

  •         单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  •         有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  •         创建线程比创建进程更快.
  •         销毁线程比销毁进程更快.
  •         调度线程比调度进程更快. 

3.When 

  1. 计算密集性任务,提升效率

  2. 当一个执行流因故堵塞,为了处理其他任务,我们可以引入多线程

二、线程的创建与启动

线程的创建总体有两种:

1. 创建一——Thread类:

(1)继承Thread类,并覆写run()方法,run()方法就是该线程执行的内容

/**线程创建方式一——继承thread类
 * @author sunny
 * @date 2022/04/17 16:32
 **/
public class creat_Method1 extends Thread{
    @Override
    public void run() {
        System.out.println("这是我的线程");
    }
}

(2)创建类的实例,如t

(3)t.start()方法,启动线程(注意,这里是start方法,执行该方法会自动调run方法)

public class Test {
    public static void main(String[] args) {
//        创建实例对象
        creat_Method1 t = new creat_Method1();
//        start()方法
        t.start();
        System.out.println("我是主方法中的程序");
    }
}

运行结果:

 结果分析:可以看到,start()方法执行的就是run方法,同时可以看到,主线程和子线程两个线程,至于哪个线程会先执行,我们是不知道的,这取决于谁先分配到了CPU被调度器选中

2. 创建二——Runnable接口

(1)实现Runnable接口

(2)实现run方法

/**线程创建方式二——Runnable接口
 * @author sunny
 * @date 2022/04/17 16:39
 **/
public class creat_method2 implements Runnable{

    @Override
    public void run() {
        System.out.println("这是我的线程");
    }
}

(3)创建Thread类的实例,利用其构造方法把Runnable实现类的对象传进去

(4)调用start方法

public class Test {
    public static void main(String[] args) {
        Creat_Method2 tt = new Creat_Method2();
//        创建Thread类的对象,并将tt传进去
        Thread t = new Thread(tt);
//        start方法执行
        t.start();
        System.out.println("这是主线程");
    }
}

执行结果:

3. 其他写法 

基于上述两种创建方式,还有使用匿名内部类,Lambda表达式等写法,如下:

/**创建线程的其他写法
 * @author sunny
 * @date 2022/04/25 16:59
 **/
public class Other {
    public static void main(String[] args) {
//    1.匿名类,继承自Thread
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("使用匿名类(继承Thread)创建的线程");
            }
        };
//    2.匿名类,实现Runnable接口
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名类(实现Runnable接口)创建的线程");
            }
        });
//        3.Lambda表达式创建
        Thread t3 = new Thread(() -> System.out.println("Lambda表达式创建"));
//        启动各个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:

【结果显示不唯一,各线程执行顺序“听天由命”】 

4. 注意事项

线程启动调用的一定是start()方法;

一个线程的start()方法不能重复调用(因为线程都必须从就绪状态开始执行);

        t.start();
        t.start();

编译不会报错,但运行后会报错

三、jconsole工具 

 jconsole工具是JDK自带的一个观察JVM运行情况的工具,比如内存情况,类加载情况,线程情况等,这里我们以观察线程情况为主:

如果没有更改安装目录,默认是在C盘——Program Filels——Java——jdk

进入jdk的bin目录下可以看到,bin目录下有很多jdk自带的工具:

 目前我们只用到了java,javac,现在接触一下jconsole工具,先开启一个待观察的线程(比如一秒打印一个1的程序),然后双击打开jconsole

import java.util.concurrent.TimeUnit;

/**每隔一秒打印一次1
 * @author sunny
 * @date 2022/04/19 20:50
 **/
public class Main3 {
    public static void main(String[] args)  throws InterruptedException{
//        获取当前线程
        Thread main = Thread.currentThread();
//        设置当前线程名字为主线程(确实是主线程,因为我获取的就是当前线程)
        main.setName("主线程");
        while(true){
//            该语句意思是sleep一秒
            TimeUnit.SECONDS.sleep(1);
            System.out.println(1);
        }
    }
}

 

四、线程的常见属性

常见属性总览:

(1)id

🧁本进程内部分配的唯一id,id具有唯一性不会重复

🧁只能get,不能set

(2)name

🍧为了方便开发者看,JVM不需要该属性

🍧可get也可set

🍧设置时可以在构造中用setName方法,也可直接父类super()

🍧默认线程名字为Thread-0,Thread-1……

//    设置名字一:可以直接super,该构造方法的参数即为name
    public Mythread(String name){
        super(name);
    }
//    设置名字二:调set方法
    public Mythread(){
       setName("线程1");
    }

(3)线程状态

new(新建状态)——runnable(就绪/运行状态)——terminated(结束状态)

blocked(阻塞状态),专指加锁失败后的状态

waiting(等待状态)

timed_waiting(带时间的等待状态)

💌只能get不能set

(4)优先级——Priority

🍭可get也可set

🍭设置时一般直接带给定的常量,不要自己随便填数字

🍭即使我们人为setPriority,也并不绝对,只是给JVM的建议

(5)前台线程VS后台线程

💫可get也可set;

💫get时使用isDaemon,返回boolean类型,true表示后台进程,false表示前台进程

💫默认是前台线程

后台——做一些支持工作的线程

前台——做一些交互工作

💫当所有前台线程都退出,JVM进程才退出(与主线程、后台线程无关)

 (6)isAlive和isInterrupted

是否存活和是否被中断,都是只能get,返回一个Boolean值

一些小练习:

public class Main1 {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        //        获取该线程id
        System.out.println(thread.getId());
        System.out.println(thread.isDaemon());
        Mythread t = new Mythread();
//        获取名字
        System.out.println(t.getName());
        t.setName("俺又改名字了");
        System.out.println(t.getName());

//        前后台线程
        //        true=后台线程,false=前台线程
        System.out.println(t.isDaemon());
        t.setDaemon(true);
        System.out.println(t.isDaemon());
    }
}

输出结果:

public class Main4 {
    public static void main(String[] args) {
        Mythread t = new Mythread();
//        是否活着,简单理解就是尚未start或者run方法运行结束就是false
        t.start();
        System.out.println(t.isAlive());
        System.out.println(t.isInterrupted());

    }
}

五、线程的方法使用——协调控制

1.join()方法

join方法可以理解为等待另一个线程

比如 t1.join() 意思就是要等t1线程执行结束才会接着继续执行后面的语句

 例子:

import java.util.concurrent.TimeUnit;

/**join方法的练习
 * @author sunny
 * @date 2022/04/30 09:18
 **/
public class join_method {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println("我是线程1我正在工作");
                try {
//                    等5秒后
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我是线程1我工作结束了");
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                System.out.println("我是线程2我开始工作了");
            }
        };
//        启动t1线程
        t1.start();
//        等t1线程结束
        t1.join();
//        t2线程才启动
        t2.start();
    }
}

结果:

而如果把join语句注释掉,结果:

在t1还未执行完之前,t2就开始工作了

2.Thread.sleep()方法

(1)sleep方法是静态方法

(2) sleep方法——让线程休眠,从运行状态变阻塞状态(让出CPU),休眠时间结束后重新进入就绪队列,而线程的调度是不可分的,故而这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

(2)我们之前用到过TimeUnit.SECONDS.sleep()方法,该方法与这里的sleep方法类似,都是休眠一段时间后再执行,只不过TimeUnit.SECONDS.sleep(1) 的单位是秒,而这里的sleep单位是毫秒,所以:

TimeUnit.SECONDS.sleep(1) = Thread.sleep(1000)

3.Thread.currentThread()方法

(1)静态方法

(2)返回当前线程的引用

上面已经演示过,这里不再赘述

4.Thread.yield()方法

(1)也是静态方法

(2)纯让CPU ,趋于0%,随时可以被继续调度

【适用于执行耗时超久的计算任务,要时不时分一些资源给其他进程,防止计算机出现卡顿】

看下列现象:

public class yield_method {
    static class MyThread extends Thread{
        private String name;
        public MyThread(String name){
            this.name = name;
        }
        @Override
        public void run() {
            while(true) {
                System.out.println("我是" + name);
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread("yiyi");
        MyThread t2 = new MyThread("sansan");
        t1.start();
        t2.start();
    }
}

 两个线程同等,所以基本会yiyi和sansan同等概率交替打印出现

 而当我们让其中一个线程主动yield时,会发现:

 结果会看到,基本全是sansan在运行,当然,yiyi也有少部分,但很明显,sansan成为主流

5.线程的停止 

(1)A方控制:

  1. 暴力停止——stop(),直接把该线程杀死,不知道也不管B的任务进度,是不可控的,所以这种方法基本已经废弃,极度不安全

  2. 协商后再停止——interrupt(),只是发了信号,并不会让B真的立马停止

    A主动给B发信号代表B停止,B.interrupt(),B看到信号后把手头工作做到一个阶段后主动停止退出(靠我们编程实现)

(2)B方感应

  1. 如果B正在正常执行代码——可通过该方法Thread.interrupted()判断B是否要被中止,true就是要停止,一般正常执行一段时间就去看一眼,至于自己要不要停止自己决定【interrupted()是静态方法,interrupt()方法不是哦】

  2. 如果B处于休眠状态(如sleep,join),意味着B无法立即执行Thread.interrupted()判断,JVM就会以异常的形式(InterruptedException)通知B,B能够捕获到InterruptedException异常,就说明有人希望我们停止 

总之,B感应到停止信号后,要不要停,怎么停,都是要靠我们人为编程控制的

举个例子来验证:

import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**线程停止例子
 *在我们输入任意字符后使B停止
 * @author sunny
 * @date 2022/04/30 10:52
 **/
public class interrupt_method {
    static class MyThread extends Thread{
        @Override
        public void run() {
            while(true){
//                每打印1000次就休眠2秒
                for (int i = 0; i < 1000; i++) {
                    System.out.println("正在打印");
                }
//                休眠2秒
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
//        在主线程得到任意一个输标准入后,使线程t停止
        System.out.println("请任意输入字符");
        Scanner scanner = new Scanner(System.in);
        scanner.next();             //只要我们不输入,主线程就阻塞在这里
//        当我们输入后,主线程去让t线程停止
        t.interrupt();
    }
}

结果可以发现:因为我们并没有编写代码去定义t的停止,所以,即使我们输入任意一个字符,线程t并没有乖乖停止,而是继续在打印

那么,怎么真正实现我们想要的效果呢?

改变如下:

完整代码看下面:

import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**线程停止例子
 *在我们输入任意字符后使B停止
 * @author sunny
 * @date 2022/04/30 10:52
 **/
public class interrupt_method {
    static class MyThread extends Thread{
        @Override
        public void run() {
            while(true){
//                每打印1000次就休眠2秒
                for (int i = 0; i < 1000; i++) {
                    System.out.println("正在打印");
                }
//                休眠之前检查有没有人让我们停止
                if(Thread.interrupted()){
//                    如果有人让我们停止,那我们就停止,跳出while循环
                    break;
                }

//                休眠2秒

                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    //     如果休眠状态有人让我们停止,我们会捕捉到这个异常,进入catch中
                    break;
                }
//                最后检查一下休眠后是否让我们停止
                if(Thread.interrupted()){
                    break;
                }
            }
//            如果停止,最后执行一句
            System.out.println("我乖乖停止了");
        }
    }
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
//        在主线程得到任意一个输标准入后,使线程t停止
        System.out.println("请任意输入字符");
        Scanner scanner = new Scanner(System.in);
        scanner.next();             //只要我们不输入,主线程就阻塞在这里
//        当我们输入后,主线程去让t线程停止
        t.interrupt();
    }
}

这次我们输入任意一个字符后,可以看到,线程停止了

六、并发VS单线程 

介绍了这么多多线程,那么,问题来啦,多线程就一定比单线程效率高吗???

先来一个小栗子:四路归并排序

import java.util.Random;

/**构造排序数组
 * @author sunny
 * @date 2022/04/30 11:26
 **/
public class ArrayHelper {
    public long[] generateArray(int n){
        Random random = new Random(100000);
        long[] array = new long[n];
        for (int i = 0; i < n; i++) {
            array[i] = random.nextInt();
        }
        return array;
    }
}

🤨🤨🤨🤨🤨单线程:

import java.util.Arrays;

/**单线程进行四路归并排序
 * @author sunny
 * @date 2022/04/30 11:24
 **/
public class SingleOrder {
    public static void main(String[] args) {
        ArrayHelper h = new ArrayHelper();
        long[] arr = h.generateArray(400000);
//        进行四路归并排序并记录时间
        long cur = System.currentTimeMillis();
        Arrays.sort(arr,0,100000);
        Arrays.sort(arr,100001,200000);
        Arrays.sort(arr,200001,300000);
        Arrays.sort(arr,300001,400000);
//        最后整体并一下即可,这里为对比观察先不处理
        long now = System.currentTimeMillis();
        System.out.println(now - cur);
    }
}

 耗时:

130

🤔🤔🤔🤔🤔多线程呢?

import java.util.Arrays;

/**
 * 多线程四路归并排序
 *
 * @author sunny
 * @date 2022/04/30 11:25
 **/
public class ThreadOrder {
    static class Thread_Order extends Thread {
        private long[] arr;
        private int left;
        private int right;

        public Thread_Order(long[] arr, int left, int right) {
            this.arr = arr;
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            Arrays.sort(arr, left, right);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long[] arr = ArrayHelper.generateArray(400000);
        long cur = System.currentTimeMillis();
        Thread_Order t1 = new Thread_Order(arr, 0, 100000);
        t1.start();
        Thread_Order t2 = new Thread_Order(arr, 100001, 200000);
        t2.start();
        Thread_Order t3 = new Thread_Order(arr, 200001, 300000);
        t3.start();
        Thread_Order t4 = new Thread_Order(arr, 300001, 400000);
        t4.start();
//        四个线程都结束再记一次时
        t1.join();
        t2.join();
        t3.join();
        t4.join();
        long now = System.currentTimeMillis();
        System.out.println(now - cur);
    }
}

耗时:

81 

💫💫💫💫💫💫💫 分析这个小栗子,让我们来明晰以下几点 💫💫💫💫💫💫💫💫💫

(1)多核CPU的并发编程一定比单线程执行快,这是明确的

(2)单CPU下并发编程也能比单线程快吗?答案是yes

即使是在单CPU下,并发耗时也可能较少:

比如现在有100个线程,公平平均情况下,单个线程下只会被分配1/100的时间,而如果把1个换成4个,会被分到4/103

所以,即使单核,一个进程中的线程越多,被分到的时间片就越多

那线程越多越好吗?——肯定不是

首先创建线程本身也需要时间;其次,时间片有极限,最大也就是100%;最后,线程调度也不是白嫖的,也需要耗时(10个中选一个和10000中选一个耗时一定不一样)【CPU是公共资源,不能这么干】

(3)并发排序耗时一定小于串行吗吗?答案是不一定

串行:T(区间1)+T(区间2)+T(区间3)+T(区间4)

并发:T(创建4个线程)+T(四个并发排序中的最大时间)+T(销毁线程)

并不能保证并发的时间加总一定比串行快

(4)最后的最后,为什么写多线程?

提升整个进程的执行速度(尤其计算密集型的程序)

当一个执行流因故堵塞,为了处理其他任务,我们可以引入多线程

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨笨在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值