万字狂淦多线程__(多线程学习笔记)

Posted z啵唧啵唧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了万字狂淦多线程__(多线程学习笔记)相关的知识,希望对你有一定的参考价值。

多线程学习总结
多线程概述
  • 什么是进程?什么是线程?

一个进程是一个应用程序,或者说是一个软件

线程是进程中的执行场景/执行单元

一个进程可以启动多个线程

  • 举个例子

在dos窗口中当输入java HelloWorld 回车之后 会先启动JVM,而JVM就是一个进程。

JVM会再启动一个主线程调用main方法,同时还会启动一个垃圾回收线程来负责看护回收垃圾

此时最起码再进程当中有两个线程是并发的,一个是执行main方法的主线程 一个是执行垃圾回收的线程

  • 进程和线程之间的关系

进程可以看作现实生活当中的公司

线程可以看作是公司当中的某个员工

比如:

公司:京东就可以看作是一个进程

员工:强东 就可以看作是一个线程

员工:妹妹 也可以看作是一个线程

  • 注意

1、假如有两个进程A和B进程A和进程B他们之间的内存是不能够共享的,他们是独立的

比如:QQ音乐和网易云音乐是两个独立的进程他们之间内存不能够共享,是相互独立的

2、假如有两个线程A、B

在java当中,线程A、B之间共享堆内存和方法区,但是他们两个的栈内存是独立的,一个线程一个栈,每个线程之间执行互不干扰,各自执行各自的,这个就是多线程

3、java中存在多线程机制的原因:提高程序的处理效率

  • 注意一个问题

采用了多线程机制之后,main方法执行完毕之后程序不一定会执行结束,因为main方法执行完毕之后,只是主线程结束了,只是主栈空了其他的栈可能还在弹栈、压栈

  • 图解线程和线程之间的关系
多线程并发的理解
  • 分析一个问题对于单核的CPU能够真正的做到多线程并发吗?

答案:不能!但是单核的CPU可以给人一种多线程并发的错觉

实际上对于单核的CPU在某一个时间节点上,只能处理一件事情,但是由于CPU处理的速度极快,线程和线程之间切换频繁,给人的感觉是多个线程同时执行

比如:以前老的电影院播放电影采用胶卷其实就是一张张的照片进行切换,只不过速度很快让人感觉到是连续的

但是对于多核CPU来说,比如4核CPU,可以真正做到4个线程并发

分析程序当中存在几个线程

分析一下下面的代码中有几个线程

package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 17:20
 **/
public class ThredTest1 {
    public static void main(String[] args) {
            m1();
    }
    private static void m1()
    {
        m2();
    }
    private static void m2()
    {
        m3();
    }
    private static void m3()
    {

    }
}

虽然在这份代码当中main()方法调用了m1方法,m1方法调用了m2方法,m2方法调用了m3方法,但是这些方法全部都是在main方法的主栈当中执行的,只有一个方法栈,所以在这份代码当中只有一个线程。

  • ThredTest1类中方法执行分析图

实现线程的第一种方式
  • 编写一个类,让这个类直接继承java.lang.Thread类重写run方法
  • 怎么创建线程对象? 直接new我们的分支线程对象即可
  • 怎么启动线程?调用start方法启动线程,调用start方法会启动一个分支栈会在JVM当中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了
  • 调用start方法只是开辟了一个新的栈空间,只要新的栈空间一开辟,start方法就结束了。分支线程也就启动成功了
  • 当线程启动成功之后就会自动调用run方法,并且run方法在新的栈空间的底部(压栈)。
  • run方法在分支栈的底部,main方法在主栈的底部,main方法的run方法是平级的
  • 如果用我们的线程对象直接调用run方法:myThread.run();会怎么样?=====>不会启动多线程,不会分配新的栈空间(这种方式其实就是一个单线程)
  • **注意一个亘古不变的道理:**方法体中的代码永远都是自上而下的顺序逐行依次执行
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 17:32
 **/
public class ThredTest2 {
    public static void main(String[] args) {
        //这是main方法,这段代码属于主线程,运行在主栈当中
        //现在新建一个线程对象
        MyThread myThread = new MyThread();
        //调用start方法启动线程
        myThread.start();
        //这下面的代码还是运行在主线程当中
        for (int i=0;i<999;i++)
        {
            System.out.println("这段代码还是运行在主线程当中");
        }
    }
}
class MyThread extends Thread
{
    @Override
    public void run() {
        //编写程序,输出这段程序运行在分支栈中
        for (int i = 0; i<999;i++)
        {
            System.out.println("这段程序运行在分支栈中");
        }
    }
}
strat和run的区别
  • 调用start方法才是真正的多线程并发,在调用start方法的时候,会在mian方法的这个主栈的外边新开辟一个栈,在这个栈空间开辟成功之后,这个方法立即失效然后JVM会默认自行调用run方法,将这个run这个方法压在新开辟的栈底当中。
  • 调用run方法本质还是个单线程,他还是在main方法中执行的并没有开辟新的栈空间
  • 有这样一份代码
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 17:32
 **/
public class ThredTest2 {
    public static void main(String[] args) {
        //这是main方法,这段代码属于主线程,运行在主栈当中
        //现在新建一个线程对象
        MyThread myThread = new MyThread();
        //调用start方法启动线程
        myThread.start();
        //调用run方法,对比一下调用start方法的区别
        //这下面的代码还是运行在主线程当中
        for (int i=0;i<100;i++)
        {
            System.out.println("主方法栈=====>"+i);
        }

    }
}
class MyThread extends Thread
{
    @Override
    public void run() {
        //编写程序,输出这段程序运行在分支栈中
        for (int i = 0; i<100;i++)
        {
            System.out.println("新开辟的栈=====>"+i);
        }
    }
}

从这个运行结果可以看出这是调用了start真正的实现了多线程,他的新开辟的栈和主栈时交替运行的,充分说明了这是两个线程并发的下面看看他的运行原理图

  • 另外一份代码
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 17:32
 **/
public class ThredTest2 {
    public static void main(String[] args) {
        //这是main方法,这段代码属于主线程,运行在主栈当中
        //现在新建一个线程对象
        MyThread myThread = new MyThread();
        //调用start方法启动线程
        //myThread.start();
        //调用run方法,对比一下调用start方法的区别
        myThread.run();
        //这下面的代码还是运行在主线程当中
        for (int i=0;i<100;i++)
        {
            System.out.println("主方法栈=====>"+i);
        }

    }
}
class MyThread extends Thread
{
    @Override
    public void run() {
        //编写程序,输出这段程序运行在分支栈中
        for (int i = 0; i<100;i++)
        {
            System.out.println("新开辟的栈=====>"+i);
        }
    }
}

从这个调用了run方法执行的结果分析:分明显并没有开启多线程,因为他是从上向下依次调用了run方法,执行完run方法中中的输出才开始执行mian方法中的输出,下面看看他的运行结构图

  • 总结一下调用run方法只是new了一个继承了线程的类,应没有启动多线程,它的本质还是一个单线程
实现多线程的第二种方式
  • 采用接口的方式实现多线程

  • 编写一个类实现java.lang.Runnable接口重写run方法

  • 需要注意的是自定义的这个实现了Runnable接口的方法并不是一个多线程对象,他需要封装到Thread类中才能获得这个多线程对象

  • 然后拿这个多线程对象,调用start方法,就能成功开启多线程了

package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 22:54
 **/
public class ThredTest3 {
    public static void main(String[] args) {
        //new 一个可运行的对象
        MyRunnable myRunnable = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread thread = new Thread(myRunnable);
        //调用start方法启动多线程
        thread.start();
        for (int i = 0;i<99;i++)
        {
            System.out.println("主方法栈===>"+i);
        }
    }
}
//创建一个类实现Runnable接口
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<99;i++)
        {
            System.out.println("分支栈===>"+i);
        }
    }
}
  • 执行结果

明显看出分支栈和主方法栈并发执行,这就是开启多线程的第二种方法

  • 注意以上两种实现多线程的方式我们推荐实现Runnable接口的方式,因为用实现接口的方式,这个类还可以继承别的类,更加灵活然而采用继承的方式就不能再实现别的类了
采用匿名内部类的方式
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/25 23:18
 **/
public class ThredTest4 {

    public static void main(String[] args) {
        //采用匿名内部类的方式来开启多线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                    for (int i=0;i<99;i++)
                    {
                        System.out.println("分支栈===>"+i);
                    }
            }
        });
        //调用start方法开启多线程
        thread.start();
        for (int i=0;i<99;i++)
        {
            System.out.println("主方法栈===>"+i);
        }
    }
}
  • 成功开启多线程

线程的生命周期

从上面一系列代码可以总结出输出结果的一些特点:

  • 分支栈和主方法栈时有先有后
  • 有多有少

分析这个问题就需要了解一下线程的生命周期

包含六个周期:

  • 新建状态:刚new出来线程对象
  • 就绪状态:新建状态调用了start方法后就会进入到就绪状态,就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺cpu时间片的权利(cpu时间片就是执行权力)当一个线程抢到时间片之后,就可以开始执行run方法,run方法执行成功之后标志着线程进入到了运行状态。
  • 运行状态:run方法开始执行就标志着线程进入到运行状态,当之前占有的cpu时间片执行完之后,会重新回到就绪状态,继续抢夺cpu时间片,当再次抢夺到cpu时间片之后,就会再次进入到运行状态,接着执行run方法,继续往下执行
  • 阻塞状态:当一个线程遇到阻塞事件之后,例如接受用户从键盘输入,或者sleep方法等,此时线程会进入到阻塞状态,阻塞状态的线程会放弃之前占用的cpu时间片,当阻塞解除的时候,需要这个线程继续回到就绪状态继续抢夺cpu时间片
  • 死亡状态:运行状态时当run方法执行结束就会进入死亡状态,整个线程也就到此结束了

获取当前线程
  • 获取线程名字的方法通过new的线程对象调用myThreTest1.getName()方法
  • 如果自己不设置线程的名字的话,线程的名字默认是Thread-x,第一个是Thread-0,第二个是Thread-1,第三个是Thread-2 …
  • 设置线程名字的方法通过new的线程对象调用setName()方法
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/26 0:30
 **/
public class ThresTest5 {
    public static void main(String[] args) {
        myThreTest myThreTest1 = new myThreTest();
        String name1 = myThreTest1.getName();
        myThreTest myThreTest2=new myThreTest();
        String name2 = myThreTest2.getName();
        System.out.println(name1);
        System.out.println(name2);
    }
}
class myThreTest extends Thread{
    @Override
    public void run() {
        for (int i=0;i<99;i++)
       {
           System.out.println();
       }
    }
}
  • 默认的线程名称

  • 通过调用setName方法来设置线程名称
myThreTest myThreTest1 = new myThreTest();
myThreTest1.setName("线程一");
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
myThreTest2.setName("线程二");
String name2 = myThreTest2.getName();
System.out.println(name1);
System.out.println(name2);

  • 获取当前线程,调用thread.currentThread方法
  • 在哪个线程中调用thread.currentThread就获取到哪个线程
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/26 0:30
 **/
public class ThresTest5 {
    public static void main(String[] args) {
        Thread thread = new Thread();
        Thread cur = thread.currentThread();
        System.out.println("当前线程的名称:"+cur.getName());
        myThreTest myThreTest1 = new myThreTest();
        myThreTest1.setName("线程一");
        String name1 = myThreTest1.getName();
        myThreTest myThreTest2=new myThreTest();
        myThreTest2.setName("线程二");
        String name2 = myThreTest2.getName();
      /*  System.out.println(name1);
        System.out.println(name2);*/
      myThreTest1.start();
      myThreTest2.start();
    }
}
class myThreTest extends Thread{
    Thread thread = new Thread();
    @Override
    public void run() {
        //在分支线程当中调用currentThread就获取到分支线程
        System.out.println("获取到分支线程名称:"+thread.currentThread().getName());
       /* for (int i=0;i<99;i++)
       {
           System.out.println();
       }*/
    }
}

  • main方法对应的线程如果不修改名称,获取到的线程对象名字就叫做main
  • 总结就是一句话:thread.currentThread在哪个线程中调用返回的就是哪个线程
sleep方法
  • 参数是毫秒

  • 是一个静态方法

  • sleep方法在哪个进程当中调用就在让那个进程进行休眠

  • 实例一个方法,每隔2秒获取一下当前线程的名称获取10次

package com.zb.test;/** * @Author 啵儿 * @Email 1142172229@qq.com * @date 2021/10/26 1:13 **/public class ThredTest6 {    public static void main(String[] args) {        myRunnable run = new myRunnable();        Thread thread = new Thread(run);        thread.start();    }}class myRunnable implements Runnable{    @Override    public void run() {        for (int i=0;i<10;i++)        {            //每隔5秒输出分支线程的名称            try {                Thread.sleep(1000*2);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("分支线程的名称"+Thread.currentThread().getName());        }    }}
  • 关于sleep方法的一道面试题
package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/26 1:25
 **/
public class ThredTest7 {

    public static void main(String[] args) {
        Thread t = new myThred();
        t.setName("分支线程1");
        t.start();
        try {
            t.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class myThred extends  Thread{
    @Override
    public void run() {
        System.out.println("====>分支线程执行");
    }
}
  • 在这份代码中myThred这个分支线程会不会睡眠

  • 答案:不会的!因为sleep是一个静态方法,他在哪个线程中调用就让哪个线程睡眠,更哪个对象去调用没有关系,主要看的是他在哪个线程中被调用!

  • 终止一个线程的睡眠(注意是终止线程的睡眠,不是终止整个线程),调用thread.interrupt()方法

  • 测试用例,现在有一个一睡睡一年的线程,现在想办法在主线程当中终止它

package com.zb.test;

/**
 * @Author 啵儿
 * @Email 1142172229@qq.com
 * @date 2021/10/26 8:34
 **/
public class ThredTest8 {
    public static void main(String[] args) {
        Thread thread = new Thread(new myRunnable1());
        thread.setName("分支线程");
        //启动分支线程
        thread.start();
        //设计一下希望5秒之后终止分支线程睡眠
        try {
            thread.sleep(2*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终止分支线程睡眠,这种终止方式,依靠java的异常处理机制
        thread.interrupt();
        for (int i = 0;i<10;i++)
    

以上是关于万字狂淦多线程__(多线程学习笔记)的主要内容,如果未能解决你的问题,请参考以下文章

尚硅谷_Java零基础教程(多线程)-- 学习笔记

Python学习笔记__10.4章 进程VS线程

Java学习笔记_网络+多线程

Java多线程_学习笔记

servlet学习笔记_2

python3 多线程笔记