0
点赞
收藏
分享

微信扫一扫

【JavaEE学习日记】----多线程(上)

fbd4ffd0717b 2022-03-23 阅读 71
javajavaee

目录

💗1.什么是线程?

💗2.有了进程为什么要有线程?

☁️2.1.

☁️2.2.并发(Concurrent)编程

💗3.经典面试题:进程和线程的区别? 

💗4.Java的线程和操作系统的线程有什么区别

💗5.第一个多线程程序

💗6.五种创建线程的方法

🐔创建一个子类继承Thread

🐛创建一个类实现Runnable

🐞使用匿名内部类

🐠使用匿名内部类

🐏使用lambda表达式

 💘7.多线程的优势

💘8.Thread类及其常见方法 

⚡8.1.Thread的常见构造方法

⚡8.2.Thread常见属性

💘9.Thread常用的一些方法

💘10.中断线程

①使用标记位

②使用Thread类自带的标记位

 💘11.等待一个线程 join()

💘12.获取当前线程的引用

💖13.线程休眠 sleep

💕💕💕💕💕💕💕💕💕💕💕💕💕💕分隔符💕💕💕💕💕💕💕💕💕💕💕💕💕💕💕

💗1.什么是线程?

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

简单来说线程是包含在进程内部的,就相当于一个工厂,这个工厂就相当于进程,工厂里面有许多的流水线,这个流水线就相当于线程

💗2.有了进程为什么要有线程?

☁️2.1.

①进程一个时间只能干一件事,如果想进程一个时间干很多事,那么进程也无能为力

②许多进程一起工作时,如果有一个进程挂了,可能会影响其他的进程,这是非常不利的

举个例子:我们炒一道菜的时候需要很多的步骤,例如炒土豆丝,首先需要清洗,然后去皮,在进行浸泡,最后在下锅,需要很多的步骤,想要在短时间内把这道菜做出来是不太可能的,如果有个东西能帮你分担这些事,你就不会这么的手忙脚乱了,这个东西就叫做线程

以上这个例子我们就能清楚的知道线程的好处

☁️2.2.并发(Concurrent)编程

随着硬件性能的迅猛发展与大数据时代的来临,并发编程日益成为编程中不可忽略的重要组成部分。简单定义来看,如果执行单元的逻辑控制流在时间上重叠,那它们就是并发(Concurrent)的。并发编程复兴的主要驱动力来自于所谓的“多核危机”

①单核CPU的发展遇到了瓶颈,想要提高算力,就需要多核CPU,而并发编程能更充分的利用多核CPU的资源

②有些任务用到了等待 IO ,为了让等待IO的时间能做其他的事情,也就需要用到并发编程

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

虽然线程比进程更轻量,但是人们还是不满足,于是又有了“线程池” 和 “协程”(此处不过多介绍)

💗3.经典面试题:进程和线程的区别? 

💗4.Java的线程和操作系统的线程有什么区别

⭐线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用❗

🐍Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装 ❗

💗5.第一个多线程程序

class Mythread extends Thread{
    @Override
    public void run() {
        System.out.println("the first thread");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

☀️具体分析:

@Override
    public void run() {
        System.out.println("the first thread");
    }

重写了Thread里的run方法,里面的代码就表示这个线程具体要干什么,这里相当于安排任务,病是不是说这里的代码一写出来就可以执行这个线程

thread.start();

start方法是真正的创建出线程的方法,只有调用了start方法,线程才能被创建出来,上面重写的run方法也才能被执行


💗6.五种创建线程的方法

🐔创建一个子类继承Thread

class Mythread extends Thread{
    @Override
    public void run() {
        System.out.println("the first thread");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

🐛创建一个类实现Runnable

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("my second thread");
    }
}

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

🐞使用匿名内部类

public class TestDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("my third thread");
            }
        };
        thread.start();
    }
}

🐠使用匿名内部类

public class TestDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("my fourth thread");
            }
        });
        thread.start();
    }
}

🐏使用lambda表达式

public class TestDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("my fifth thread");
        });
        thread.start();
    }
}

相比较以上四种方式,lambda表达式可能会比较难理解一点,但却实最简洁的一种写法


 💘7.多线程的优势

创建一个变量让他们分别自增10_0000_0000次,分别使用串行执行和并发执行,分别使用前后的时间戳来表示他们的时间

public class Demo4 {

    public static final long count = 1000000000;

    /**
     * 串行形式
     */
    public static void serial(){
        long start = 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((end - start)+ "ms");
    }

    /**
     * 并发执行
     */
    public static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            long a = 0;
            for (int i = 0; i < count; i++) {
                a++;
            }

        });
        Thread thread2 = new Thread(() ->{
            long b = 0;
            for (int i = 0; i < count; i++) {
                b++;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");

    }

这里要注意的一点就是 .join() 方法,这个方法的意思是等待该线程执行完

可以很清楚的看到并发执行的速度要大于串发执行的速度 ,但是多线程并不是万能的,多线程适合于CPU密集型的程序,程序要进行大量的计算,使用多线程可以更充分的利用CPU的多核资源


💘8.Thread类及其常见方法 

Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之相关联。

根据上述例子,每个执行流,也需要有一个对象来描述,而Thread类的对象就是用来藐视一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理

⚡8.1.Thread的常见构造方法

⚡8.2.Thread常见属性

♠️ id :id是线程的唯一标识,不同的线程不能有相同的id

♠️名称:名称是帮助程序员调试用的比如使用 jconsole

JVM调优之JConsole和JVisualVM工具使用_执笔记忆的空白的博客-CSDN博客_jconsole jvisualvm

♠️状态:状态表示线程当前所处的一个情况

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

♦️是否是后台进程:关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行

♦️是否存活:是否存活,即简单的理解,为 run 方法是否运行结束了

♦️是否被中断:字面意思

强调两个方法:

①isDaemon():如果线程是后台进程,就不影响进程退出,如果线程是前台线程,就会影响到进程的退出。

例如:如果创建的thread1线程和thread2线程都是前台线程,即使main方法执行完毕,进程也不能退出,得等thread1和thread2都执行完,整个进程才能退出,如果两个都是后台线程,则main执行完毕后,整个进程能直接退出

②isAlive():操作系统中对应的线程是否正在运行。Thread thread 对象的生命周期和内核中对应的线程,生命周期并不完全一致,创建出threa对象之后,在调用start之前,系统中没有对应线程的,在run方法执行完之后,系统中的线程就销毁了,但是thread这个对象可能还在,通过isAlive就能判定当前系统线程的运行情况

如果调用start之后,run执行完之前,isAlive就是返回true

如果调用start之前,run执行完之后,isAlive就返回false


💘9.Thread常用的一些方法

public class TestDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("my fifth thread");
        });
//        thread.start();
        thread.run();
    }
}

执行结果:my fifth thread

public class TestDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("my fifth thread");
        });
        thread.start();
//        thread.run();
    }
}

执行结果:my fifth thread

可以看到,两段代码执行的结果都是一样的,那么这两个有什么区别吗?

💊run 方法只是一个普通的方法,单纯的描述了线程里面的内容,在main里调用run方法,就是单纯的串行执行代码,start则是一个特殊的方法,内部会在系统中创建线程


💘10.中断线程

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

以上的线程会不断的打印 my thread ,并不会停下来,那么如何能够终止一个线程呢?

①使用标记位

public class TestDemo3 {
    private static boolean isQuit = false;

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

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

        isQuit = true;
        System.out.println("终止 t 线程!");
    }
}

②使用Thread类自带的标记位

public class TestDemo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("my thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread.interrupt();
    }
}

但是使用Thread类自带的标记位会出现以下的情况

 这是为啥呢,难道出错了 ❔❔❔

调用这个方法可能会产生两种情况:

①如果thread 线程处于就绪状态,则设置线程的标志位会变成true

②如果thread线程处于阻塞状态(例如sleep休眠),就会触发一个InterruptException


 💘11.等待一个线程 join()

多个线程之间,调度顺序是不确定的,线程之前的执行是按照调度器来安排的,这个过程可以视为“无需,随机”,这样不太好,有些时候我们需要能控制线程之间的顺序,而线程等待就是其中一种解决办法,此处的线程等待主要是控制线程结束的先后顺序

join() : 前面也是介绍了join 的使用方法,哪一个线程使用jion,哪一个线程就会阻塞等待,直到对应的线程执行完毕,即run方法执行完毕

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

        System.out.println("my main");

    }
}

根据上述代码,首先,调用这个方法的线程是main线程,针对thread这个线程对象调用的,意思是让main等待thread,调用join方法之后,main线程就会进入阻塞状态,意思是暂时无法被调度到CPU上执行,直到thread这个线程的run跑完之后main这个线程才开始运行

join这个操作默认情况下是死等 💩

当然join也提供了另外一个版本,也就是可以执行等待时间,设置最长等待多长时间,如果超过了等待时间,那么程序就直接扯,开始下一个线程的执行,对于以后的开发中,一般用的都是第二个版本


💘12.获取当前线程的引用

💩💩💩 这个方法前面也是用过

Thread.currentThread(),能够获取得到当前线程引用(Thread实例的引用),哪个线程调用的这个currentThread,那个线程就能获取到当前线程的实例

1)

public class Test {
    public static void main(String[] args)  {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println(this.getName());
            }
        };
        thread.start();
    }
}

这段代码是通过继承Thread的方式来创建线程,此时在run方法中,直接通过this拿到的就是Thread的实例

2)

public class Test {
    public static void main(String[] args)  {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(this.getName()); //这里是错误的,代码不能执行
            }
        });
        thread.start();
    }
}

此处的this并不是指向Thread类型,而是指向Runnable,而Runnable只是一个单纯的任务,没有name属性 

正确的用法:

public class Test {
    public static void main(String[] args)  {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()); //这里是错误的,代码不能执行
            }
        });
        thread.start();
    }
}

通过Thread.currentThread获取到当前线程的实例,就能获取到线程的名字了


💖13.线程休眠 sleep

如果是一个进程有多个线程,此时每个线程都有一个PCB,一个进程就对应一组PCB,PCB上有一个字段tgroupid,这个id相当于进程的id,同一个进程中的若干个线程的tgroupid是相同的

对于前面我们的理解,PCB应该是属于进程专有的啊,那么这里的PCB和进程的PCB有什么区别呢?

其实对于Linux内核来说不区分进程和线程,进程和线程只是我们认为的定义的,实际上Linux内核只认PCB,在Linux中我们把线程称为轻量级进程,具体为何轻量,可以翻看我的上一篇博文

如果某个线程使用了sleep方法,这个线程的PCB就会进入到阻塞队列,操作系统进行调度的时候,就只从就绪队列中调训啊合适的PCB到CPU上运行,阻塞队列里的PCB只能干等着,当睡眠时间到,系统就会吧刚才这个PCB从阻塞队列中移除,放到就绪队列中 

举报

相关推荐

0 条评论