原创】Java并发编程系列2:线程观点与基础操作

【原创】Java并发编程系列2:线程观点与基础操作

伟大的理想只有经由忘我的斗争和牺牲才气胜利实现。

本篇为【Dali王的手艺博客】Java并发编程系列第二篇,讲讲有关线程的那些事儿。主要内容是如下这些:

  • 线程观点
  • 线程基础操作

线程观点

历程代表了运行中的程序,一个运行的Java程序就是一个历程。在Java中,当我们启动main函数时就启动了一个JVM的历程,而main函数所在的线程就是这个历程中的一个线程,称为主线程。
历程和线程的关系如下图所示:
原创】Java并发编程系列2:线程观点与基础操作

由上图可以看出来,一个历程中有多个线程,多个线程共享历程的堆的方式区资源,然则每个线程有自己的程序计数器和栈区域。

线程基础操作

线程建立与运行

Java中有三种线程建立方式,划分为:继续Thread类并重写run方式,实现Runnable接口的run方式,使用FutureTask方式。
先看继续Thread方式的实现,代码示例如下:

public class ThreadDemo {
    public static class DemoThread extends Thread {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.")
        DemoThread thread = new DemoThread();
        thread.start();
    }
}

上面代码中DemoThread类继续了Thread类,并重写了run方式。在main函数里建立了一个DemoThread的实例,然后挪用其start方式启动了线程。

tips:挪用start方式后线程并没有马上执行,而是处于停当状态,也就是这个线程已经获取了除CPU资源外的其他资源,守候获取CPU资源后才会真正处于运行状态。
使用继续方式,利益在于通过this就可以获取当前线程,瑕玷在于Java不支持多继续,若是继续了Thread类,那么就不能再继续其他类。而且义务与代码耦合严重,一个线程类只能执行一个义务,使用Runnable则没有这个限制。
来看实现Runnable接口的run方式的方式,代码示例如下:

public class RunnableDemo {
    public static class DemoRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        DemoRunnable runnable = new DemoRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

上面代码两个线程共用一个Runnable逻辑,若是需要,可以给RunnableTask添加参数举行义务区分。在Java8中,可以使用Lambda表达式对上述代码举行简化:

 public static void main(String[] args) {
    System.out.println("this is main thread.");
    Thread t = new Thread(() -> System.out.println("this is child thread"));
    t.start();
}

上面两种方式都有一个瑕玷,就是义务没有返回值,下面看第三种,使用FutureTask的方式。代码示例如下:

public class CallableDemo implements Callable<JsonObject> {
    @Override
    public JsonObject call() throws Exception {
        return new JsonObject();
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo());   // 1. 可复用的FutureTask
        new Thread(futureTask).start();
        try {
            JsonObject result = futureTask.get();
            System.out.println(result.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 2. 一次性的FutureTask
        FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("name", "Dali");
            return jsonObject;
        });
        new Thread(innerFutureTask).start();

        try {
            JsonObject innerResult = innerFutureTask.get();
            System.out.println(innerResult.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如上代码,CallableDemo实现了Callable接口的call方式,在main函数中使用CallableDemo的实例建立了一个FutureTask,然后使用建立的FutureTask工具作为义务建立了一个线程并启动它,最后通过FutureTask守候义务执行完毕并返回效果。
同样的,上面的操作过程适合于需要复用的义务,若是对于一次性的义务,大可以通过Lambda来简化代码,如注释2处。

守候线程终止

在项目中经常会遇到一个场景,就是需要守候某几件事情完成后才气继续往下执行。Thread类中有一个join方式就可以用来处置这种场景。直接上代码示例:

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main thread starts");
        Thread t1 = new Thread(() -> System.out.println("this is thread 1"));
        Thread t2 = new Thread(() -> System.out.println("this is thread 2"));
        t1.start();
        t2.start();
        System.out.println("main thread waits child threads to be over");
        t1.join();
        t2.join();
        System.out.println("child threads are over");
    }

上面代码在主线程里启动了两个线程,然后划分挪用了它们的join方式,主线程会在挪用t1.join()后被壅闭,守候其执行完毕后返回;然后主线程挪用t2.join()后再次被壅闭,守候t2执行完毕后返回。上面代码的执行效果如下:

main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over

需要注重的是,线程1挪用线程2的join方式后会被壅闭,当其他线程挪用了线程1的interrupt方式中止了线程1时,线程1会抛出一个InterruptedException异常而返回。

void指针和数组指针之间的转换

让线程睡眠

Thread类中有一个static的sleep方式,当一个执行中的线程挪用了Thread的sleep方式后,挪用线程会暂时让出指定时间的执行权,也就是在这时代不介入CPU的调剂,然则该线程所拥有的监视器资源,好比锁照样不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于停当状态,然后守候CPU的调剂执行。

tips:面试当中wait和sleep经常会被用来对照,需要多加体会二者的区别。
挪用某个工具的wait()方式,相当于让当前线程交出此工具的monitor,然后进入守候状态,守候后续再次获得此工具的锁;notify()方式能够叫醒一个正在守候该工具的monitor的线程,当有多个线程都在守候该工具的monitor的话,则只能叫醒其中一个线程,详细叫醒哪个线程则不得而知。
挪用某个工具的wait()方式和notify()方式,当前线程必须拥有这个工具的monitor,因此挪用wait()方式和notify()方式必须在同步块或者同步方式中举行(synchronized块或者synchronized方式)。

看一个线程睡眠的代码示例:

private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
       // 获取独占锁
       lock.lock();
       System.out.println("thread1 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread1 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });
    Thread t2 = new Thread(() -> {
        // 获取独占锁
        lock.lock();
        System.out.println("thread2 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread2 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });

    t1.start();
    t2.start();
}

上面的代码建立了一个独占锁,然后建立了两个线程,每个线程在内部先获取锁,然后睡眠,睡眠竣事后会释放锁。执行效果如下:

thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake

从执行效果来看,线程1先获取锁,然后睡眠,再被叫醒,之后才轮到线程2获取到锁,也即在线程1sleep时代,线程1并没有释放锁。
需要注重的是,若是子线程在睡眠时代,主线程中止了它,子线程就会在挪用sleep方式处抛出了InterruptedException异常。

线程让出CPU

Thread类中有一个static的yield方式,当一个线程挪用yield方式时,现实就是表示线程调剂器当前线程请求让出自己的CPU使用,若是该线程另有没用完的时间片也会放弃,这意味着线程调剂器可以举行下一轮的线程调剂了。
当一个线程挪用yield方式时,当前线程会让出CPU使用权,然后处于停当状态,线程调剂器会从线程停当行列内里获取一个线程优先级最高的线程,固然也有可能会调剂到刚刚让出CPU的谁人线程来获取CPU执行权。
请看代码示例:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 2
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });

    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 1
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });
    t1.start();
    t2.start();
}

在如上的代码中,两个线程的功效一样,运行多次,统一线程的两行输出是顺序的,然则整体顺序是不确定的,取决于线程调剂器的调剂情形。
当把上面代码中1和2处代码注释掉,会发现效果只有一个,如下:

current thread: Thread[Thread-1,5,main] yield cpu
current thread: Thread[Thread-0,5,main] yield cpu
current thread: Thread[Thread-1,5,main] is over
current thread: Thread[Thread-0,5,main] is over

从效果可知,Thread.yiled方式生效使得两个线程划分在执行过程中放弃CPU,然后在调剂另一个线程,这里的两个线程有点相互忍让的感受,最终是由于只有两个线程,最终照样执行完了两个义务。

tips:sleep和yield的区别:
当线程挪用sleep方式时,挪用线程会壅闭挂起指定的时间,在这时代线程调剂器不会去调剂该线程。而挪用yield方式时,线程只是让出自己剩余的时间片,并没有被壅闭挂起,而是出于停当状态,线程调剂器下一次调剂时就可能调剂到当前线程执行。

线程中止

Java中的线程中止是一种线程间的协作模式。每个线程工具里都有一个boolean类型的标识(通过isInterrupted()方式返回),代表着是否有中止请求(interrupt()方式)。例如,当线程t1想中止线程t2,只需要在线程t1中将线程t2工具的中止标识置为true,然后线程2可以选择在合适的时刻处置该中止请求,甚至可以不剖析该请求,就像这个线程没有被中止一样。
在上面章节中也讲到了线程中止的一些内容,此处就不再用代码来展开了。

Java并发编程纲领

继续附上Java编程的系统学习纲领以供参考:
Java并发编程.png原创】Java并发编程系列2:线程观点与基础操作

【参考资料】

  1. 《Java并发编程之美》

本文由微型民众号【Dali王的手艺博客】原创,扫码关注获取更多原创手艺文章。
原创】Java并发编程系列2:线程观点与基础操作

原创文章,作者:2d28新闻网,如若转载,请注明出处:https://www.2d28.com/archives/2087.html