【多线程】(基础一)

这篇博客介绍了多线程编程的基础知识,包括如何创建线程、线程的状态转换以及Thread类的相关方法。通过代码示例展示了线程创建的不同方式,并分析了单线程与多线程的速率对比,强调了多线程在CPU密集型和IO密集型场景的应用。同时,讨论了线程的中断、等待和休眠操作,以及线程状态的枚举类型Thread.State。
摘要由CSDN通过智能技术生成

多线程编程(基础一)

创建线程

通过代码认识线程

image-20220726124740853

  • 即使是最简单的main方法在运行的时候,也涉及到线程了,一运行程序,先产生了一个java进程,这个进程里会有一个线程调用main方法,调用main方法的线程也叫主线程。
  • 上述代码一运行,会创建多个线程,除了调用main方法的主线程,还有其他线程,比如有关垃圾回收的线程等,所以这么简单的程序也是一个多线程程序。

创建一个线程

一个创建线程的方法:写一个子类,继承Thread类,并重写父类的run()方法

image-20220726134302665

image-20220726134322327

  • 这里重写run()方法,是为了明确新创建的出来的线程的任务。
  • myThread.start()是真正的开始创建线程(创建了线程也不一定立刻上CPU执行,得等操作系统调度),如果将myThread.start()改为myThread.run(),则不会创建新的线程,而是在原来的线程里调用run()方法

我们从代码中看到的是先创建了线程,后打印了"hello main",但是执行结果却恰恰相反,这是为什么呢?

  • 其实并不是先执行完我们创建的这个线程,再接着执行调用main方法的主线程。因为实际上每个线程都是一个独立的执行流,两个线程是并发执行的,所以哪个线程先执行完,这个是不确定的,是随机的。到底哪个先执行完和线程本身以及操作系统的调度有关。
  • 这两个线程可能被分配到CPU的同一核心上(通过时间片轮转的方式执行),也可能被分配到CPU的不同核心上,这个也是不确定的。
  • 这里看到的先打印main后打印thread,大概率是我们写的这个线程的创建需要一定的时间开销,而与此同时主线程会继续执行,所以可能先打印出了main,后打印的thread
  • Process finished with exit code 0 代表进程结束了,退出码是0。操作系统中用进程的退出码来表示进程的执行结果。0代表执行完毕,结果正确,非0代表执行完毕,结果不正确,还有一种情况是进程中途崩溃了,返回的很可能是个随机值。

进程A创建了进程B,那么就说进程A是进程B的父进程,但是线程之间没有这种关系(即使一个线程在执行的过程中创建了另一个线程),我们认为线程之间是平等的。 👈

查看线程

image-20220726143020792

先给代码加上一个死循环,让进程一直不结束,然后运行bin目录下的jconsloe.exe就可以看到当前进程中的线程了

image-20220726143153172

image-20220726143220183

  • 这里我们也可以看到当前进程中有很多线程,main是调用main方法的主线程,Thread-0是我们自己创建的线程,其他线程是辅助线程。

创建线程的几种方式:

  1. 第一种就是我们上面写的,写一个类继承Thread并且成重写run()方法
  2. 创建一个类,实现Runable()接口,重写run()方法

image-20220729104714406

  • 这种写法相比与第一种是把任务和线程分离开了,Runable代表要执行的任务,Thread实例代表一个线程,start()代表线程开始创建并执行。
  1. 使用匿名内部类继承Thread,实现继承,重写run()方法,new内部类实例三部一体

image-20220729132200332

  1. 使用匿名内部类实现Runnable接口,让实现接口,重写方法,new内部类对象三部一体

image-20220729132918298

使用了内部类就会比前两种代码更为简洁。

  1. 使用lambda表达式

image-20220729134648215

单多线程速率对比

单线程方式:

public class Test {
    public static final long COUNT = 20_0000_0000;
    //单线程
    public static void func1(){
        long a = 0;
        long beg = System.currentTimeMillis();
        for (long i = 0; i < COUNT; i++) {
            a++;
        }
        a = 0;
        for (long i = 0; i < COUNT; i++) {
            a++;
        }
        long end = System.currentTimeMillis();
        System.out.println((end-beg)+"ms");
    }
    
    //多线程
    public static void func2(){
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });
        Thread t2 = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println((end-beg)+"ms");
    }

    public static void main(String[] args) {
        func2();
        //func1();
    }
}

image-20220729184931034

image-20220729184954976

  • 由时间来看,多线程确实比单线程快了不少
  • 但是我们看到,时间上并不是两个线程并发执行,时间上并不是砍半原因是:
  1. 线程的创建也需要时间开销,
  2. 两个线程在执行上不一定是一直并行执行,也可能是一部分时间并行执行,一部分时间并发执行(因为线程多了之后,在一段时间内,各个线程上CPU执行的时间就会相应减少)
  3. 线程的调度也需要时间开销

多线程使用场景:

  1. CPU密集型场景(代码中CPU运算较多),使用多线程可以更好的CPU资源
  2. IO密集型场景,IO操作几乎不消耗CPU,这时候就可以 给CPU找点活干,使用多线程,避免闲置,

Thread类及常见方法

构造方法

image-20220729222824965

  • Thread()在第一种创建线程的方式中用到了
  • Thread()在第二种方式中用到了
  • Thread(Runnable target,String name) 是给创建的线程命名
public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("haha");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t = new Thread(runnable,"我的线程");
        t.start();
    }

在用jconsole命令打开的窗口中就可以看到这个自己命名的线程

image-20220729223650436

属性

image-20220729225921255

  • 当创建一个线程时,就会随之为这个线程创建这些属性(在Thread类中)
  1. ID : 线程的身份标识,线程的身份标识有好几个,操作系统内核的PCB中有身份标识,用户态线程库里有个身份标识(pthread,操作系统提供的线程库),JVM里有个身份标识(JVM Thread类底层也是调用的phread库)
  2. 名称: 线程的名字,默认是Thread0,Thread1,也可以自己指定名字
  3. 状态:JVM中的线程的的状态(JVM中有自己的状态体系,比操作系统内核中的状态更丰富),不是内核中PCB中的状态
  4. 优先级:获取到优先级
  5. isDaemon() ,daemon称为守护线程,也叫后台线程,线程分前台线程和后台线程,一个线程创建出来默认是前台线程,只有前台线程执行完了,进程才会结束,进程结束时不会管后台线程是否结束。
package thread;

public class Demo1 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable,"我的线程");
        thread.start();

        System.out.println(thread.getId());
        System.out.println(thread.getName());
        System.out.println(thread.getState());
        System.out.println(thread.getPriority());
        System.out.println(thread.isDaemon());
    }
}

image-20220730095429252

image-20220730100015327

注意:上面这些操作都是获取到该线程某一时刻的信息(比如某一时刻的状态),所以每次运行获取到的线程的状态都是不确定的

启动线程

启动线程用start()方法,调用start()方法是线程真正创建执行,run()方法是描述一个任务,创建一个Thread实例是准备好线程

中断线程

有的线程任务是比较复杂的,执行时间比较长,有时候我们想让线程提前结束,就需要中断线程,中断线程就是在一个线程中控制另一个线程结束

中断线程有两种方式:

第一种方式:自己设立一个标志位:

package thread;

public class Demo2 {
    private static boolean flg = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(!flg){
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程执行结束");
            }
        };

        Thread thread = new Thread(runnable,"我的线程");
        thread.start();

        Thread.sleep(5000);
        System.out.println("控制线程结束");
        flg = true;
    }
}

  • 通过自己设立的标志位,来控制线程结束

第二种方式:使用Thread类中的标志位

package thread;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted()){
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //break; //立即退出
                        
                         /*try {
                            Thread.sleep(3000);
                        } catch (InterruptedException ex) {
                            ex.printStackTrace();
                        }
                        break;*/ //稍后退出
                    }
                }
            }
        };

        Thread t = new Thread(runnable,"我的线程");
        t.start();
        Thread.sleep(5000);

        t.interrupt();
    }
}


  • Thread.currentThread() 会返回当前线程对应的Thread对象(哪个线程中调用了这个方法就会返回这个线程对应的Thread对象),用这个对象调用isInterrupted()会返回当前对象中的标志位,默认是false
  • interrupt() :如果t线程当前没有处在阻塞状态,就会修改标志位为true;如果t线程正在处在阻塞状态,就会让t线程内部产生阻塞的方法,如sleep()抛出异常,但是并不会修改标志位

image-20220730134458944


  • 从控制台中可以看到捕获并打印了异常,说明调用interrupt() 时t进程正处于阻塞状态。但是打印完异常,然后t进程还是接着执行,并没有中断线程,因为我们在catch中只是打印了异常,并没有做处理。也就是说当线程处于阻塞状态时,会触发异常,但是到底是否中断,何时中断,是由我们程序员自己控制的(也就是改变catch中的实现逻辑),
while(!Thread.currentThread().isInterrupted()){
    System.out.println("线程运行中");
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}


Thread t = new Thread(runnable,"我的线程");
t.start();
Thread.sleep(5000);

t.interrupt();

注意: t线程大部分时间是处于阻塞状态,很小一部分时间是处于正常运行状态,(始终在阻塞和正常运行状态切换的)所以我们调用interrupt() 时,大概率会产生异常,也有很小的概率会修改标志位。如果我们把Thread(5000)注释掉,则大概离会修改标志位


==总结:==interrupt() 并不会立马让t进程中断,当调用interrupt()时,如果t线程是非阻塞状态,则修改标志位为true, 然后等t 线程再次运行到类似while(!Thread.currentThread().isInterrupted()) 的判定条件时,则不进入循环;如果t线程是阻塞状态,则会立即在让t线程产生阻塞状态的方法处抛出异常,然后我们可以在catch() 中实现相应的逻辑。

==思考:==为什么线程阻塞状态下要走异常?

因为线程阻塞时,如果不走异常就要等sleep()完,然后走到while()中的判定条件才能让线程中断,假如极端情况下,sleep()要等一小时,那得一小时后才能执行到判断条件让线程中断,这样就不太合理,==我们期望一调用interrupt() ,该线程就能立即做出反应,而不是过了好大一会才做出反应,==对于阻塞中的线程,它不能上CPU,也就不能做出反应,所以让异常把sleep()唤醒,然后执行catch()中的逻辑,做出相应的反应。

等待线程

有时候需要等另一个线程执行完,才能执行这个线程,这时候就需要用到join(),当一个线程因为调用join()而进入阻塞状态时,对应的的是WAITING状态。

package thread;

public class Demo4 {
    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                int i = 3;
                while(i>0){
                    System.out.println("哈哈");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    i--;
                }
            }
        };
        Thread t1 = new Thread(runnable);
        t1.start();
        
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("嘻嘻");
    }
}

上面代码中,我们在主线程中调用了t1.join(),意思就是让主线程等待t1执行完(让主线程处于阻塞状态),主线程再接着往下执行。如果主线程执行到t1.join()时,t1已经执行完毕了,那主线程不会阻塞,接着往下执行就行了。(这是join()的两种行为)

调用join,使某个线程等待,然后再interrupt让某个线程中断,也会抛出异常

image-20220731095823290

使用join(long millis)还可以设定最大等待时间

获取当前线程的引用

Thread.currentThread();

在哪个线程中调用这个静态方法,就可以返回哪个线程的Thread实例

休眠线程

  • 休眠线程用的是sleep()方法,那sleep() 具体是干什么的,我们看下面的图

image-20220801103353988

  • 在操作系统内核中有很多PCB(一个PCB对应一个线程),这些PCB是通过双向链表连接起来的(有多个链表),大致分为就绪队列(操作系统调度时就会从就绪队列中选线程上CPU执行)和阻塞队列,就绪队列中的PCB对应的线程是已经准备好的,随时可以上CPU执行;阻塞队列中PCB对应的线程是因为一些原因还没有准备好上CPU执行的,所以不能上CPU执行的。当我们在正常运行的线程中调用sleep()时,该线程对应的PCB就会从就绪队列转移到阻塞队列,当sleep()结束后,PCB就会从阻塞队列转移到就绪队列中。
  • 假如sleep(1000),1000ms之后PCB就会从阻塞队列转移到就绪队列,虽然转移到就绪队列中了,但是什么时候上CPU执行还得看调度器的行为,所以sleep(1000)真实的休眠时间是不少于1000ms的
  • 同一进程中的线程对应的PCB其实还是有一些关联关系的,PCB中还有一个进程组id,这些id是一样的;另外,这些PCB中的内存指针和文件描述符表是一样的
  • 操作系统只认PCB,不认进程或者线程,只不过一些PCB中有关联关系(内存指针和文件描述符表是一样的)我们把这几个有关联关系的PCB对应的线程的总和称为进程
  • 在Linux中没有TCB(线程控制块),只有PCB表示线程,其他系统中可能有TCB

还有一个yield()方法其实和sleep(0)是一样的,就是让当前线程短暂放弃CPU,排到就绪队伍的后面位置,所以和sleep()效果是一样的。

问题:当线程上CPU执行时,其PCB还在就绪队列中吗?

线程的状态

线程的状态是一个枚举类型Thread.State,可以通过以下这种方式获取到线程的状态。下面这几种状态是JVM中的状态,并不是操作系统中的状态,JVM中的状态比操作系统中的状态更复杂一些

image-20220801100310355

NEW

  • NEW状态代表创建了Thread实例,但是还没有start(),也就是系统内核中还没有线程。创建Thread实例到创建线程之间状态为NEW
package thread;

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);

        System.out.println(thread.getState());
        thread.start();
        
    }
}

image-20220801101328590


就绪状态

RUNNABLE
  • 就绪状态,就绪状态其实包含了两种状态:一种是正在CPU上执行,一种是已经准备好了(对应的PCB在就绪队列中),准备上CPU执行,这俩种状态都是RUNNABLE状态
package thread;

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

        System.out.println(thread.getState());
    }
}

image-20220801114842328

  • 现在拿到的状态是RUNNABLE状态,但是也有可能拿到TIMED_WAITING状态,因为获取线程状态时,线程可能在运行,也可能在sleep()

阻塞状态

阻塞状态包含了三种状态:BOLCKED WAITING TIMED_WAITING (这三种状态都是阻塞状态,只不过导致阻塞的原因不相同)。当线程进入这三种状态时,线程对应的PCB都会进入内核中相应的阻塞队列,但是这几种状态下的线程对应的PCB是进入一个阻塞队列还是不同的阻塞队列,这个不确定,得看系统的具体实现。操作系统内核是不区分这三种状态的,这三种状态时JVM里的状态,操作系统衡量线程的状态时另外的方式,没有JVM这么细。这三种状态的进入方式不同,唤醒方式也不同。

BOLCKED

当两个线程针对同一个对象竞争锁时,其中没有竞争到锁的线程就会进入BLOCKED状态,然后等线程释放锁之后,由操作系统来把BLOCKED状态的线程唤醒。

WAITING

当一个线程中调用wait()方法后,该线程就会释放锁,然后进入WAITING状态,等待另一个线程中调用notify()方法后,该线程才有可能被唤醒,

TIMED_WAITING

当在线程中调用sleep()方法时,线程就是TIMED_WAITING状态,等sleep的时间过了之后,操作系统就会把该线程唤醒。

package thread;

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);

        System.out.println(thread.getState());

    }
}

image-20220801115248054

  • 那当然这里也有可能是RUNNABLE状态,因为俩线程并发执行

TERMINATED

  • TERMINATED代表线程执行结束了
package thread;

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);

        thread.start();
        thread.join();
        
        System.out.println(thread.getState());
    }
}

image-20220801114423419

线程状态转换

image-20220801124646020

主干道是:NEW -> RUNNABLE -> TERMINATED

RUNNABLE时会根据特定任务进入支线,这些支线都是阻塞状态

这三种阻塞状态进入方式不同,阻塞时间不同,被唤醒方式也不同

之前学的isAlive()方法可以认为除了NEW和TERMINATED状态其他都是活着的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值