掌握之并发编程-2.线程

掌握高并发、高可用架构

第二课 并发编程

从本课开始学习并发编程的内容。主要介绍并发编程的基础知识、锁、内存模型、线程池、各种并发容器的使用。

创新互联公司总部坐落于成都市区,致力网站建设服务有做网站、成都网站设计、网络营销策划、网页设计、网站维护、公众号搭建、微信小程序定制开发、软件开发等为企业提供一整套的信息化建设解决方案。创造真正意义上的网站建设,为互联网品牌在互动行销领域创造价值而不懈努力!

第二节 线程

并发编程 并发基础 进程 线程 线程通信

上一节学习了进程和线程的关系,CPU和线程的关系。在程序开发过程中,最主要的还是线程,毕竟它是用来执行任务的。所以就需要知道,如何启动和停止线程;线程的状态;线程间如何通信。

线程的启动
  1. 实现Runnable接口,然后当成Thread的构造参数生成线程对象,调用t.start()方法
public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("thread02");
    }

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

这是线程最本质的实现。Thread类实现了Runnable接口,在执行t.start()时,会调用Threadrun()方法,从而间接调用target.run()

Thread类实现Runnalbe接口:

public class Thread implements Runnable {
    private Runnable target;
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

2 继承Thread类,然后调用start()方法

public class MyThread extends Thread {
    public void run() {
        System.out.println("thread01");
    }
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

由于Thread实现了Runnable,所以继承Thread来重写run()方法的本质依然是实现Runnable接口的定义。此时,由于target对象为null,所以Threadrun()方法不会执行target.run(),而是直接执行自定义的run()方法。

3 实现Callable接口,并通过FutureTask

public class MyCallable implements Callable {

    @Override
    public String call() throws Exception {
        return null;
    }

    public static void main(String[] args) {
        MyCallable m = new MyCallable();
        FutureTask f = new FutureTask<>(m);
        Thread t = new Thread(f);
        t.start();

        String result = f.get(); // 同步获取任务执行结果
        System.out.println(result);
    }
}

由于FutureTask实现了RunnableTask接口,而RunnableTask又实现了RunnableFuture接口,因此在构造Thread时,FutureTask还是被转型为Runnable来使用了。

前两种方法只能执行任务,而不能得到任务的结果;第三种方法可以通过FutureTaskget()方法同步的获取任务结果。当任务执行中时,其会阻塞直到任务完成。

4 匿名内部类

public class DemoThread {

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                //...
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //...
            }
        }).start();
    }
}

5 Lambda表达式

public class Demo {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("running")).start();
    }
}

6 线程池

public class MyThreadPool implements Runnable {
    @Override
    public void run() {
        // TODO
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();

        MyThreadPool m = new MyThreadPool();
        exec.execute(m);
    }
}

把任务的执行交给ExecutorService去处理,最终还是利用Thread创建线程。优点是线程的复用,省去了每个线程的创建和销毁过程,从而提高效率。

7 定时器

public class MyTimer {

    public static void main(String[] args) {
        Timer t = new Timer();

        t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                // TODO
            }
        }, 2000, 1000);
    }
}

TimerTask实现了Runnable接口,Timer内部有个TimerThread继承了Thread,所以还是Thread+Runnable

线程的停止
  1. 当线程的run方法执行完成后,线程自动释放资源进而终止。
  2. 在另外的线程中调用interrupt来中断某个线程。这是线程间通信,我们后续再讲
线程的状态

先上图(借用CSDN博主 潘建南 的图)。

掌握之并发编程-2.线程

所以,线程的状态一共有6种。下面咱们来详细讲解。

  1. 初始状态 NEW

通过实现Runnable或继承Thread得到一个线程类,并使用new创建出一个线程对象,就进入了初始状态。此时,还未调用start方法。

  1. 运行状态 RUNNABLE

JAVA中将 就绪(READY)和 运行中(RUNNING)两种状态统称为“运行”状态。

就绪 READY:就是说线程有资格运行,但此时调度程序还未选择线程。当以下行为发生时,线程进入就绪状态。

  • 调用线程的start方法
  • 当前线程的sleep结束
  • 其他线程join结束
  • 等待用户输入,但用户输入完毕
  • 线程拿到对象锁
  • 当前线程的时间片用完了
  • 调用当前线程的yield方法

运行中 RUNNING:调度程序从就绪的线程池中选择一个线程使其成为当前线程,此时线程处于的状态就是运行中。

  1. 阻塞 BLOCKED

阻塞状态是线程在获取对象的同步锁synchorized时,因为该锁被其他线程占用而放弃CPU使用权,暂时停止运行的状态。此时的线程会被JVM放入锁池中。

  1. 等待 WAITING

运行的线程执行wait()方法,会释放线程占用的所有资源,并进入等待池中。此时,线程是不能自动唤醒的,必须依靠其他线程调用notify()notifyAll()方法才能唤醒。

  1. 超时等待 TIMED_WAITING

运行的线程执行sleep()join()方法,或者发出I/O请求时的状态。此时线程会放弃CPU使用权。当sleep()超时、join()等待线程终止或超时、I/O处理完毕时,重新转入就绪。

  1. 终止 TERMINATED

线程执行完成或因异常而退出run方法体的状态。

线程各个状态之间的跳转,可以仔细看图。

线程间通信
  1. 通过共享变量通信

在共享对象的变量中设置信号量。线程A修改信号量的值,线程B根据信号量来做不同的处理。

  1. 通过wait()notify()notifyAll()来通信

JAVA要求wait()notify()notifyAll()必须在同步代码块中使用。就是说,必须要获得对象锁。所以wait()notify()notifyAll()经常和sychronized搭配使用。

执行了锁定对象的wait()方法后,当前线程会释放获得的对象锁,进入锁定对象的等待池

在执行同步代码块的过程中,如果调用Thread.sleep()Thread.yield(),当前线程只是放弃CPU,并不会释放对象锁

JOIN

作用:让 主线程 等待 子线程 执行完成再继续运行。

// 主线程
public class Father extends Thread {
    public void run() {
        Son son = new Son();
        son.start();
        son.join();
        ...
    }
}

// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

在Father主线程中,先启动Son子线程,然后调用son.join(),此时,Father主线程会一直等待,直到子线程执行完成,才能继续运行。

分析源码可以知道,JOIN的实现原理是:只要子线程是活动的,就一直触发主线程的wait()方法,使其一直处于等待状态。

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
yield

调用yield()方法,意思是放弃CPU使用权,回到就绪状态。


本文标题:掌握之并发编程-2.线程
文章网址:http://pwwzsj.com/article/psgpej.html