> 文档中心 > 多线程——初阶

多线程——初阶

目录

  • 线程的创建
  • Thread类的常用方法
  • 线程的状态
  • 线程安全
  • synchronized的特性
  • wait 和 notify
  • 总结

线程的创建

在Java中提供了一个线程类Thread,我们利用Thread类来创建线程。下面我们来介绍线程的几种不同的创建形式。

  1. 继承Thread类
public class Demo {    public static void main(String[] args) { Thread a1 = new MyThreadByThread(); a1.start();    }}class MyThreadByThread extends Thread{    @Override    public void run(){ System.out.println("This is a Thread by extends Thread");    }}
  1. 实现Runnable接口
public class Demo {    public static void main(String[] args) { Runnable a1 = new MyThreadByRunnable(); a1.run();    }}class MyThreadByRunnable implements Runnable{    @Override    public void run(){ System.out.println("This is a Thread by implements Runnable");    }}
  1. 利用匿名内部类
public class Demo {    public static void main(String[] args) {    //匿名内部类是Runnable接口 Thread a = new Thread(new Runnable() {     @Override     public void run() {  System.out.println("This is a Thread by 匿名内部类");     } }); a.start();    }}public class Demo {    public static void main(String[] args) {    //匿名内部类是Thread类 Thread a = new Thread(new Thread()); a.start();    }}
  1. 利用lambda表达式
public class Demo {    public static void main(String[] args) { Thread a = new Thread(() -> {     System.out.println("This is a Thread by Lambda表达式"); }); a.start();    }}

通常认为Thread()中的参数为Runnable要比Thread更好使用Runnable能够使线程和线程之间更好的解耦,Runnable描述的是一个任务,关心的是任务的具体内容是什么,并不关心是通过哪种方式来执行任务。

Thread类的常用方法

Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有唯一的Thread对象与之关联。下面我们来介绍Thread类常用的一些成员。

  1. 常见属性及其获取方法
    • getId():获取线程的id号
    • getNanme(): 获取线程的名字
    • getState(): 获取线程的状态
    • getPriority(): 获取线程的优先级
    • isAlive():是否存活
    • isInterrupted():是否被中断
Thread t = new Thread(() -> {    System.out.println(Thread.currentThread().getId());    System.out.println(Thread.currentThread().getName());    System.out.println(Thread.currentThread().getState());    System.out.println(Thread.currentThread().getPriority());    System.out.println(Thread.currentThread().isAlive());    System.out.println(Thread.currentThread().isInterrupted());});t.start();

多线程——初阶

  1. 线程的启动 start():重写run()方法意味着我们创建了一个线程对象并为其分配了具体的任务,但标志一个线程正式运行的方法是start()方法,调用了start()方法,才真的在操作系统的底层创建出一个线程。
  2. 线程的中断 interrupted():Thread类中提供了两种判断线程是否被中断的方法:Thread.interrputed()和Thread.currentThread.isInterrputed(),前者在判断完线程的中断标志是否设置后会清除标志位,后者不会清除标志位
 //验证isInterrputed方法不会清除标志位 Thread.currentThread().interrupt(); System.out.println(Thread.currentThread().isInterrupted()); System.out.println(Thread.currentThread().isInterrupted()); System.out.println("===================================="); //验证interrupted方法会清除标志位 Thread.currentThread().interrupt(); System.out.println(Thread.interrupted()); System.out.println(Thread.interrupted());

多线程——初阶
由代码运行结果可以看出isInterrupted方法不在设置完中断标志位后不会清除,而interrupted方法则会中断标志位。

Thread t = new Thread(() -> {     while(!Thread.interrupted()){  //也可以是while(!Thread.currentThread().isInterrupted())      System.out.println("没有中断继续输出。。");  try {      Thread.sleep(1000);  } catch (InterruptedException e) {      e.printStackTrace();      System.out.println("外界因素导致线程中断不能输出");      break;  }     } }); t.start(); Thread.sleep(10000); t.interrupt();

在这里插入图片描述
thread收到通知的方式有两种,一种是线程调用了wait/sleep/join等方法使线程阻塞挂起,这时会抛出InterruptedException异常来通知并清除中断标志,当触发该异常后,是否结束线程取决于catch代码的写法。第二种则是通过Thread内部的interrupt()方法去设置中断标志然后调用线程中断的方法来通知。
4. 线程的等待join():我们知道线程和线程之间是并发进行的,但有些情况下,线程之间需要有一定的先后顺序。比如只有先买完火车票,然后才能再手机上查看到自己购买的火车票。这时我们就需要让一个线程去等待另一个线程运行结束后再启动。

Thread t1 = new Thread(() -> {      for(int i = 0; i < 10; i++){    System.out.println("转账10元");}   });   t1.start();   try {t1.join();   } catch (InterruptedException e) {e.printStackTrace();   }   Thread t2 = new Thread(() -> {for(int i = 0; i < 10; i++){    System.out.println("花费10元");}   });   t2.start();

在这里插入图片描述

  1. 线程的休眠sleep():当线程中的任务被分成若干份,每份和每份之间需要一定的时间间隔才能运行成功时,我们需要在任务之间加上一个休眠方法sleep()。比如泡一碗精致的方便面时,我们需要在把面放入水中进行加热后,等待t1时间后方便面筋道程度达到最佳,然后才能放各种调料包。
Thread t = new Thread(() -> {     System.out.println("水热了之后将面下入锅中");     try {  Thread.sleep(10000);     } catch (InterruptedException e) {  e.printStackTrace();     }     System.out.println("向锅中倒入各种调料包"); });

多线程——初阶
我们可以看到在代码中我们设置等待的是10000ms,但实际等待的时长是10007ms,这是因为线程的调度是不可控的,如果需要更高进度的休眠,可以使用Thread.sleep(long mills,int nanos)方法

线程的状态

在Java中的Thread类中,对线程的状态进行了细化,大致可以分成5种状态:

  1. NEW:创建了线程对象,对没有启动线程(没有调用start方法)
Thread t = new Thread(() ->{    System.out.println("任务已创建");   });    System.out.println(t.getState()); // 运行结果为NEW
  1. RUNNABLE:工作状态。有两种形式,一种是运行中,一种是就绪状态。
static int a = 0; public static void main(String[] args) throws InterruptedException {     Thread t = new Thread(() ->{  while(true){     a++;  }     });     t.start();     Thread.sleep(2000);     System.out.println(t.getState()); //结果为RUNNABLE,此处的状态为运行中状态  }Thread t = new Thread(() ->{    while(true){    //yield()方法的作用是当执行到这里时,将执行该方法的线程占用的CPU资源释放出来然后供给所有线程去抢占   Thread.yield();}   });   t.start();   Thread.sleep(2000);   System.out.println(t.getState()); //结果为RUNNABLE,此处的状态为就绪状态  
  1. TIMED_WAITING:因调用了join(),sleep()等等待有限时间方法而处于一种阻塞状态、
 Thread t = new Thread(() ->{      while(true){    System.out.println("任务A");    try { Thread.sleep(1000);    } catch (InterruptedException e) { e.printStackTrace();    }}   });   t.start();   //如果这里不设置等待,t的状态为RUNNABLE。因为在线程刚启动时任务A是可以执行的(只能执行1次),之后大部分时间都处于因sleep而阻塞的状态。   Thread.sleep(2000); //也可以是 t.join(2000)   System.out.println(t.getState()); //运行结果为TIMED_WAITING
  1. WAITING:当前线程等待被唤醒而处于一种阻塞状态。
 Thread t = new Thread(() ->{    synchronized (Thread.currentThread()){   System.out.println("hh");   try {Thread.currentThread().wait();   } catch (InterruptedException e) {e.printStackTrace();   }      }   });   t.start();   Thread.sleep(1000);    System.out.println(t.getState()); //运行结果为WAITING
  1. BLOCKED:当前线程因为在等待锁而处于一种阻塞状态
static Object locker = new Object();    public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{   synchronized (locker){while(true){}   }});Thread t2 = new Thread(() ->{    synchronized (locker){ while(true){ }    }});t1.start();t2.start();Thread.sleep(1000);//由于两个线程同时竞争一把锁,且都处于一种无限循环的运行中,所以二者一定有一个线程抢到了锁,然后另一个线程处于等待加锁的过程。System.out.println(t1.getState()); System.out.println(t2.getState());    }
  1. TERMINATED:操作系统中的线程已经执行完毕,但Thread对象仍在
Thread t = new Thread(() ->{   System.out.println("任务已创建");   });   t.start();   t.join();    System.out.println(t.getState()); //运行结果为TERMINATED

线程之间的转换图如下:在这里插入图片描述

这些线程状态可以帮助我们在写线程相关的代码时遇见bug时可以通过线程的状态测试哪一部分代码处于异常状态

线程安全

  1. 概念:线程安全指的是当我们在多线程中运行代码得到的结果应该和在单线程中运行结果相一致
  2. 线程不安全代码分析:
static int count = 0;    public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() ->{     for(int i = 0; i < 50_000;i++){  count++;     } }); Thread t2 = new Thread(() ->{     for(int i = 0; i < 50_000; i++){  count++;     } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count的值为:" + count );    }

上述代码描述的是两个线程分别对count变量自增50000次,我们理想中得到的结果应该是100000,但代码运行结果却不尽一致。我们经过多次运行验证,发现最终的运行结果处于50000~100000之间。这是为什么呢?
原因就是count++这个步骤并不是原子性的线程调用count++一共3个步骤,首先将内存中的count存入到CPU的寄存器中(load);然后将CPU寄存器中存储的count加1(add);然后将操作完的数据重新写到内存中(save)
可能光这么说并不能直观地展示问题所在,我们通过以下这张图来分析出问题到底处在了哪里?在这里插入图片描述
3. 线程不安全的原因

  1. 线程是抢占式进行的,线程间的调度充满随机性(线程不安全的万恶之源)
    2. 多个线程对同一个变量进行数据的更新操作,只读数据的化不会产生线程不安全
    3. 针对的变量的操作不是原子(不可分割,执行的操作统一执行) (加锁就是将几条操作打包成一个原子)
    4. 内存可见性:在某些特殊情况下,线程本来要读取内存中的数据,但改变成了读取CPU寄存器中的数据内存可见性属于编译器优化的副作用
    5. 指令重排序:编译器优化的副作用:编译器会在保证逻辑不变的情况下去调整某些指令的运行顺序来提高程序的效率,但对于多线程而言,该优化可能导致误判。
    volatile可以防止内存可见性和指令重排序,但不能保证原子性
  1. 解决办法:用synchronized关键字给关键方法加锁
    static int count = 0;static Object locker = new Object();  public static void main(String[] args) throws InterruptedException {      Thread t1 = new Thread(() ->{   for(int i = 0; i < 50_000;i++) {synchronized (locker){    count++;}   }      });      Thread t2 = new Thread(() ->{   for(int i = 0; i < 50_000; i++){synchronized (locker){    count++;}   }      });      t1.start();      t2.start();      t1.join();      t2.join();      System.out.println("count的值为:" + count );  }

当对两个线程加一把锁时,当其中一个线程抢占到锁时,对count进行数据更新操作,另一个线程处于BLOCKED状态,等第一个线程把锁释放后,两个线程重新竞争锁。这样可以导致每个线程中的操作都不会受另外一个线程的干扰

synchronized的特性

synchronized的使用示例

class SynchronizedDemo{    synchronized public void func(){ System.out.println("给普通方法加锁");    }    synchronized static public void func1(){ System.out.println("给静态方法加锁");    }    public void func2(){ //给当前对象加锁 synchronized (this){     System.out.println("给普通方法通过代码块的方式加锁"); }    }    static public void func3(){ //给类对象加锁 synchronized (SynchronizedDemo.class){     System.out.println("给静态方法通过代码块的方式加锁"); }    }}
  1. 互斥:当多个线程同时竞争一把锁时,当其中一个线程执行到对象的synchronized中时,其他线程就需要在阻塞队列中进行阻塞等待。
    synchronized用的锁是存在对象头里的,可以粗略的理解存储在内存中的每个对象都有一块内存空间用来表示当前的状态是处于抢到锁还是阻塞等待的过程
    阻塞队列是针对每一把锁,操作系统内部会维护一个阻塞队列,当这个锁的某个线程抢占到该锁以后,其他线程再想进行加锁就会加锁失败,然后进行阻塞等待,直到加上锁的线程释放锁以后,该锁的所有线程再重新竞争该锁(竞争到该锁的概率是均等的,不遵循先来后到的原则)。
  2. 刷新内存
    synchronized的工作流程:
    1. 获取互斥锁
    2. 从主内存拷贝变量的最新副本到工作的内存
    3. 执行代码
    4. 将更改后的变量的值刷新到主内存
    5. 释放互斥锁
  3. 可重入:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。什么叫死锁?死锁就是当一个线程进行嵌套加锁时出现的一种逻辑上的死结
static Object locker = new Object();public static void func(){int count = 0;synchronized(locker){synchornized(locker){count++;}}}

对于上述代码,我们可以看到进行了两次嵌套加锁,在func方法中,第一个锁加上之后,第二个锁处于阻塞等待的过程,直到第一个锁被释放之后才能加上锁然后才能继续执行下面的代码,但第一个锁释放需要完成func方法,因此构成了死循环(类似于西安的一码通程序员与保安大野的段子)。
但synchronized是可重入锁,因此不会产生死锁。在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息,线程持有者负责记录当前加锁的线程对象,当对已经加锁的线程对象再次进行加锁操作时,线程持有者不会变,计数器加1;在释放锁时,先将计数器递减为0之后再真正释放锁

wait 和 notify

  1. wait():使当前线程处于等待的状态
    1. 使当前线程进行等待
    2. 释放当前的锁
    3. 当满足某个条件时(被其他线程唤醒或者到达等待结束时间),重新获取这把锁
 public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {//注意:wait方法使用的前提是该线程对象已经抢占到锁    synchronized (Thread.currentThread()){ System.out.println("执行任务A"); try {     Thread.currentThread().wait(); } catch (InterruptedException e) {     e.printStackTrace(); } System.out.println("等待结束"); System.out.println("执行任务B");    }});    }
  1. notify():唤醒1个等待的线程
    1. 唤醒竞争同一把锁并处于等待过程中的线程,当存在多个等待的线程时,会随机调度一个线程进行唤醒。
    2. 执行notify()方法的线程再执行完notify()方法后不会马上释放锁,而是将该线程执行完毕后再释放锁
class A{    static Object locker  = new Object();    public static void main(String[] args) { Thread t1 = new Thread(() ->{     synchronized (locker){  System.out.println("执行任务A");  try {  //等待其他线程的唤醒      locker.wait();  } catch (InterruptedException e) {      e.printStackTrace();  }  System.out.println("等待结束");  System.out.println("执行任务B");     } }); Thread t2 = new Thread(() ->{     synchronized(locker){     //随机唤醒一个处于等待的线程  locker.notify();  System.out.println("执行任务C");     } }); t1.start(); t2.start();    }}
  1. notifyAll():和notify一样,只不过是可以一次性唤醒所有其他处于等待过程中的线程

总结

本次主要介绍了多线程当中的一些基础概念以及对应的实现方式。希望大家能够有所收获!