Java创建和使用线程的四种方式

多线程

创建线程的方式

继承Thread类

public static void main(String[] args) {
    SellTicket sellTicket = new SellTicket();
    sellTicket.start();
}

static class SellTicket extends Thread {

    @Override
    public void run(){
        System.out.println("西安 -> 兰州的车票开始买了");
    }
}

当然如果SellTicket这个类只用一次,我们可以不用显示的创建,通过匿名内部类的方式去简写

Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println("西安 -> 兰州的车票开始买了");
    }
};
thread.start();

如果是用java8的话,我们还可以通过lambda表达式去进一步简化:

new Thread(() -> {
    System.out.println("西安 -> 兰州的车票开始买了");
}).start();

实现Runnable接口

public static void main(String[] args) {
    // Thread的有参构造函数,支持将实现了Runnable的接口实现类传入
    Thread thread = new Thread(new SellTicket());
    thread.start();
}

static class SellTicket implements Runnable {

    @Override
    public void run(){
        System.out.println("西安 -> 兰州的车票开始买了");
    }
}

和前面类似的做法是,如果SellTicket只用一次,不不需要显示创建的话,我们也可以用匿名内部类的方式去简写:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("西安 -> 兰州的车票开始买了");
    }
}).start();

实现Callable接口

前面说的两种方式,都可以用来创建和使用线程,但是遗憾的是,他们都没有办法带回来返回值。有些场景,我们是需要返回值的,比如:我们交给线程异步的去做一个数学计算,去做一个耗时的查询,那么我们是需要知道计算的结果或者查询到的数据的。这样的场景,我们就需要用到实现Callable接口这种方式了。参考代码:

这里需要注意的几点是:

  1. Callable接口只有一个方法:call(),用来执行我们需要异步执行的业务逻辑,比如这里让线程异步去执行了一个加法计算
  2. Callable的实现类不能作为参数去构造Thread类,Thread只能接受实现了Runnable接口的实现类,下面的代码中FutureTask是实现了Runnable接口的,因此我们用FutureTaskCallable接口进行了包装,这里有适配器模式的应用,不展开分析
  3. FutureTask是做什么用的,一言以蔽之:用来获取异步计算的执行结果。如何获取的,这里不展开分析。感兴趣,可参考我的另一篇博客:线程池源码学习
  4. 异步执行的结果如何获取,通过FutureTaskget()方法获取。
public static void main(String[] args) throws ExecutionException, InterruptedException {
    int a = 4, b = 5;
    FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() {
            return a + b;
        }
    });
    new Thread(task).start();
    System.out.println(task.get());
}

// 输出结果:9

// 上面3-8行代码可以用lambda的方式简写为下面这样:
FutureTask<Integer> task = new FutureTask<>(() -> a + b);

通过上面的方式,我们已经可以获取到线程的执行结果了,那么再看这样一段代码:我们在第5行休息了5秒钟,用来模拟调用远程接口耗时或者执行业务逻辑复杂的过程。执行的结果是,第8行会一直阻塞等待结果,直到5秒结束,拿到返回结果。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    int a = 4, b = 5;
    FutureTask<Integer> task = new FutureTask<>(() -> {
        Thread.sleep(5000);
        return a + b;
    });
    new Thread(task).start();
    System.out.println(task.get());
}

这样的场景可能并不是理想的,5秒我们可以等,如果是10秒呢?1个小时呢?我们等不等,答案是否定的。古语有云,时光匆匆,如白驹过隙,我们不能把大量的时间用在等待上,那么唯一的结果就是我们变老了。我们也不能让一个线程耗在这里,什么都做不了,这也是对资源的一种浪费。因此我们有了下面的代码:我们可以通过public V get(long timeout, TimeUnit unit)方法设置一个超时时间。如果超过设定的时间,就不在等待,学会放弃。错过一朵玫瑰,身后也许还有一片花园。我们这里设定的是等待1秒。1秒钟之后,如果没有结果,会抛出一个异常:TimeoutException

// 实际代码中不要出现1这样的魔法数字,这里为了紧凑,没有定义静态常量
Integer result = task.get(1, TimeUnit.SECONDS);

通过线程池的方式创建和使用线程

创建一个线程,我们知道需要有线程的上下文,用来在CPU通过时间片轮转的线程调度方式调度线程的时候保存线程的一些中间数据,我们还知道在JAVA中,每一个线程会在JVM的栈区分配一个独立的线程栈,用来保存线程的局部变量、程序计数器等等。那么如果在Linux操作系统中,我们创建的线程叫做用户线程,他还需要和内核线程进行一对一或者一对多的绑定(这个根据操作系统决定),因此创建一个线程的代码很简单,但底层需要做的事情还是很多的,这些都是需要消耗资源,消耗时间成本的。那么有没有办法能创建一个线程,反复利用呢?这就是线程池的事情了。

假如你是一个老板,开了一家制作口罩的工厂。前面几种方式,就相当于有一笔订单,你就招聘一个工人,然后带她熟悉设备,教他如何制作口罩,等他熟悉了,然后做完这笔订单之后,你就开除他了。如果下次还来一个订单,你就需要又重新招聘一个工人,继续培训他,等他完成。。。这样的老板,生活中也许并不多见。

我们常见的老板是什么样的呢?你有一个生产口罩的想法,于是注册了一家公司,开个工厂,工人还没招聘,因为可能一开始没有生意嘛,招太多人可能用不上,也发不起工资。所以先去跑订单,好不容易来了一笔订单,你就招了一个工人去做。由于你干的不错,又来一个订单,那么你就又招了一个工人,就这样,有一天你的公司已经扩大到了10个人工人的规模。突然有一天,疫情爆发了,社会上需要大量的口罩,你接到了大笔的订单,但是你的10个工人都忙的晕头转向的,怎么办呢?先将这些订单按照来的顺序找个地方放着吧,你准备等工人闲下来就去做。但是情况不容乐观,疫情越来越严重了,作为一个爱国商人,你准备回报社会,抓紧生产口罩,将他们捐献到需要的地方。因此你招了很多临时工,做了简单的培训后,让他们也参与到制作口罩的队伍中。但是由于疫情太严重了,还是有源源不断的订单过来,临时工和老员工都在加班干,桌子上的订单都放满了,你也毕竟只是个普通人。该做的你已经做了,那就只能拒绝了。最后在全国人民的齐心协力下,我们战胜了病毒,人们再也不用戴口罩了。口罩的订单降下来了,你没有那么忙了,临时工也闲了,是时候让他们回家了。剩下的工作,几个老员工完全能应付过来。

这个故事就是线程池设计的思想,线程池就是那个老板,他有创建线程、管理线程的能力;老员工我们把它们叫做核心线程,这些线程被创建之后一直在线程中,随时准备处理业务逻辑;订单处理不了,暂时存放的地方,我们叫阻塞队列,和队列的区别体现在一个阻塞上,第一点:往队列中放元素的时候如果队列满,不会放弃,会阻塞等待队列有空间在尝试往进放。第二点:从队列中取元素的时候,如果队列为空,不会放弃,会阻塞等待有其他线程往队列中放入了元素,在取;临时工,我们叫临时线程,临时线程在线程池空闲下来的时候会进行释放;拒绝别人的方式有很多,直接说我不干,是一种,很容易得罪人,委婉的说明原因,客客气气的也是一种,因此关于如何拒绝的方法,线程池中我们叫做拒绝策略

详细步骤,参考下图:
Java创建和使用线程的四种方式_第1张图片

代码参考:

public class Main {

    private static final ThreadPoolExecutor THREAD_POOL;

    /**
     * 核心线程数
     */
    private static final int CORE_POOL_SIZE = 10;

    /**
     * 最大线程数,减去核心线程数为临时线程数
     */
    private static final int MAX_POOL_SIZE = 15;

    /**
     * 临时线程空闲多少时间会被释放
     */
    private static final long KEEP_ALIVE_TIME = 0L;

    /**
     * 阻塞队列长度,如果不设置,那么有可能队列变得无限大,导致内存溢出
     */
    private static final int QUEUE_SIZE = 50;

    /**
     * 静态代码块会在类加载的时候就加载,在这里初始化线程池
     */
    static {
        ThreadFactory threadFactory = new ThreadFactory() {

            private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
            // AtomicInteger是原子类,保证了线程安全
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            // 这里重写ThreadFactory创建线程的方法,给线程设置一个和业务相关的名字,便于排查问题
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = defaultFactory.newThread(r);
                thread.setName("MyThreadName-" + threadNumber.getAndIncrement());
                return thread;
            }
        };
		
        // 最后一个参数是拒绝策略,我们用了默认的抛弃任务策略
        THREAD_POOL = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(QUEUE_SIZE),
                threadFactory, new ThreadPoolExecutor.DiscardPolicy());
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        THREAD_POOL.execute(() -> {
            System.out.println("线程池安排线程 :"+ Thread.currentThread().getName() + " 执行了A任务");
        });
		
        // 这里的Future参考上面的FutureTask,是他的顶级接口,定义了get()方法
        Future<String> task = THREAD_POOL.submit(() -> {
            System.out.println("线程池安排线程 :"+ Thread.currentThread().getName() + " 执行了B任务,并且返回了一个结果");
            return "我可以带回执行结果";
        });
        System.out.println(task.get());
    }
}

线程生命周期

线程的生命周期:

  1. 通过new Thread()创建了一个线程,这个时候线程不会执行
  2. 调用了start()方法,线程进入就绪状态(READE),等待被CPU时间片的调度
  3. 线程获得了CPU使用权,开始干活,干完活之后进入终止状态(TERMINATED)
  4. 如果在第三步,线程干了一版,被调用了yield()方法,则会让出CPU执行权,重新进入就绪状态(READE)
  5. 如果线程被调用了wait()等方法,会被放到一个等待池中,进入等待状态(WAITING),等待其他线程执行notify()、notifyAll()方法唤醒,重新进入就绪状态(READE)
  6. 如果线程被调用了wait(timeout)方法,也会被放入到一个等待池中,进入等待状态(TIMED_WAITING),这个状态可以通过其他线程执行notify()、notifyAll()方法唤醒,也可以在设置的时候到达后重新进入就绪状态(READE)
  7. 如果线程执行需要获取锁,那么会进入阻塞状态(BLOCKED),等待获取锁。。。

Java创建和使用线程的四种方式_第2张图片

守护线程

默认创建的线程都是非守护线程。创建守护线程时,需要将 Thread 的 daemon 属性设置成 true,当 JVM 退出时,会关心非守护线程是否结束,但不关心守护线程的。适合用来做一些监控的工作。

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("------");
    }
});
// 设置为守护线程
t.setDaemon(true);

你可能感兴趣的:(java,多线程,并发编程)