[Java并发编程][EP1][线程基础]

[EP1][线程基础]

进程和线程的基本概念

进程

进程是程序运行的单元 可以认为一个程序启动后就是一个进程——进行中的程序

线程

线程是操作系统调度资源的基本单位 是进程的一个子任务
一个进程至少包含一个线程

线程是比进程更加轻量级的调度单位,线程的引入可以把进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。

知道了以上的基本概念

就明白了并发编程为什么是多线程编程
多线程编程实际上就是程序员调度多个线程
让其“同时”执行任务的过程

(但是现在又出现了一种更灵活的 “轻量级进程”——协程 可以进行更简化的并发编程)

Java线程的实质

Java的线程,运行的实质是Java虚拟机将用户创建的用户级线程,绑定在操作系统内核的轻量级线程或者内核级线程上,其对CPU时间的抢占,调度都是通过操作系统内核调度来完成的

用户级线程

User Thread,完全建立在用户空间的线程称为用户线程,用户线程的建立、调度和销毁都在用户态里完成。

优点:

  • 用户线程的建立、调度和销毁都在用户态里完成,代价低廉,可以支持大规模用户线程并发。
    缺点:
  • 缺少内核的支持,线程的各种操作都需要自己实现,实现起来很复杂。

操作系统的线程

内核线程:Kernel Level Thread,它是直接由系统内核支持的线程,该线程有系统内核完成切换,通过调度器把每个线程映射到每个处理器上。

轻量级进程:Light Weight Process,也称为协程,是应用程序进行并发运行的高级程序接口,底层同样由内核线程实现。

优点:

  • 实现简单,线程的创建、调度与销毁都有内核来完成。
    缺点:
  • 基于内核实现,需要进行系统调用,也就是内核态和用户态的切换,代价相对较高,且需要消耗系统资源。

Java虚拟机的提供的线程,在主流平台都是基于用户线程绑定内核线程实现的,所以其进行线程切换时,不仅要切换线程上下文,还要从用户态转入内核态,对性能的损失还是比较大的。

创建线程

Java的线程创建有三种方式

  • 新建类继承Thread

  • 新建类实现Runable接口

  • 新建类实现Callable接口

最基础也是最容易理解的就是新建类继承Thread类
直接创建一个线程并运行 最符合常人思维

查看Thread源码

public class Thread implements Runnable

可以发现 Thread是实现了Runnable接口
所以Runnable和Thread本质上是一样的
只是实现Runnable接口可以避免Java不能多继承的问题

至于Callable接口 是Java 5后在Java Concurren 并发工具包中新增的内容
主要配合FutureTask进行异步编程
其和Runnable的区别是Callable有返回值

构造一个线程

  • 使用Thread

新建类继承Thread类 构造线程 很简单

public class TheFirstThread extends Thread{      
  @Override
  public void run(){    
        System.out.println("线程运行----->");
        System.out.println("线程终止----->");
    } 
    }

这样一个线程就定好了
最终要的是重写Thread 类的run()方法
run() 方法体中的内容 就是线程运行时执行的内容

运行时 执行start()方法 启动线程

Thread theFirstThread=new TheFirstThread();
theFirstThread.start();

这样就能运行了

结果

线程运行----->
线程终止----->
  • 使用Runnable

新建类实现Runnable接口即可

public class TheFirstRunnable implements Runnable{
    @Override
    public void run() {
          System.out.println("Runnable 线程运行----->");
          System.out.println("Runnable 线程终止----->");
      } 
    }

运行时和 Thread稍有不同

TheFirstRunnable tfr=new TheFirstRunnable();
Thread runT=new Thread(tfr);
runT.start();

以实现Runnable接口形式创建的进程
必须被Thread类加载后才能启动运行

可以把Runnable看成是一个线程的任务
Thread是其宿主,负载Runnable来运行

运行结果

Runnable 线程运行----->
Runnable 线程终止----->
  • 使用Callable

Callable 是Java 5才引入的
其和Runnable的区别是 Runnable接口是无返回值的
而Callable接口有返回值
引入Callable接口主要是为了异步任务
这个后面再详谈

显然 Callable既然是有返回值的
则理所当然应该为一个泛型接口
这里使用了String型为泛型限定符

public class TheFirstCallable implements Callable<String> {
    @Override
    public String call() throws Exception {    
          System.out.println("Callable 线程开始");
          System.out.println("Callable 线程结束");
          return "Callable 线程已结束";
      } 
    }

创建完毕

运行


TheFirstCallable theFirstCallable=new TheFirstCallable();
FutureTask<String> futureTask=new FutureTask(theFirstCallable);
new Thread(futureTask).start();

String s=futureTask.get();
System.out.println("异步运行结果-》"+s);

与Thread ,Runnable都不同
Callable 必须先被FutureTask 未来任务加载
然后再有Thread搭载运行
Callable是一个回调的机制 运行完毕后回调 未来任务 发送结果
所以最后调用FutureTask来获取结果

结果

Callable 线程开始
Callable 线程结束
异步运行结果-》Callable 线程已结束

线程的基本操作

线程名与线程标识符

线程可以设置线程名
以便为用户提供一个友好的区分手段

Thread形式

Thread theFirstThread=new TheFirstThread();
theFirstThread.setName("第一个thread");
System.out.println(theFirstThread.getName());

Runnable形式
可以在构造时传入 或者后面调用set方法设置

TheFirstRunnable tfr=new TheFirstRunnable();
Thread runT=new Thread(tfr,"第一个Runnable");
runT.setName("第一个Runnable");
runT.start();

Callable同理

而线程的唯一标识符是其ID

Thread theFirstThread=new TheFirstThread();
theFirstThread.start();
System.out.println(theFirstThread.getId());

与线程名不同 标识符是JVM自动分配的
无法更改

currentThread() 方法

Thread.currentThread() 方法返回正在调用代码段的线程

从前面的介绍可知 通常情况下 我们编写的是都是需要并发(重复)执行的代码段
然后构造新的Thread类搭载代码段任务去执行
那么如果需要在代码中判断此时加载代码段的是哪个线程

那就需要调用currentThread()方法

currentThread()方法是Thread类的一个静态方法

public class TheCurrentRunnable implements Runnable{
      @Override
      public void run() {    
          String name=Thread.currentThread().getName();
          Long id=Thread.currentThread().getId();
          System.out.printf("%s %d",name,id);
          System.out.println();
        } 
    }

运行

TheFirstRunnable tfr=new TheFirstRunnable();
for (int i=0;i<10;i++) {
  Thread thread = new Thread(tfr, "第" + i + "个线程");
    thread.start();
    thread.join();
}

结果

第0个线程 14
第1个线程 15
第2个线程 16
第3个线程 17
第4个线程 18
第5个线程 19
第6个线程 20
第7个线程 21
第8个线程 22
第9个线程 23

sleep()方法

Thread.sleep() 方法让线程休眠 在指定时间内停止运行

sleep()方法会让线程休眠 让出CPU时间片以供其他线程运行
但是并不会释放资源和锁 休眠时间到后自动恢复运行

调用从方法很简单

System.out.println("线程运行----->");
try {
  System.out.println("线程休眠----->");
    Thread.sleep(5);
} catch (InterruptedException e) {
  e.printStackTrace();
} System.out.println("线程终止----->");

但涉及到多线程资源竞争问题 后面再详细研究

isAlive()方法

isAlive()方法 获取线程是否还活动的状态

线程自身代码同上

测试代码如下

Thread theFirstThread = new TheFirstThread();
theFirstThread.start();
System.out.println(theFirstThread.isAlive());

结果

true
线程运行----->
线程休眠----->

停止和销毁线程

线程的停止 并不是一件简单的事 并不能直接粗暴的终止线程
因为线程停止时 必须释放资源和锁 否则可能会引起其他问题
如死锁

废弃的stop(),suspend(),resume()方法

查看Thread的源码 发现源码中有三个方法
停止stop方法,暂停supend方法,恢复resume方法

@Deprecated(since="1.2") 
public final void stop()


@Deprecated(since="1.2") 
public final void suspend()

@Deprecated(since="1.2") 
public final void resume()

有点像播放器的按钮
当然也确实是类比播放器来设置的
以停止来终止线程
暂停和恢复来控制线程运行

但是 这三个方法已经标注为废弃@Deprecated
(虽然从Jdk 1.2就标准废弃 但到了Java 11还没有删掉 手动滑稽)

比如 suspend()方法在暂停线程时 并不会释放资源 比如锁
所以可能会造成死锁问题 现在由wait方法代替

stop()方法会粗暴的停止线程 并不会保证线程占有的资源
安全的正常的释放

因为这三个方法是有问题的
所以Java官方并不建议我们使用这三个方法来控制线程

interrupt() 中断方法

那么怎么停止线程呢
Java提供了 interrupt()中断方法

public void interrupt()

调用代码

LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();

看起来还是很简单的
不够事实上并不是那么容易
中断并不是终止线程 而是给目标线程打上一个终止标记

所以调用interrupt()中断方法 并不能保证线程立即终止

所以 真正的终止 在下一节 安全的终止线程

线程通过检查自身是否被中断来进行响应,通过方法isInterruped()
来进行判断是否被中断,也可以调用静态方法Thread.interruped()
对当前线程的中断标识位进行复位。

如果该线程已经终止 此时调用该对象的isInterruped()方法只
会返回false

LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
TimeUnit.SECONDS.sleep(1);
timeRunThread.interrupt();

Boolean isInterrupt=timeRunThread.isInterrupted();
System.out.println(isInterrupt); //false

结果就是 false

在线程抛出中断异常InterruptException时 虚拟机会先将异常标志
位清除 然后抛出InterruptException 则此时调用isInterruped()方
会返回false


public class InterruptExceptionThread extends Thread{    

  @Override
  public void run(){    
      try {
            throw new InterruptedException();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }    
        
        try {
              Thread.sleep(5000);
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
 } 
 }

测试代码


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
TimeUnit.SECONDS.sleep(6);

timeRunThread.interrupt();
Boolean isInterrupt=timeRunThread.isInterrupted();
System.out.println(isInterrupt);

结果

java.lang.InterruptedException
	at xyz.my.LongTimeRunThread.run(LongTimeRunThread.java:21)
	
false

安全的停止线程

停止线程是多线程开发重要的技术点,停止一个线程即在线程处理任务并未完成之前,放弃当前的操作,因为停止线程并不像跳出循环那么简单干脆,而是需要一些技巧性的处理。

在Java中 可以使用三种方法推出正在运行的线程

  1. 使用退出标志 使线程执行完 run()方法正常退出
  2. 可以使用Thread.stop()方法粗暴的停止线程,但是前面说个 这个方法是不安全的。
  3. 可以使用Thread.interrupt()方法中断线程,但是前面也说个 此方法并不是真正停止线程 而是打上一个中断标记

使用退出标志 正常退出线程

使用退出标志是终止线程中最容易理解的一种
因为其逻辑是正常的运行完分支内容并退出
和其他的类中的方法 没有什么区别

线程代码

public class StopThread implements Runnable {    

    private String runFlag;
    public StopThread(String runFlag) {
      this.runFlag=runFlag;
    }    
    
    public String getRunFlag() {
      return runFlag;
    }    
    
    public void setRunFlag(String runFlag) {
      this.runFlag = runFlag;
    }    
    
    @Override
    public void run() {    
    while ("run".equals(runFlag)) {    
          System.out.println("线程还在运行");
        }
      System.out.println("线程终止");
    } 
}

运行代码


StopThread stopThread = new StopThread("run");
Thread thread = new Thread(stopThread,"1");
thread.start();
Thread.sleep(1);
stopThread.setRunFlag("stop");

结果

线程还在运行
线程还在运行
线程还在运行
线程还在运行
线程还在运行
线程终止

可以看出 其逻辑还是比较清晰明了的

但实际操作起来并不是这么容易
这段简单的代码是依靠死循环来强制
校验变量是否变化以达到进入终止分
支的目的 而众所周知,死循环是十分
耗费系统资源的

所以 在实际编程中 如何控制 还需更多考虑

使用interrupt()方法停止线程

使用中断法退出 前面在介绍interrupt()方法
时已经说明 调用interrupt()方法并不是真的
停止了线程 而是在线程上打上停止标记
具体何时停止要看虚拟机的调度情况

知道了怎么停止线程 还要看线程是否停止
前面已经提到 Thread提供了两个方法
检测线程是否终止

分别是

检测当前线程是否已中断

public static boolean interrupted()

检测线程是否已中断

public boolean isInterrupted()

这两个方法的区别是什么 在其Java doc
中已经写的非常清楚了

总结下

  1. interrupted() 检测当前线程是否是已中断的状态
    执行后将清除中断状态位(将其置为false)
  2. isInterrupted() 检测对象线程是否是已中断的状态
    但不清除中断位状态标志

实例
isInterrupted()


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();
Boolean isInterrupt=timeRunThread.isInterrupted();
Boolean isInterrupts=timeRunThread.isInterrupted();
System.out.println(isInterrupt); // true
System.out.println(isInterrupts); // true

interrputed()

与isInterrupted() 这是一个静态方法 主要在线程自身中调用


public class LongTimeRunThread extends Thread{    
  @Override
  public void run(){    
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted()); //true
        System.out.println(Thread.interrupted()); //false

    } }

使用异常法停止线程

前面提到 调用interrupt()方法并不是真的
停止了线程 而是在线程上打上停止标记
具体何时停止要看虚拟机的调度情况

那么 怎样才能立即停止线程呢
就是使用 在线程中抛出中断异常
的方法 立即停止线程

先看看 不抛出异常的情况


public class LongTimeRunThread extends Thread{    
  @Override
  public void run(){    
  for (int i = 0; i < 100000; i++) {
      if (this.isInterrupted()){
      System.out.println("发生中断 正在退出");
                    break;
                }
      System.out.println(i++);
        }
    System.out.println("已中断 但未立即退出");
    } 
}


运行


LongTimeRunThread timeRunThread=new LongTimeRunThread();
timeRunThread.start();
timeRunThread.interrupt();

结果

发生中断 正在退出
已中断 但未立即退出

从运行结果 可知 线程发生中断时并不是立即退出的

下面 使用异常停止

public class LongTimeRunThread extends Thread {    
  @Override
  public void run() {    
        try {
            for (int i = 0; i < 100000; i++) {
            if (this.isInterrupted()) {
                System.out.println("发生中断 正在退出");
                throw new InterruptedException();
            }
              System.out.println(i++);
            }
        System.out.println("已中断 但未立即退出");
        }
        catch (InterruptedException e){    
           e.printStackTrace();
       }
  } 
}

运行代码同上

结果


发生中断 正在退出
java.lang.InterruptedException
	at xyz.my.LongTimeRunThread.run(LongTimeRunThread.java:20)

抛出异常后立即就停止了

在沉睡中停止

如果线程在sleep状态下 停止 会发生什么?

public class SleepThread extends Thread {    
  @Override
  public void run() {
  try {
            System.out.println("run");
            Thread.sleep(100000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }     
     } 
}
SleepThread sleepThread=new SleepThread();
sleepThread.start();
sleepThread.interrupt();

结果

run
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at xyz.my.SleepThread.run(SleepThread.java:16)

和普通的中断无太大区别 只是指出了抛出异常时的状态 “sleep”

使用stop()强制停止线程

虽然自Java 2开始 stop()方法 就被声明为废弃
但目前 Java 11时 stop()方法 仍未被删除

使用很简单

LongTimeRunThread longTimeRunThread = new LongTimeRunThread();
longTimeRunThread.start();
longTimeRunThread.stop();

但是stop方法停止线程是非常粗暴的
强制停止可能会造成锁生效
引起数据异常
所以并不应该在正常的程序中使用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值