java多线程(二四)java多线程基础总结

news2024/9/24 9:26:25

一、进程与线程

1.进程

进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动。操作系统中,几乎所有运行中的任务对应一条进程(Process)。一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。描述进程的有一句话非常经典的话——进程是系统进行资源分配和调度的一个独立单位。
进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间。进程的实质,就是程序在多道程序系统中的一次执行过程,它是动态产生,动态消亡的,具有自己的生命周期和各种不同的状态。进程具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进。

(注意,并发性(concurrency)和并行性(parallel)是不同的。并行指的是同一时刻,多个指令在多台处理器上同时运行。并发指的是同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,看起来就好像多个指令同时执行一样。)
进程由程序、数据和进程控制块三部分组成。

2、线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
在Java Web中要注意,线程是JVM级别的,在不停止的情况下,跟JVM共同消亡,就是说如果一个Web服务启动了多个Web应用,某个Web应用启动了某个线程,如果关闭这个Web应用,线程并不会关闭,因为JVM还在运行,所以别忘了设置Web应用关闭时停止线程。

二、线程的生命周期及五种基本状态

在这里插入图片描述
上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。

Java线程具有五种基本状态

新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread()。

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 1.等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

  • 2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

  • 3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、创建多线程的方式

Java中线程的创建有如下三种基本形式。

1、继承Thread类,重写该类的run()方法。

package com.demo.test;

public class MyThread extends Thread {
    
    private String name;
    
    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "运行  :" + i);
        }
    }
}

package com.demo.test;

public class ThreadTest {

     public static void main(String[] args) {
         Thread myThread1 = new MyThread("A");     // 创建一个新的线程  myThread1  此线程进入新建状态
         Thread myThread2 = new MyThread("B");     // 创建一个新的线程 myThread2 此线程进入新建状态
         myThread1.start();  // 调用start()方法使得线程进入就绪状态
         myThread2.start();  // 调用start()方法使得线程进入就绪状态
    }
}


如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

2、实现java.lang.Runnable接口

具体做法:实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

package com.demo.test;

public class MyRunnable implements Runnable{

    private String name;
    
    public MyRunnable(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + "运行  :" + i);
        }
    }
}

package com.demo.test;

public class ThreadTest {

     public static void main(String[] args) {

         Runnable myRunnable = new MyRunnable("A"); // 创建一个Runnable实现类的对象
         Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
         Runnable myRunnable1 = new MyRunnable("B");
         Thread thread2 = new Thread(myRunnable1);
         thread1.start(); // 调用start()方法使得线程进入就绪状态
         thread2.start();
    }
}

3、使用Callable和Future接口创建线程。

具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

package com.demo.test;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer>{
    
    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() throws Exception{
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            //System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

package com.demo.test;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {

     public static void main(String[] args) {
         
         Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
         FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

         Thread thread = new Thread(ft);  //FutureTask对象作为Thread对象的target创建新的线程
         thread.start();  //线程进入到就绪状态

         try {
             Thread.sleep(1000);
         } catch (InterruptedException e1) {
             e1.printStackTrace();
         }
         
         System.out.println("主线程在执行任务");
        
         try {
             int sum = ft.get(); //取得新创建的线程中的call()方法返回的结果
             System.out.println("task运行结果,sum = " + sum);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
         
         System.out.println("所有任务执行完毕");
        
    }
}

首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:


public class FutureTask<V> implements RunnableFuture<V>

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
执行下此程序,我们发现sum = 4950永远都是最后输出的。那么为什么sum =4950会永远最后输出呢?原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。
上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。

四、线程调度

1.线程加入——join()

join —— 让一个线程等待另一个线程完成才继续执行。如A线程执行体中调用B线程的join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行。

join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。

join是Thread类的一个方法,启动线程后直接调用,join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。

为什么要用join() 方法?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

不加join的情况:

package com.demo.test;

public class Thread1 extends Thread{
    
    private String name;
    public Thread1(String name) {
        super(name);
        this.name=name;
    }
    
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
    }
}

package com.demo.test;

public class Main {
    
    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    }
}
运行结果:

main主线程运行开始!
A 线程运行开始!
B 线程运行开始!
main主线程运行结束!
子线程B运行 : 0
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程B运行 : 2
子线程A运行 : 2
子线程B运行 : 3
子线程A运行 : 3
子线程B运行 : 4
子线程A运行 : 4
B 线程运行结束!
A 线程运行结束!

加join 的情况:

package com.demo.test;

public class Main {
    
    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    }
}

运行结果:


main主线程运行开始!
A 线程运行开始!
B 线程运行开始!
子线程A运行 : 0
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
B 线程运行结束!
main主线程运行结束!

五、线程安全与线程同步

1、一个典型的Java线程安全例子

package com.demo.test;

public class Account {

    private String accountNo;
    private double balance;

    public Account() {

    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

}

package com.demo.test;

public class DrawMoneyRunnable implements Runnable{
    
    private Account account;
    private double drawAmount;

    public DrawMoneyRunnable(Account account, double drawAmount) {
        super();
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run() {
        if (account.getBalance() >= drawAmount) {  //1
            System.out.println("取钱成功, 取出钱数为:" + drawAmount);
            double balance = account.getBalance() - drawAmount;
            account.setBalance(balance);
            System.out.println("余额为:" + balance);
        }
    }

}

package com.demo.test;

public class ThreadTest {

    public static void main(String[] args) {
        Account account = new Account("123456", 1000);
        DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
        Thread myThread1 = new Thread(drawMoneyRunnable);
        Thread myThread2 = new Thread(drawMoneyRunnable);
        myThread1.start();
        myThread2.start();
    }

}

上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟两个人同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:

取钱成功, 取出钱数为:700.0
余额为:300.0
取钱成功, 取出钱数为:700.0
余额为:-400.0

也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。
经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。
因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。

2、多线程的同步

为何要使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

(1)同步方法

 对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...。

在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synchronized关键字即可。

public synchronized void run() {

}

(2)同步代码块

正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心…)

关于synchronized关键字的说明:

关于synchronized关键字的说明:

① 原理

在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当前线程调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj),当前线程就获取了“obj这个对象”的同步锁。

不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

② 基本规则

第一条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

第二条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。

第三条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

③ 实例锁和全局锁

实例锁 – 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。

全局锁 – 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

就是说,一个非静态方法上的synchronized关键字,代表该方法依赖其所属对象。一个静态方法上synchronized关键字,代表该方法依赖这个类本身。

六、线程通信 wait()/notify()/notifyAll()

wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

package com.demo.test;

public class Account {

    private String accountNo;
    private double balance;
    // 标识账户中是否已有存款
    private boolean flag = false;

    public Account() {

    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
    
    /**
     * 存钱
     * 
     * @param depositeAmount
     */
    public synchronized void deposite(double depositeAmount, int i) {

        if (flag) {
            // 账户中已有人存钱进去,此时当前线程需要等待阻塞
            try {
                System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i);
                wait();
                // 1
                System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            // 开始存钱
            System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
            setBalance(balance + depositeAmount);
            flag = true;

            // 唤醒其他线程
            notifyAll();

            // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i);
        }
    }

    /**
     * 取钱
     * 
     * @param drawAmount
     */
    public synchronized void draw(double drawAmount, int i) {
        if (!flag) {
            // 账户中还没人存钱进去,此时当前线程需要等待阻塞
            try {
                System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i);
                wait();
                System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            // 开始取钱
            System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i);
            setBalance(getBalance() - drawAmount);

            flag = false;

            // 唤醒其他线程
            notifyAll();

            System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); // 3
        }
    }

}

package com.demo.test;

public class DrawMoneyThread extends Thread{
    
    private Account account;
    private double amount;

    public DrawMoneyThread(String threadName, Account account, double amount) {
        super(threadName);
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            account.draw(amount, i);
        }
    }

}

package com.demo.test;

public class DepositeMoneyThread extends Thread{
    
    private Account account;
    private double amount;

    public DepositeMoneyThread(String threadName, Account account, double amount) {
        super(threadName);
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            account.deposite(amount, i);
        }
    }

}

package com.demo.test;

public class ThreadTest{

    public static void main(String[] args) {
        Account account = new Account("123456", 0);

        Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
        Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700);

        drawMoneyThread.start();
        depositeMoneyThread.start();
    }

}


运行结果如下

取钱线程 开始要执行wait操作 -- i=0
存钱线程 存款:700.0 -- i=0
存钱线程-- 存钱 -- 执行完毕 -- i=0
存钱线程 开始要执行wait操作 -- i=1
取钱线程 执行了wait操作 -- i=0
取钱线程 取钱:700.0 -- i=1
取钱线程-- 取钱 -- 执行完毕 -- i=1
取钱线程 开始要执行wait操作 -- i=2
存钱线程 执行了wait操作 -- i=1
存钱线程 存款:700.0 -- i=2
存钱线程-- 存钱 -- 执行完毕 -- i=2
存钱线程 开始要执行wait操作 -- i=3
取钱线程 执行了wait操作 -- i=2
取钱线程 取钱:700.0 -- i=3
取钱线程-- 取钱 -- 执行完毕 -- i=3
取钱线程 开始要执行wait操作 -- i=4
存钱线程 执行了wait操作 -- i=3
存钱线程 存款:700.0 -- i=4
存钱线程-- 存钱 -- 执行完毕 -- i=4
取钱线程 执行了wait操作 -- i=4

由此,我们需要注意如下几点:

1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续执行,直到当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,如果下面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于相同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。

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

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

相关文章

前装L2标配车型均价连续第二年「低于」L1,市场进入爆发期

L2级辅助驾驶&#xff0c;正在进入市场红利期。 高工智能汽车研究院监测数据显示&#xff0c;2022年度中国市场&#xff08;不含进出口&#xff09;乘用车前装标配搭载辅助驾驶&#xff08;L0-L2&#xff09;交付1001.22万辆&#xff0c;首次突破千万辆规模&#xff0c;同时&a…

带你玩转spring声明式事务-使用中需要注意的点

本文向大家介绍spring声明式事务使用过程中需要注意的地方。事务特性1. 原子性&#xff08;Atomicity&#xff09;事务是一个原子操作&#xff0c;由一系列动作组成。事务的原子性确保动作要么全部完成&#xff0c;要么完全不起作用。2. 一致性&#xff08;Consistency&#xf…

九龙证券|6G概念重新活跃 数字经济板块引领A股尾盘回升

周三&#xff0c;沪深两市缩量调整&#xff0c;沪指全天以弱势震荡为主&#xff0c;尾盘在数字经济概念带动下快速拉升&#xff0c;全天微跌0.06%&#xff0c;报3283.25点&#xff1b;深证成指跌落0.09%&#xff0c;报15598.29点&#xff1b;创业板指跌落0.26%&#xff0c;报23…

[算法]归并排序

参考&#xff1a;《漫画算法-小灰的算法之旅》 目录 参考&#xff1a;《漫画算法-小灰的算法之旅》 1、什么是归并排序 2、归并的具体操作 3、代码 4、时间复杂度和空间复杂度 5、归并排序是稳定排序 1、什么是归并排序 归并排序就像是组织一场元素之间的“比武大会”&…

【C++】30h速成C++从入门到精通(二叉树)

说明为什么要在C当中单独再次提及数据结构中的二叉树&#xff1a;map和set特性需要先铺垫二叉搜索树&#xff0c;而二叉搜哦书也是一种树形结构二叉搜索树的特性了解&#xff0c;有助于更好的理解map和set特性二叉树中部分面试题有难度有些OJ使用C语言实现比较麻烦二叉搜索树概…

Kubernetes14:Helm为了部署像微服务这种的大型项目

Kubernetes14&#xff1a;Helm介绍&#xff08;为了部署像微服务这种的大型项目&#xff09; 1、Helm的引入 (1)之前方式部署应用基本过程 编写yaml文件 1、deployment kubectl create deployment nginx --imagenginx --dryrun -o yaml > nginx.yaml2、Service kubect…

Web前端:前端开发人员的职责有哪些?

前端开发&#xff0c;就是要创造上面提到的网站面向用户的部分背后的代码&#xff0c;并通过建立框架&#xff0c;构建沉浸性的用户体验。前端工程师还需要确保网站在各种浏览器和设备上都能正常运行&#xff0c;并且能够根据用户需求不断优化和改进网站。前端开发人员的角色和…

【C语言进阶】文本与二进制操作文件,优化通讯录。

前言&#xff1a;上篇文章&#xff0c;我们已经学习了有关本地磁盘文件的常用文件操作&#xff0c;已经能够对本地文件进行调用与读写。我们磁盘中还存在着一些内容用二进制存储的文件&#xff0c;这也就是我们今天将要讲解的内容。一、文本文件与二进制文件根据数据的组织形式…

SpiderFlow爬虫获取网页节点

SpiderFlow爬虫获取网页节点 一、SpiderFlow 文档地址&#xff1a;https://www.spiderflow.org/ 二、问题&#xff1a;获取一篇文章的标题、来源、发布时间、正文、下载附件该怎么获取&#xff1f; 举例&#xff1a;【公示】第三批智能光伏试点示范名单公示 三、抓取网页步骤…

Heatmap-based Out-of-Distribution Detection 论文阅读

原文地址 概要 我们的工作将分布失调【out-of-distribution,OOD】检测作为神经网络输出解释问题进行研究。我们学习了一种热图【heatmap】表示&#xff0c;用于检测OOD图像&#xff0c;同时可视化ID和OOD的图像区域。给定一个训练过的固定分类器&#xff0c;我们训练一个解码…

ArrayList源码分析(JDK17)

ArrayList类简介类层次结构构造无参构造有参构造添加元素add&#xff1a;添加/插入一个元素addAll:添加集合中的元素扩容mount与迭代器其他常见方法不常见方法不常见方法的源码和小介绍常见方法的源码和小介绍积累面试题ArrayList是什么&#xff1f;可以用来干嘛&#xff1f;Ar…

funkyheatmap | 用这个包来完美复刻Nature Biotechnology的高颜值神图吧!~

1写在前面 天气开始暖和了☀️&#xff0c;发现旅游的人好多啊&#xff01;~&#x1f972; 不知道自己什么时候能有时间出去看看外面的世界&#xff0c;实在是太忙了。&#x1f637; 最近用到的有个包感觉很不错&#xff0c;分享给大家&#xff0c;funkyheatmap包。&#x1f61…

进程与多线程(入门)

什么是线程 要了解什么是线程&#xff0c;得先知道什么是程序。 程序&#xff1a;为完成特定任务&#xff0c;用某种语言编写的一组指令的集合。 例如&#xff0c;QQ&#xff0c;Steam&#xff0c;亦或者java写的helloword。 这些都是程序 了解了程序&#xff0c;还得清楚什么…

d3.js绘制饼状图,悬浮出现字以及点击事件

代码以及注释如下&#xff1a; const width 300; // 定义圆的宽度 const height 300; // 定义圆的高度 const radius Math.min(width, height) / 2; // 算出半径 const color d3.scaleOrdinal() .range(["#98abc5", "#8a89a6", "#6b486b&qu…

【MySQL高级篇】第05章_存储引擎

第05章_存储引擎 1. 查看存储引擎 查看mysql提供什么存储引擎 show engines;2. 设置系统默认的存储引擎 查看默认的存储引擎 show variables like %storage_engine%; #或 SELECT default_storage_engine;修改默认的存储引擎 如果在创建表的语句中没有显式指定表的存储引擎…

一分钟成为签到达人!Redis BitMap轻松解决,Spring Boot带你飞

如何实现签到功能&#xff0c;尤其是如何实现高效的签到与统计&#xff0c;是开发者们需要考虑的问题。在本篇文章中&#xff0c;我们将介绍如何利用Spring Boot整合Redis BitMap实现签到与统计。 Redis BitMap简介 在介绍如何利用Redis BitMap实现签到与统计之前&#xff0c;…

unity动画--动画绑定,转换,用脚本触发

文章目录如何制作和添加动画大概过程示例图将多组图片转化为动画放在对象身上实现动画之间的切换使用脚本触发Parameters(Trigger)如何制作和添加动画 大概过程示例图 将多组图片转化为动画放在对象身上 首先&#xff0c;我们要为我们要对象添加animator 然后我们要设置对应的…

计算机网络-应用层

文章目录前言概述Https协议(443)Http协议(80)HttpsTLS/SSL 协议TLS的四次握手总结前言 本博客仅做学习笔记&#xff0c;如有侵权&#xff0c;联系后即刻更改 科普&#xff1a; 概述 Https协议(443) 参考网址 Http协议(80) 谈到Https必然要先将httpHTTP 请求报文结构 请求…

谷粒学院开发(一):基础准备

商业模式 常见商业模式 B2C模式&#xff1a; 两个角色&#xff1a; 管理员&#xff1a;增加&#xff0c;修改&#xff0c;删除普通用户&#xff1a;查询 商家到用户&#xff0c;自己制作大量自有版权的视频&#xff0c;放在自有平台上&#xff0c;让用户付费。 这是这个项目使…

Linux下查看图片中某点的像素X、Y坐标

在做目标检测、目标追踪的任务过程中&#xff0c;我们会用到一些开源的数据集&#xff0c;比如MOT16多目标追踪数据集。这些数据集会提供数据标注文件gt.txt,里面的内容如下1,1,912,484,97,109,0,7,12,1,912,484,97,109,0,7,13,1,912,484,97,109,0,7,14,1,912,484,97,109,0,7,1…