多线程看着篇就够了

认识多线程:

计算机的操作系统大多采用多任务,就是在一个系统中可以同时运行多个程序,例如可以在听音乐的同时聊天。即就是有多个任务,每个任务对应一个进程,每个进程可以 产生多个线程。

 什么是进程:

 进程就是一段程序的运行过程,程序就是一段静态代码,进程是系统应用程序的基本单位。

什么是多线程:

 线程是进程的一个最小的单位,在一个进程里至少有一个线程,线程的运行必须依靠进程,多个同类的线程可以共享一个进程的堆和方法区的资源,所以线程也被称为是轻量级的进程。

如何编写一个线程:

每个程序至少自动拥有一个主线程,也称为核心级线程,当程序加载到内存时启动主线程,java中的 public class void main()方法就是主线程的入口,运行java时会先执行这个方法,在开发中用户编写的线程是除了主线程之外的线程,我们称为用户级线程,使用一个线程可以分为以下四个步骤:

  1. 定义一个线程,同时指明这个线程所要执行的代码也就是需要完成的功能。
  2. 创建线程对象
  3. 启动线程
  4. 终止线程

定义一个线程通常有两种方法:

继承java.lang .Thread类或者实现java.lang.Runnable接口:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class BootViewApplicationTests {
public static class Mythread extends Thread  {
    @Override
    public void run() {

        System.out.println("这是我继承thread类定义的一个线程");
    }
}
    @Test
    void contextLoads() {
        Mythread mythread = new Mythread();
        mythread.start();
    }

}
@SpringBootTest
class BootViewApplicationTests {
public static class Myrunnabe implements Runnable {
    @Override
    public void run() {

        System.out.println("这是我实现runnable类定义的一个线程");
    }
}
    @Test
    void contextLoads() {
     Thread thread = new Thread(new Myrunnabe());
       thread.start();
    }

}

继承Thread类的方式简单明了,那么为什么要实现Runnable呢,我们都知道继承是单继承的,而实现,可以多实现,所以当一个类已经继承了某个类,那么他就无法在继承Thread。因此可以用接口去实现它。其实Thread类也是继承runnable接口的,而runnable接口中只有一个抽象方法,就是run()方法。

线程的状态:

线程的生命周期可以分为四个阶段,每个阶段对应一种状态。

  • 新建状态:创建线程对象但是未调用start()方法之前,这个线程有了生命,但是此时线程只是一个空对象,系统没有为其分配资源。此时线程只能启动和终止,任何其他操作都会引发异常。
  • 可运行状态:当调用了start()方法启动线程后,系统会为线程分配除了CPU之外所需的资源,这个线程就成为可运行状态。在这个状态中,线程可能正在运行,也可能没有运行。只有当线程获得CPU资源的时候,此时系统才会调用run方法,才会真正处于运行状态。
  • 阻塞状态:线程在运行过程中由于某种原因不能继续运行,就会进入堵塞状态。堵塞状态是一种不可运行状态,而处于这种状态的线程在得到一个特定事件后才会转成可运行状态。导致线程堵塞的原因有(1)调用了sleep()方法。(2)一个线程执行一个I/O操作尚未完成,线程会进入堵塞状态。(3)如果一个对象的执行需要另一个对象的锁,而这个对象的锁正被其他线程占用,那么此线程会被阻塞。
  • 死亡状态:一个线程run()方法执行完毕,stop方法被调用,或者在运行中出现没有被捕获的异常时,线程就会进入死亡状态。

线程调度:

当同时有多个线程处于可运行状态,这样就会遇见一个问题,排队等待CPU分配资源从而调用run方法进入运行状态,那么线程的优先级就决定了排队的顺序。优先级越高,那么他就被先分配cup资源,总结一句就是先到先服务。

在java中有线程的调度管理器只专门负责管理线程优先级的,他是根据线程的调度算法进行调度的。

线程的优先级:优先级用1-10 表示,10表示优先级最高,默认是5

 /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

我们看一下Threa的类中的源码,其中每个优先级对应一个公共的静态常量。

实现调度的三种方法:join(),sleep(),yield()

我们先看一下第一种:join()方法,使得当前线程暂停执行,等待调用方法的线程结束后,在继续执行本线程。

我们看一下Thread类中定义join方法的三种重载:

   //第一种:调用join方法,等待其他线程,等待时间是millis
 public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

//第二种
    public final synchronized void join(long millis, int nanos)
    throws InterruptedException {

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        join(millis);
    }

  //第三种:调用join方法,等待其他线程执行完毕
    public final void join() throws InterruptedException {
        join(0);
    }

三种方法,后两种最终都调用了第一种方法。

实战1:写一个join方法阻塞线程:

第一步:定义线程类,打印五次。

第二步:定义一个测试类,使用join方法阻塞这个线程。

@SpringBootTest
class BootViewApplicationTests {
    //定义一个线程,并打印五次
public static class Myrunnabe implements Runnable {
    @Override
    public void run() {
     int a=5;
        for (int i = 0; i < a; i++) {
            System.out.println(i+"这是我实现runnable类定义的一个线程");
        }

    }
}
//在主线程中执行五次后,调用定义的线程
    @Test
    void contextLoads() {
     int b=10;
        for (int i = 0; i < b; i++) {
            System.out.println("这是主线程");
            //当1=5的时候,调用上边的线程
            if (i==5){
                Thread thread = new Thread(new Myrunnabe());
                try {
                    thread.start();
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

我们看一下测试结果:

 当主线程执行到第五次的时候,就会调用我定义的方法,调用join方法,会堵塞当前的线程,也就是主线程,等定义的线程执行完毕在去执行主线程。

实战2:写一个join方法实现两个线程间的数据传递:

步骤一:定义线程类,为变量赋值。

步骤二:编写测试类

我们先看一下不使用join(),实现数据传递会出现什么结果。

class BootViewApplicationTests {
public static class Myrunnabe extends  Thread{
      public String a;
       public String b;
    @Override
    public void run() {
       a="zl的测试数据1";
       b="zl的测试数据2";
        }

    }
    
    @Test
    void contextLoads() {

        Myrunnabe thread = new Myrunnabe();
        thread.start();
        System.out.println("a:"+thread.a);
        System.out.println("b:"+thread.b);
    }

}

测试结果:

a:null
b:null

为什么会出现这种情况,因为start后,线程是处于可运行状态,可运行状态有两种可能,就是正在运行,和尚未运行,显然在执行了start方法后,run方法还没来得及执行赋值的方法就被打印了。所以可以使用join方法,去处理这个问题:

@SpringBootTest
class BootViewApplicationTests {
public static class Myrunnabe extends  Thread{
      public String a;
       public String b;
    @Override
    public void run() {
       a="zl的测试数据1";
       b="zl的测试数据2";
        }

    }

    @Test
    void contextLoads() {

        Myrunnabe thread = new Myrunnabe();

        try {
            thread.start();
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("a:"+thread.a);
        System.out.println("b:"+thread.b);
    }

}

测试结果:

a:zl的测试数据1
b:zl的测试数据2

这样就可以拿到数据了。

我们再看一下第二种实现线程调度的方法:sleep()方法,他会让当前线程睡眠多少秒,线程进入不可运行 状态,等睡眠时间结束后,会进入可运性状态。

实战1:使用sleep方法堵塞线程

步骤一:定义线程

步骤二:在run方法中sleep休眠

步骤三:编写测试

@SpringBootTest
class BootViewApplicationTests {
public static void sleep(long s){
    for (int i = 0; i<5; i++) {
        System.out.println(i+1+"s");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    }

    @Test
    void contextLoads() {
        System.out.println("等待");
        sleep(5);

        System.out.println("恢复");
    }

}

测试结果:

等待
1s
2s
3s
4s
5s
恢复

主线程等待5s,然后执行定义的线程,5s后主线程恢复,执行主线程

我们再看一下第二种实现线程调度的方法:yield()方法:yield方法可让当前线程暂停执行,允许其他线程执行,但该线程还是处于可运行状态,并不变成阻塞状态,此时系统选择相同或者更高优先级的线程执行,如果没有,则该线程继续执行。

实战:使用yeid()方法暂停线程

步骤一:定义两个线程

步骤二:在run方法中使用yeid()方法暂停线程

步骤三:测试类

  public static class mythread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("mythread1第:"+(i + 1 )+ "次执行");
                Thread.yield();
            }


        }
    }
    public static class mythread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("mythread2第:"+(i + 1 )+ "次执行");
                Thread.yield();
            }
        }
    }
    @Test
    void contextLoads() {
        mythread1 mythread1 = new mythread1();
        mythread2 mythread2 = new mythread2();
        mythread1.start();
        mythread2.start();
    }

}

测试结果:

mythread2第:1次执行
mythread1第:1次执行
mythread2第:2次执行
mythread2第:3次执行
mythread2第:4次执行
mythread2第:5次执行
mythread1第:2次执行
mythread1第:3次执行
mythread1第:4次执行
mythread1第:5次执行

我们可以看到,当使用了yeid()方法之后,线程并不是转入了堵塞状态,依旧是可运行状态,两个线程相互之间抢夺CPU资源,就会出现这样的结果。

实现线程调度的三种方法讲完了,我们再来总结一下:

  1. join方法和sleep方法都是将线程的可运行状态打破,进入不可运行状态,也就是堵塞状态,而yeid方法只是让线程暂停,而没有改变线程的状态
  2. join方法暂停当前线程,等待其他线程执行结束在重新启用,也就是需要其他线程唤醒,而sleep方法,是休眠,休眠结束后线程自己就苏醒。不需要其他线程的唤醒。
  3. yeid方法是暂停当前线程,先执行线程优先级高的,自身线程一直处于可运行状态

线程同步:

前面的案例线程都是独立且异步的,所以不需要外部的资源和方法,也不需要关系其他线程的状态和行为。但是如果同时运行的多个线程共享同一个外部资源,那么就需要考虑线程的状态和行为。

线程同步的方法:

     1.synchronized,这个关键字能够保证方法或代码块在某一时刻只能有一个线程访问。他可以作用于两个地方,一个是方法上,一个是代码块上。

     2.volatile关键字,这个关键字是线程同步的轻量级实现,效率要远远高于synchronized,但是有个弊端就是volatile只能用于修饰变量。

线程通讯
举个栗子 :生产者和消费者,生产者生产产品,通知消费者消费,如果消费者没有消费则停止生产,等到消费者消费完成后,再生产,如果生产者没有生产,消费者则等待生产者生产。
线程间实现通讯的方法有三个:wait()挂起线程,notify()唤起线程,notifyAll()唤起所有线程
案例:
步骤一:定义共享资源
步骤二:定义生产者线程
步骤三:定义消费者线程
步骤四:测试
public class Acount {
  private char c;
  private boolean isProduced=false;
  public synchronized void pShar(char c){
      //如果产品未消费则停止生产
      if(isProduced){
          try {
              System.out.println("消费者还未消费因此生产者停止生产");
              wait();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      this.c=c;
      isProduced=true;
      notify();
      System.out.println("生产者生产了"+c+",通知消费者消费");

  }
    public synchronized char cShar(){
      //如果产品没生产,则消费者停止消费
        if(!isProduced){
            try {
                System.out.println("生产者还未生产,因此消费者停止消费");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        isProduced=false;
        notify();
        System.out.println("消费者消费了"+c+",通知生产者生产");
     return this.c;
    }
}
@SpringBootTest
class BootViewApplicationTests {
    //定义生产线程
class Product extends Thread{
    private Acount acount;
    Product(Acount acount){
        this.acount=acount;
    }
    @Override
    public void run() {
        char c;
        for(c='A';c<='D';c++){
            try {
                Thread.sleep((int)Math.random()*3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //将产品放入仓库
            acount.pShar(c);
        }
    }
}
//定义消费者线程
    class Consumer extends Thread{
        private Acount acount;
        Consumer(Acount acount){
            this.acount=acount;
        }

        @Override
        public void run() {
            char ch;
            do{
                try {
                    Thread.sleep((int)Math.random()*3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //从仓库中取走产品
                ch=acount.cShar();
            }while (ch!='D');

        }
    }
    @Test
    //测试类
    void contextLoads() {
        Acount acount = new Acount();
        new Consumer(acount).start();
        new Product(acount).start();
    }

测试结果:

生产者还未生产,因此消费者停止消费
生产者生产了A,通知消费者消费
消费者还未消费因此生产者停止生产
消费者消费了A,通知生产者生产
生产者还未生产,因此消费者停止消费
生产者生产了B,通知消费者消费
消费者还未消费因此生产者停止生产
消费者消费了B,通知生产者生产
生产者还未生产,因此消费者停止消费
生产者生产了C,通知消费者消费
消费者还未消费因此生产者停止生产
消费者消费了C,通知生产者生产
生产者生产了D,通知消费者消费
消费者消费了D,通知生产者生产

 
原文地址:https://www.cnblogs.com/javazl/p/12721451.html