并发编程(一):线程基础、线程之间的共享与协作

版权声明:转载请注明出处 https://blog.csdn.net/aimashi620/article/details/82017700

一、基础概念

1.1 CPU核心数、线程数

**两者的关系:**cpu的核心数与线程数是1:1的关系,例如一个8核的cpu,支持8个线程同时运行。但在intel引入超线程技术以后,cpu与线程数的关系就变成了1:2。此外在开发过程中并没感觉到线程的限制,那是因为cpu时间片轮转机制(RR调度)的算法的作用。什么是cpu时间片轮转机制看下面1.2.

1.2 CPU时间片轮转机制

**含义就是:**cpu给每个进程分配一个“时间段”,这个时间段就叫做这个进程的“时间片”,这个时间片就是这个进程允许运行的时间,如果当这个进程的时间片段结束,操作系统就会把分配给这个进程的cpu剥夺,分配给另外一个进程。如果进程在时间片还没结束的情况下阻塞了,或者说进程跑完了,cpu就会进行切换。cpu在两个进程之间的切换称为“上下文切换”,上下文切换是需要时间的,大约需要花费5000~20000(5毫秒到20毫秒,这个花费的时间是由操作系统决定)个时钟周期,尽管我们平时感觉不到。所以在开发过程中要注意上下文切换(两个进程之间的切换)对我们程序性能的影响。

1.3 什么是进程和线程

进程:它是属于程序调度/运行的资源分配的最小单位,一个进程的内部可能有多个线程,多个线程之间会共享这个进程的资源。进程与进程直线是相互独立的
线程:它是cpu调度的最小单位,线程本身是不能独立进行的,它必须依附某个进程,线程本身是不拥有系统资源的。

1.4 什么是并行和并发

并行:例如一个饭堂有八个窗口,也就是说,同一时刻可以有8个人进行打饭,那么也就说这个饭堂的并行度是8
并发:它严格说起来是要与一个时间单位相关的,简单来说就是一个时间段内可以同时处理的事情数。例如这个饭堂有8个窗口,每个人打饭需要花费30秒钟,那么一分钟内(8个窗口 * 60秒 / 每人打饭需要30秒 = 16)这个饭堂的并发度就是16
总结:并行是同一时刻可以处理多少件事,并发是在单位时间内可以处理多少件事情。

1.5 高并发编程的意义、好处和注意事项

通过以上1.1~1.4的了解,我们可以知道高并发编程可以充分利用cpu的资源,例如一个8核的cpu跑一个单线的程序,那么意味着在任意时刻,我有7个CPU核心是浪费掉的。另外可以充分地加快用户的响应时间。同时使用并发编程可以使我们的代码模块化、异步化。
注意事项/难点: 有与线程之间会共享进程的资源,既然说是共享资源,就有可能存在冲突。在高并发编程中如果控制不好,还有可能会造成线程的死锁(什么是死锁在我之后的文章会详细讲解,读者也可自行百度)。每启动一个线程,操作系统就需要为这个线程分配一定的资源,线程数太多还可能会把内容消耗完毕,会导致系统死机。

二、java中线程基础

2.1 启动和终止线程

启动线程的方式:
① 类Thread
② 接口Runnable(推荐使用这种,因为接口可以多实现)
③ 接口Callable:与Runnable的区别是,实现Runnabble接口里的run方法是没有返回值的,而Callable是允许有返回值的

package javax.test;

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

/**
 * @auther xiehuaxin
 * @create 2018-08-24 10:56
 * @todo
 */
public class Test {

    private static class RunnableThread implements Runnable {
        @Override
        public void run() {
            System.out.println("实现Runnable方式创建线程");
        }
    }

    private static class CallableThread implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "this is return result";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        RunnableThread runnableThread = new RunnableThread();
        //要启动实现Runnablede的线程的话还需要把runnable的实例传到Thread里
        new Thread(runnableThread).start();

        CallableThread callableThread = new CallableThread();
        //由于new Thread只接受Runnable类型的构造参数,所以要先把Callable包装一下
        FutureTask<String> futureTask = new FutureTask<>(callableThread);
        new Thread(futureTask).start();
        //获取返回值,get方法是阻塞的
        System.out.println(futureTask.get());
    }
}

输出:

this is return result
实现Runnable方式创建线程

线程结束的方法:
① 方法执行完自动终止
② 抛出异常,又没有捕获异常

③ 早期还提出过三个方法终止线程stop(), resume(), suspend() ,这三个方法都不建议用于终止线程
原因:一旦调用stop会强行终止线程,无法保证线程的资源正常释放。suspend()调用后线程是不会释放资源的,很容易引起死锁。

正确的终止线程的方法:interrupt(),isinterrupted()以及静态方法interrupted().
三者的区别:
① interrupt():是属于Thread类的方法,作用终止一个线程,但并不是强行关闭一个线程(java的线程是协作式的,不是强迫式的,调用一个线程的interrupt()方法并不会强制关闭一个线程,它就好比其他线程对要关闭的线程打了一声招呼,告诉被关闭线程它要中断了,但被关闭线程什么时候关闭完全由它自身做主),线程调用该方法并不会立刻终止。既然调用interrupt()方法起不到终止线程的目的,那么它为什么要这样设计?这样设计时为了让每个线程有充分的时间去做线程的清理工作。进行开发的时候要对线程的中断请求进行处理,否则就不是一个设计良好的并发程序。总的来说,它的目的只是把线程中的“中断标志位”置为true
② isInterrupted(),判定当前线程是否处于中断状态。通过这个方法判断中断标志位是否为true。
③ static方法isInterrupted(), 也是判断当前线程是否处于中断状态。当调用此方法时,它会把中断标志位改为false。

2.2 线程再认识

① 当线程调用了wait(),join(),sleep()方法时,方法会抛出InterruptedException,这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。

package javax.test;

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

/**
 * @auther xiehuaxin
 * @create 2018-08-24 10:56
 * @todo 如何安全地中断线程
 */
public class Test{

    private static class UseThread extends Thread {
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            //如果现在这个while的条件不是“!isInterrupted()”而是“true”,那么即使main方法里调用了test.interrupt()还是无法终止线程的,这就是java协作式。
            while (!isInterrupted()) {
                System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    interrupt();//当线程调用了wait(),join(),sleep()方法时,方法会抛出InterruptedException,这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。
                }
            }
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        UseThread test = new UseThread("myThread");
        test.start();
        //让main线程等待
        Thread.sleep(500);
        test.interrupt();

    }
}

如果是通过实现Runnable接口创建的线程,那么怎么正确地停止线程呢?因为interrupt()方法和isInterrupted()方法都是属于Thread类的,这个时候我们就要通过Thread.currentThread()来调用了,如下

  private static class UseRunnable implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();//当线程调用了wait(),join(),sleep()方法时,方法会抛出InterruptedException,这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。
                }
            }
        }
    }

另外,有的人喜欢在类里定义一个boolean的标志来中断线程。这种方式一般情况下能工作,但有些情况又不能工作、譬如在线程内调用了一个阻塞的方法(sleep\wait…),那么线程会一直阻塞在那个地方,这个时候即使你改变了自己定义的标志,它也不会去判断while的条件的,一定要线程从阻塞中被唤醒、并且执行完阻塞后的代码后才会回到while的判断,这就造成了线程在处理响应时非常不及时。

② 开发过程中也可为线程设置优先级,线程的优先级的范围为1~10,缺省值为5,优先级较高的线程获得分配的时间片就较高。调用Thread.setPriority()方法进行设置。这个优先级的设置在不同的操作系统中会有不一样的结果,有些系统设置会忽略这个优先级的设定,有的操作系统可能全部给你设置为5,所以在程序开发过程中, 不能指望这个操作。

③ 守护线程:守护线程的线程和主线程是同时销毁的,主线程退出了,守护进程就一定会结束。设置守护进程是通过Thread.setDaemon(true)进行设置的,而且需要在调用start()方法之前设置。使用守护线程需要注意:守护进程里非try..finally是不能保证一定执行finally的。

④ volatile是最轻量级的保证同步的方法,但它一般只使用于一个线程写,多个线程读这种场景。

⑤ run()和start()的区别:调用run方法其实就像普通的类调用类中的方法一样,run()方法由谁调用就归宿与哪个线程。只有调用start()方法才是真正的启动线程。

这里写图片描述
⑥ 就绪状态也成为可运行状态,调用了一个线程的start()方法,形成就处于可运行状态,但这个时候并不是运行状态,只有当cpu调度到该线程才是运行状态。

⑦ yield()方法的作用是,当一个线程运行完了,就把cpu的时间让出来。那么它与sleep()方法的区别呢?调用sleep()方法,在sleep()的时间段内,cpu是不会再调度该线程,但是调用了yield()方法的下一时刻,cpu还是有可能会调度到该线程的

2.3 线程间的共享

synchronized(内置锁),要么加载方法上面,要么是用作同步块的形式来使用,最大的作用是确保在多个线程在同一时刻只能有一个线程处于方法或同步块之中,这样它就保证了线程对变量访问的可见性与排差性。 锁的是对象,不是代码块,每个对象在内存的对象头上有一个标志位,标志位上有1~2个字节标志它为一个锁,synchronized的作用就是当所有的线程去抢这个对象的标志位,谁把这个标志位指向了自己,那就认为这个线程抢到了这个锁。

对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

在WEB框架开发过程中,常使用到spring框架,在spring框架里面没有做特殊设置的话,sping为我们创建的对象缺省是只有一个的,所以这个时候可以保证锁住的是同一个对象,但是如果在配置中,sping运行new出多个对象,这个时候加锁的时候就要考虑锁的是不是同一个对象了。

volatile是虚拟机提供的最轻量的同步机制(因为它的读取和写入并没有做synchronized处理),声明了该变量为volatile的话,就是告诉虚拟机,每次要用该变量时,总是要在主内存中读取。volatile并不是线程安全的,只能保证变量的可见性,不能保证原子性,例如:age = age+20,虚拟机不能保证你这个操作时原子的。

ThreadLoacl(线程变量):可以确保每个线程只使用自己那一部分的东西。例如一个变量使用ThreadLocal包装的话,那么每个线程都是使用自己的那一份变量的拷贝。可以理解为Map

package javax.test;

/**
 * @auther xiehuaxin
 * @create 2018-08-26 14:40
 * @todo
 */
public class UseThreadLoacal {

/**
 *初始ThreadLocal
 *底层用map实现
 *这里的Thread存放的是Integer类型的数据,假如说要用来存放一个超级庞大的数据,由于每个线程都有一个自己的副本,这样对内存的资源的占用是相当大的,所以ThreadLocal尽量用来存放比较小的数据。
 */
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    public void startThreadArray() {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new TestThread(i));
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id) {
            this.id = id;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":start");
            Integer s = threadLocal.get();//获取变量的值
            s = s + id;
            threadLocal.set(s);//把值写回去
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }
    }

    public static void main(String[] args) {
        UseThreadLoacal useThreadLoacal = new UseThreadLoacal();
        useThreadLoacal.startThreadArray();
    }
}

2.4 线程间的协作

2.4.1 Thread.join:假如线程A调用了线程B的join方法,那么线程A必须等到线程B执行完成才能继续执行。

package javax.test;

/**
 * @auther xiehuaxin
 * @create 2018-08-26 15:49
 * @todo
 */
public class UseJoin {

    private static class JumpQuery implements Runnable{
        private Thread thread;//用来插队的线程
        public JumpQuery (Thread thread) {
            this.thread = thread;
        }
        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminted.");
        }
    }

    public static void main(String[] args) {
        Thread previous = Thread.currentThread();//初始时,这个是主线程
        for(int i = 0; i < 10; i++) {
            //i=0是,previous是主线程,i=1时,previous是i=0这个线程
            Thread thread = new Thread(new JumpQuery(previous),String.valueOf(i));
            System.out.println(previous.getName() + "jump a queue the thread:" + thread.getName());
            thread.start();
            previous = thread;
        }
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " termined.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

输出:

mainjump a queue the thread:0
0jump a queue the thread:1
1jump a queue the thread:2
2jump a queue the thread:3
3jump a queue the thread:4
4jump a queue the thread:5
5jump a queue the thread:6
6jump a queue the thread:7
7jump a queue the thread:8
8jump a queue the thread:9
main termined.
0 terminted.
1 terminted.
2 terminted.
3 terminted.
4 terminted.
5 terminted.
6 terminted.
7 terminted.
8 terminted.
9 terminted.

② 调用yield()方法和sleep()方法以后,持有的锁是不释放的,所以一般调用这两个方法的良好写法是不要写在synchronized代码块外面。
③ 调用wait()方法和notify()方法是会释放锁的,调用这两个方法的前提是必须持有锁,而且调用这两个方法之后,还是会把这两个方法所在的synchronized代码块中的代码执行完成才释放锁(调用这两个方法是不会释放锁的),所以良好的写法是写在synchronized代码块中的最后一行。
④ wait()、notify()、notifyAll()方法是和对象绑定一起的,话句话来说,就是你在object1上调用了notifyAll方法,那么通知的就是在object1上等待的线程,并不能通知到object2对象上的线程。

2.4.2 等待和通知

轮询无法保证及时性,资源的开销也比较大,大部分时间都在做无用功。为了解决这种情况,java里提供了一种等待和通知机制,当线程调用wait方法会进入等待状态,当调用notify或notifyAll(首选notifyAll,因为notify通知的是等待队列中的一个线程,有可能发生信号丢失的情况。看如下代码,把Express.java中的notifyAll方法改成notify观察输出结果)方法就会唤醒线程。wait、notify、notifyAll这三个方法是对象本身的方法,并不是线程的方法。

package com.cvc.nelson;

/**
 * @auther xiehuaxin
 * @create 2018-08-28 17:25
 * @todo
 */
public class Express {

    public static final String CITY = "ShangHai";
    //快递运输的里程数
    private int km;
    //快递到达的地点
    private String site;

    public Express() {
    }
    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /**
     * 公里数发生变化,然后通知处于wait状态并需要处理公里数的线程进行业务处理
     */
    public synchronized void changeKm() {
        this.km = 101;
        notify();
    }

    /**
     * 地点发生变化,然后通知处于wait状态并需要处理公里数的线程进行业务处理
     * 标准范式的第一步是等待和通知都要先拿到锁,所以加个synchronized
     */
    public synchronized void changeSite() {
        this.site = "BeiJing";
        notify();
    }


    public synchronized void waitKm() {
        //当里程数小于100时就继续等待
        while (this.km <= 100) {
            try {
                wait();
                System.out.println("check km thread["+Thread.currentThread().getId()+"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("km做自己的业务.........");
    }

    public synchronized void waitSite() {
        while (CITY.equals(this.site)) {
            try {
                wait();
                System.out.println("check site thread["+Thread.currentThread().getId()+"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("site做自己的业务.........");
    }
}
package com.cvc.nelson;

/**
 * @auther xiehuaxin
 * @create 2018-08-28 18:00
 * @todo
 */
public class TestWaitAndNotifyAll {
    private static Express express = new Express(0,Express.CITY);

    private static class CheckKm extends Thread {
        @Override
        public void run() {
            express.waitKm();
        }
    }

    private static class CheckSite extends Thread {
        @Override
        public void run() {
            express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0; i<3; i++) {
            new CheckSite().start();
        }
        for (int i = 0; i < 3; i++) {
            new CheckKm().start();
        }

        Thread.sleep(1000);
        express.changeKm();//快递地点发生变化
    }
}

在线程之间进行通信,往往有一个”等待和通知的标准范式”,如下:
调用wait的线程(等待方):
① 获取对象的锁
② 在一个循环里判定条件是否满足,不满足就调用wait方法
③ 条件满足就执行业务逻辑
通知方:
① 获取对象的锁
② 改变条件
③ 通知所有等待在对象的线

===================================
等待超时模式
如上代码,假如线程没有被唤醒的话,将会无限期等待下去,但是现实中我们是不允许这种情况存在的,一般是等待某一时间以后,如果拿到了结果就返回,如果没拿到结果依然要返回,返回之后根据没有拿到结果这种情形做另外一种处理。
假设等待时长为T,那超时的时间截点就是(当前时间+T)
伪代码如下:
定义一个超时时间long overTime = now + T;
long remain = T;//等待的持续时间
while(result不满足条件 && remain>0) {
wait(remain);
//
remain = overTime - now;//等待还剩下的持续时间
}
return result;
“等待超时模式”实现一个连接池

package com.xiangxue.ch1.pool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import com.xiangxue.tools.SleepTools;

/**
 *@author Mark老师   享学课堂 https://enjoy.ke.qq.com 
 *
 *类说明:实现了数据库连接的实现
 */
public class SqlConnectImpl implements Connection{

    /*拿一个数据库连接*/
    public static final Connection fetchConnection(){
        return new SqlConnectImpl();
    }
}
package com.xiangxue.ch1.pool;

import java.sql.Connection;
import java.util.LinkedList;

/**
 *@author Mark老师   享学课堂 https://enjoy.ke.qq.com 
 *
 *类说明:实现一个数据库的连接池
 */
public class DBPool {

    //数据库池的容器
    private static LinkedList<Connection> pool = new LinkedList<>();

    public DBPool(int initalSize) {
        if(initalSize>0) {
            for(int i=0;i<initalSize;i++) {
                pool.addLast(SqlConnectImpl.fetchConnection());
            }
        }
    }

    //在mills时间内还拿不到数据库连接,返回一个null
    public Connection fetchConn(long mills) throws InterruptedException {
        synchronized (pool) {
            if (mills<0) {
                while(pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            }else {
                long overtime = System.currentTimeMillis()+mills;
                long remain = mills;
                while(pool.isEmpty()&&remain>0) {
                    pool.wait(remain);
                    remain = overtime - System.currentTimeMillis();
                }
                Connection result  = null;
                if(!pool.isEmpty()) {
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }

    //放回数据库连接
    public void releaseConn(Connection conn) {
        if(conn!=null) {
            synchronized (pool) {
                pool.addLast(conn);
                pool.notifyAll();
            }
        }
    }


}
package com.xiangxue.ch1.pool;

import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *@author Mark老师   享学课堂 https://enjoy.ke.qq.com 
 *
 *类说明:
 */
public class DBPoolTest {
    static DBPool pool  = new DBPool(10);
    // 控制器:控制main线程将会等待所有Woker结束后才能继续执行
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
        // 线程数量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        int count = 20;//每个线程的操作次数
        AtomicInteger got = new AtomicInteger();//计数器:统计可以拿到连接的线程
        AtomicInteger notGot = new AtomicInteger();//计数器:统计没有拿到连接的线程
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot), 
                    "worker_"+i);
            thread.start();
        }
        end.await();// main线程在此处等待
        System.out.println("总共尝试了: " + (threadCount * count));
        System.out.println("拿到连接的次数:  " + got);
        System.out.println("没能连接的次数: " + notGot);
    }

    static class Worker implements Runnable {
        int           count;
        AtomicInteger got;
        AtomicInteger notGot;

        public Worker(int count, AtomicInteger got,
                               AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run() {
            while (count > 0) {
                try {
                    // 从线程池中获取连接,如果1000ms内无法获取到,将会返回null
                    // 分别统计连接获取的数量got和未获取到的数量notGot
                    Connection connection = pool.fetchConn(1000);
                    if (connection != null) {
                        try {
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            pool.releaseConn(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                        System.out.println(Thread.currentThread().getName()
                                +"等待超时!");
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

========================================================

其他补充知识:

①如何通过java代码的形式

package javax.test;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * @auther xiehuaxin
 * @create 2018-08-24 10:35
 * @todo
 */
public class Test {
    public static void main(String[] args) {
        //jdk提供的虚拟机线程管理的接口ThreadMXBean,一般除了对系统进行监控,其他情况基本用不着这个接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        /**
         * 查看当前系统有哪些线程
         */
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
        for(ThreadInfo info : threadInfos) {
            System.out.println("[" + info.getThreadId() + "] " + info.getThreadName());
        }
    }
}

输出:

[6] Monitor Ctrl-Break
[5] Attach Listener//负责获取当前程序运行的各种信息,包括内存印象,线程的栈,系统属性等等
[4] Signal Dispatcher//它专门向虚拟机分发处理信号的
[3] Finalizer//调用对象的final方法的线程
[2] Reference Handler//用来负责清除引用的线程
[1] main//主线程

推荐书籍:《java并发编程实战》

猜你喜欢

转载自blog.csdn.net/aimashi620/article/details/82017700
今日推荐