• Java多线程详解


    多线程

    基本概念

    每个运行的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个进程。

    进程的特性:

    独立性:每一个进程都拥有自己的私有地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

    动态性:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。

    并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会有影响。

    并发性和并行性:

    并发性:在同一时刻只能有一条命令执行,但多个进程指令被快速轮换执行。

    并行性:在同一时刻,有多条指令在多个处理器上同时执行。

    多线程的优势:

    线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程之间的隔离程度要小。他们共享内存、文件句柄和其他每个进程应有的状态。

    文件句柄:在文件I/O中,要从一个文件读取数据,应用程序首先要调用操作系统函数并传送文件名,并选一个到该文件的路径来打开文件。该函数取回一个顺序号,即文件句柄(file handle),该文件句柄对于打开的文件是唯一的识别依据。要从文件中读取一块数据,应用程序需要调用函数ReadFile,并将文件句柄在内存中的地址和要拷贝的字节数传送给操作系统。当完成任务后,再通过调用系统函数来关闭该文件。

    多个线程共享内存,极大地提高了程序的运行效率。

    多线程和多进程:

    当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多进程实现并发的性能高得多。

    进程之间不能共享内存,但线程之间共享内存非常容易。

    系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程并发比多进程效率高。

    java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。

    java虚拟机本身就在后台提供了一个超级进程来回收垃圾。

    java使用Thread类代表线程,所有线程对象必须是Thread类或其子类的实例。

    继承Thread类来创建并启动多线程:

    继承Thread类,重写该类的run()方法,run()方法代表线程需要完成的任务。

    创建Thread子类的实例,也就是线程对象。

    调用线程对象的start()方法来启动该线程。

    例子:

    package com.jiangwenzhang.mybootbill.learn;
    
    /**
     * @包名 com.jiangwenzhang.mybootbill.learn
     * @创建人 蒋文章
     * @日期 2018/5/19 0019
     * @时间 8:57
     * @描述
     */
    
    public class ThreadClass extends Thread{
    
        private int i ;
        // 重写run方法,run方法的方法体就是线程执行体
        public void run()
        {
            for ( ; i < 100 ; i++ )
            {
                // 当线程类继承Thread类时,直接使用this即可获取当前线程
                // Thread对象的getName()返回当前该线程的名字
                // 因此可以直接调用getName()方法返回当前线程的名
                System.out.println(getName() +  " " + i);
            }
        }
        public static void main(String[] args)
        {
            for (int i = 0; i < 100;  i++)
            {
                // 调用Thread的currentThread方法获取当前线程,currentThread方法返回当前正在执行的线程对象
                System.out.println(Thread.currentThread().getName()
                        +  " " + i);
                if (i == 20)
                {
                    // 创建、并启动第一条线程
                    new ThreadClass().start();
                    // 创建、并启动第二条线程
                    new ThreadClass().start();
                }
            }
        }
    }

    可见程序有三个线程,一个主线程和我们创建的两个子线程。

    主线程的线程执行体不是 run() 方法确定的,而是 main() 方法确定的, main() 方法的方法体代表主线程的方法体。

    使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

    实现Runnable接口创建线程类:

    步骤:定义Runnable接口的实现类,并重写run方法。创建Runnable实现类的实例,以此实例作为Thread的target来创建Thread对象,该Thread对象时线程对象。调用线程对象的start()方法启动线程。

    public class SecondThread implements Runnable
    {
        private int i ;
        // run方法同样是线程执行体
        public void run()
        {
            for ( ; i < 100 ; i++ )
            {
                // 当线程类实现Runnable接口时,
                // 如果想获取当前线程,只能用Thread.currentThread()方法。
                System.out.println(Thread.currentThread().getName()
                        + "  " + i);
            }
        }
    
        public static void main(String[] args)
        {
            for (int i = 0; i < 100;  i++)
            {
                System.out.println(Thread.currentThread().getName()
                        + "  " + i);
                if (i == 20)
                {
                    SecondThread st = new SecondThread();     //// 通过new Thread(target , name)方法创建新线程
                    new Thread(st , "新线程1").start();
                    new Thread(st , "新线程2").start();
                }
            }
        }
    }

    注意:

    通过加成Thread类获得线程对象使用this关键字获取当前线程对象,实现Runnable接口获得当前线程对象,则需要使用Thread.currentThread()方法。

    Runnable接口中只包含一个抽象方法,从Java8开始,Runnable接口使用@FunctionalInterface 修饰,也就是说Runnable接口是函数式接口,可以使用Lambda表达式创建Runnable对象。

    使用Callable和Future创建线程

    从Java5开始,Java提供了Callable接口,该接口提供了一个call()方法作为线程执行体,但call()方法比run()方法更强大。call()方法可以有返回值,可以生命抛出异常。

    因此可以提供一个Callable对象作为Thread的target,线程执行体就是Callable对象的call()方法。但是Callable接口是Java5新增的接口,而且Callable不是Runnable的子接口,所以Callable对象不能直接作为Thread的targer。而且call方法还有一个返回值,call()

     方法并不是直接调用,它是作为线程执行体被调用的。

    Java5提供了Future接口代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,可以作为Thread的target。

    创建并启动有返回值的线程步骤:

    创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且有返回值,在创建Callable实现类的实例。从Java8快开始可以直接使用Lambda表达式创建Callable对象。

    使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。

    使用FutureTask对象作为Thread对象的target创建并启动新线程。

    调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

    public class ThirdThread
    {
        public static void main(String[] args)
        {
            // 创建Callable对象
            ThirdThread rt = new ThirdThread();
            // 先使用Lambda表达式创建Callable<Integer>对象
            // 使用FutureTask来包装Callable对象
            FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
                int i = 0;
                for ( ; i < 100 ; i++ )
                {
                    System.out.println(Thread.currentThread().getName()
                            + " 的循环变量i的值:" + i);
                }
                // call()方法可以有返回值
                return i;
            });
            for (int i = 0 ; i < 100 ; i++)
            {
                System.out.println(Thread.currentThread().getName()
                        + " 的循环变量i的值:" + i);
                if (i == 20)
                {
                    // 实质还是以Callable对象来创建、并启动线程
                    new Thread(task , "有返回值的线程").start();
                }
            }
            try
            {
                // 获取线程返回值
                System.out.println("子线程的返回值:" + task.get());
            }
            catch (Exception ex)
            {
                ex.printStackTrace();
            }
        }
    }

    创建线程对比:

    继承Thread:

    不能再继承其他类,编写简单,直接使用this可以获得当前线程。

    实现Runnable / Callable:

    还可以继承其它类,多个线程共享同一个target对象,适合多个线程处理同一个资源的情况,访问当前线程必须使用 Thread.currentThread()

    线程的生命周期

    新建和就绪状态

    new了一个线程后处于新建状态,和其他Java对象一样由JVM为其分配内存,并初始化成员变量的值。

    调用了start()方法后,处于就绪状态,JVM为其创建方法调用栈和程序计数器。线程还没开始运行,只是表示可以运行了。

    public class InvokeRun extends Thread
    {
        private int i ;
        // 重写run方法,run方法的方法体就是线程执行体
        public void run()
        {
            for ( ; i < 100 ; i++ )
            {
                // 直接调用run方法时,Thread的this.getName返回的是该对象名字,
                // 而不是当前线程的名字。
                // 使用Thread.currentThread().getName()总是获取当前线程名字
                System.out.println(Thread.currentThread().getName()
                    +  " " + i);   //
            }
        }
        public static void main(String[] args)
        {
            for (int i = 0; i < 100;  i++)
            {
                // 调用Thread的currentThread方法获取当前线程
                System.out.println(Thread.currentThread().getName()
                    +  " " + i);
                if (i == 20)
                {
                    // 直接调用线程对象的run方法,
                    // 系统会把线程对象当成普通对象,run方法当成普通方法,
                    // 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
                    new InvokeRun().run();
                    new InvokeRun().run();
                }
            }
        }
    }

    上面程序创建线程后直接调用了run方法,结果是以单线程运行该程序,另外不能直接通过getName()方法获得当前执行线程的名字。

    调用了run方法后,线程不在处于新建状态。

    运行和阻塞

    就绪状态的线程获得了CPU,开始执行run方法的线程执行体,线程处于运行状态。如果只有一个CPU那只能有一个线程处于运行状态,多核CPU将会有多个线程并行。

    阻塞状态:电泳sleep()方法主动放弃处理器资源,或者调用了一个阻塞式IO方法,方法返回之前,线程被阻塞,或者线程试图获取一个同步监视器,但该同步监视器正被其它线程所持有,或者线程等待某个通知(notify),或者程序调用了线程的suspeng()方法将线程挂起。

    当前正在执行的线程被阻塞之后,其它线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,需要重新等待线程调度器再度激活他。

    解除阻塞,重新就绪的情况:

    调用sleep()方法的线程经过了指定时间。

    线程调用的阻塞式IO方法已经返回。

    线程成功获得了试图取得的同步监视器。

    线程正在等待通知,其他线程发来了一个通知。

    处于挂起状态的线程被调用了resume()方法。

    线程在阻塞状态只能进入就绪状态,无法直接进入运行状态。就绪状态和运行状态转换通常不受程序控制,但是调用yield()方法可以让运行状态的线程转入就绪状态。

    线程死亡:

    线程结束的三种方式:

    run() / call() 执行完成。

    线程抛出一个未捕获的异常。

    直接调用该线程的stop()方法结束线程。

    当主线程结束,其他线程不受影响,并不会随之结束,一旦子线程启动就拥有和主线程相同的地位,不会受主线程影响。

    使用isAlive()方法测试线程是否已经死亡:线程处于就绪、运行、阻塞方法返回true,新建、死亡状态方法返回false。

    已死亡的线程不能使用start()方法使他重新启动:

    public class StartDead extends Thread
    {
        private int i ;
        // 重写run方法,run方法的方法体就是线程执行体
        public void run()
        {
            for ( ; i < 100 ; i++ )
            {
                System.out.println(getName() +  " " + i);
            }
        }
        public static void main(String[] args)
        {
            // 创建线程对象
            StartDead sd = new StartDead();
            for (int i = 0; i < 300;  i++)
            {
                // 调用Thread的currentThread方法获取当前线程
                System.out.println(Thread.currentThread().getName()
                    +  " " + i);
                if (i == 20)
                {
                    // 启动线程
                    sd.start();
                    // 判断启动后线程的isAlive()值,输出true
                    System.out.println(sd.isAlive());
                }
                // 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
                // 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
                // 那只能是死亡状态了。
                if (i > 20 && !sd.isAlive())
    
                {
                    // 试图再次启动该线程
                    sd.start();
                }
            }
        }
    }

    控制线程

    join线程 

    join()方法:让一个线程等待另一个线程完成的方法,某个线程执行六中调用其他线程的join方法时,调用线程将被阻塞,知道被join方法加入的join线程执行完。

    public class JoinThread extends Thread
    {
        // 提供一个有参数的构造器,用于设置该线程的名字
        public JoinThread(String name)
        {
            super(name);
        }
        // 重写run()方法,定义线程执行体
        public void run()
        {
            for (int i = 0; i < 100 ; i++ )
            {
                System.out.println(getName() + "  " + i);
            }
        }
        public static void main(String[] args)throws Exception
        {
            // 启动子线程
            new JoinThread("新线程").start();
            for (int i = 0; i < 100 ; i++ )
            {
                if (i == 20)
                {
                    JoinThread jt = new JoinThread("被Join的线程");
                    jt.start();
                    // main线程调用了jt线程的join()方法,main线程
                    // 必须等jt执行结束才会向下执行
                    jt.join();
                }
                System.out.println(Thread.currentThread().getName()
                        + "  " + i);
            }
        }
    }

    后台线程(守护线程、精灵线程):

    在后台运行的,他的任务是为其它的线程提供服务,这种线程被称为后台线程。JVM垃圾回收线程就是典型的后台线程。

    特征:如果所有前台线程都死亡,后台线程会自动死亡。

    调用Thread的setDeamon(true)方法将指定线程设置为后台线程‘’

    public class DaemonThread extends Thread
    {
        // 定义后台线程的线程执行体与普通线程没有任何区别
        public void run()
        {
            for (int i = 0; i < 1000 ; i++ )
            {
                System.out.println(getName() + "  " + i);
            }
        }
        public static void main(String[] args)
        {
            DaemonThread t = new DaemonThread();
            // 将此线程设置成后台线程
            t.setDaemon(true);
            // 启动后台线程
            t.start();
            for (int i = 0 ; i < 10 ; i++ )
            {
                System.out.println(Thread.currentThread().getName()
                    + "  " + i);
            }
            // -----程序执行到此处,前台线程(main线程)结束------
            // 后台线程也应该随之结束
        }
    }

    本来该线程应该运行到999,但是主线程也就是唯一的前台线程运行结束后,JVM自动退出,所以后台线程也结束。

    线程休眠

    public class SleepTest
    {
        public static void main(String[] args)
                throws Exception
        {
            for (int i = 0; i < 10 ; i++ )
            {
                System.out.println("当前时间: " + new Date());
                // 调用sleep方法让当前线程暂停1s。
                Thread.sleep(1000);
            }
        }
    }

    上程序中只有一个主线程。

    Thread还提供了一个yield()方法,让当前正在执行的线程暂停进入就绪状态但不阻塞,yield()方法调用之后,只有优先级不低于当前线程的线程才会获得执行机会。

    区别:

    sleep()暂停后给他线程机会,不理会优先级,yield()只给优先级相同或更高的线程机会。

    sleep()方法是县城进入阻塞状态,经过阻塞时间进入就绪状态,yield()方法强制进入就绪状态。

    sleep()方法抛出异常。

    sleep()方法有更好的移植性,通常不建议使用yield()。

    改变线程优先级

    每个线程执行时都有一定的优先级,优先级高的线程会获得较多的执行机会。每个线程的默认优先级和创建他的父线程相同。默认主线程具有普通优先级。设置优先级为整数,1-10。但是不同系统的优先级并不相同,应尽量使用静态常量设置优先级。

    public class PriorityTest extends Thread
    {
        // 定义一个有参数的构造器,用于创建线程时指定name
        public PriorityTest(String name)
        {
            super(name);
        }
        public void run()
        {
            for (int i = 0 ; i < 50 ; i++ )
            {
                System.out.println(getName() +  ",其优先级是:"
                    + getPriority() + ",循环变量的值为:" + i);
            }
        }
        public static void main(String[] args)
        {
            // 改变主线程的优先级
            Thread.currentThread().setPriority(6);
            for (int i = 0 ; i < 30 ; i++ )
            {
                if (i == 10)
                {
                    PriorityTest low  = new PriorityTest("低级");
                    low.start();
                    System.out.println("创建之初的优先级:"
                        + low.getPriority());
                    // 设置该线程为最低优先级
                    low.setPriority(Thread.MIN_PRIORITY);
                }
                if (i == 20)
                {
                    PriorityTest high = new PriorityTest("高级");
                    high.start();
                    System.out.println("创建之初的优先级:"
                        + high.getPriority());
                    // 设置该线程为最高优先级
                    high.setPriority(Thread.MAX_PRIORITY);
                }
            }
        }
    }

    线程同步

    线程安全

    典型问题:

    账户类:

    public class Account
    {
        // 封装账户编号、账户余额的两个成员变量
        private String accountNo;
        private double balance;
        public Account(){}
        // 构造器
        public Account(String accountNo , double balance)
        {
            this.accountNo = accountNo;
            this.balance = balance;
        }
        // 此处省略了accountNo和balance的setter和getter方法
    
        // accountNo的setter和getter方法
        public void setAccountNo(String accountNo)
        {
            this.accountNo = accountNo;
        }
        public String getAccountNo()
        {
            return this.accountNo;
        }
    
        // balance的setter和getter方法
        public void setBalance(double balance)
        {
            this.balance = balance;
        }
        public double getBalance()
        {
            return this.balance;
        }
    
        // 下面两个方法根据accountNo来重写hashCode()和equals()方法
        public int hashCode()
        {
            return accountNo.hashCode();
        }
        public boolean equals(Object obj)
        {
            if(this == obj)
                return true;
            if (obj !=null
                && obj.getClass() == Account.class)
            {
                Account target = (Account)obj;
                return target.getAccountNo().equals(accountNo);
            }
            return false;
        }
    }

    取钱线程类:

    public class DrawThread extends Thread
    {
        // 模拟用户账户
        private Account account;
        // 当前取钱线程所希望取的钱数
        private double drawAmount;
        public DrawThread(String name , Account account
            , double drawAmount)
        {
            super(name);
            this.account = account;
            this.drawAmount = drawAmount;
        }
        // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
        public void run()
        {
            // 账户余额大于取钱数目
            if (account.getBalance() >= drawAmount)
            {
                // 吐出钞票
                System.out.println(getName()
                    + "取钱成功!吐出钞票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改余额
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("	余额为: " + account.getBalance());
            }
            else
            {
                System.out.println(getName() + "取钱失败!余额不足!");
            }
        }
    }
    public class DrawTest
    {
        public static void main(String[] args)
        {
            // 创建一个账户
            Account acct = new Account("1234567" , 1000);
            // 模拟两个线程对同一个账户取钱
            new DrawThread("甲" , acct , 800).start();
            new DrawThread("乙" , acct , 800).start();
        }
    }

    注释线程休眠的运行结果:

    出现这种错误是因为线程调度的不确定性。

    同步代码块:

    用账户类作为同步监视器

    public class DrawThread extends Thread
    {
        // 模拟用户账户
        private Account account;
        // 当前取钱线程所希望取的钱数
        private double drawAmount;
        public DrawThread(String name , Account account
                , double drawAmount)
        {
            super(name);
            this.account = account;
            this.drawAmount = drawAmount;
        }
        // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
        public void run()
        {
            // 使用account作为同步监视器,任何线程进入下面同步代码块之前,
            // 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
            // 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
            synchronized (account)
            {
                // 账户余额大于取钱数目
                if (account.getBalance() >= drawAmount)
                {
                    // 吐出钞票
                    System.out.println(getName()
                            + "取钱成功!吐出钞票:" + drawAmount);
                    try
                    {
                        Thread.sleep(1);
                    }
                    catch (InterruptedException ex)
                    {
                        ex.printStackTrace();
                    }
                    // 修改余额
                    account.setBalance(account.getBalance() - drawAmount);
                    System.out.println("	余额为: " + account.getBalance());
                }
                else
                {
                    System.out.println(getName() + "取钱失败!余额不足!");
                }
            }
            // 同步代码块结束,该线程释放同步锁
        }
    }

    同步方法:

    synchronize修饰的实例方法,就是同步方法,不用指定同步监视器,同步方法的同步监视器就是this。

    通过使用同步方法可以方便的实现线程安全的类。该类的对象可以被多个线程安全地访问,每个线程调用对象的任意方法都将得到正确的结果,每个线程调用该对象的任一方法之后该对象状态依然保持合理状态。

    public class Account
    {
        // 封装账户编号、账户余额两个成员变量
        private String accountNo;
        private double balance;
        public Account(){}
        // 构造器
        public Account(String accountNo , double balance)
        {
            this.accountNo = accountNo;
            this.balance = balance;
        }
    
        // accountNo的setter和getter方法
        public void setAccountNo(String accountNo)
        {
            this.accountNo = accountNo;
        }
        public String getAccountNo()
        {
            return this.accountNo;
        }
        // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
        public double getBalance()
        {
            return this.balance;
        }
    
        // 提供一个线程安全draw()方法来完成取钱操作
        public synchronized void draw(double drawAmount)
        {
            // 账户余额大于取钱数目
            if (balance >= drawAmount)
            {
                // 吐出钞票
                System.out.println(Thread.currentThread().getName()
                    + "取钱成功!吐出钞票:" + drawAmount);
                try
                {
                    Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                }
                // 修改余额
                balance -= drawAmount;
                System.out.println("	余额为: " + balance);
            }
            else
            {
                System.out.println(Thread.currentThread().getName()
                    + "取钱失败!余额不足!");
            }
        }
    
        // 下面两个方法根据accountNo来重写hashCode()和equals()方法
        public int hashCode()
        {
            return accountNo.hashCode();
        }
        public boolean equals(Object obj)
        {
            if(this == obj)
                return true;
            if (obj !=null
                && obj.getClass() == Account.class)
            {
                Account target = (Account)obj;
                return target.getAccountNo().equals(accountNo);
            }
            return false;
        }
    }

    以上增加了取钱的方法,去掉了设置金额的方法。

    public class DrawThread extends Thread
    {
        // 模拟用户账户
        private Account account;
        // 当前取钱线程所希望取的钱数
        private double drawAmount;
        public DrawThread(String name , Account account
            , double drawAmount)
        {
            super(name);
            this.account = account;
            this.drawAmount = drawAmount;
        }
        // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
        public void run()
        {
            // 直接调用account对象的draw方法来执行取钱
            // 同步方法的同步监视器是this,this代表调用draw()方法的对象。
            // 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
            account.draw(drawAmount);
        }
    }

    在实体类中设置取钱方法,而不是在run方法中实现逻辑,这样更符合面向对象思想。

    注意:不要线程安全类所有方法都进行同步,如果可变类有多线程和单线程两种运行环境,则应为该可变类提供两种版本。

    释放同步监视器的锁定

    线程进入同步代码块同步方法之前,必须先获得对同步监视器的锁定,会在以下几种情况释放对同步监视器的锁定。

    同步锁

    通过显示定义同步锁对象来实现同步,同步锁Lock对象。

    Lock提供了比同步代码块和同步方法更广泛的锁定操作,Lock允许实现灵活的结构,可以具有差别很大的属性,并支持多个相关的Condition对象

    Lock是控制多个线程对共享资源进行访问的工具。

    public class Account
    {
        // 定义锁对象
        private final ReentrantLock lock = new ReentrantLock();
        // 封装账户编号、账户余额的两个成员变量
        private String accountNo;
        private double balance;
        public Account(){}
        // 构造器
        public Account(String accountNo , double balance)
        {
            this.accountNo = accountNo;
            this.balance = balance;
        }
    
        // accountNo的setter和getter方法
        public void setAccountNo(String accountNo)
        {
            this.accountNo = accountNo;
        }
        public String getAccountNo()
        {
            return this.accountNo;
        }
        // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
        public double getBalance()
        {
            return this.balance;
        }
    
        // 提供一个线程安全draw()方法来完成取钱操作
        public void draw(double drawAmount)
        {
            // 加锁
            lock.lock();
            try
            {
                // 账户余额大于取钱数目
                if (balance >= drawAmount)
                {
                    // 吐出钞票
                    System.out.println(Thread.currentThread().getName()
                        + "取钱成功!吐出钞票:" + drawAmount);
                    try
                    {
                        Thread.sleep(1);
                    }
                    catch (InterruptedException ex)
                    {
                        ex.printStackTrace();
                    }
                    // 修改余额
                    balance -= drawAmount;
                    System.out.println("	余额为: " + balance);
                }
                else
                {
                    System.out.println(Thread.currentThread().getName()
                        + "取钱失败!余额不足!");
                }
            }
            finally
            {
                // 修改完成,释放锁
                lock.unlock();
            }
        }
    
        // 下面两个方法根据accountNo来重写hashCode()和equals()方法
        public int hashCode()
        {
            return accountNo.hashCode();
        }
        public boolean equals(Object obj)
        {
            if(this == obj)
                return true;
            if (obj !=null
                && obj.getClass() == Account.class)
            {
                Account target = (Account)obj;
                return target.getAccountNo().equals(accountNo);
            }
            return false;
        }
    }

    使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同时只能有一个线程能进入临界区。

    ReentrantLock 具有可重入性,一个线程对已被加锁的ReentrantLock锁可以再次加锁,ReentrantLock对象会维持一个计数器追踪lock()方法的嵌套调用,线程在每次调用lock()后,必须显示的调用unlock()方法释放锁,所以一段被锁保护的代码可以嗲用另一个被相同锁保护的方法。

    死锁

    两个线程相互等待对方释放同步监视器。

    class A
    {
        public synchronized void foo( B b )
        {
            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()
        {
            System.out.println("进入了A类的last()方法内部");
        }
    }
    class B
    {
        public synchronized void bar( A a )
        {
            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()
        {
            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();
            // 以dl为target启动新线程
            new Thread(dl).start();
            // 调用init()方法
            dl.init();
        }
    }

    线程通信

    传统的线程通信

    假设现在不同地点的存钱者和取钱者,需要不断地交替存取,不允许连续存两次或取两次,

    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;
        }
    
        // accountNo的setter和getter方法
        public void setAccountNo(String accountNo)
        {
            this.accountNo = accountNo;
        }
        public String getAccountNo()
        {
            return this.accountNo;
        }
        // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
        public double getBalance()
        {
            return this.balance;
        }
    
        public synchronized void draw(double drawAmount)
        {
            try
            {
                // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
                if (!flag)
                {
                    wait();
                }
                else
                {
                    // 执行取钱
                    System.out.println(Thread.currentThread().getName()
                        + " 取钱:" +  drawAmount);
                    balance -= drawAmount;
                    System.out.println("账户余额为:" + balance);
                    // 将标识账户是否已有存款的旗标设为false。
                    flag = false;
                    // 唤醒其他线程
                    notifyAll();
                }
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
        }
        public synchronized void deposit(double depositAmount)
        {
            try
            {
                // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
                if (flag)             //
                {
                    wait();
                }
                else
                {
                    // 执行存款
                    System.out.println(Thread.currentThread().getName()
                        + " 存款:" +  depositAmount);
                    balance += depositAmount;
                    System.out.println("账户余额为:" + balance);
                    // 将表示账户是否已有存款的旗标设为true
                    flag = true;
                    // 唤醒其他线程
                    notifyAll();
                }
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
        }
    
        // 下面两个方法根据accountNo来重写hashCode()和equals()方法
        public int hashCode()
        {
            return accountNo.hashCode();
        }
        public boolean equals(Object obj)
        {
            if(this == obj)
                return true;
            if (obj !=null
                && obj.getClass() == Account.class)
            {
                Account target = (Account)obj;
                return target.getAccountNo().equals(accountNo);
            }
            return false;
        }
    }
    public class DrawThread extends Thread
    {
        // 模拟用户账户
        private Account account;
        // 当前取钱线程所希望取的钱数
        private double drawAmount;
        public DrawThread(String name , Account account
            , double drawAmount)
        {
            super(name);
            this.account = account;
            this.drawAmount = drawAmount;
        }
        // 重复100次执行取钱操作
        public void run()
        {
            for (int i = 0 ; i < 100 ; i++ )
            {
                account.draw(drawAmount);
            }
        }
    }
    public class DepositThread extends Thread
    {
        // 模拟用户账户
        private Account account;
        // 当前取钱线程所希望存款的钱数
        private double depositAmount;
        public DepositThread(String name , Account account
            , double depositAmount)
        {
            super(name);
            this.account = account;
            this.depositAmount = depositAmount;
        }
        // 重复100次执行存款操作
        public void run()
        {
            for (int i = 0 ; i < 100 ; i++ )
            {
                account.deposit(depositAmount);
            }
        }
    }
    public class DrawTest
    {
        public static void main(String[] args)
        {
            // 创建一个账户
            Account acct = new Account("1234567" , 0);
            new DrawThread("取钱者" , acct , 800).start();
            new DepositThread("存款者甲" , acct , 800).start();
            new DepositThread("存款者乙" , acct , 800).start();
            new DepositThread("存款者丙" , acct , 800).start();
        }
    }

    使用Condition控制线程通信

    如果程序直接使用Lock对象保证线程同步,则系统不存在隐式的同步监视器,也就不能用wait()等方法进行线程通信。

    当使用Lock对象保证同步时,Java提供了一个Contidion类来协调,使用Contidion可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象。Contidion也可以唤醒其他处于等待的线程。

    Contidion将同步监视器方法(wait等)分解成截然不同的对象,以便通过这些对象与Lock对象组合使用,为每个对象提供多个等待集。

    Contidion实例被绑定在一个Lock对象上,要获得特定的Lock实例的Contidion实例,调用Lock对象的newContidion()方法即可。

    public class Account
    {
        // 显式定义Lock对象
        private final Lock lock = new ReentrantLock();
        // 获得指定Lock对象对应的Condition
        private final Condition cond  = lock.newCondition();
        // 封装账户编号、账户余额的两个成员变量
        private String accountNo;
        private double balance;
        // 标识账户中是否已有存款的旗标
        private boolean flag = false;
    
        public Account(){}
        // 构造器
        public Account(String accountNo , double balance)
        {
            this.accountNo = accountNo;
            this.balance = balance;
        }
    
        // accountNo的setter和getter方法
        public void setAccountNo(String accountNo)
        {
            this.accountNo = accountNo;
        }
        public String getAccountNo()
        {
            return this.accountNo;
        }
        // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
        public double getBalance()
        {
            return this.balance;
        }
    
        public void draw(double drawAmount)
        {
            // 加锁
            lock.lock();
            try
            {
                // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
                if (!flag)
                {
                    cond.await();
                }
                else
                {
                    // 执行取钱
                    System.out.println(Thread.currentThread().getName()
                        + " 取钱:" +  drawAmount);
                    balance -= drawAmount;
                    System.out.println("账户余额为:" + balance);
                    // 将标识账户是否已有存款的旗标设为false。
                    flag = false;
                    // 唤醒其他线程
                    cond.signalAll();
                }
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            // 使用finally块来释放锁
            finally
            {
                lock.unlock();
            }
        }
        public void deposit(double depositAmount)
        {
            lock.lock();
            try
            {
                // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
                if (flag)             //
                {
                    cond.await();
                }
                else
                {
                    // 执行存款
                    System.out.println(Thread.currentThread().getName()
                        + " 存款:" +  depositAmount);
                    balance += depositAmount;
                    System.out.println("账户余额为:" + balance);
                    // 将表示账户是否已有存款的旗标设为true
                    flag = true;
                    // 唤醒其他线程
                    cond.signalAll();
                }
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            // 使用finally块来释放锁
            finally
            {
                lock.unlock();
            }
        }
    
        // 下面两个方法根据accountNo来重写hashCode()和equals()方法
        public int hashCode()
        {
            return accountNo.hashCode();
        }
        public boolean equals(Object obj)
        {
            if(this == obj)
                return true;
            if (obj !=null
                && obj.getClass() == Account.class)
            {
                Account target = (Account)obj;
                return target.getAccountNo().equals(accountNo);
            }
            return false;
        }
    }

    使用阻塞队列控制线程通信

    Java5提供了BlockingQueue接口,主要用途是作为线程同步的工具。特征:当生产者线程像BlockingQueue中放入元素时,如果队列已满线程被阻塞,当消费者试图从BlockingQueue取出元素,如果队列空线程被阻塞。

    BlockingQueue还有基于数组、链表等的实现类。

    public class BlockingQueueTest
    {
        public static void main(String[] args)
            throws Exception
        {
            // 定义一个长度为2的阻塞队列
            BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
            bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
            bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
            bq.put("Java"); // ① 阻塞线程。
        }
    }

    程序会一直阻塞。

    class Producer extends Thread
    {
        private BlockingQueue<String> bq;
        public Producer(BlockingQueue<String> bq)
        {
            this.bq = bq;
        }
        public void run()
        {
            String[] strArr = new String[]
            {
                "Java",
                "Struts",
                "Spring"
            };
            for (int i = 0 ; i < 999999999 ; i++ )
            {
                System.out.println(getName() + "生产者准备生产集合元素!");
                try
                {
                    Thread.sleep(200);
                    // 尝试放入元素,如果队列已满,线程被阻塞
                    bq.put(strArr[i % 3]);
                }
                catch (Exception ex){ex.printStackTrace();}
                System.out.println(getName() + "生产完成:" + bq);
            }
        }
    }
    class Consumer extends Thread
    {
        private BlockingQueue<String> bq;
        public Consumer(BlockingQueue<String> bq)
        {
            this.bq = bq;
        }
        public void run()
        {
            while(true)
            {
                System.out.println(getName() + "消费者准备消费集合元素!");
                try
                {
                    Thread.sleep(200);
                    // 尝试取出元素,如果队列已空,线程被阻塞
                    bq.take();
                }
                catch (Exception ex){ex.printStackTrace();}
                System.out.println(getName() + "消费完成:" + bq);
            }
        }
    }
    public class BlockingQueueTest2
    {
        public static void main(String[] args)
        {
            // 创建一个容量为1的BlockingQueue
            BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
            // 启动3条生产者线程
            new Producer(bq).start();
            new Producer(bq).start();
            new Producer(bq).start();
            // 启动一条消费者线程
            new Consumer(bq).start();
        }
    }

    线程组和未处理异常

    ThreadGroup代表线程组,Java允许程序直接对线程组进行控制,对线程组的控制相当于同时控制这批线程。

    class MyThread extends Thread
    {
        // 提供指定线程名的构造器
        public MyThread(String name)
        {
            super(name);
        }
        // 提供指定线程名、线程组的构造器
        public MyThread(ThreadGroup group , String name)
        {
            super(group, name);
        }
        public void run()
        {
            for (int i = 0; i < 20 ; i++ )
            {
                System.out.println(getName() + " 线程的i变量" + i);
            }
        }
    }
    public class ThreadGroupTest
    {
        public static void main(String[] args)
        {
            // 获取主线程所在的线程组,这是所有线程默认的线程组
            ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
            System.out.println("主线程组的名字:"
                + mainGroup.getName());
            System.out.println("主线程组是否是后台线程组:"
                + mainGroup.isDaemon());
            new MyThread("主线程组的线程").start();
            ThreadGroup tg = new ThreadGroup("新线程组");
            tg.setDaemon(true);
            System.out.println("tg线程组是否是后台线程组:"
                + tg.isDaemon());
            MyThread tt = new MyThread(tg , "tg组的线程甲");
            tt.start();
            new MyThread(tg , "tg组的线程乙").start();
        }
    }

    为主线程设置异常处理器

    class MyExHandler implements Thread.UncaughtExceptionHandler
    {
        // 实现uncaughtException方法,该方法将处理线程的未处理异常
        public void uncaughtException(Thread t, Throwable e)
        {
            System.out.println(t + " 线程出现了异常:" + e);
        }
    }
    public class ExHandler
    {
        public static void main(String[] args)
        {
            // 设置主线程的异常处理器
            Thread.currentThread().setUncaughtExceptionHandler
                (new MyExHandler());
            int a = 5 / 0;     //
            System.out.println("程序正常结束!");
        }
    }

    线程池

     系统启动一个线程的成本是比较高的,当程序中需要创建大量生存期限很短的线程时,更应该考虑使用线程池。

    Java8改进的线程池

    Java5增加的Executors工厂类来产生线程池,该工厂类提供一下几个静态工厂方法创建线程池

    用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接受新任务,但会将以前所有已提交的任务执行完成。

    也可以调用shutdownNow()方法关闭线程池,该方法会试图停止所有正在执行的活动任务,,暂停处理正在等待的任务,并返回等待执行的任务列表。

    使用线程池执行线程步骤:

    调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。

    创建Runnable实现类或Callable实现类的实例,作为线程执行任务。

    调用ExecutorService对象的submit()方法来提交Run那边了实例或Callable实例。

    当不想提交任务时,调用ExecutorService对象得shutdown()方法来关闭线程池。

    public class ThreadPoolTest
    {
        public static void main(String[] args)
            throws Exception
        {
            // 创建足够的线程来支持4个CPU并行的线程池
            // 创建一个具有固定线程数(6)的线程池
            ExecutorService pool = Executors.newFixedThreadPool(6);
            // 使用Lambda表达式创建Runnable对象
            Runnable target = () -> {
                for (int i = 0; i < 100 ; i++ )
                {
                    System.out.println(Thread.currentThread().getName()
                        + "的i值为:" + i);
                }
            };
            // 向线程池中提交两个线程
            pool.submit(target);
            pool.submit(target);
            // 关闭线程池
            pool.shutdown();
        }
    }

    Java8增强的ForkJoinPool

    Java7提供了ForkJoinPool将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。。ForkJoinPool是ExecutorService的实现类,是一种特殊的线程池。

    Java8拓展了ForkJoinPool的功能,增加了通用池的功能。ForkJoinPool是一个抽象类,有两个抽象子类:RecursiveAction和RecursiveTask

    // 继承RecursiveAction来实现"可分解"的任务
    class PrintTask extends RecursiveAction
    {
        // 每个“小任务”只最多只打印50个数
        private static final int THRESHOLD = 50;
        private int start;
        private int end;
        // 打印从start到end的任务
        public PrintTask(int start, int end)
        {
            this.start = start;
            this.end = end;
        }
        @Override
        protected void compute()
        {
            // 当end与start之间的差小于THRESHOLD时,开始打印
            if(end - start < THRESHOLD)
            {
                for (int i = start ; i < end ; i++ )
                {
                    System.out.println(Thread.currentThread().getName()
                        + "的i值:" + i);
                }
            }
            else
            {
                // 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过50个
                // 将大任务分解成两个小任务。
                int middle = (start + end) / 2;
                PrintTask left = new PrintTask(start, middle);
                PrintTask right = new PrintTask(middle, end);
                // 并行执行两个“小任务”
                left.fork();
                right.fork();
            }
        }
    }
    public class ForkJoinPoolTest
    {
        public static void main(String[] args)
            throws Exception
        {
            ForkJoinPool pool = new ForkJoinPool();
            // 提交可分解的PrintTask任务
            pool.submit(new PrintTask(0 , 300));
            pool.awaitTermination(2, TimeUnit.SECONDS);
            // 关闭线程池
            pool.shutdown();
        }
    }

    使用RecursiveTask对一个长度为100的数组的元素值进行累加

    // 继承RecursiveTask来实现"可分解"的任务
    class CalTask extends RecursiveTask<Integer>
    {
        // 每个“小任务”只最多只累加20个数
        private static final int THRESHOLD = 20;
        private int arr[];
        private int start;
        private int end;
        // 累加从start到end的数组元素
        public CalTask(int[] arr , int start, int end)
        {
            this.arr = arr;
            this.start = start;
            this.end = end;
        }
        @Override
        protected Integer compute()
        {
            int sum = 0;
            // 当end与start之间的差小于THRESHOLD时,开始进行实际累加
            if(end - start < THRESHOLD)
            {
                for (int i = start ; i < end ; i++ )
                {
                    sum += arr[i];
                }
                return sum;
            }
            else
            {
                // 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
                // 将大任务分解成两个小任务。
                int middle = (start + end) / 2;
                CalTask left = new CalTask(arr , start, middle);
                CalTask right = new CalTask(arr , middle, end);
                // 并行执行两个“小任务”
                left.fork();
                right.fork();
                // 把两个“小任务”累加的结果合并起来
                return left.join() + right.join();    //
            }
        }
    }
    public class Sum
    {
        public static void main(String[] args)
            throws Exception
        {
            int[] arr = new int[100];
            Random rand = new Random();
            int total = 0;
            // 初始化100个数字元素
            for (int i = 0 , len = arr.length; i < len ; i++ )
            {
                int tmp = rand.nextInt(20);
                // 对数组元素赋值,并将数组元素的值添加到sum总和中。
                total += (arr[i] = tmp);
            }
            System.out.println(total);
            // 创建一个通用池
            ForkJoinPool pool = ForkJoinPool.commonPool();
            // 提交可分解的CalTask任务
            Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
            System.out.println(future.get());
            // 关闭线程池
            pool.shutdown();
        }
    }

    线程相关的类

    ThreadLocal

    线程局部变量,为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立的改变自己的副本,而不会和其他线程副本产生冲突。

    class Account
    {
        /* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
        每个线程都会保留该变量的一个副本 */
        private ThreadLocal<String> name = new ThreadLocal<>();
        // 定义一个初始化name成员变量的构造器
        public Account(String str)
        {
            this.name.set(str);
            // 下面代码用于访问当前线程的name副本的值
            System.out.println("---" + this.name.get());
        }
        // name的setter和getter方法
        public String getName()
        {
            return name.get();
        }
        public void setName(String str)
        {
            this.name.set(str);
        }
    }
    class MyTest extends Thread
    {
        // 定义一个Account类型的成员变量
        private Account account;
        public MyTest(Account account, String name)
        {
            super(name);
            this.account = account;
        }
        public void run()
        {
            // 循环10次
            for (int i = 0 ; i < 10 ; i++)
            {
                // 当i == 6时输出将账户名替换成当前线程名
                if (i == 6)
                {
                    account.setName(getName());
                }
                // 输出同一个账户的账户名和循环变量
                System.out.println(account.getName()
                    + " 账户的i值:" + i);
            }
        }
    }
    public class ThreadLocalTest
    {
        public static void main(String[] args)
        {
            // 启动两条线程,两条线程共享同一个Account
            Account at = new Account("初始名");
            /*
            虽然两条线程共享同一个账户,即只有一个账户名
            但由于账户名是ThreadLocal类型的,所以每条线程
            都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
            线程访问同一个账户时看到不同的账户名。
            */
            new MyTest(at , "线程甲").start();
            new MyTest(at , "线程乙").start ();
        }
    }

    线程安全的集合类

    线程安全的集合类可以分为:

    以Concurrent开头的集合类,,CopyOnWrite开头的集合类

    Concurrent开头的集合类代表了支持并发访问的集合支持多个线程并发写入访问,写入操作都是线程安全的,但读取操作却不锁定。

    当多个线程访问一个公共集合,可以使用ConcurrentLinkedQueue。ConcurrentLinkedQueue不允许null。

    ConcurrentHashMap最多支持16个线程并发写入,多余时需要等待。

    CopyOnWriteArraySet底层封装了CopyOnWriteArrayList,因此实现机制完全类似。CopyOnWriteArrayList采用复制底层数组的方式实现写操作。

    Java9新增的发布-订阅框架

    public class PubSubTest
    {
        public static void main(String[] args)
        {
            // 创建一个SubmissionPublisher作为发布者
            SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
            // 创建订阅者
            MySubscriber<String> subscriber = new MySubscriber<>();
            // 注册订阅者
            publisher.subscribe(subscriber);
            // 发布几个数据项
            System.out.println("开发发布数据...");
            List.of("Java", "Kotlin", "Go", "Erlang", "Swift", "Lua")
                .forEach(im -> {
                // 提交数据
                publisher.submit(im);
                try
                {
                    Thread.sleep(500);
                }
                catch (Exception ex){}
            });
            // 发布结束
            publisher.close();
            // 发布结束后,为了让发布者线程不会死亡,暂停线程
            synchronized("fkjava")
            {
                try
                {
                    "fkjava".wait();
                }
                catch (Exception ex){}
            }
        }
    }
    // 创建订阅者
    class MySubscriber<T> implements Subscriber<T>
    {
        // 发布者与订阅者之间的纽带
        private Subscription subscription;
        @Override  // 订阅时触发该方法
        public void onSubscribe(Subscription subscription)
        {
            this.subscription = subscription;
            // 开始请求数据
            subscription.request(1);
        }
        @Override  // 接收到数据时触发该方法
        public void onNext(T item)
        {
            System.out.println("获取到数据: " + item);
            // 请求下一条数据
            subscription.request(1);
        }
        @Override // 订阅出错时触发该方法
        public void onError(Throwable t)
        {
            t.printStackTrace();
            synchronized("fkjava")
            {
                "fkjava".notifyAll();
            }
        }
        @Override  // 订阅结束时触发该方法
        public void onComplete()
        {
            System.out.println("订阅结束");
            synchronized("fkjava")
            {
                "fkjava".notifyAll();
            }
        }
    }
  • 相关阅读:
    Distributed
    Archi
    SpringCloud
    Java 多线程
    Java 基础
    Java 基础
    Java 基础
    Java 基础
    python--all与any
    pandas--apply/applymap/map
  • 原文地址:https://www.cnblogs.com/jiangwz/p/9051337.html
Copyright © 2020-2023  润新知