• 线程创建及生命周期


    线程创建

    多线程,也叫做并发编程 JUC

    首先理解进程:通俗来讲运行中的程序就叫做进程。例如我们电脑中的APP、编写好的程序在未启动的时候是静态的,并未在运行。而一旦启动,就会开始运行,就可以称为是进程

    进程的特征:

    1. 动态性:运行中的进程会动态的占有cpu以及内存资源等

    2. 独立性:进程与进程之间相互独立,不会产生相互影响

    3. 并发性:假设CPU是单核,那么在某一时刻CPU当中其实只有一个程序在运行,CPU会分时的切换为每个进程服务,只是切换的速度很快,给人的感觉就是很多进程在同时被执行,这就是并发性

    并行是与并发相关的一个概念:四核CPU中同时有四个进程正在运行,这就被称为并行

    线程:一个进程中会至少有一个顺序执行流,每个顺序执行流被称为线程,当有两个或以上的线程时就称为多线程。

    ​ 理解如:WechatAPP:app运行时便会在内存中开辟空间占用CPU资源,而WeChat运行时不仅要支持发消息还要收消息、更新朋友圈等等,那么就牵扯到多线程,每个线程负责一个功能单元。所以多线程也是具有并发性的

    进程与线程:

    1. 一个进程可以有多个线程,但至少有一个线程。而一个线程只能在所属进程的内存中活动
    2. 资源分配给进程:进程中的所有线程共享资源
    3. CPU分享给线程:即真正在处理器中运行的是线程
    4. 线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步

    线程创建

    线程创建三种方式概述:

    1. 创建一个类并且继承Thread线程类,类中重写run方法(run方法就是此线程的线程主体),创建该类对象,调用start方法启动线程。
    2. 创建一个类实现Runnable接口,类中重写run方法,创建该类对象,然后将该类对象包装成线程对象,调用线程对象start方法启动线程
    3. 创建一个类实现Callable接口,将其实现类对象包装成FutureTask对象,再注入线程类对象

    注意事项:
    start方法不能连续调用,启动线程会失败
    不能直接调用run方法,此方法调用不会创建新线程,只是普通方法调用

    线程创建方式一

    创建步骤:

    1. 编写一个类,此类继承Thread类

    2. 重写run方法,此方法是子线程的主体

    3. 调用star方法启动子线程

    class MyThread extends Thread{
        //此时创建的叫做线程类  并不是一个线程!
        //重写run方法
        @Override
        public void run() {
            System.out.println("子线程名称="+Thread.currentThread().getName()+"子线程ID="+Thread.currentThread().getId());
        }
    }
    public class Demo02 {
        //此时的Demo02可以看作是一个进程,而main方法是主线程
        public static void main(String[] args) {
            System.out.println("主线程名称="+Thread.currentThread().getName()+"主线程ID="+Thread.currentThread().getId());
            //创建线程对象:
            Thread t = new MyThread();//多态写法
            //此时t线程创建完毕  但是并未启动  只有主线程(main线程)在运行,但是子线程并未启动运行
            t.start();
            //此时子线程启动,  子线程与主线程将会由于线程并发性相互争夺CPU资源,所以如果数据较多时将会发现主线程与子线程的运行是无序的
            //或者直接利用对象调用:  new MyThread().start();
        }
    }
    /*
    start方法的底层:
    	调用start方法后会在底层执行group.add(this),也就是将当前线程放入争夺CPU资源的组当中去
    	可以理解为start底层是在CPU中注册此线程并登录运行(触发run方法执行)
    由此(方式一)可以得出:
    	线程类继承Thread,进而无法继承其他类。此处也体现了Java的单继承特性
    	启动线程必须调用start方法  直接调用run方法则当作普通类处理,则还是只有主线程在运行
    	多线程是并发执行,在执行过程中各个线程会争夺cpu,进而导致程序执行的随机性
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    线程创建方式二

    创建步骤:

    1. 创建一个类实现Runnable接口
    2. 重写run方法
    3. 创建实现类对象
    4. 创建线程类对象并将实现类对象注入线程类构造器
    5. 利用线程类对象调用start方法启动线程
    //创建一个类实现Runnable接口
    class TestThread implements Runnable{
        //重写run方法
        @Override
        public void run() {
            Thread.currentThread().setName("子线程");
            System.out.println(Thread.currentThread().getName());
        }
    }
    public class Demo04 {
        public static void main(String[] args) {
            //创建实现类对象
            TestThread testThread = new TestThread();
            //创建线程类对象并将实现类对象注入线程类构造器
            Thread thread = new Thread(testThread,"子线程1");
            //启用子线程
            thread.start();
            //继续使用同一个实现类对象包装成其他线程
            Thread thread1 = new Thread(testThread,"子线程2");
            thread1.start();
            //方式二的匿名内部类写法:
            //匿名内部类写法
            new Thread(new TestThread(){
                @Override
                public void run() {
                    System.out.println("创建线程方式二写法-->匿名内部类");
                }
            }).start();//创建并直接启动该线程
        }
    }
    /*
    由此(方式二)可以得出:
    	-- 实现类可以继续实现其他接口,也可以继承其他类,避免了单继承的局限性
    	-- 线程代码和线程相互独立  线程代码可以被多个线程共享  同一个实现类对象可以被包装成多个线程
    	-- 适合多个线程共享同一个资源
    	缺点:
    	-- 不能直接获取线程返回的结果{
    		1. 不能抛出异常  因为其所实现的接口并未抛出异常  只能自己处理
    		2. 没有返回值  当需要获取线程执行后的返回值时需要在外部定义变量  最后在线程中存储并获取
    	}
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    线程创建方式三

    创建步骤:

    1. 创建类并实现Callable接口
    2. 重写call方法
    3. 创建Callable实现类对象
    4. 创建FutureTask对象并将Callable实现类对象注入FutureTask构造器
    5. 创建Thread线程类对象并将FutureTask对象注入线程类构造器
    6. 利用线程类对象调用start方法启动线程
    7. 接受Callable实现类的call方法返回值使用FutureTask的get方法即可
    //创建Callable实现类
    class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            int sum = 0;
            for (int i = 1; i <= 10; i++) {
                sum += i;
            }
            //返回值
            return Thread.currentThread().getName()+"线程执行结果为="+sum;
        }
    }
    public class Demo05 {
        public static void main(String[] args) {
            //创建Callable实现类对象
            MyCallable myCallable = new MyCallable();
            /**
             * 为什么要创建FutureTask对象并注入Callable实现类对象?
             * 打开FutureTask原码发现:
             * public class FutureTask implements RunnableFuture
             * Future实现了RunnableFuture接口
             * 而RunnableFuture接口实现了Runnable接口
             * public interface RunnableFuture extends Runnable, Future
             * 综上:由于Callable实现类对象无法直接注入线程类构造器,所以使用了FutureTask类
             *       最终将FutureTask对象包装成线程类对象
             * FutureTask提供了get方法来获取Callable实现类重写call方法的结果
             *      并且在线程执行完毕后可以获取线程的执行结果
             */
            //创建FutureTask对象并注入Callable实现类对象
            FutureTask<String> futureTask = new FutureTask<>(myCallable);
            //将FutureTask对象包装成线程类对象
            Thread thread = new Thread(futureTask);
            //利用线程类对象启动线程
            thread.start();
            //此处利用futureTask获取call方法返回的结果
            try {
                System.out.println(futureTask.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    Thread类API

    • 构造器
    -------public  Thread(){}  //无参构造
    -------public  Thread(String name){}   //注入线程名称
    -------public  Thread(Runnable target){}   //注入Runnable实现类对象
    -------public  Thread(Runnable target,String name){}   //注入Runnable实现类对象及线程名称
    
    • 1
    • 2
    • 3
    • 4
    • getId():返回此线程的标识符 getName():返回此线程名称 setName():设置此线程名称
    • 这三个都是非静态方法
    • currentThread():返回对当前正在执行的线程对象的引用:通俗点说就是通过此方法可以获取当前的线程对象,进而可以直接调用Thread的其他方法
    • 可以通过currentThread()静态方法调用getName和getId

    用法:

    class MyThread1 extends Thread{
        @Override
        public void run() {
            Thread.currentThread().getId();//获取子线程Id
            Thread.currentThread().getName();//获取子线程Name
        }
    }
    public class Demo03{
        public static void main(String[] args) {
            Thread.currentThread().setName("超级线程");
            Thread.currentThread().getId();//获取主线程Id
            Thread.currentThread().getName();//获取主线程Name  输出---> 超级线程
        }
    }
    /*
    Thread.currentThread().getName();放在哪个线程下就返回哪个线程的名称
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 线程休眠静态方法sleep() :使当前线程休眠指定时间(以毫秒为单位)
    • 用法:注意异常处理
    public class Demo03{
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(1000);//每次输出一个数据后休眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 通过有参构造器为线程命名,可以不使用setName()
    • 用法:
    class MyThread1 extends Thread{
        //创建子类构造器
        public MyThread1(String name) {
            super(name);
        }
        //通过super将名字送上去到父类去接收
        //父类的实现:private volatile String name;
        @Override
        public void run() {
        }
    }
    public class Demo03{
        public static void main(String[] args) {
            Thread t1 = new MyThread1("子线程是也");
            System.out.println(t1.getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    线程生命周期

    线程状态

    • 当线程被创建并启动以后 ,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程
      的生命周期中,它要经过新建(New)、就绪(Runnable)、运行 (Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

    新建状态

    • 当程序使用new关键字创建了 一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体 。

    就绪状态

    • 当线程对象调用了 start()方法之后,该线程处于就绪状态, Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM线程调度器的调度。

    • 注意问题

      • 需要指出的是,调用了线程的run()方法之后,该线程己经不再处于新建状态,不要再次调用线程对象的 start()方法。
      • 启动线程使用start()方法,而不是run()方法!永远不要调用 线程对象的run方法,调用 start()法来启动线程,系统会把该 run()方法当 成线程执行体来处理;但如果直接调用线程对象的 run()方法,则 run()方法立即就会被执行,而且在 run()方法返回之前其他线程无法并发执行一一也就是说,如果直接调用线程对象的 run()方法 ,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法 , 而不是线程执行体 。
      public class InvokeRun extends Thread
      {
      	private int i ;
      	// 重写run方法,run方法的方法体就是线程执行体
      	public void run()
      	{
      		for ( ; i < 100 ; i++ )
      		{
      			// 直接调用run方法时,Thread的this.getName返回的是该对象名字,
      			// 而不是当前线程的名字。
      			// 使用Thread.currentThread().getName()总是获取当前线程名字
      			System.out.println(Thread.currentThread().getName()
      				+  " " + i);   // ①
      		}
      	}
      	public static void main(String[] args)
      	{
      		for (int i = 0; i < 100;  i++)
      		{
      			// 调用Thread的currentThread方法获取当前线程
      			System.out.println(Thread.currentThread().getName()
      				+  " " + i);
      			if (i == 20)
      			{
      				// 直接调用线程对象的run方法,
      				// 系统会把线程对象当成普通对象,run方法当成普通方法,
      				// 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
      				new InvokeRun().run();
      				new InvokeRun().run();
      			}
      		}
      	}
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34

    运行状态

    • 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个 CPU , 那么在任何时刻只有一个线程处于运行状态 。 当然,在一个多处理器的机器上,将会有多个线程并行(注意是并行 : parallel ) 执行;当线程数大于处理器数时,依然会存在多个线程在同一个 CPU 上轮换的现象。

    阻塞状态

    • 在某一时刻某一个线程在运行一段代码的时候,这时候另一个线程也需要运行,但是在运行过程中的那个线程执行完成之前,另一个线程是无法获取到CPU执行权的(调用sleep方法是进入到睡眠暂停状态,但是CPU执行权并没有交出去,而调用wait方法则是将CPU执行权交给另一个线程),这个时候就会造成线程阻塞。

    • 如何进入阻塞状态

      • 线程调用sleep()方法主动放弃所占用的处理器资源。
      • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
      • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
      • 线程在等待某个通知 ( notify )。
      • 程序调用了线程的 suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
    • 如何解除阻塞状态

      • 调用 sleep()方法的线程经过了指定时间。
      • 线程调用的阻塞式IO方法已经返回。
      • 线程成功地获得了试图取得的同步监视器。
      • 线程正在等待某个通知时,其他线程发出了一个通知。
      • 处于挂起状态的线程被调用了resume()恢复方法。
    • 阻塞状态转换图

    在这里插入图片描述

    • 阻塞注意问题
      • 线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪状态和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态 的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用 yield()方法可以让运行状态的线程转入就绪状态。

    线程死亡

    • 线程会以如下三种方式结束,结束后就处于死亡状态 。

      • run()或 call()方法执行完成,线程正常结束 。
      • 线程抛出 一个未捕获的 Exception 或 Error
      • 直接调用该线程的 stop()方法来结束该线程一一该方法容易导致死锁,通常不推荐使用 。
      • 不要试图 对一个已经死亡的线程调用 start()方法使它重新启动, 死亡就是死亡 ,该线程将不可再次作为线程执行 。
    • 不使用stop方法的原因

      程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
      
      • 1
      • 举例

        package com.gec.创建线程.stop用法;
        
        class MyThread extends Thread
        {
            @Override
            public void run() {
        
        
                try {
                    System.out.println("my thread name="+Thread.currentThread().getName());
                    //抛出 ThreadDeatherror 的错误
                    this.stop();
                } catch (ThreadDeath e) {
                    System.out.println("进入catch块了");
                    e.printStackTrace();
                }
        
            }
        }
        
        public class StopThreadMainTest {
        
            public static void main(String[] args) {
        
                MyThread t=new MyThread();
                t.start();
            }
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
    • 使用退出标志退出线程

      • 一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出

        public class ThreadSafe extends Thread {
        	public volatile boolean exit = false; 
        	public void run() { 
        			while (!exit){
        				//do something
        			}
        	} 
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
    • 使用退出标志结束线程

    
    public class Demo01 {
        //利用退出标志终止子线程
        public static void main(String[] args) throws Exception {
            MyThread myThread = new MyThread();
            Thread thread = new Thread(myThread);
            thread.start();
            //主线程休眠退出抢夺cpu资源队列,资源提供给子线程执行
            Thread.sleep(200);
            myThread.setFlag(true);//子线程执行完毕后关闭线程
            System.out.println("子线程已关闭");
        }
    }
    class MyThread extends Thread{
        private boolean flag = false;
        public void run(){
            while (!flag){
                System.out.println("子线程执行体");
            }
        }
        public void setFlag(boolean flag){
            this.flag = flag;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    线程安全

  • 相关阅读:
    Completeness (order theory)
    2022装备制造业服务数字化报告
    js实现广告条+缓冲效果/键盘事件实现小人跑步
    一篇适合大一同学的算法学习建议
    Postman关联
    【Linux篇】第十七篇——信号量
    TiUP Cluster
    华为OD真题--字符串摘要--带答案
    java-php-python-ssm郑工社团交流服务信息平台计算机毕业设计
    HDFS High Availability(HA)高可用配置
  • 原文地址:https://blog.csdn.net/yuqu1028/article/details/126859529