线程创建及生命周期

线程创建

多线程,也叫做并发编程 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. 创建一个类实现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. 创建类并实现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<V> implements RunnableFuture<V>
         * Future实现了RunnableFuture接口
         * 而RunnableFuture接口实现了Runnable接口
         * public interface RunnableFuture<V> extends Runnable, Future<V>
         * 综上:由于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();
        }
    }
}

Thread类API

  • 构造器
-------public  Thread(){}  //无参构造
-------public  Thread(String name){}   //注入线程名称
-------public  Thread(Runnable target){}   //注入Runnable实现类对象
-------public  Thread(Runnable target,String name){}   //注入Runnable实现类对象及线程名称
  • 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();放在哪个线程下就返回哪个线程的名称
*/
  • 线程休眠静态方法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();
            }
        }
    }
}
  • 通过有参构造器为线程命名,可以不使用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());
    }
}

线程生命周期

线程状态

  • 当线程被创建并启动以后 ,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程
    的生命周期中,它要经过新建(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();
    			}
    		}
    	}
    }
    
    

运行状态

  • 如果处于就绪状态的线程获得了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 方法来终止线程。
    
    • 举例

      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();
          }
      }
      
      
  • 使用退出标志退出线程

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

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


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;
    }
}


线程安全

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

绿仔牛奶_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值