Java多线程-0401~03

多线程

创建和启动线程方式一:Thread类

  1. 概述
    1. Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所
      有的线程对象都必须是 Thread 类或其子类的实例。
    2. Thread 类的特性
      • 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此 把 run()方法体称为线程执行体。
      • 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()
      • 要想实现多线程,必须在主线程中创建新的线程对象。
  2. 步骤:
    1. 创建一个继承于Thread类的子类
    2. 重写run方法,
    3. 创建子类对象
    4. 通过对象调用start方法
      1. 启动线程
      2. JVM去调用线程的run方法
  3. 注意:
    1. 不能让已经start的线程再次start,否则报异常
    2. 如果直接在主线程里面调用run方法,相当于是方法嵌套,并没有创建新的线程
  4. 代码:
     static class PrintEvenNumber extends Thread {
    
            @Override
            public void run() {
    
                for(int i=0;i<=100;i++){
                    if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i);
                }
            }
        }
    
    
     public static void main(String[] args) {
            PrintEvenNumber t1 = new PrintEvenNumber();
            PrintOddNumber t2 = new PrintOddNumber();
            t1.start();
            t2.start();
    }
    
  5. 也可以使用匿名内部类的方式创建
    Thread thread = new Thread() {
                @Override
                public void run() {
                    super.run();
                }
            };
            thread.start();
    
            //  也可以直接不起名字
            new Thread() {
                @Override
                public void run() {
                    super.run();
                }
            }.start();
    

创建和启动线程方式二:实现Runnable

  1. 对于继承Thread的类,他没法再去继承其他类,有一定局限性,所以有了Runnable接口。
  2. 步骤:
    1. 创建一个实现Runnable接口的类
    2. 实现接口中的run方法
    3. 创建当前实现类的对象
    4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
    5. Thread类的实例调用start方法:1.启动线程 2.调用当前线程的run方法
      public class SecondWayToCreateThread {
      
          public static void main(String[] args) {
              PrintEvenNumber printEvenNumber = new PrintEvenNumber();
              Thread thread  = new Thread(printEvenNumber);
              thread.start();
      
              //  main方法对应的主线程
              for(int i=0;i<=100;i++){
                  if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i);
              }
          }
      
      
          public static class PrintEvenNumber implements Runnable{
      
              @Override
              public void run() {
                  for(int i=0;i<=100;i++){
                      if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i);
                  }
              }
          }
      }
      
  3. 如果想再启动一个线程跑相同的事情,主需要创建一个新线程,将printEvenNumber传递为参数即可。
  4. 由此可以想到以匿名内部类去实现线程,如果有条件,还可以使用函数式接口去实现lambda
    试了一下,你别说,还真有条件:只有一个抽象方法的接口,满足了函数式接口的条件
    //  配合lambda
            new Thread(()->{
                for(int i=0;i<=100;i++){
                    if(i%2 == 1) System.out.println(Thread.currentThread().getName()+":"+ i);
                }
            }).start();
    

对比两种方法

共同点:

  1. 启动线程,都是使用Thread类中的start方法
  2. 创建的线程对象,都是Thread类或其子类的实例

不同点:

  1. 一个是类的继承,一个是接口实现。
    • 建议使用Runnable接口实现方式
    • Runnable接口实现的好处:
      1. 实现方式避免类单继承的局限性
      2. 更适合处理共享数据的问题,多个线程可以共享一份数据,就是实现在Runnable继承类里面,作为Thread的入参,这样继承类里面的数据可以被多个线程共享。使用继承Thread也可以实现共享,使用静态变量也可以共享
      3. 数据和代码分离。run里面是处理数据,生成一个接口实现的实例就是共享数据,建立线程的时候是代码逻辑。

联系:

  1. public class Thread implements Runnable(代理模式) 。我们用的Thread也是Runnable的实现类。
    //	思考创建一个B(a)的线程执行run方法,结果是什么?class A extends Thread{
            @Override
            public void run() {
                System.out.println("Thread A is running!");
            }
        }
    
    
        class B extends Thread{
    
            private A a;
    
            public B(A a){
                super(a);
            }
    
            @Override
            public void run() {
                System.out.println("Thread B is running");
            }
        }
    
    
    			 Thread a = new A();
           Thread b = new B((A)a);
           a.start();
           b.start();
    
    //	因为A、B都继承Thread,Thread又继承了Runnable,所以这里调用super(a)等于是调用了Thread(p),生成了一个带有Runnable实现类参数的Thread对象,执行的结果会取决于p的run方法,所以执行出来会是Thread A is running!
    
    结果:
    Thread A is running!
    Thread B is running
    
    原因:因为B里面重载了run方法,所以优先调run重载的写法;如果把重载删除掉,就像定义Runnable实现类的实例后传参时那样,不再去重载run方法,就会默认调用Thread里面的target方法。

Thread常用方法

  1. 线程中的常用结构

    1. 线程中的构造器
      1. public Thread() :分配一个新的线程对象。
      2. public Thread(String name) :分配一个指定名字的新的线程对象。
      3. public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口 中的 run 方法
      4. public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指 定名字。
    2. 线程中的常用方法系列
      1. public void run() :此线程要执行的任务在此处定义代码。
      2. public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
      3. public String getName() :获取当前线程名称。
      4. public void setName(String name):设置该线程名称。 Thread.currentThread().setName(“这样命名就行”)
      5. public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
      6. public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时 停止执行)。
      7. public static void yield():yield (主的释放CPU的执行权)只是让当前线程暂停一下,让系统的线程调度器重新 调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这 个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调 度器又将其调度出来重新执行。
      8. void join() :等待该线程终止。例如b线程里面调用了 a.join() 则b线程会阻塞,等待a终止之后再执行,相当于同步操作。
        • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间 到,将不再等待。
        • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
      9. public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未 终止,则为活动状态。
      10. public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入 死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据 库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处 理,出现数据不一致的问题。
      11. void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成 对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁 资源,导致其它线程都无法访问被它占用的锁,直到调用 resume()。已过时,不建议 使用
    3. 线程优先级
      1. 每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
      2. Thread 类的三个优先级常量:
        • MAX_PRIORITY(10):最高优先级
        • MIN _PRIORITY (1):最低优先级
        • NORM_PRIORITY (5):普通优先级,默认情况下 main 线程具有普通优先 级。
      3. public final int getPriority() :返回线程优先级
      4. public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
    4. 守护线程
      1. 有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线 程被称为“守护线程”。JVM 的垃圾回收线程就是典型的守护线程。
      2. 守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死 亡。形象理解:兔死狗烹,鸟尽弓藏
      3. 用 setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前 设置,否则会报IllegalThreadStateException 异常。调用 isDaemon()可以判断线程是否是守护线程。

Thread生命周期

  1. JDK1.5 之前:5种状态:线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行 (Running)、阻塞(Blocked)、死亡(Dead)。

请添加图片描述

  1. JDK1.5 **及之后:**6 种状态

请添加图片描述

  1. NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。
  2. RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只 能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的, 而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
  3. Teminated(被终止):表明此线程已经结束生命周期,终止运行。
    1. 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKEDWAITINGTIMED_WAITING
    2. BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视 器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行 机会。
      • 比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到 锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。
    3. TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待 另一个线程执行一个(唤醒)动作的线程处于这一状态。
      • 当前线程执行过程中遇到 Thread 类的 sleepjoin,Object 类 的 wait,LockSupport 类的 park 方法,并且在调用这些方法时, 设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间 到,或被中断。
    4. WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线 程执行一个特别的(唤醒)动作的线程处于这一状态。
    5. 当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的 join,LockSupport 类的 park 方法,并且在调用这些方法时,没 有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。
      • 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的 notify/notifyAll 唤醒;
      • 通过 Condition 的 await 进入 WAITING 状态的要有 Condition 的 signal 方法唤醒;
      • 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有 LockSupport 类的 unpark 方法唤醒
      • 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join 方法的线程对象结束才能让当前线程恢复;
    6. 说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现 当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。

线程安全

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条 记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如 果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

案例:火车售票问题

public class SalesTicket implements Runnable{

    int ticket = 100;
    @Override
    public void run() {
        while (ticket>0){
            System.out.println(Thread.currentThread().getName() + "售票,票号为: "+ticket--);
        }
        if(ticket==0) System.out.println(Thread.currentThread().getName() + "票卖完了");
    }
}

class WindowTest{
    public static void main(String[] args) {
        SalesTicket s = new SalesTicket();

        Thread t1 = new Thread(s,"窗口一");
        Thread t2 = new Thread(s,"窗口二");
        Thread t3 = new Thread(s,"窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

这样售票会出现重复售票、错票问题(卖出-1号票)的问题。

什么原因导致

线程1操作ticket过程中,尚未结束的情况下,其他线程就参与进来,也对ticket进行操作

解决:必须保证一个a线程在操作ticket的时候,其他线程必须等待,直到线程a操作完成之后,其他才可以操作

线程同步机制:

方式一:同步代码块

synchronized(同步监视器){
  //	需要被同步的代码
}
//	说明:需要被同步的代码,即操作共享数据的代码
//	共享数据:即多个线程都需要操作的数据,即ticket
//	需要被同步的代码,在被synchronized包裹之后,就使得一个线程在操作这些代码的过程中,其他线程必须等待
//	同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码
//	同步监视器,可以使用任何一个类的对象充当。但是多个线程必须共用一个同步监视器
//	锁可以自己随便创建一个,也可以用this,但是要确保唯一,
//	也可以用反射,例如Window.class,这个肯定是唯一的,
@Override
    public void run() {
        while (true){

            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            synchronized (this){		//	此时this指向的是Runnable的继承类,唯一的
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName() + "售票,票号为: "+ticket--);
                }else{
                    System.out.println(Thread.currentThread().getName() + "票卖完了");
                    break;
                }
            }
        }
    }

注意:在实现Runnable接口的方式中,锁可以考虑使用this

​ 在Thread继承类中,锁可以慎用this。

方式二:同步方法

如果操作共享数据的代码完整的声明在了一个方法中,我们只需要将这个方法直接声明为同步方法即可。

//	对于实现Runnable的处理方式
public class SalesTicket implements Runnable {

    int ticket = 100;
    Object Lock = new Object();
    boolean isFlag = true;

    @Override
    public void run() {

        while (isFlag) {
            sales();
        }

    }

    public synchronized void sales() {      //  同步监视器是默认的,此时是this。这个问题中是唯一的

        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        synchronized (Lock) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--);
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                isFlag = false;
            }
        }
    }
}

同步方法默认的同步监视器(只能是默认的):

  1. 静态方法:当前类的 Class 对象(类名.class)
  2. 非静态方法:this
//	继承Thread类实现
//	此方法会出现问题,如果想解决,可以将sales改成静态方法,有些时候也不能改,例如使用了实例变量就不行。
//	或者使用同步代码块包一下,然后自己手动指定锁。
class SalesTicketThread extends Thread {

    int ticket = 100;
    Object Lock = new Object();
    boolean isFlag = true;

    @Override
    public void run() {

        while (isFlag) {
            sales();
        }
    }

    public synchronized void sales() {      //  同步监视器是默认的

        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--);
        } else {
            System.out.println(Thread.currentThread().getName() + "票卖完了");
            isFlag = false;
        }
    }
}

synchronized好处

好处:解决了线程安全问题

弊端:操作临界资源的时候,实际是串行执行的。

案例二:银行存钱,对于继承Thread的同步方法,还可以这样操作使得其同步监视器唯一:

public class AccountThread {
    public static void main(String[] args) {
        Account account = new Account(0);

        Customer customer1 = new Customer(account, "甲");
        Customer customer2 = new Customer(account, "乙");

        customer1.start();
        customer2.start();
    }
}


class Account {
    private double balance;

    public Account(double b) {
        this.balance = b;
    }
		
  	//		此处之所以可以让非静态方法使用this,因为Account对象唯一,只创建了一个
    public synchronized void deposit(double amount) {				
        if (amount > 0) {
            balance += amount;
        }
        System.out.println(Thread.currentThread().getName() + "存钱" + amount + "块,余额:" + balance);
    }


}

class Customer extends Thread {

    Account account;

    public Customer(Account a) {
        this.account = a;
    }

    public Customer(Account a, String name) {
        super(name);
        this.account = a;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            account.deposit(1000);
        }

    }
}

单例模式中的懒汉式线程安全问题

**饿汉式 **: 在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的。不存在线程安全问题

懒汉式 : 延迟创建对象,第一次调用 getInstance 方法再创建对象。存在线程安全问题(需要使用同步机制来处理)

 //  实现线程安全的方式一
    public static synchronized Bank getInstance() {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }


    //  实现线程安全的方式二
    public static Bank getInstance() {


        synchronized (Bank.class) {
            if (instance == null) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                instance = new Bank();
            }
        }

        return instance;
    }

    //  实现线程安全的方式三,相较与方式一二效率更高,不需要卡在不必要的同步锁之前,直接判断即可。
    //  对于每次访问,原本都需要串行化,现在只有为创建时的访问的线程需要串行
    public static Bank getInstance() {

        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    instance = new Bank();
                }
            }
        }
        return instance;
    }

/*
	注意:上述方式 3 中,有指令重排问题
	mem = allocate(); 为单例对象分配内存空间
	instance = mem; instance 引用现在非空,但还未初始化 ctorSingleton(instance); 为单例对象通过 instance 调用构造器
	从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要 volatile 关键字,避免指令	重排。  
*/


private static volatile Bank instance = null; 	

方式三虽然效率高,但是存在问题:创建对象的语句不止一步,先进行创建,然后进行初始化。创建了对象之后,判断已经不是null了,但是还没有初始化完成,所以直接让其他线程拿走instance是不对的。

volatile作用:可见性和有序性。一旦变化,其他线程都能看见;并且对他的操作都是有序的。

同步机制的死锁问题

  1. 死锁概念:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
    请添加图片描述
  2. 诱发死锁的原因:
    1. 互斥条件(互斥访问)
    2. 占用且等待
    3. 不可抢夺
    4. 循环等待
  3. 解决死锁:
    1. 死锁一旦出现,基本很难人为干预,只能尽量规避,可以考虑打破上面的诱发条件。
    2. 针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问 题。
    3. 针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问 题。
    4. 针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到, 就主动释放掉已经占用的资源。
    5. 针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这 样避免循环等待问题。
  4. jkd5.0提供的Lock锁的方式:解决线程安全
    1. 步骤:
      //  1.这里一定需要静态的,不然三个Thread对象,三把锁,依然无效
      private static final ReentrantLock lock = new ReentrantLock();
      
      
      try {						//	2.	执行lock方法,锁定对共享资源的调用
                      lock.lock();
      
                      try {
                          Thread.sleep(5);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
      
      
                      if (ticket > 0) {
                          System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--);
                      } else {
                          System.out.println(Thread.currentThread().getName() + "票卖完了");
                          isFlag = false;
                      }
                  //  使用finally确保一定执行解锁
                  } finally {
        							//	3.unlock()的调用,释放对共享数据的锁定
                      lock.unlock();
                  }
      

线程通信

  1. 线程间通信
    当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那 么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同 操作一份数据。

  2. 等待唤醒机制
    这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争 (race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
    在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)), 等 待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可 以使用 *notifyAll()*来唤醒所有的等待线程。wait/notify 就是线程间的一种协 作机制。

    1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它 还要等着别的线程执行一个特别的动作,也即“通知(notify)”或者等待时间 到,在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列 (ready queue)中
    2. notify :一旦执行次方法,就会唤醒被wait()的线程中优先级最高的那一个线程。如果被wait的多个线程的优先级相同,则随机唤醒一个。被唤醒的线程,从当初被wait的位置的代码逻辑继续执行。
    3. notifyAll:则释放所通知对象的 wait set 上的全部线程。
    4. 使用注意:
      1. 谁在调用wait和notify?凡是在方法中,没有写谁来调的,如果是非静态方法就是this的省略,如果是静态方法就是当前类在调用。
      2. 次三个通信方法的使用必须在同步代码块或同步方法中。Lock是不行的。Lock通信需要配合condition实现线程间通信。
      3. 此三个方法的调用者,必须是同步代码块或者同步方法的监视器。所以Lock不可以,因为他没有同步监视器。所以也不是必须要this来调用notify和wait,也可以声明一个唯一的object来调用。
      4. 此三个方法声明在Object类中。native方法。
    5. 比较wait和sleep方法
      1. 相同点:一旦执行,当前线程都会进入阻塞状态。
      2. 不同点:
        1. 声明位置:wait声明在Object中的,sleep声明在Thread中,并且是一个静态方法。
        2. 场景不一样:wait只能使用在同步代码块和同步方法中。sleep可以使用在任何地方
        3. 使用在临界资源中时:wait会释放同步监视器(锁),而sleep不会释放。
        4. 结束阻塞的方法:wait两种情况,到达指定时间自动唤醒或被notify唤醒而结束阻塞。sleep只有超时唤醒。
  3. 案例一:让两个线程交替打印

    class PrintNumber implements Runnable{
    
        private int number;
    
        @Override
        public void run() {
    
            while(true){
    
                synchronized (this) {
    
                    notify();   //  唤醒操作
    
                    if(number<=100){
    
                        try {
                            Thread.sleep(10);   //  sleep并不会释放同步监视器,就可能会出现死锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread().getName()+" : "+number++);
    
                        try {
                            wait(); //  线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                    }else{
                        break;
                    }
                }
            }
        }
    }
    
  4. 案例二:生产者&消费者案例
    生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产 品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的 产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生 产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通 知消费者来取走产品。

    public class ProducerConsumerTest {
        public static void main(String[] args) {
    
            Clerk clerk = new Clerk();
            Producer p = new Producer(clerk,"生产者1");
            Consumer c1 = new Consumer(clerk,"消费者1");
            Consumer c2 = new Consumer(clerk, "消费者2");
    
            p.start();
            c1.start();
            //c2.start();
    
        }
    }
    
    class Clerk {
    
        private int productNum = 0;
    
        public synchronized void addProduct() {
    
            if (productNum < 20) {
    
                System.out.println(Thread.currentThread().getName() + "生产了第" + ++productNum + "个产品");
                notifyAll();
    
    
            } else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public synchronized void minusProduct() {
            if (productNum > 0) {
    
                System.out.println(Thread.currentThread().getName() + "消费了第" + productNum-- + "个产品");
                notifyAll();
    
            } else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    
    
    class Producer extends Thread {
    
        private Clerk clerk;
    
        public Producer(Clerk clerk, String name) {
            super(name);
            this.clerk = clerk;
        }
    
        @Override
        public void run() {
            while (true) {
    
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    
                clerk.addProduct();
            }
        }
    }
    
    class Consumer extends Thread {
    
        private Clerk clerk;
    
        public Consumer(Clerk clerk, String name) {
            super(name);
            this.clerk = clerk;
        }
    
        @Override
        public void run() {
            while (true) {
    
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    
                clerk.minusProduct();
            }
        }
    }
    

Callable

  1. 与使用 Runnable 相比, Callable 功能更强大些
    • 相比 run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
  2. Future 接口(了解)
    • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完 成、获取结果等。
    • FutureTask 是 Futrue 接口的唯一的实现类
    • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被 线程执行,又可以作为 Future 得到 Callable 的返回值
  3. 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
  4. 代码:
     /**
     * 创建多线程的方式三:实现 Callable (jdk5.0 新增的)
     **/
    
    //1.创建一个实现 Callable 的实现类
    class NumThread implements Callable {
        //2.实现 call 方法,将此线程需要执行的操作声明在 call()中 @Override
        public Object call() throws Exception {
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                if (i % 2 == 0) {
                    System.out.println(i);
                    sum += i;
                }
            }
            return sum;
        }
    }
    
    public class CallableTest {
        public static void main(String[] args) {
            //3.创建 Callable 接口实现类的对象 NumThread numThread = new NumThread();
            NumThread numThread = new NumThread();
            //4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中, 创建 FutureTask 的对象
            FutureTask futureTask = new FutureTask(numThread);
            //5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Th read 对象,并调用 start()
            new Thread(futureTask).start(); // 接收返回值
    
            try {
            //6.获取 Callable 中 call 方法的返回值
            //get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
                Object sum = futureTask.get();
                System.out.println("总和为:" + sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    

线程池

  1. 现有问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
    那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
  2. 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

请添加图片描述
3. 好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管,可以设置相关参数
    1. corePoolSize:核心池的大小
    2. maximumPoolSize:最大线程数
    3. keepAliveTime:线程没有任务时最多保持多长时间后会终止
  4. 线程池相关 API
    • JDK5.0 之前,我们必须手动自定义线程池。从 JDK5.0 开始,Java 内置线程池相关的 API。在 java.util.concurrent 包下提供了线程池相关 API:ExecutorServiceExecutors
    • ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
      • void execute(Runnable command)* :执行任务/命令,没有返回值,
        一般用来执行 Runnable
      • Future submit(Callable task):执行任务,有返回 值,一般又来执行 Callable
      • void shutdown() :关闭连接池
    • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
      • Executors.newCachedThreadPool():创建一个可根据需要创建新线 程的线程池
      • Executors.newFixedThreadPool(int nThreads); 创建一个可重用 固定线程数的线程池
      • Executors.newSingleThreadExecutor() :创建一个只有一个线程的 线程池
      • Executors.newScheduledThreadPool(int corePoolSize):创建 一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

面试题目:

public static void main(String[] args) {
			Thread t1 = new Thread(()->{
        	...	
      });
  		
  		t1.start();
  		
  		try {
            t1.sleep(5);
      } catch (InterruptedException e) {
            throw new RuntimeException(e);
      }
  	
			 System.out.println("End");
}
  1. 对于上面这段代码:sleep()阻塞的是哪个进程?
    1. 答案:主进程。调用sleep时,就当作是一个对象的方法调用即可,并不是调用谁的sleep谁就会阻塞,而是看逻辑而言,整个处理逻辑是在主进程中的。
  2. synchronized同步方式与Lock对比?
    1. Lock更好一些,释放对共享数据的方式不一样,更加灵活,synchronized释放锁(同步监视器)必须等到大括号结束。
    2. Lock是一个接口,提供了多种实现类,适合更多更复杂的场景,效率更高一些。
  3. 多线程使用场景
    1. Tomcat服务器上的web应用,多个客户端发起请求,对每个请求开辟线程进行处理
  4. 如何实现多线程(实现多线程有哪些方式):4种
    类似问题:创建多线程用Runnable还是Thread
  5. start和run方法区别:
    1. start:开启线程;调用run方法
    2. run:相当于没有启动新线程,只是方法调用
  6. Runnable和Callable有什么不同?
    • 与使用 Runnable 相比, Callable 功能更强大些
      • 相比 run()方法,call()方法可以有返回值
      • call()方法可以抛出异常
      • 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
    • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
  7. 什么是线程池,为什么用它?(好处)
    1. 提高响应速度(减少了创建新线程的时间)
    2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    3. 便于线程管,可以设置相关参数
      1. corePoolSize:核心池的大小
      2. maximumPoolSize:最大线程数
      3. keepAliveTime:线程没有任务时最多保持多长时间后会终止
  8. sleep()和yield()区别?
    1. sleep:调用进入阻塞,有限时间等待,进入TIMED_WAITING
    2. yield:释放cpu执行权,但是还在runnable状态下
  9. 线程创建中的方法、属性情况(略)
  10. 线程声明周期(略)
  11. 线程基本状态和以及状态直接的转化(略)
  12. stop和suspend方法为什么不推荐?
    1. stop:执行,线程就结束,可能导致run有未执行的代码。会释放同步监视器,导致线程安全问题
    2. suspend:搭配resume使用,可能导致死锁
  13. 优先级定义:三个常量,并且java是抢占式的
  14. 如何理解线程安全,线程安全问题是如何造成的?
  15. 保证线程安全问题的方式:
    1. 同步机制 synchronized
      1. 代码块
      2. 方法
    2. Lock接口
  16. synchronized修饰静态方法和普通方法的区别:同步监视器不同
  17. 当一个线程进入一个对象的一个synchronized方法后,其线程是否可以进入此对象的其他方法
    • 答:需要看其他方法是否使用了synchronized修饰,并且还需要看他同步监视器和刚才进入的是不是一个(this或类对象)
  18. 线程阻塞与同步的关系?同步一定阻塞吗?阻塞一定同步吗?
    • 答:同步一定阻塞,阻塞不一定是因为同步,可能是sleep等
  19. 什么是死锁,产生死锁等原因和必要条件,如何避免死锁
  20. notify和notifyAll有什么区别?
  21. 为什么wait和notify要放在同步代码块调用
    1. 同步监视器才可以调用wait和notify
    2. 确保多线程通信和协调的可靠性
      1. wait和notify操作需要保证其原子性
        • 例如:一个生产者生产出产品后立刻唤醒消费者,如果不保证原子性,被其他消费者消费后,再去唤醒的消费者依然吃不到产品。
  22. 单例模式线程安全
    1. 饿汉式是线程安全的
    2. 懒汉式单例安全:需要对创建instance的部分加同步,并且在同步代码块外层加一层判断用以加快效率,但是有可能创建+初始化比较慢,需要加volatile
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值