并发编程笔记三:java线程的三生三世

内容多有疏漏,有问题欢迎提出 

  

目录

  1. 什么是线程
  2. 线程的创建和终止
  3. 线程的中断
  4. 等待(wait)和通知(notify)
  5. 等待线程结束(join)和谦让(yeild)
  6. 总结

什么是线程

在了解线程之前,我们有必要认识一下线程的“母亲”--进程:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

以上对于进程的解释来源于百度百科,翻译成人话就是,进行就是一个运行的程序。例如,你使用谷歌浏览器浏览网页,你打开了eclipse进行编码,你用迅雷下载小电影,这些任务在计算机中的体现就是进程。

而线程呢,是程序执行的最小单元。一个进程可以包含多个线程。比如你使用迅雷下载3个电影,启动迅雷本身是一个大的进程,而下载这三个电影就是三个不同的线程。

所以我们也就了解到。线程是程序执行的最小单元,而进程是程序本身,他们是从属关系,进程可以包含多个线程。

线程的创建和终止

  • 线程的创建

为人熟知的有两种方式,一种是继承Thead类,另一种是实现Runnable接口。

/**
 * @author Bruce_Q
 * @Description: 通过继承Thread类创建线程
 * @date 2019/1/27 21:28
 */
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("I'm a new thread.");
    }

    public static void main(String[] args) {
        MyThread1 myThread = new MyThread1();
        myThread.start();
    }
}
/**
 * @author Bruce_Q
 * @Description: 通过实现Runnable接口创建线程
 * @date 2019/1/27 21:32
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("I'm a new thread.");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

注:启动线程一定要使用start(),而不是run(),前者是启动线程,后者只是串行执行run()方法中的代码。

基于java是单继承的特性,两种方法既然都能实现创建线程的效果,那么继承的方式无疑就是一种资源的浪费,所以,我们主推使用Runnable的方式创建线程。

以上两种创建线程的方式都没有返回值,接下来讲第三种有返回值的创建线程的方式:

/**
 * @author Bruce_Q
 * @Description: 通过FutureTask方式创建线程
 * @date 2019/1/27 22:13
 */
public class FutureTastThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "I'm result.";
    }

    public static void main(String[] args) throws ExecutionException {
        FutureTask<String> futureTask = new FutureTask<String>(new FutureTastThread());
        String result = null;
        new Thread(futureTask).start();
        try {
            //等待任务执行完毕,并返回结果
            result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用FutureTask方式需要重写call方法,实现的功能和run方法相同,调用get方法后,会获取到线程执行结束之后的返回结果,这个特性是前面两种方法做不到的。

  • 线程的终止

对于线程的终止,一般来说,线程执行完毕之后就会结束,无需手动关闭。但凡事都有个例外,jdk中也提供了一个stop()的方法进行线程终止。但是再深入了解下就不难看到,stop()方法是一个被标注为废弃的方法。原因是这个方法终止线程过于暴力,可能会引起一系列数据不一致的问题,程序也会抛出各种异常。

一种优雅的终止线程的方式:通过埋点(加标识),让程序自己判断什么时候要终止线程,实现方法如下:

 

/**
 * @author Bruce_Q
 * @Description: 终止线程
 * @date 2019/1/27 22:49
 */
public class StopThread implements Runnable {
    @Override
    public void run() {
        try {
            //如果当前线程被中断则退出循环
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread() + " hello");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        //启动子线程
        thread.start();
        //主线程休眠1s,以便中断前让子线程输出
        Thread.sleep(1000);
        System.out.println("main thread interrupt thread");
        thread.interrupt();
        //等待子线程执行完毕
        thread.join();
        System.out.println("main is over");
    }
}

具体实现效果:

并发编程笔记三:java线程的三生三世 

 

可以看到,终止线程和中止线程结束之间,子线程还是有日志在打印,说明这种终止方式并不是立即终止,但是能保证线程终止更加的安全。

线程的中断

在上面讲线程终止的时候,我们已经用到了线程中断interrupt()方法。但实际上,这个方法只是设置线程的中断标识,并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

当线程在wait、jion或者sleep被挂起时,如果这时候其他线程调用了当前线程的interrupt()方法,当前线程会在调用这些方法的地方抛出异常。

public class InterruptThread {
    public static void main(String[] args)  {
        Thread t1 = new Thread();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                t1.interrupt();
            }
        });
        t1.start();
        try {
            //线程等待
            t1.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //唤醒等待线程
        t1.notify();
        t2.start();
    }
}

并发编程笔记三:java线程的三生三世 

等待(wait)和通知(notify)

wait()是对线程施以等待指令的操作,在执行过wait方法后,需要接收到notify或者notifyAll的指令,线程才可以继续执行。

wait和sleep的都是让线程等待的操作,而wait需要主动唤醒线程,sleep是自动等待线程到时间继续执行。

举例说明wait和notify的使用方法:

 

public class ProAndConThread {
    final static Object object = new Object();

    public static class product extends Thread {
        public void run() {
            //获取对象锁
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ": product start");
                try {
                    System.out.println(System.currentTimeMillis() + ": product wait for object");
                    //线程等待
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ": product end");
            }
        }
    }

    public static class consumer extends Thread {
        public void run() {
            //获取对象锁
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ": consumer start notified");
                //唤醒线程
                object.notifyAll();
                System.out.println(System.currentTimeMillis() + ": consumer end");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread product = new product();
        Thread consumer = new consumer();
        product.start();
        consumer.start();
    }
}

并发编程笔记三:java线程的三生三世 

从结果中我们看到,product先执行,然后等待,直到consumer执行后,唤醒了product线程,product才结束了自己的操作。

在以上的例子中,如果有多个product线程都在等待的话,我们可以把notify换成notifyAll方法,对等待的线程全部进行唤醒。

但是有一点需要注意,notify和notifyAll都只能唤醒自己执行前已经等待的线程,对于之后再启动的等待线程,是不会唤醒的。

注:由于多线程启动本身就是随机操作,有可能存在consumer再product之前执行,这样的话,线程就会一直等待,永远不会被唤醒。在实际的开发中,我们要尤其注意。

等待线程结束(join)和谦让(yeild)

线程由于本身是异步执行,通过join的方式可以使join之后的方法等待线程执行结束后再开始执行。这在实际开发中经常使用到。

yeild是一个静态的方法,可以使用Thread.yeild()直接调用,目的是让出当前线程的执行权,让当前线程歇一会儿,先让其他线程继续往下执行。

总结

本章从java的线程的创建、终止讲起,也对线程的一些基本调用方法做了一些阐述,相信看完之后会对线程有了一个基本的概念。

下面一章,我们会重点讲解线程池的概念和用法。