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)同一个线程的不同方法获取的一定是同一个实例。

java怎么模拟并发 java并发编程教程_java


java怎么模拟并发 java并发编程教程_多线程_02


Thread类中有一个threadLocals对象,该对象是一个ThreadLocalMap类型的变量,ThreadLocalMap是一个定制化的HashMap,这个Map中保存了当前线程关联的多个ThreadLocal变量。

注意,如果当前线程不消亡,多个ThreadLocal本地对象会一直存在,造成内存溢出,因此使用完后要调用ThreadLocal的remove方法。

另外,上图上可以看到Thread类中存在inheritableThreadLocals这个成员变量,这是用于在子进程中访问父进程的ThreadLocal变量而设计的。