• 【Java进阶篇】第七章 多线程


    一、多线程概述

    1、进程与线程

    • 进程是一个应用程序(一个进程是一个软件)
    • 线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程

    举个例子:

    DOS窗口运行java HelloWorld,先启动JVM,JVM是一个进程,JVM启动一个主线程调用main方法,同时再启动一个垃圾回收线程来负责看护、回收垃圾。(也就是说Java程序至少两线程并发,main方法对应的主线程+GC)

    2、进程与线程的关系

    把进程看作是现实生活中的公司,如京东。线程则可看作是其下的某一个职能部门,负责完成某任务,如开发部门。

    • 进程A和进程B的内存独立不共享
    • Java中,线程A和线程B,堆内存和方法区内存共享,但栈内存独立,一个线程一个栈

    如启动了10个线程,就会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

    🍁Java中的多线程机制,目的就是为了提高程序的处理效率, 如火车站看成是一个进程,则每个售票小窗口就是一个个线程,甲在窗口1买票,乙在窗口2买票,谁也不用等谁 一个个售票窗口就像一个个栈,有自己独立的空间。售票大厅这个共用空间就像堆和方法区。

    多线程
    引入多线程以后,main方法的结束,不再意味着程序的结束。main方法结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈

    二、多线程并发的实现

    真正的多线程并发是:t1线程执行t1,t2线程执行t2,t1不影响t2,t2不影响t1。4核的CPU,在同一时间点,可以真正的有4个进程并发执行,单核的CPU,在某一个时间点上实际只能处理一件事情,但由于CPU的处理速度极快,多个线程之间频繁切换,从而造成了多个事情在同时处理的视觉假象。

    public class Thread1 {
        public static void main(String[] args) {
            m1();    
        }
        public static void m1(){
            m2();    
        }
        public static void m2(){
            m3();
        }
        public static void m3(){
            System.out.println("m3 excute");
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    分析以上:只有一个主线程,主栈,没有分支线程被启动
    示意图

    1、线程的实现方式一

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

    class MyThread extends Thread{
    	//这段代码运行在分支线程中
        public void run(){
            for(int i=0;i<100;i++){
                System.out.println("分支线程" + i);
            }
        }
    }
    public class ThreadTest{
        public static void main(String[] args) {
        	//创建分支线程对象
            MyThread myThread = new MyThread();
            //启动分支线程
            myThread.start();
            //这里仍然运行在主线程当中
            for(int i=0;i<100;i++){
                System.out.println("主线程" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这个任务完成后,这段代码就瞬间执行结束了

    启动成功后的线程会自动调用我重写的run方法,并且run方法在分支栈的栈底部(main方法在主栈的底部,故run和main是平级的)

    myThread.run();
    
    • 1

    如果直接调用我重写的run方法,则不会启动线程,不会分配新的分支栈,此时,就是单线程了。
    内存图
    运行结果:
    run

    2、线程的实现方式二

    编写一个类,实现java.lang.Runable接口,重写run方法

    public class ThreadTest {
        public static void main(String[] args) {
        	//创建一个可运行的对象
            MyRunnable r = new MyRunnable();
            //给Thread类的构造方法传入Runnable类的对象
            //将可运行的对象封装成一个线程对象
            Thread t = new Thread(r);
            t.start();
            for(int i=0;i<100;i++){
                System.out.println("主线程"+ i);
            }
        }
    }
    
    //这不是线程,仅仅是一个可运行的类
    class MyRunnable implements Runnable{
        public void run(){
            for(int i=0;i<100;i++){
                System.out.println("分支线程"+i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    总结线程的实现:

    🍁编写一个类继承Thread类并重写run方法

    public class MyThread extends Thread{
    	public void run(){
    	}
    }
    
    MyThread t = new MyThread();
    t.start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    🍁编写一个类,实现Runnable接口并重写run方法

    public class MyRunnbale implements Runnable{
    	public void run(){
    	}
    }
     
    Thread t = new Thread(new MyRunnable());
    t.start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由于Java的单继承,第一种方式中,不能再继承别的类了,而第二种可以,面向接口编程更优。

    //方式二的匿名内部类写法:
    
    Thread t2 = new Thread(new MyRunnable(){
    				       public void run(){
    				           for(int i=0;i<100;i++){
    				               System.out.println(i);
    				           }
    				       }
    				   });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    tip

    run()方法中要是有异常,也只能捕捉,不能上抛,因为run方法在父类中没有抛出任何异常,做为子类,重写时不能比父类抛出更多的异常。

    三、线程的生命周期

    1、线程的五个生命周期

    生命周期图

    • 🍁 新建状态:
      刚new出来的线程对象

    • 🍁就绪状态:
      又叫做可运行状态,表示当前线程具有抢夺CPU时间片的能力(CPU时间片就是执行权)当一个线程抢夺到CPU时间片后,开始执行run方法,run方法的执行标志着线程进入运行状态。

    • 🍁运行状态:
      run方法开始执行,线程进入运行状态,当之前占有的CPU时间片用完之后,重新回到就绪状态继续抢夺CPU时间片,待抢到后,重新进入run方法上次执行的地方继续执行

    • 🍁死亡状态:
      run方法执行结束,线程到达死亡状态

    • 🍁阻塞状态:
      当一个线程遇到阻塞事件,如接收用户键盘输入,sleep方法,则进入阻塞状态,此时线程会放弃之前抢占到的CPU时间片

    2、常用方法

    获取线程的名字getName()

    MyThread myThread = new MyThread();
    String tName = myThread.getName();
    //Thread-0
    System.out.println(tName);
    //更改
    myThread.setName("code-9527 's Thread");
    System.out.println(myThread.getName());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    获取当前线程对象currentThread()

    //静态方法获取当前线程对象,返回一个Thread类型对象
    Thread currentThread = Thread.currentThread();
    //返回线程名
    System.out.println(currentThread.getName());
    
    • 1
    • 2
    • 3
    • 4

    3、线程的sleep

    //毫秒
    static void sleep(Long millis);
    
    • 1
    • 2

    作用是让当前线程进入休眠,进入阻塞状态,放弃占有CPU时间片,让给其他线程去使用,出现的地方,对应的线程休眠

    public static void main(String[] args) {
          try{
              Thread.sleep(1000*5); //让当前线程(main)休眠五秒
          }catch(InterruptedException e){
              e.printStackTrace();
          }
          //五秒后被输出
          System.out.println("sleep结束");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实现间隔特定的时间去执行一段特定的代码

    sleep是静态方法,若上面的Thread.sleep改成:

    Thread t = new MyThread();
    ...
    t.sleep(1000*5);
    
    • 1
    • 2
    • 3

    执行的时候,t.sleep(1000*5);还是会被当作Thread.sleep(1000*5);,被休眠的也还是main线程,而不是t线程。

    4、终止线程的睡眠状态

    t.interrupt();
    
    • 1

    干扰,即中断t线程的睡眠,执行后sleep()出现异常,即catch(InterruptedException e),这种中断睡眠状态的方式,依靠的时Java的异常处理机制。

    class MyThread extends Thread{
        public void run(){
            try{
                Thread.sleep(1000*5);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            for(int i=0;i<100;i++){
                System.out.println("分支线程" + i);
            }
        }
    }
    class ThreadTest1{
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
            //线程myThread计划sleep5秒
            //扔出interrupt后就提前醒来了
            myThread.interrupt();
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    示意图

    5、强行终止线程的执行

    线程对象的引用.stop()
    
    • 1

    stop方法已过时,容易丢数据。这种方式是直接将线程杀死了,线程没有保存的数据会丢失

    MyThread myThread = new MyThread();
    myThread.start();
    //终止
    myThread.stop();
    
    • 1
    • 2
    • 3
    • 4

    强行终止

    6、合理终止一个线程的执行

    • 编写的类中加入run属性:boolean run = true;
    • 重写run方法的时候,if(run)…
    • else中写终止线程之前要保存的数据和操作+return;
    • 以后则只需改线程对象的run属性即可终止线程
    class MyRun implements Runnable{
        boolean run = true;
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (this.run) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                    System.out.println(this.run);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("这是一些终止线程前要做的事");
                    System.out.println("保存数据中..终止线程成功!");
                    return;
                }
            }
        }
    }
    public class ThreadTest2 {
        public static void main(String[] args) {
            MyRun r = new MyRun();
            Thread t = new Thread(r);
            t.start();
            //sleep主线程三秒
            try{
                Thread.sleep(3000);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            //终止,改run属性为false
            r.run = false;
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    运行效果:
    run

    四、线程的调度

    1、线程调度的模型

    • 🍁抢占式调度模型:
      哪个线程的优先级比较高,抢到CPU时间片的概率就高一些/多一些。Java中采用的就是抢占式调度模型。

    • 🍁均分式调度模型:
      平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样

    2、线程调度的方法—优先级

    • 设置线程的优先级
    void setPriority(int newPriority)
    
    • 1
    • 获取线程的优先级
    int getPriority()
    
    • 1
    //最低优先级为1
    static int MIN_PRIORITY
    //默认优先级为5
    static int NORM_PRIORITY
    //最高优先级为10
    static int MAX_PRIORITY
    
    System.out.println(Thread.MAX_PRIORITY);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    举例:

    Thread currentThread = Thread.currentThread();
    
    System.out.println(currentThread.getName()+"的优先级是:"+ currentThread.getPriority());
    
    currentThread.setPriority(9);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3、线程调度—让位

    //静态方法

    static void yield()
    
    • 1

    暂停当前正在执行的线程对象,去执行其他线程,yield方法的执行会让当前线程从运行状态进入就绪状态 ,注意不是阻塞状态。

    class MyRunnable2{
        public void run(){
            for(int i=0;i<101;i++){
            	//每循环10次,让当前线程暂停让位一下
                if(i%10 == 0){
                    Thread.yield();
                }
                System.out.println(Thread.currentThread().getName() + i);
            }
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4、线程调度–线程合并

    实例方法void join(),注意线程合并不是栈的合并

    MyThread t = new MyThread();
    //让当前线程阻塞,t线程执行,直到t线程执行结束,当前线程才执行
    t.join();
    
    • 1
    • 2
    • 3

    五、线程安全

    线程安全问题的产生条件:

    • 多线程并发
    • 有贡献数据
    • 共享数据有修改行为

    线程安全
    线程安全问题的解决–线程同步机制

    线程同步即线程排队执行,不能并发了(可能会牺牲一部分效率,但数据安全是一切的前提)

    1、同步与异步

    • 🍁异步模型:
      异步就是并发,线程t1和线程t2各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,即多线程并发,效率较高。

    • 🍁同步模型:
      线程t1和t2,t1执行的时候,必须等待t2执行结束,效率较低。

    ❀❀❀账户安全问题的代码模拟:

    
    /**
     * 账户类
     */
    public class Account {
        private String actno;
        private double balance;
    
        public Account(){
    
        }
    
        public Account(String actno, double balance) {
            this.actno = actno;
            this.balance = balance;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        /**
         * 取款方法
         * @param money
         */
        public void withdraw(double money){
            double before = this.getBalance();
            double after = before - money;
            //别立即更新余额,使用休眠模拟网络延迟
            try{
                Thread.sleep(1000*5);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    public class AccountThread extends Thread{
        //该类的对象有Account属性
        //某人“有一个账户”
        private Account account;
        //通过构造方法传递账户对象
        public AccountThread(Account account){
            this.account = account;
        }
        public void run(){
            double money = 5000;
            account.withdraw(money);
            System.out.println(Thread.currentThread().getName() + "对账户:" + account.getActno() +
                    "取款:" + money + ",余额:"+ account.getBalance());
        }
    }
    
    class Test{
        public static void main(String[] args) {
            Account account = new Account("act-001",10000);
            //两个线程共用一个账户对象
            AccountThread t1 = new AccountThread(account);
            AccountThread t2 = new AccountThread(account);
            t1.setName("t1");
            t2.setName("t2");
            t1.start();
            t2.start();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果:

    run

    2、同步机制synchronized

    🍁语法:

    synchronized(){

    线程同步代码块

    }

    小括号中传的是多个线程共享的对象,若有t1、t2、t3、t4、t5线程,t1、t2、t3需要排队,t4、t5不用,则()中是一个t1、t2、t3共享的对象,而这个对象t4、t5不共享

    由此,上面例题中的取款方法变为:

    public void withdraw(double money){
         synchronized(this) {
             double before = this.getBalance();
             double after = before - money;
             try {
                 Thread.sleep(1000 * 5);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             this.setBalance(after);
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    简单的说就是synchronized内部放的是要排队执行的代码块

    🍁对象锁:

    在Java中,任何一个对象都有“一把锁”,这把锁其本质是一个标记,一个对象一把锁。

    当运行状态的线程遇到synchronized关键字:

    • 在锁池lockpool中找共享对象的对象锁。线程进入锁池找共享对象的对象锁之前,会释放之前占有的CPU时间片
    • 若找到了,则进入就绪状态继续抢夺CPU时间片,若没找到,则在锁池中等待

    🍺例:
    当t1和t2线程并发:
    t1先遇到synchronized,自动找所共享对象的对象锁,找到之后,占有这把锁,然后执行同步代码块中的代码, 直到同步代码块执行结束,这个锁才释放

    —>>>

    t1占有对象锁后,t2线程若也遇到了synchronized,在找对象锁时,发现被t1占有,则t2在同步代码块外等待t1结束并释放对象锁后,再占有对象锁、执行同步代码块

    3、有线程安全的变量

    存在于堆区中的实例变量、存在于方法区中的静态变量,因为堆和方法区均只有一个,且是多线程共享的,有可能存在安全问题。

    局部变量存在于栈区中,永远不会有线程安全问题,因为一个线程一个栈。

    4、synchronized出现在实例方法上

    旧版
    改为:

    public synchronized void withdraw(double money){
            double before = this.getBalance();
            double after = before - money;
            try{
                Thread.sleep(1000*5);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样写的缺点:

    • synchronized出现在实例方法上,表示整个方法都需要同步(实际只有其中一部分代码需要同步),这样就扩大了同步的范围,导致程序的执行效率变低
    • synchronized出现在实例方法上,锁的就一定是this,就不能是其他对象了

    5、synchronized的三种写法

    🍁

    synchronized(线程共享对象){
    		同步代码块;
    }
    
    • 1
    • 2
    • 3

    🍁
    在实例方法中使用synchronized,表示共享的对象一定是this,并且同步的代码块是整个方法体

    🍁
    在静态方法中使用synchronized,表示找类锁,类锁永远只有1把(对象锁是100个对象就有100个对象锁)

    六、死锁

    1、原理

    死锁示意图
    t1线程执行某同步代码块,用到了对象1和2,即t1线程需要先锁对象1,再锁对象2,全锁以后,算同步代码块执行结束,然后一下释放两个对象锁

    t2线程执行另一个同步代码块,需要先锁对象2,再锁对象1才算这个同步代码块执行结束,然后释放两个对象锁。

    如此:
    t1锁到对象2的时候,发现已被锁,则等待,而另一边:t2锁到对象1的时候,发现对象1已被锁,两个线程同时陷入无休止的等待…尬住了

    2、代码实现

    class MyThread1 extends Thread{
        Object o1;
        Object o2;
        public MyThread1(Object o1, Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
        public void run(){
            //同步代码块开始
            synchronized(o1){
                try{
                    //别着急锁o2,为了保证死锁必现,这里等两秒
                    Thread.sleep(2000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                //synchronized的嵌套
                //从而实现:对象o1和o2都锁了才算同步代码块结束
                synchronized(o2){
    
                }
            }
            //同步代码块结束
        }
    }
    
    class MyThread2 extends Thread{
        Object o1;
        Object o2;
        public MyThread2(Object o1,Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
        public void run(){
            synchronized (o2){
                try{
                    Thread.sleep(2000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(o1){
    
                }
            }
        }
    }
    
    public class DeadLock{
        public static void main(String[] args) {
            Object o1 = new Object();
            Object o2 = new Object();
            //启动两个线程,共用对象o1和o2
            MyThread1 t1 = new MyThread1(o1,o2);
            MyThread2 t2 = new MyThread2(o1,o2);
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    运行结果:

    run
    死锁发生后,程序不出异常,也不报错,会一直僵持着,不易发现并调试。

    tips

    关于synchronized死锁和线程安全的优化:

    synchronized会让程序执行效率变低,系统吞吐量降低,用户体验变差。解决线程安全,可考虑:

    • 使用局部变量代替实例变量和静态变量
    • 若必须使用实例变量,考虑多创建几个对象,别对象共享了也就没有安全问题了
    • 若以上两条都做不到,则用synchronized

    七、线程守护

    1、线程守护的概述

    Java中,线程分为两大类:

    • 用户线程,如主线程main线程
    • 守护线程,如经典的垃圾回收线程

    守护线程的特点是:

    一般守护线程是一个死循环,所有用户线程结束的时候,守护线程自动结束

    2、实现守护线程

    通过实例方法setDaemon:

    public class DaemonTest {
        public static void main(String[] args) {
            Thread t = new BackupThread();
            t.setName("备份守护线程");
            //传入true,则普通线程变守护线程
            t.setDaemon(true);
            t.start();
        }
    
    }
    class BackupThread extends Thread{
        public void run(){
            //要进行的守护动作写在run方法中即可
            int i = 0;
            while(true){
                //即使是死循环,但做为守护线程,当所有线程都结束的时候,也会自动结束
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    3、定时器

    🍁作用:
    间隔特定的时间,执行特定的程序

    🍁应用场景:
    如每周进行银行账户的总账操作,每天进行数据库的备份操作

    🍁实现方式:

    • 用sleep方法,设置线程睡眠,睡到某时刻醒来执行代码完成任务
    • 用java.util.Timer
    • 用Spring框架中的SpringTask框架(底层还是java.util.Timer)

    4、实现定时器

    用到的java.util.Timer类中的方法

    • Time类的无参构造方法,创建定时器对象
    Timer timer = new Timer();
    
    • 1
    • Timer类的有参构造
    //传入true,表示以守护线程的方式
    Timer timer = new Timer(true);
    
    • 1
    • 2
    • schedule方法
    /**安排任务从指定时间开始进行重复固定的延迟执行
    * TimerTask是一个抽象类
    * Date firstTime即第一次执行的时间
    * Long period 即间隔多少毫秒
    */
    void schedule(TimerTask task, Date firstTime , Long period)
    
    //安排任务在指定时间执行任务task
    void schedule(TimerTask task, Date time)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ❀代码实现–编写一个定时器任务类记录日志

    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class LogTimerTask extends TimerTask {
        /**
         * 重写父类TimerTask中的抽象方法run
         * TimerTask类实现了Runnable接口,所以有run方法
         */
        public void run(){
            //这里写要执行的任务
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime = sdf.format(new Date());
            System.out.println(strTime + "日志记录成功");
        }
    }
    
    class TimerTest{
        public static void main(String[] args) {
            Timer timer = new Timer();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                Date firstTime = sdf.parse("2022-12-06 08:10:06");
                timer.schedule(new LogTimerTask(),firstTime,1000*5);
            } catch (ParseException e) {
                e.printStackTrace();
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    运行效果:
    run

    5、实现线程的第三种方式

    JDK8新特性,可实现Callable接口。这种方式实现的线程可以获得线程的返回值。前两种实现方式不能获取返回值,因为run方法的返回值类型是void。

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

    缺点:
    效率较低,在获取t线程执行的结果时,当前线程需要等待,等拿到返回值以后再往下执行其余code

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class ThreadTest0 {
        public static void main(String[] args) {
            //创建一个”未来任务类“的对象,传参为Callable接口实现类的对象
            FutureTask task = new FutureTask(new Callable(){  //匿名内部类
                //call方法相当于run方法,不过其有返回值
                public Object call() throws Exception{
                    System.out.println("call method begin!");
                    Thread.sleep(1000*6);
                    System.out.println("call method end");
                    int a = 100;
                    int b = 200;
                    //线程执行一个任务,执行完可能有返回结果
                    //这里自动装箱
                    return a+b;
                }
    
            });
            Thread t = new Thread(task);
            t.start();
            try {
                //当前为主线程,获取t线程的执行结果
                Object obj = task.get();
                System.out.println("线程执行结果:" + obj);
                //此处get方法要拿另一个线程的执行结果,可能要很久
                //但这导致了下面main线程要等待get执行结束。
                //这就导致了”当前线程的阻塞“
                System.out.println("这里要等get拿到线程的返回值才能执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    运行结果:
    run

    八、wait和notify

    1、概述

    wait 和notify方法是Java中任何一个Java对象都有的方法,因为这两个方法是Object类自带的。不是给线程对象用的,所以别t.wait();

    Object o = new Object();
    o.wait();
    
    • 1
    • 2

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

    示意图

    T线程在o对象上活动,o.wait()后,T线程进入无限期等待,并且释放o对象的对象锁

    o.notify()让正在o对象上等待的线程被唤醒

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


    2、生产者和消费者模式

    生产者-消费者

    🍁代码实现:

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 生产线程
     */
    class Producer implements Runnable{
        /**
         * 仓库
         */
        private List list;
        public Producer(List list){
            this.list = list;
        }
        public void run(){
            while(true){
                synchronized(list){
                    if(list.size()>0){ //仓库有东西,则生产线程wait
                        try {
                            list.wait(); //进入等待状态,释放之前占有的list的对象锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //能到这说明仓库空了,开始生产
                    Object obj = new Object();
                    list.add(obj);
                    System.out.println(Thread.currentThread().getName() + "-->" + obj);
                    list.notifyAll(); //生产完了以后唤醒消费线程来消费
                }
            }
        }
    }
    
    /**
     * 消费线程
     */
    class Consumer implements Runnable{
        private List list;
        public Consumer(List list){
            this.list = list;
        }
        public void run(){
            while(true){
                synchronized(list){
                    if(list.size() == 0){ //仓库已空,暂停消费
                        try {
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //消费
                    Object obj = list.remove(0);
                    System.out.println(Thread.currentThread().getName() + "-->" + obj);
                    //唤醒生产线程来生产
                    list.notifyAll();
                }
            }
        }
    }
    
    /**
     * 测试
     */
    public class PC_Moudle {
        public static void main(String[] args) {
            //创建一个仓库,生产和消费线程共享
            List list = new ArrayList();
            Thread t1 = new Thread(new Producer(list));
            Thread t2 = new Thread(new Consumer(list));
            t1.setName("生产者线程");
            t2.setName("消费者线程");
            t1.start();
            t2.start(); 
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    运行效果:
    run

  • 相关阅读:
    50etf期权的隐含波动率是什么意思?最通俗易懂的解释!
    hive笔记(三):DDL数据定义/DML数据操作-数据库、表、数据导入导出
    35【Aseprite 作图】苹果——拆解
    非稳压高压电源模块12SMV300 HV05200S-W2 HV12180S-2W直流转换器
    自定义过滤器配置 Shiro 认证失败返回 json 数据
    计算机二级WPS 选择题(模拟和解析五)
    前端设计模式和设计原则之设计模式
    HTML小游戏15 —— 网页版3D反恐英雄(附完整源码)
    java版 Spring Cloud+uniapp b2b2c o2o 多商家入驻商城 直播带货商城 电子商务
    马斯克:让我成功的其实是“第一性原理”(PM必读)
  • 原文地址:https://blog.csdn.net/llg___/article/details/128120064