Java并发编程基础篇(一)——线程的创建与使用
Java并发编程是深入了解Java的必备知识。本系列综合了《Java并发编程之美》、《Java并发编程艺术》等经典书籍,也参考了廖雪峰的Java教程,针对Java并发编程的知识点进行梳理。
不同于Redis系列从底层数据实现到多机数据库再到实操的视角,Java并发系列将会采用自顶向下的视角,先从使用侧角度讲述如何进行并发编程,再探讨并发编程乃至JUC包的底层原理。因为Redis使用起来比较简单,设计也比较简洁;而Java并发光是使用就已经很让人头疼了,一上来就讲原理更是过于劝退。
并发编程基础篇将会分为三个部分:线程的创建与常用方法、各类锁的使用方法、其他JUC工具类的使用方法。让我们先从最简单的线程的创建与常用方法开始吧!
1、线程的创建
一共有三种不同的创建线程的方式:
(1)继承Thread类并重写run方法:
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
(2)创建实现Runnable接口、重写run方法的类:
Java不支持多继承,所以继承了Thread类就无法继承其他类;此外,任务和代码没有分离,多个线程执行相同任务需要写多个任务代码,用Runnable就没有这个限制:
public class Main {
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
new Thread(task).start();
}
}
public static class RunnableTask implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
可以用Lamda表达式简化为:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
或者使用匿名类:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(){
public void run(){
// 匿名类中用到外部的局部变量teemo,必须把teemo声明为final
// 但是在JDK7以后,就不是必须加final的了
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};
t.start();
}
}
(3)使用FutureTask的方式
Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不便。所以,Java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值:
// 创建Callable任务类
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
return "hello";
}
}
public static void main(String[] args) throws InterruptedException {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//启动线程
new Thread(futureTask).start();
try {
// 等待任务执行完毕,返回结果
Stringresult = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。
2、线程的常用方法
方法名 | 功能 |
---|---|
start() | 启动线程 |
join() | 例如,主线程创建了一个子线程threadOne并调用threadOne.join(),则主线程会在调用该方法后被阻塞,等待threadOne执行完后,该方法就会返回 |
sleep(int t) | 当前线程暂停t毫秒;如果在线程暂停时停止该线程,会抛出InterruptedException 中断异常 |
setPriority(int n) | 设置线程优先级 |
yield() | 当前线程主动让出CPU |
setDaemon(True) | 将线程设置为守护线程 |
interrupt() | 中断线程 ,事实上是将对应线程的running标记位设为false,对应线程需要自己判断是否isInterrupted()并且做出相应处理 |
3、ThreadLocal
ThreadLocal可以在一个线程中传递同一个对象。在创建一个ThreadLocal对象后,访问这个对象的每个线程都复制一个对象到自己的本地内存:
(1)不同线程操作这个对象时,实际操作的只是自己本地内存的这个变量,而不是共享变量,因此一定是不同的实例;
(2)同一个线程的不同方法获取的一定是同一个实例。
Thread类中有一个threadLocals对象,该对象是一个ThreadLocalMap类型的变量,ThreadLocalMap是一个定制化的HashMap,这个Map中保存了当前线程关联的多个ThreadLocal变量。
注意,如果当前线程不消亡,多个ThreadLocal本地对象会一直存在,造成内存溢出,因此使用完后要调用ThreadLocal的remove方法。
另外,上图上可以看到Thread类中存在inheritableThreadLocals这个成员变量,这是用于在子进程中访问父进程的ThreadLocal变量而设计的。