并发编程之线程创建到销毁、常用API

  在前面一篇介绍了线程的生命周期【并发编程之多线程概念 】,在本篇将正式介绍如何创建、中断线程,以及线程是如何销毁的。最后,我们会讲解一些常见的线程API。

 

线程创建

  Java 5 以前,实现线程有两种方式:扩展java.lang.Thread类,实现java.lang.Runnable接口。这两种方式都是都是直接创建线程,而每次new Thread都会消耗比较大的资源,导致每次新建对象时性能差;而且线程缺乏统一管理,可能无限制新建线程,相互之间竞争,很可能占用过多系统资源导致死机或OOM。同时,new Thread的线程缺乏更多功能,如定时执行、定期执行、线程中断。

  Java 5开始,JDK提供了4中线程池(newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool、newSingleThreadExecutor)来获取线程。这样做的好处是:可以重用已经存在的线程,减少对象创建、消亡的开销,性能佳;而且线程池可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。通过特定的线程池也可以实现定时执行、定期执行、单线程、并发数控制等功能。

  创建线程的代码实现

  • 扩展java.lang.Thread类
    • 自定义一个类继承java.lang.Thread
    • 重写Thread的run(),把自定义线程的任务定义在run方法上
    • 实例化自定义的Thread对象,并调用start()启动线程
//1.自定义一个类继承Thread类
public class ExThread extends Thread {
    //2.重写run()
    @Override
    public void run() {
       for (int i = 0; i < 100; i++) {
           System.out.println(Thread.currentThread().getName()+”:”+i);
       }
    }

    public static void main(String[] args) {
        //3.创建Thread子类对象
        ExThread exThread = new ExThread();
        //4.调用start方法启动自定义线程
        exThread.start();
    }
}

 

  • 实现java.lang.Runnable接口
    • 自定义一个类实现Runnable接口
    • 实现Runnable接口中的run(),把自定义线程的任务定义在run方法上
    • 创建Runnable实现类的对象
    • 创建Thread对象,并且把Runnable实现类的对象作为参数传递
    • 调用Thread对象的start()启动自定义线程
//1.自定义一个类实现Runnable接口
public class ImThread implements Runnable{
    //2.实现run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+”:”+i);
        }  
    }
   
    public static void main(String[] args) {
        //3.创建Runnable实现类对象
        ImThread imThread = new ImThread();
        //4.创建Thread对象
        Thread thread = new Thread(imThread);
        //5.调用start()开启线程
        thread.start();
    }
}

  

  • newFixedThreadPool

    创建一个固定线程数的线程池,可控制线程最大并发数,超出的线程会在队列中等待。【线程数量可以参考CPU数量,获取CPU数量:Runtime.getRuntime().availableProcessors()】

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CreateThreadByFixedPool {

    /**
     * Cover Runnable.run()
     */
    private static void run(){
        System.out.println(Thread.currentThread().getName()+" is running...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            pool.execute(CreateThreadByFixedPool::run);
        }
    }
}

 

  • newCachedThreadPool

    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.

    线程池的容量为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CreateThreadByCachedPool {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pool.execute(() -> System.out.println(Thread.currentThread().getName()+" is running..."));
        }
    }
}

 

  • newScheduledThreadPool

    创建一个固定线程数的线程池,支持定时及周期性任务执行。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class CreateThreadByScheduledPool {

    public static void main(String[] args) {

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//delay 2s excute. pool.schedule(() -> System.out.println(Thread.currentThread().getName()+" delays 2s "), 2, TimeUnit.SECONDS);
//delay 2s and every 3s excute. pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName()+" delays 2s every 3s execte"), 2, 3, TimeUnit.SECONDS); } }

 

  • newSingleThreadExecutor

    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

public class CreateThreadBySingleThreadPool {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            pool.execute(() ->{
                System.out.println(String.format("The thread %d (%s) is running...",
                        index,Thread.currentThread().getName()));
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

 

  Thread负责线程本身相关的职责和控制,Runnable负责逻辑业务。

  在实现自定义线程时,推荐使用Runnable接口,因为其具有以下几个优点:
  • Java是单继承多实现的,实现接口有利于程序拓展
  • 实现Runnable接口可为多个线程共享run() 【继承Thread类,重写run()只能被该线程使用】
  • 不同线程使用同一个Runnable,不用共享资源

 

线程中断

    interrupt()方法可以用来请求终止线程。
  • 当对一个线程调用interrupt方法时,线程的中断状态(boolean标志)会被置位。
  • 判断当前线程是否中断,可使用Thread.currentThread.isInterrupt()
  • 中断并不是强制终止线程,中断线程只是引起当前线程的注意,由它自己决定是否响应中断。【有些(非常重要的)线程会处理完异常后继续执行,并不理会中断;但是更加普遍的情况是:线程简单的将中断作为一个终止请求。】

 

线程销毁

    线程销毁的几种情景:
  • 线程结束,自行关闭
  • 异常退出
  • 通过interrupt()修改isInterrupt()标志进行结束
public static void main(String[] args) throws InterruptedException {
        Thread t  = new Thread(){
            @Override
            public void run() {
                System.out.println("I will start work.");
                while(!isInterrupted()){
                    System.out.println("working....");
                }
                System.out.println("I will exit.");
            }
        };
        t.start();
        TimeUnit.MICROSECONDS.sleep(100);
        System.out.println("System will exit.");
        t.interrupt(); 
    }
  • 使用volatile修饰的开关flag关闭线程(因为线程的interrupt标识很可能被擦除)
public class FlagThreadExit {

    static class MyTask extends Thread{
        
        private volatile boolean closed = false;
        
        @Override
        public void run() {
            System.out.println("I will start work.");
//isInterupted()是Thread类中的方法
while(!closed && !isInterrupted()){ System.out.println("working...."); } System.out.println("I will exit."); } public void closed(){ this.closed = true; this.interrupt(); } } public static void main(String[] args) throws InterruptedException { MyTask task = new MyTask(); task.start(); TimeUnit.MICROSECONDS.sleep(100); System.out.println("System will exit."); task.closed(); } }

 

多线程API

方法
返回值
作用
 
yield()
static void
暂停当前正在执行的线程对象,并执行其他线程。
只有优先级大于等于该线程优先级的线程(包括该线程)才有机会被执行
释放CPU资源,不会放弃monitor锁
sleep()
static void
使当前线程休眠,其它任意线程都有执行的机会
释放CPU资源,不会放弃monitor锁
wait()
void
使当前线程等待
Object的方法
interrupt()
void
中断线程
可中断方法
interrupted()
static boolean
判断当前线程是否中断
 
isInterrupted()
boolean
测试线程是否已经中断
 
join()
void
在线程A内,join线程B,线程A会进入BLOCKED状态,直到线程B结束生命周期或者线程A的BLOCKED状态被另外的线程中断
可中断方法
 
  • 可中断方法被打断后会收到中断异常InterruptedException.
  • yield()和sleep()的比较
    • 都是Thread类的静态方法
    • 都会使当前处于运行状态的线程放弃CPU
    • yield只会让位给相同或更高优先级的线程,sleep让位给所有的线程
    • 当线程执行了sleep方法后,将转到阻塞状态,而执行了yield方法之后,则转到就绪状态;
    • sleep方法有可能抛出异常,而yield则没有【sleep是可中断方法,建议使用sleep】
  • sleep和wait的比较
    • wait和sleep方法都可以使线程进入阻塞状态
    • wait和sleep都是可中断方法
    • wait使Object的方法,sleep是Thread的方法
    • wait必须在同步代码中执行,而sleep不需要
    • 在同步代码中,sleep不会释放monitor锁,而wait方法会释放monitor锁
    • sleep方法在短暂休眠后主动退出阻塞,而(没有指定时间的)wait方法则需要被其它线程notify或interrupt才会退出阻塞

 

wait使用

  • 必须在同步当法中使用wait和notify方法(wait和notify的前提是必须持有同步方法的monitor的所有权)
  • 同步代码的monitor必须与执行wait和notify方法的对象一致
public class WaitDemo {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.execute(() -> {
            synchronized (WaitDemo.class){
                System.out.println("Enter Thread1...");
                System.out.println(Thread.currentThread().getName()+" is waiting...");
                try {
                    WaitDemo.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread1 is going...");
                System.out.println("Shut down Thread1.");
            }
        });

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        pool.execute(() ->{
            synchronized (WaitDemo.class) {
                System.out.println("Enter Thread2...");
                System.out.println(Thread.currentThread().getName()+" is notifying other thread...");
                WaitDemo.class.notify();
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread2 is going...");
                System.out.println("Shut down Thread2.");
            }

        });
    }

}

 

 

补充

  • 弃用stop()和suspend()的原因
                stop()用来终止一个线程,但是不安全的; stop()会终止当前线程的所有未结束的方法,包括run()。当前线程被终止,立即释放被它锁住的锁。这会导致对象处于不一致的状态。【在转账过程中,可能钱刚转出,还未转入另一个账户,线程就被中断。导致对象被破坏,数据出错,其他线程得到的将会是错误的对象数据】
                suspend()用来阻塞一个线程,直至另一个线程调用resume()。suspend()会经常导致死锁。 调用suspend()的时候,目标线程会被挂起,但是仍然会持有之前获得的锁定。此时,其他线程都不能访问被锁定的资源。如果调用suspend()的线程试图获取同一个锁,那么线程死锁(被挂起的线程等着恢复,而将其挂起的线程等待锁资源)
  • suspend()的替代方案
                应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
 
  •  在实际开发中,调用start()启动线程的方法已不再推荐。应该从运行机制上减少需要并行运行的任务数量。如果有很多任务,要为每个任务创建一个独立线程的编程所付出的代价太大了。可以使用线程池来解决这个问题。
  •     线程信息查看工具:JDK自带的Jconsole
 
posted @ 2019-09-18 19:04  blue星空  阅读(3141)  评论(0编辑  收藏  举报