Java并发编程系列--并发基础

java从诞生之日起,就明智的选择了内置对多线程的支持。

几个概念
在开始写并发之前,先介绍几个简单的概念:

  • 并发和并行: 并发指多个任务交替的执行,并行指多个任务同时执行
  • 临界区:表示一种公共资源或者共享数据,一次只能有一个线程访问它
  • JMM的特性: 原子性,可见性,有序性

程序、进程、线程

  • 程序:具有某些功能的代码。
  • 进程:操作系统进行资源分配和资源调度的基本单位。进程是程序执行的实体。
  • 线程:轻量级的进程,程序执行的最小单位,线程中含有独立的计数器,堆栈和局部变量等属性,必须拥有一个父进程,并且共享进程中所拥有的全部资源。

进程之间不能共享内存,线程共享内存非常容易。

线程的状态
线程是有生命周期的,生命周期的状态如下:

  • NEW :新建
  • READY:就绪
  • RUNNABLE : 运行
  • BLOCKED :阻塞
  • WAITING : 等待
  • TIME_WAITING:超时等待
  • TERMINATED : 终止

Java程序中的线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换。转换图如下:


线程生命周期状态图.png

新建状态

  • 使用new关键字创建线程之后,线程就处于新建状态,此时JVM为其分配了内存,并初始化了成员变量。此时并没有表现出线程的任何动态特征,程序也不会执行线程执行体。

就绪状态

  • 对象调用了start()方法后,该线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有运行,只是表示可以运行了,何时运行,取决于系统调度。

运行状态

  • 就绪状态的线程,获得CPU之后,就处于运行状态,当当前的时间片用完,或者运行yield()/sleep()方法时,线程就必须放弃CPU,结束运行状态。

阻塞状态/等待/超时等待

  • 线程调用sleep()/join()/wait()方法,放弃CPU资源,线程被阻塞/等待
  • 线程调用了一个IO方法,在该方法返回前,被阻塞
  • 线程试图获取一个同步监视器,但是失败了,被阻塞
  • 线程在等待某个通知

线程结束

  • run()/call()方法执行体执行完成,线程正常结束
  • 抛出Exception/Error
  • 调用线程结束控制

上面提到了线程创建的三种方式,在代码层级来看看线程的具体实现:

线程的创建

继承Thread类


public class ThreadTest extends  Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("我是线程执行体 !");
    }
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

实现Runnable接口


class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println("runnable 执行体");
    }
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableTest());
        thread.start();
    }
}

实现Callable接口和FutureTask

class callableTest implements Callable{

    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return 3;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        callableTest callableTest = new callableTest();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest);
        Thread thread = new Thread(futureTask , "有返回值的线程");
        thread.start();
        System.out.println(Thread.currentThread().getName());
        System.out.println(futureTask.get());
    }
}

上面展示了三种创建线程的三种方式,下面做一下简要的分析:

  • 实现接口的方式,还可以继承其他的类
  • 多个线程可以共享同一个target对象,适合多个线程来处理同一份资源。但是编程稍微复杂
  • 继承Thread类的方式不能在继承其它的类,但是编写简单。

一般推荐使用实现接口的方式来创建多线程。

线程执行的任务定义在了run()方法中,只有通过start()方法才能创建线程,这是为什么呢,通过源代码来分析一下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runable是一个函数式接口,定义也比较简单,只是定义了一个抽象的方法。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable也是一个函数式接口,并且支持泛型,并返回泛型的类型。

看一下Thread类的实现,Thread类的代码比较多,这里只阐述一些重要的点:

先来看Thread类的定义

public class Thread implements Runnable

Thead类实现了Runable接口

构造方法: Thread类提供了9个构造方法,只阐述2个构造方法,这两个也是最常用的方法,如下。

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

//init 方法重载
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {

        this.name = name.toCharArray();
        //删除了一些代码,这些代码会获取一些父线程的属性,并设置到当前线程
        ....  

        this.target = target;  //敲黑板,画重点
    }

上面的代码是一个线程的创建过程,包括获取一些父进程的属性,如设置线程的名字,设置是否是守护进程,优先级等。如果在创建线程的时候没有指定线程的名字,那么线程的名字为:Thread-num的形式,就是通过上面的代码来创建的,nextThreadNum()方法会返回一个数字,nextThreadNum()是一个被synchronized关键字修饰的方法,会将一个被static int类型修饰的整数进行+1操作,并返回,这样就保证了线程的名字不会重复,当然也可以自己指定线程的名字,在创建的时候传入即可。

上面代码target是一个私有的Runable变量,定义如下:

    private Runnable target;

看一下Thread类的start()方法:

public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();   //敲黑板,画重点,调用start0()方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
private native void start0();

start()方法是一个同步方法,并且会调用start0()来运行,start0()方法是一个被native修饰的本地方法,将委托给操作系统来运行,并调用run()方法。

定义如下:

private native void start0();

调用start()方法后,线程就处于了就绪状态,系统调度后,变为运行状态,并调用run()方法, 看一下run()方法的定义:

 @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

Thread类实现了Runbale接口所以必须要实现run(),run()方法的实现比较简单,首先判断target对象是否为null,如果为null就什么也不做,如果不为null,则调用target对象的run()方法。

分析到这,应该就能明白,有很多书上都说,无论是继承Thread类,还是实现了Runable接口都必须要重写run()方法,原因就在这。

继承Thread类的对象,调用Thread类的无参构造方法,此时target对象为空,但是它重写了run()方法,它会调用自己的run方法,实现了Runnable接口的对象,需要借助Thread(Runable target ) 构造方法运行 , 此时target对象不为空,会调用target的run()方法。

关于Callable和FutureTask的实现方式,这里简单说一下, FutureTask实现了 RunnableFuture接口, 而RunnableFuture接口继承了Runable接口, 所以它能通过new Thread(Runnable target)构造函数来运行。会调用FutureTask的run()方法,在run()方法中调用了Callable对象的call()方法,获得了其返回值,换句话说 FutureTask对象封装了该Callable对象的返回值。通过FutureTask的get()方法来获得子线程执行结束后的返回值。

线程控制

  • join线程:让一个线程等待另一个线程执行完成的方法 join() ,主线程创建了一个子线程,并且调用了join()方法,那么主线程只有等待子线程执行完成后,才能向下运行。如果调用了join(long millis)主线程会在等到millis时间后向下执行。
  • 守护线程(daemon):一个线程设置成为守护线程后,会随着前台线程的结束而结束。GC(垃圾回收)就是一个非常典型的守护进程。
  • 线程睡眠sleep():使线程进入阻塞状态,不在运行
  • 线程让步yield():yield和sleep有点类似,不同是它进入的不是阻塞状态,而是就绪状态。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,331评论 3 87
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,709评论 12 45
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,949评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15