多线程和定时器详解及Object类中的wait和notify方法(生产者和消费者模式)

多线程和定时器详解及Object类中的wait和notify方法(生产者和消费者模式)

程序、进程和线程之间的关系及多线程

程序: 是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程: 是一个应用程序(一个进程是一个软件)

  • 进程A与进程B的内存独立不共享

线程: 是一个进程中的执行场景/执行单元

  • 线程A和线程B,堆内存和方法区内存共享;但是栈内存独立,不共享资源

一个进程可以启动多个线程

多线程并发: 假设启动10个线程,会有10个栈空间,每个栈和每 个栈之间互不干扰,各自执行各自的

多线程机制: 目的是为了提高程序的处理效率

简而言之: 一个程序运行后至少有一个进程,一个进程中可以包含多个线程

---->注:使用多线程机制之后,main方法结束,有可能程序也不会结束。因为main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈,弹栈。

run()方法和start()方法的

run()方法:

在这里插入图片描述

start()方法:

作用: 启动一个分支线程,在JVM中开辟出一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
在这里插入图片描述
区别:

  1. 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

  2. run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由JVM的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

sleep()方法和wait()方法

sleep()方法:

    static void sleep(long millis)
    1.静态方法:Thread。sleep(1000);
    2.参数是毫秒
    3.作用:让当前线程进入休眠,进入“阻塞状态”,
    	    放弃占有的CPU时间片,让给其它线程使用
      这行代码出现在A线程,A线程就会进入休眠
      这行代码出现在B线程,B线程就会进入休眠
    4.Thread.sleep()方法,可以做到如下效果:
        间隔特定的时间,去执行一段特定的代码,每隔多久执行一次
    public static void main(String[] args) {
        //让当前线程进入休眠,睡眠5秒
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //5秒之后执行这里的代码
        System.out.println("hello world");
    }

wait()方法 :

wait()方法可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个方法的同时,应该用notifyAll()方法通知所有由于使用了这个同步方法而处于等待的线程结束等待,曾中断的线程就会从刚才中断处继续执行这个同步方法(并不是立马执行,而是结束等待),并遵循“先中断先继续”的原则。

wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify()方法(notify()并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify()方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy()方法后增加一个等待和一些代码,看看效果)

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Thread1()).start();
        Thread.sleep(5000);
        //主动让出CPU,让CPU去执行其他的线程。
        //在sleep指定的时间后,CPU回到这个线程上继续往下执行
        new Thread(new Thread2()).start();
 
    }
  class Thread1 implements Runnable{
      @Override
      public void run() {
          synchronized (MultiThread.class){
              System.out.println("进入线程1");
              try{
                  System.out.println("线程1正在等待");
                  Thread.sleep(5000);
                  //MultiThread.class.wait();
					/*wait是指一个已经进入同步锁的线程内
				   (此处指Thread1),让自己暂时让出同步锁*/
                    /*以便其他在等待此锁的线程
                    (此处指Thread2)可以得到同步锁并运行*/
              }catch(Exception e){
                  System.out.println(e.getMessage());
                  e.printStackTrace();
              }
              System.out.println("线程1结束等待,继续执行");
              System.out.println("线程1执行结束");
          }
      }
  }
  class Thread2 implements Runnable{
      @Override
      public void run() {
          synchronized (MultiThread.class){
              System.out.println("进入线程2");
              System.out.println("线程2唤醒其他线程");
              MultiThread.class.notify();
              /*Thread2调用了notify()方法,但该方法不会释放
              对象锁,只是告诉调用wait方法的线程可以去参与获得
              锁的竞争了。但不会马上得到锁,因为锁还在别人手里,
              别人还没有释放。如果notify()后面的代码还有很多,
              需要执行完这些代码才会释放锁。*/
              try {
                  Thread.sleep(5000);
              }
              catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("线程2继续执行");
              System.out.println("线程2执行结束");
          }
      }
  }

区别:

  1. “sleep是Thread类的方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是有区别的。
  2. Thread.sleep()不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep()不会让线程释放锁。如果能够帮助你记忆的话,可以简单认为和锁相关的方法都定义在Object类中,因此调用Thread.sleep()是不会影响锁的相关行为。
  3. Thread.sleep()和Object.wait()都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify()/notifyAll()才能够重新获得CPU执行时间。
  4. 线程的状态参考 Thread.State的定义。新创建的但是没有执行(还没有调用start())的线程处于“就绪”,或者说Thread.State.NEW状态。
    Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,因为锁不能获取到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。BLOCKED状态下线程,OS调度机制需要决定下一个能够获取锁的线程是哪个,这种情况下,就是产生锁的争用,无论如何这都是很耗时的操作。
  5. 两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁 。
  6. 两者都可以暂停线程的执行。
    Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
    wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。

线程的实现

java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就行。

方式一:编写一个类,直接继承java.lang.Thread,重写run()方法

    public static void main(String[] args) {
        //这里是main方法,这里的代码属于主线程,在主栈中运行

        //新建一个分支线程对象
        MyThread myThread = new MyThread();

        //启动线程
        myThread.start();
        /*
        这段代码的任务只是为了开启一个新的栈空间,
        只要新的栈空间开出来,start()方法就结束了,
        线程启动成功。

        启动成功的线程会自动调用run方法,
        并且run方法在分支栈的栈底部(压栈)。

        run方法在分支栈底部,main方法在主栈的栈底部。
        run和main是平级的
         */

        //这里的代码还在运行在主栈中
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->"+i);
        }
    }
class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程--->"+i);
        }
    }
}

方式二:编写一个类,实现java.lang.Runnable接口,实现run方法

    public static void main(String[] args) {
        /*
        //创建一个可运行的对象
        Runnable r = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);
        */
        
        Thread t = new Thread(new Runnable());

        //启动线程
        t.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->"+i);
        }
    }
//这并不是一个线程类,是一个可运行的类。它还不是一个线程
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程--->"+i);
        }
    }
}

注:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活

采用匿名内部类的方式

    public static void main(String[] args) {

        //创建线程对象,采用匿名内部类
        //这是通过一个没有名字的类,new出来的对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("t线程--->"+i);
                }
            }
        });

        //启动线程
        t.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程--->"+i);
        }

    }

方式三:实现Callable接口

这种方式实现的线程可以获取线程的返回值。前两种方式是无法获取线程返回值得,因为run方法返回void

优点: 可以获取线程的执行结果

缺点: 在获取t线程执行结果的时候,当前线程受阻塞,效率较低

public static void main(String[] args) throws ExecutionException, InterruptedException {

        //第一步:创建一个"未来任务类"对象
        //参数非常重要,需要给一个Callable接口实现类对象
        FutureTask task = new FutureTask(new Callable() {
            //call()方法就相当于run方法。只不过有返回值
            //线程执行一个任务,执行之后肯会有一耳光执行结果
            //模拟执行
            @Override
            public Object call() throws Exception {
                System.out.println("call method begin");
                Thread.sleep(1000*10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b;//自动装箱(300结果变成Integer)
            }
        });

        //创建线程对象
        Thread t = new Thread(task);

        //启动线程
        t.start();

        //在主线程获取t线程的返回结果
        //get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果" + obj);

        //main方法这里的程序想要执行必须等待get()方法的结束
        //而get()方法可能需要很久。
        //因为get()方法为了拿另一个线程的执行结果
        //另一个线程执行是需要时间的。

    }

线程的五种状态及生命周期

  • 新建状态

    刚new出来的线程对象
    在这里插入图片描述

  • 就绪状态

    就绪状态又叫做可运行状态,表明当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,ran方法的开始执行标志着线程进入运行状态
    在这里插入图片描述

  • 运行状态

    ran方法的开始执行标志着线程进入运行状态,当之占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上次的代码继续往下执行
    在这里插入图片描述

  • 阻塞状态

    当一个线程遇到一个阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片

    阻塞解除,由于之前的时间片没了,需要再次回到就绪状态,抢夺CPU时间片
    在这里插入图片描述

  • 死亡状态

图示:
在这里插入图片描述

终止线程的睡眠——interrupt()方法

不是终断线程的执行,是终止线程的睡眠

public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();

        //希望5秒之后。t线程醒来
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终断t线程的睡眠
        ///(这种终断睡眠的方式依靠了java的异常处理机制)
        t.interrupt();
    }
class MyRunnable2 implements Runnable{
    //run()中的异常不能throws,只能try catch
    //因为run()方法在父类中没有抛出任何异常
    //(子类重写父类的方法)子类不能比父类抛出更多的异常
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            //睡眠1年
            Thread.sleep(1000*60*60*24*365);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //1年之后才会执行
        System.out.println(Thread.currentThread().getName() + "---> end");
    }
}

终止线程的执行

注:不建议用t.stop()方法,容易损失数据

    public static void main(String[] args) {
        MyRunnable4 r = new MyRunnable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        //模拟5秒
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终止线程
        //你想什么时候终止t的执行,
        //那么你把标记修改为false,就结束了
        r.run = false;
    }
class MyRunnable4 implements Runnable{

    //打一个布尔标记
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if(run) {
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                //终止当前线程
                return;
            }
        }
    }
}

定时器

在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的

作用:

间隔特定的时间,执行特定的程序

实现方式:

方式一: 使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式最原始的定时器(low)

方式二: java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。

不过,这种方式在目前的开发中也很少用,因为现在很多高级框架都是支持定时任务的。在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

实现

    public static void main(String[] args) throws ParseException {

        //创建定时器对象
        Timer timer = new Timer();

        //指定定时任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date fisrttime = sdf.parse("2021-1-29 23:37:37");
        timer.schedule(null,fisrttime,1000*10);

    }
//编写一个定时任务类,因为TimerTask是一个抽象类,没法直接new
class LogTimerTask extends TimerTask{
    @Override
    public void run() {
        //编写你需要执行的任务就行了
    }
}

Object类中的wait和notify方法(生产者和消费者模式)

wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类自带的

wait方法和notif方法建立在synchronized线程同步的基础之上

wait()方法

Object o = new Object;
o.wait();

表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。

o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态,直到最终调用o.notify()方法。

o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁

notify()方法

Object o = new Object;
o.notify();

表示:唤醒正在o对象上等待的线程

还有一个notifyAll()方法:唤醒o对象上处于等待的所有线程

o.notify()方法只会通知,不会释放之前占有的o对象的锁

生产者和消费者模式

生产者和消费者模式是为了专门解决某个特定需求的,最终要达到生产和消费必须均衡
在这里插入图片描述

 

posted @ 2021-01-29 01:03  许君  阅读(300)  评论(0编辑  收藏  举报