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

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

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

  • 线程概念
  • 线程基础操作

线程概念

进程代表了运行中的程序,一个运行的Java程序就是一个进程。在Java中,当我们启动main函数时就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,称为主线程。

进程和线程的关系如下图所示:

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

线程基础操作

线程创建与运行

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异常而返回。

让线程睡眠

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

【参考资料】

  1. 《Java并发编程之美》

本文由微型公众号【Dali王的技术博客】原创,扫码关注获取更多原创技术文章。

原创】Java并发编程系列2:线程概念与基础操作的更多相关文章

  1. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  2. Java并发编程系列-(7) Java线程安全

    7. 线程安全 7.1 线程安全的定义 如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的. 类的线程安全表现为: 操作的原子性 内存的可见性 不 ...

  3. Java并发编程系列-(6) Java线程池

    6. 线程池 6.1 基本概念 在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理.如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:如果并发的请求数 ...

  4. Java并发编程系列-(1) 并发编程基础

    1.并发编程基础 1.1 基本概念 CPU核心与线程数关系 Java中通过多线程的手段来实现并发,对于单处理器机器上来讲,宏观上的多线程并行执行是通过CPU的调度来实现的,微观上CPU在某个时刻只会运 ...

  5. Java并发编程系列-(8) JMM和底层实现原理

    8. JMM和底层实现原理 8.1 线程间的通信与同步 线程之间的通信 线程的通信是指线程之间以何种机制来交换信息.在编程中,线程之间的通信机制有两种,共享内存和消息传递. 在共享内存的并发模型里,线 ...

  6. Java并发编程系列-(5) Java并发容器

    5 并发容器 5.1 Hashtable.HashMap.TreeMap.HashSet.LinkedHashMap 在介绍并发容器之前,先分析下普通的容器,以及相应的实现,方便后续的对比. Hash ...

  7. Java并发编程系列-(4) 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  8. Java并发编程系列-(3) 原子操作与CAS

    3. 原子操作与CAS 3.1 原子操作 所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程. 为了实 ...

  9. Java并发编程系列-(9) JDK 8/9/10中的并发

    9.1 CompletableFuture CompletableFuture是JDK 8中引入的工具类,实现了Future接口,对以往的FutureTask的功能进行了增强. 手动设置完成状态 Co ...

随机推荐

  1. spring学习笔记一:spring介绍

    jar包下载地址:http://repo.spring.io/release/org/springframework/spring/ spring特点: 1.非侵入性 spring框架的API不会在业 ...

  2. Android开发之《异常处理》

    Android NDK(七):JNI异常处理 Android NDK开发Crash错误定位 adb logcat | ndk-stack -sym /Users/yangxin/Documents/d ...

  3. Java的同步和异步

    同步:发送一个请求,等待返回,然后再发送下一个请求 异步:发送一个请求,不等待返回,随时可以再发送下一个请求 同步可以避免出现死锁,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同 ...

  4. [PyTorch入门之60分钟入门闪击战]之神经网络

    神经网络 来源于这里. 神经网络可以使用torch.nn包构建. 现在你对autograd已经有了初步的了解,nn依赖于autograd定义模型并区分它们.一个nn.Module包含了层(layers ...

  5. JavaScript之三 - 语法

    1.block 一般就是{}包括起来的代码块,注意的是,js没有块作用域,但是有函数作用域,全局作用域. 2.var 1 var a = b = 1; 如: 123456 function () { ...

  6. 如何在NLP领域第一次做成一件事

    作者简介 周明,微软亚洲研究院首席研究员.ACL候任主席(president).中国计算机学会中文信息技术专委会主任.中国中文信息学会常务理事.哈工大.天津大学.南开大学.山东大学等多所学校博士导师. ...

  7. redis集群配置及python操作

    之前我们分析过喜马拉雅的爬取信息,使用分布式爬取,而且需要修改scrapy-redis的过滤算法为布隆过滤来减少redis内存占用,最后考虑这样还是不一定够,那么redis集群就是更好的一种选择方式了 ...

  8. CSS——NO.7(布局模型)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  9. 国际控制报文协议ICMP

    国际控制报文协议ICMP ICMP简介 ICMP 用于主机或路由器报告差错情况和提供有关异常情况的报告(检测网络错误). ICMP 不是高层协议,而是 IP 层的协议. ICMP 报文的格式 ICMP ...

  10. Java入门教程十二(集合与泛型)

    在 Java 中数组的长度是不可修改的.然而在实际应用的很多情况下,无法确定数据数量.这些数据不适合使用数组来保存,这时候就需要使用集合. Java 的集合就像一个容器,用来存储 Java 类的对象. ...