Java高级-多线程

news2024/12/25 1:43:16

5aa1e6d795bbc

本篇讲解java多线程

基本概念: 程序、进程、线程

**程序(program)**是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

**进程(process)**是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期

  • 如:运行中的QQ,运行中的MP3播放器
  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

若一个进程同一时间并行执行多个线程,就是支持多线程的

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

一个进程中的多个线程共享相同的内存单元/内存地址空间 --> 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

进程与线程图解

单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱, 那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费) 。 但是因为CPU时间单元特别短,因此感觉不出来。
  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
  • 一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并行与并发

  • 并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
  • 并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

使用多线程的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

  2. 提高计算机系统CPU的利用率

  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

线程的创建和使用

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。

  • Thread类的特性

    每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
    通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

Thread类

Thread类的使用

  1. 构造器

Thread(): 创建新的Thread对象

Thread(String threadname): 创建线程并指定线程实例名

Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法

Thread(Runnable target, String name): 创建新的Thread对象

  1. 创建线程的两种方式

    JDK1.5之前创建新执行线程有两种方法:

    • 继承Thread类的方式

      1. 定义子类继承Thread类。

      2. 子类中重写Thread类中的run方法。

      3. 创建Thread子类对象,即创建了线程对象。

      4. 调用线程对象start()方法:启动线程,调用run方法

        //1. 创建一个继承于Thread类的子类
        class MyThread extends Thread {
            //2. 重写Thread类的run()
            @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) {
                //3. 创建Thread类的子类的对象
                MyThread t1 = new MyThread();
        
                //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
                t1.start();
        }
        

        注意:

        1. 调用start()方法,如果调用的是run(),并不会开启新的线程,而是当前线程直接执行内部的代码,和之前定义方法然后让对象调用是一样的。
        2. 如果想再生成一个线程,那就再new一个线程对象。不可以还让已经start()的线程去执行。会报IllegalThreadStateException

        使用匿名子类简化创建方式:

        new Thread(){
            public void run(){
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }.start();
        
    • 实现Runnable接口的方式

      1. 创建一个实现了Runnable接口的类
      2. 实现类去实现Runnable中的抽象方法:run()
      3. 创建实现类的对象
      4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
      5. 通过Thread类的对象调用start()
      class MyThreadObj implements Runnable{//1. 创建一个实现了Runnable接口的类
          @Override
          public void run() {//2.实现类去实现Runnable中的抽象方法:run()
              for (int i = 0; i < 100; i++) {
                  System.out.println(Thread.currentThread().getName() + ":" + i);
              }
          }
      }
      
      public class ThreadTest {
          public static void main(String[] args) {
              //3.创建实现类的对象
              MyThreadObj myThreadObj = new MyThreadObj();
              //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
              Thread thread = new Thread(myThreadObj);
              thread.setName("hengxing");
              thread.start();//5.通过Thread类的对象调用`start()`
          }
      }
      
  2. 比较创建线程的两种方式

    开发中:优先选择–>实现Runnable接口的方式

    原因:

    1. 实现的方式没有类的单继承性的局限性
    2. 实现的方式更适合来处理多个线程有共享数据的情况。

    联系:public class Thread implements Runnablethread其实也是实现了Runnable接口,实际上和第二种方式没区别

    相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

Thread类的常用方法

方法作用
void start()启动线程,并执行对象的run()方法
run()线程在被调度时执行的操作
String getName()返回线程的名称
void setName(String name)设置该线程名称
static Thread currentThread()返回当前线程。
在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield()线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
join()当某个程序执行流中调用其他线程的join()方法时, 调用线程将被阻塞,直到join()方法加入的 join 线程执行完为止
低优先级的线程也可以获得执行
但是要注意:执行前确保线程已被启动。这个方法是等待join的线程完成,但是你如果连线程都没有开始执行,那不就直接结束了吗?
static void sleep(long millis)令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
抛出InterruptedException异常
boolean isAlive()返回boolean,判断线程是否还活着

线程的调度

调度策略

  • 时间片
image-20230131153103245
  • 抢占式: 高优先级的线程抢占CPU

Java的调度方法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级

  • MAX_PRIORITY: 10
  • MIN _PRIORITY: 1
  • NORM_PRIORITY: 5 --> 默认优先级

如何获取和设置当前线程的优先级:

getPriority(): 获取线程的优先级

setPriority(int p): 设置线程的优先级

说明:

高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

线程创建时继承父线程的优先级

理解线程

线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程

  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
  • 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程
  • Java垃圾回收就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将退出

线程的生命周期

线程有五种状态:

  1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  2. 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  3. 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  4. 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  5. 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

多线程

  1. 关于声明周期我们需要关注两个概念:

    状态、相应方法

    状态a --> 状态b:哪些方法执行了(回调方法)

    某个方法主动调用:状态a --> 状态b (例如:wait(),sleep()

  2. 阻塞只是临时状态,死亡才是最终状态。程序如果一直卡在阻塞状态,就是一种异常的状态。例如:死锁。

线程的同步

同步是为了解决线程安全问题。先来看一个例子会更好理解:

创建三个窗口卖票,总票数为100张。使用实现Runnable接口的方式

public class WindowTest1 {
    public static void main(String[] args) {
        window w = new window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1:");
        t2.setName("窗口2:");
        t3.setName("窗口3:");

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

class window implements Runnable{
    private int ticket = 100;

    @Override
    public void run() {
        if (ticket <= 0) {
            break;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
    }
}

我们会发现,有重票的情况出现,这是因为在某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

Runnable窗口买票

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。这就是线程安全问题。

理想状态下,三个线程会同时进入判断语句,均判断票号为0,跳出循环

理想状态

极端状态下,三个线程均进入阻塞状态,结束阻塞后,都执行后面的买票代码。后两个线程便会输出错票。

极端状态

所以我们使用同步代码块的方式解决这个问题。

同步代码块

先来介绍同步代码块:

synchronized (同步监视器){
	//需要被同步的代码
}
  1. 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。

  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。

  3. 同步监视器,俗称:。任何一个类的对象,都可以充当锁。

    ⭐要求:多个线程必须要共用同一把锁。

在上面的例子中,我们需要在操作共享数据时使用同步锁:

@Override
public void run() {
    while (true) {
        synchronized (this) {
            if (ticket <= 0) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
        }
    }
}

但如果时使用继承方式实现的线程,由于其生成了多个对象,所以不能使用this作为当前同步的锁,考虑使用window.class当前类名来作为锁(类在程序中只会加载一次,这个知识会在讲“反射”时提到。)

@Override
public void run() {
    while (true) {
        synchronized (window.class) {
            if (ticket <= 0) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
        }
    }
}

同步方法

如果一整个方法都需要同步,那不妨将方法声明为同步方法。

private synchronized void ticket(){
    if (ticket <= 0) return;
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
}

但如果是在使用继承Thread类方式实现的线程中,你会发现同步锁失效了。这就是我们的另一个知识点:

细心的你一定发现了,同步方法没有要求我们写同步监视器,那,他就不存在了吗?

不是的。它默认使用this代替。恰巧我们这种方式实现的线程又会生成多个对象,用当前对象肯定不行。

解决方式就是将此同步方法声明为静态的,这时他会使用当前类来代替this–>window.class

class window extends Thread{
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            ticket();
        }
    }

    private static synchronized void ticket(){
        if (ticket <= 0) {
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":买票票号为;" + ticket--);
    }
}

同步的利弊

同步的方式,解决了线程的安全问题。—好处

操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性

懒汉式单例改进

之前我们写懒汉式单例提到,它是线程不安全的。现在进行改进

public static Bank getInstance(){
    //方式一:只解决线程安全,效率低,所有
    synchronized (Bank.class) {
        if (bank == null) {
            bank = new Bank();
        }
        return bank;
    }

    //方式二:效率更高,之后的线程不必在同步锁外等待
    if (bank == null) {
        synchronized (Bank.class) {
            if (bank == null) {
                bank = new Bank();
            }
        }
    }
    return bank;
}

死锁问题

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

如何解决?

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

一个死锁的实例:

class A {
	public synchronized void foo(B b) { //同步监视器:A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器:A类的对象:a
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器:b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器:b
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();//副线程启动

		dl.init();//主线程启动
	}
}

Lock(锁)

从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock类实现了 Lock ,它拥有与synchronized相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁。

注意:Lock方式中并没有同步监视器这个概念,但是我们可以把private ReentrantLock lock = new ReentrantLock();中的lock视为同步监视器,如果线程间没有使用同一个lock对象,就相当于没有使用同一把锁。lock不可调用wait()notify()notifyAll()方法,但是可以通过相关的Condition对象来实现更多操作。

使用方式

//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
    while(true){
        try{
            //2.调用锁定方法lock()
            lock.lock();
            //需要同步的代码
        }finally {
            //3.调用解锁方法:unlock()
            lock.unlock();
        }
    }
}

因为最后一步一定要解锁,所以使用try--finally的方式

synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁, synchronized有代码块锁和方法锁
  3. 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

推荐使用顺序

优先使用顺序:

Lock --> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法(在方法体之外)

练习

银行有一个账户。

有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

  1. 使用继承Thread方式,synchronized同步方法
public class DepositTest {
    public static void main(String[] args) {
        Account account = new Account();
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);

        c1.setName("Tom");
        c2.setName("Jerry");
        c1.start();
        c2.start();
    }
}
 
class Account{
    double balance;

    public synchronized void deposit(double awt){
        if (awt <= 0) {
            return;
        }
        balance += awt;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
    }
}

class Customer extends Thread{
    Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            
            acct.deposit(1000);
        }
    }
}
  1. 使用继承Thread方式,lock同步
public void deposit(double awt){
    if (awt <= 0) {
        return;
    }
    lock.lock();
    try {
        balance += awt;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
    } finally {
        lock.unlock();
    }
}
  1. 使用继承Runnable方式,synchronized同步方法
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {
    public static void main(String[] args) {
        Account account = new Account();
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);

        Thread t1 = new Thread(c1);
        Thread t2 = new Thread(c2);

        t1.setName("Tom");
        t2.setName("Jerry");
        t1.start();
        t2.start();
    }
}

class Account{
    double balance;
    ReentrantLock lock = new ReentrantLock();

    public synchronized void deposit(double awt){
        if (awt <= 0) {
            return;
        }
        balance += awt;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
    }

class Customer implements Runnable{
    Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            acct.deposit(1000);
        }
    }
}
  1. 使用继承Runnable方式,lock同步
import java.util.concurrent.locks.ReentrantLock;
public class DepositTest {
    public static void main(String[] args) {
        Account account = new Account();
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);

        Thread t1 = new Thread(c1);
        Thread t2 = new Thread(c2);

        t1.setName("Tom");
        t2.setName("Jerry");
        t1.start();
        t2.start();
    }
}

class Account{
    double balance;
    ReentrantLock lock = new ReentrantLock();

    public void deposit(double awt){
        if (awt <= 0) {
            return;
        }
        lock.lock();
        try {
            balance += awt;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":sucessful deposit.The balance is " + balance);
        } finally {
            lock.unlock();
        }
    }
}

class Customer implements Runnable{
    Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            acct.deposit(1000);
        }
    }
}

线程通信

使用两个线程打印 1-100。线程1, 线程2 交替打印

/**
 * 使用两个线程打印 1-100。线程1, 线程2 交替打印
 */
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
    }
}

class Number implements Runnable{
    private int number = 1;
    
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();//将阻塞的进程唤醒
                if (number > 100) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + ":" + number++);    
                try {
                    wait();//令当前进程阻塞,等待唤醒。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

涉及到的三个方法:

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明:

  1. wait()notify()notifyAll()三个方法必须使用在同步代码块或同步方法中,lock锁的方式都不可以。意味着这三个方法是依赖于同步监视器的。

  2. wait()notify()notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

    否则,会出现IllegalMonitorStateException异常。

  3. wait()notify()notifyAll()三个方法三个方法是定义在java.lang.Object类中。

面试题:sleep() 和 wait()的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:

  1. 两个方法声明的位置不同:

    Thread类中声明sleep()

    Object类中声明wait()

  2. 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中

  3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

线程通信的应用

经典例题:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

分析:

  1. 是否是多线程问题?是,生产者线程,消费者线程
  2. 是否有共享数据?是,店员(或产品)
  3. 如何解决线程的安全问题?同步机制,有三种方法
  4. 是否涉及线程的通信?是,生产者通知消费者进行消费,消费者通知生产者进行生产
public class CommunicationTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Customer customer = new Customer(clerk);
        Productor productor = new Productor(clerk);

        customer.setName("customer");
        productor.setName("productor");
        productor.start();
        customer.start();
    }
}

class Clerk{
    private int number = 0;

    public synchronized void produce(){
        if (number >= 20) {
            //等待消费
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        System.out.println(Thread.currentThread().getName() + "开始生产第" + ++number + "个产品");
        notify();//生产后,唤醒消费者
    }

    public synchronized void custom(){
        if (number <= 0) {
            //等待生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        System.out.println(Thread.currentThread().getName() + "开始消费第" + number-- + "个产品");
        notify();//消费后,唤醒生产者
    }

}

class Productor extends Thread{
    Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produce();
        }
    }
}

class Customer extends Thread{
    Clerk clerk;
    
    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.custom();
        }
    }
}

新增线程创建方式

JDK5.0 新增

新增方式一:实现Callable接口

与使用Runnable相比, Callable功能更强大些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

实现步骤为:

  1. 创建一个实现Callable的实现类

  2. 实现call方法,将此线程需要执行的操作声明在call()

  3. 创建Callable接口实现类的对象

  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

  5. FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

  6. 获取Callable中call方法的返回值(可选)

    get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。

//1.创建一个实现`Callable`的实现类
class Number implements Callable{
    private int count = 0;

    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
            count += i;
        }
        return count;
    }
}

public class NewThread {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        Number number = new Number();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(number);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread thread = new Thread(futureTask);
        thread.start();

        //6.获取Callable中call方法的返回值
        //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
        try {
            Object o = futureTask.get();
            System.out.println("总和为:" + o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

  1. call()可以有返回值的。
  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

新增方式二:使用线程池

背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API

JDK 5.0起提供了线程池相关API: ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行
    Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行
    Callable
  • ``void shutdown()`:关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
  • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
    行命令或者定期地执行

使用实例:

public static void main(String[] args) {
    //1. 创建线程池
    ExecutorService service = Executors.newFixedThreadPool(10);

    //2. 放入线程并启动
    service.execute(new OddNum());//Runnable线程启动

    FutureTask futureTask = new FutureTask(new EvenNum());
    service.submit(futureTask);//Callable线程启动

    //3. 关闭线程池
    service.shutdown();
}

线程管理

由于我们接收线程池对象时是使用多态方式接收的,我们可以查看newFixedThreadPool源码,看到它返回的是ThreadPoolExecutor,若想使用线程管理,就必须先进行强转。

/**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

转换为ThreadPoolExecutor再进行管理

ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(3);//核心池的大小

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/337180.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

12年老外贸的经验分享

回想这12年的经历&#xff0c;很庆幸自己的三观一直是正确的&#xff0c;就是买家第一不管什么原因&#xff0c;只要你想退货&#xff0c;我都可以接受退款。不能退给上级供应商&#xff0c;我就自己留着&#xff0c;就是为了避免因为这个拒收而失去买家。不管是什么质量原因&a…

2022年11月软考领证通知

纸质证书领取时间 根据往年各地软考证书的领取时间看&#xff0c;上半年软考证书领取一般在10月底陆续开始&#xff0c;下半年的证书领取时间一般在次年2/3月份左右开始&#xff08;各地证书领取具体时间不一样&#xff0c;届时请多留意当地证书领取通知。&#xff09; 1、证…

PyTorch学习笔记:nn.LeakyReLU——LeakyReLU激活函数

PyTorch学习笔记&#xff1a;nn.LeakyReLU——LeakyReLU激活函数 功能&#xff1a;逐元素对数据应用如下函数公式进行激活 LeakyReLU(x)max⁡(0,x)α∗min⁡(0,x)\text{LeakyReLU}(x)\max(0,x)\alpha*\min(0,x) LeakyReLU(x)max(0,x)α∗min(0,x) 或者 LeakyReLU(x){x,ifx≥0α…

在浏览器输入url到发起http请求,这过程发生了什么

当用户输入url&#xff0c;操作系统会将输入事件传递到浏览器中&#xff0c;在这过程中&#xff0c;浏览器可能会做一些预处理&#xff0c;比如 Chrome 会根据历史统计来预估所输入字符对应的网站&#xff0c;例如输入goog&#xff0c;根据之前的历史发现 90% 的概率会访问「ww…

1理想的大数据处理框架设计

以下内容基于极客 蔡元楠老师的《大规模数据处理实战》做的笔记哈。感兴趣的去极客看蔡老师的课程即可。 MapReduce 缺点 高昂的维护成本 因为mapreduce模型只有map和reduce两个步骤。所以在处理复杂的架构的时候&#xff0c;需要协调多个map任务和多个reduce任务。 例如计…

C#开发的OpenRA的扩展方法

C#开发的OpenRA的扩展方法 在我们以往的开发方法认知里, 对一个类进行扩展方法,只有继父类,然后在子类里创建新的内容。 但是C#又给我们上了一课,它不但可以采用前面的方法, 而且可以对类没有进行继承,也能扩展类型的方法。 这种方式,对于没有进行学习之前,看到代码就是…

Allegro更改线段,丝印,走线,形状,铜箔到不同层的方法

更改线段到不同的Class和Subclass的方法下面以更改线段为例进行讲解1、原先线段在Board Geometry→Soldermask_Top层2、选中线段&#xff0c;鼠标右击选择→Change class/subclass更改到所想要的Class和Subclass3、更改后的线段到Package Geometry→Silkscreen_Top层更改丝印&a…

详解shell中的运算符

目录 前言 一、运算指令 二、运算符号 练习 总结 前言 上一篇文章我们着重学习了 &#xff0c;shell中的执行流控制&#xff0c;本章我很学习和执行流控制相结合使用的运算符号与运算指令。 一、运算指令 计算的三种方式 (()) ##((a12)) let …

51单片机——74HC595的应用(SPI实践)

目录 SPI总线 SPI总线概述 SPI总线分类 SPI 优点及缺点 SPI接口硬件原理 SPI四种工作模式 74HC595应用 74HC595芯片概述 74HC595封装及管脚功能 74HC595工作原理 ​编辑 74HC595串行转并行点亮LED灯 程序实现 Proteus运行结构示意图 SPI总线 SPI总线概述 SPI&#…

【FiddlerScript】利用Fiddler中的FiddlerScript解除7K7K小游戏的防沉迷

本文仅供技术探讨&#xff0c;切勿用于非法用途案例网站:http://www.7k7k.com/准备的工具:配置好的Fiddler一个Fiddler官方英文版配置教程:https://www.bilibili.com/video/BV1rP4y1t7ZLFiddler中文版配重教程:https://www.bilibili.com/video/BV1CP4y1t7DR开始教程来到Fiddler…

10 个最难理解的 Python 概念

文章目录技术提升面向对象编程 (OOP)装饰器生成器多线程异常处理正则表达式异步/等待函数式编程元编程网络编程大家好&#xff0c;与其他编程语言相比&#xff0c;Python 是一门相对简单的编程语言&#xff0c;如果你想真正学透这门语言&#xff0c;其实可能并不容易。 今天我…

彻底弄懂HTTP缓存机制及原理(二)

强制缓存 从上文我们得知&#xff0c;强制缓存&#xff0c;在缓存数据未失效的情况下&#xff0c;可以直接使用缓存数据&#xff0c;那么浏览器是如何判断缓存数据是否失效呢&#xff1f; 我们知道&#xff0c;在没有缓存数据的时候&#xff0c;浏览器向服务器请求数据时&…

Linux(十一)生产者与消费者模型

引言 一、实现一个网关来过滤流经网关的数据 二、农忙时节收割麦子 生产者与消费者模型 模型实现 完整源码&#xff1a; 引言 阐述这个模型之前先引入俩个例子&#xff1a; 一、实现一个网关来过滤流经网关的数据 网关会捕捉大量的数据然后进行分析处理&#xff0c;之后…

VHDL语言基础-状态机设计-时序电路与状态机的关系

目录 时序电路的概念&#xff1a; 下面以一个简单的三位计数器为例&#xff0c;说明时序电路的结构&#xff1a; 三位计数器的结构&#xff1a; 次态逻辑关系的推导&#xff1b;画出卡诺图如下&#xff1a; 电路图&#xff1a; 时序电路与状态机的关系&#xff1a; 状态机…

PyTorch学习笔记:nn.Tanh——Tanh激活函数

PyTorch学习笔记&#xff1a;nn.Tanh——Tanh激活函数 torch.nn.Tanh()功能&#xff1a;逐元素应用Tanh函数&#xff08;双曲正切&#xff09;对数据进行激活&#xff0c;将元素调整到区间(-1,1)内 函数方程&#xff1a; Tanh(x)tanh(x)ex−e−xexe−x\text{Tanh}(x)\text{ta…

每天10个前端小知识 【Day 11】

前端面试基础知识题 1. 浏览器的垃圾回收机制有哪些&#xff1f; JS会在创建变量时自动分配内存&#xff0c;在不使用的时候会自动周期性的释放内存&#xff0c;释放的过程就叫 “垃圾回收”。 一方面自动分配内存减轻了开发者的负担&#xff0c;开发者不用过多的去关注内存…

单链表--C语言版(从0开始,超详细解析,小白一看就会)

目录 一、前言 &#x1f34e; 为什么要学习链表 &#x1f4a6;顺序表有缺陷 &#x1f4a6; 优化方案&#xff1a;链表 二、链表详解 &#x1f350;链表的概念 &#x1f349;链表的结构组成&#xff1a;节点 &#x1f353;链表节点的连接&#xff08;逻辑结构与物理结构的区…

java spring注解方式 实现基本类型属性注入

之前 我们看了几个注入属性的注解 但他们都是注入对象类型的 那么 下面我们就看一个 给基本属性注入值的注解 value 我们直接代码快速演示一下 创建一个项目 然后引入 spring 所需要的依赖 然后在src下创建包 Bean 在 Bean目录下创建一个包 叫 UserData 然后在src下创建 bean…

leaflet 上传geojson文件,在地图上显示图形(示例代码053)

第053个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet示例中上传geojson文件,通过L.geojson解析,在地图上显示图形。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共97行)相关API参考:专栏目标…

微服务项目【mybatis-plus与微服务注册】

Mybatis与微服务注册 一、SpringBoot整合MybatisPlus 创建自动生成代码子模块 基于maven方式创建子模块zmall-generator&#xff0c;用于结合mybatis-plus生成代码。 在公共模块zmall-common中注释掉mybatis的依赖引入&#xff0c;改换成mybatis-plus依赖引入 <!-- myba…