Java 线程详解(上)

梦想不抛弃苦心追求的人,只要不停止追求,你们会沐浴在梦想的光辉之中。再美好的梦想与目标,再完美的计划和方案,如果不能尽快在行动中落实,最终只能是纸上谈兵,空想一番。只要瞄准了大方向,坚持不懈地做下去,才能够扫除挡在梦想前面的障碍,实现美好的人生蓝图。Java 线程详解(上),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

线程作为CPU调度的最小单位,它属于程序进程的子集,关于程序进程和线程的介绍,可以参考《详解操作系统进程》《详解操作系统线程》两篇文章,这篇文章主要介绍Java线程的相关原理。

一、线程创建

从实现上来说,Java提供了三种创建线程的方式,但从原理上来看,其实只有一种方式,我们先从实现上来简单介绍一下这三种方式

1.1 继承Thread类

直接创建一个ThreadTest的实例,调用它的start()方法就可以创建一个线程了

class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

1.2 实现Runnable接口

如果只是简单的实现了Runnable接口,它与线程并没有任何关系,只是相当于创建了一个线程执行的任务类而已,要想真正的创建线程,还是需要创建一个Thread对象,把RunnableTest实例作为构造方法的入参

class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public class CreateThreadTest {

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        Thread thread = new Thread(runnableTest);
        thread.start();
    }
}

1.3 实现Callable接口

与Runnable很相似,它相当于也是也个任务的实现类,需要结合线程池的submit()方法才能使用,但与Runnable最本质的区别是,Callable的call()方法可以有返回值

class CallableTest implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return ThreadLocalRandom.current().nextInt();
    }
}

public class CreateThreadTest {

    public static void main(String[] args) {
        CallableTest callableTest = new CallableTest();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<Integer> future = executorService.submit(callableTest);
    }

}

1.4 Lambda表达式

这种方式与第二种方式其实是一样的,只是写法比较简洁明了

Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName()));

二、线程实现原理

2.1 只有一种线程创建方式

我们前面说过从实现原理上来讲,创建线程只有一种方式,我们从源码上来分析这种说法

继承Thread和实现Runnable接口两种方式本身就是一种方式,通过创建Thread实例,然后调用start()方法来创建实例

我们先主要看一下Callable接口实现类的使用,我们具体看一下ExecutorService的submit()方法

在submit()方法中,首先将Callable实例封装成一个FutureTask实例,FutureTask实现了RunnableFuture接口,而RunnableFuture又实现了Runnable接口,也就是说封装后的FutureTask仍然只是一个任务实例,此时与线程并没有任何关系,真正建立关系是在execute()方法中

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

execute()方法是线程池的核心方法,该方法在后面介绍线程池的文章中会对其进行详细介绍,现在我们主要看它的addWorker()方法,该方法就是去创建一个线程

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    ……
}

在addWorker()方法中,会去创建一个Worker实例,而在Worker的构造方法中,会去创建一个Thread实例

private boolean addWorker(Runnable firstTask, boolean core) {
    ……
    w = new Worker(firstTask);
    final Thread t = w.thread;
    ……
}

首先会去拿到一个ThreadFactory实例,我们以DefaultThreadFactory为例,看下newThread()方法的实现,就是去创建了一个Thread实例

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

总结:从上面对Callable的分析,我们可以得出结论,所有创建线程的方式都可以归结为一种方式,那就是创建Thread实例

那么问题就来了,我们创建了一个Thread实例,就完成了线程的创建吗?那我们的run()和start()方法又有什么区别呢?带着这样的问题,我们深入JVM和操作系统层面,来看一个Java线程创建的过程到底是什么样的

2.2 线程创建原理

2.2.1 run()与start()

首先我们先看一下run()和start()方法的区别,如果我们自定义一个类ThreadTest,然后继承了Thread类,可以选择是否重写run()方法,这个时候,我们创建了一个ThreadTest的实例,当我们用这个实例去调用run()方法时,这就是一个简单的方法调用,与线程没有任何关系

class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

同样如果我们实现了Runnable接口,先创建一个Runnable接口的实例,然后作为构造方法入参创建一个Thread实例

class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public class CreateThreadTest {

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        Thread thread = new Thread(runnableTest);
    }

}

在构造方法中会去调用init()初始化方法,初始化方法中把Runnable的实例存在了Thread实例的target属性中

当调用Thread实例的run()方法时,就是简单的去调用Runnable实例的run()方法,也与线程的创建没有关系,只是普通的方法调用

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

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ……
    this.target = target;
    ……
}

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

下面我们看一下start()方法的源码,在start()方法中,会去调用本地方法start0(),这个方法才是真正去创建一个线程

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

private native void start0();

2.2.2 线程创建流程

在Thread初始化的时候,首先会去调用本地方法registerNatives(),这个方法的主要作用是绑定线程相关的本地方法和真正JVM方法之间的映射关系

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

JNINativeMethod中建立了JNI的映射关系
Java 线程详解(上)

创建线程

当Thread对象调用start0()本地方法时,会去调用JVM的JVM_StartThread()方法进行线程的创建的和启动,而在该方法中,会调用navite_thread = new JavaThread(&thread_entry,sz)进行线程的创建。

在该方法中,会去调用操作系统的线程创建的方法,以X86的linux系统为例,会去调用create_thread()方法,而在该方法中又去调用pthread_create(),这个方法才是去真正的创建一个线程。

线程创建完成之后,一直处于初始化的状态,所以会一直进行阻塞,直到被唤醒

Java 线程详解(上)

上面创建线程的过程都是在navite_thread = new JavaThread(&thread_entry,sz)中进行的,这个方法会得到一个JavaThread对象,这是JVM的层面的线程对象,接下来,它需要与Java的Thread对象进行绑定native_thread ->prepare(jthread)

启动线程

完成上面内核线程创建和绑定工作之后,开始执行创建的内核线程,执行thread_entry()方法,里面会去调用start()方法Thread:start(native_thread),接着就是去调用操作系统的start方法os::start_thread(thread),将线程状态设置为RUNNABLE状态

Java 线程详解(上)

在Linux中最后真正调用的是pd_start_thread(Thread* thread)方法,该方法会去唤醒前面创建线程后一直处于阻塞状态的线程,最后调用JVM中的JavaThread的run()方法

Java 线程详解(上)

JavaThread::run会去调用JavaThread::thread_main_inner,在thread_main_inner()方法中,会去执行this->entry_point()(this,this),最后调用到thread_entry()方法,在该方法中,会根据前面JVM的JavaThread与Java的Thread对象的绑定关系,去调用Theaad对象的run()方法,至此一个线程就完全创建完成并开始执行业务了。

Java 线程详解(上)

注:从上面线程创建的流程中可以看出,Java的线程属于内核级线程,完全基于操作系统线程模型来实现,JVM与操作系统之间采用一对一的线程模型实现。

2.3 协程

Coroutines是一种基于线程之上,但是比线程更轻量级的存在,协程不被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对操作系统内核不可见的特性。这样带来的好处就是性能得到了极大的提升,不会像线程切换那样消耗CPU资源

Java 线程详解(上)

Java中的协程框架:kilim、quasar

def A():
	print '1'
	print '2'
	print '3'
def B():
	print 'x'
	print 'y'

上面的代码,如果由一个线程来执行,输出的结果就是1 2 3 x y,假设由协程来执行,在执行A的过程中可以随时中断然后去执行B,B也可能随时中断去执行A,可能就会出现1 x 2 y 3这样的结果

协程的特点在于是一个线程执行,那么和多线程相比,协程有什么优势呢?

  • 线程的切换由操作系统来完成,而协程由用户自己调度,因此减少了上下文切换,提高了效率
  • 线程默认stack大小为1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不需要加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注:协程适合用于被阻塞的,且需要大量并发的场景(网络IO),不适合大量计算的场景

三、线程调度机制

在操作系统中,线程调度分为协同式调度和抢占式调度:

  • 协同式线程调度

    线程执行时间由线程本身来控制,线程完成自身工作之后,主动通知系统切换到另一个线程上。这种方式最大的优点就是实现简单,不需要关心线程间同步的问题。缺点也非常明显,由于线程执行时间不可控制,如果一个线程有问题,就可能一直阻塞在那里

  • 抢占式线程调度

    每个线程由操作系统来分配执行时间,线程的切换不由线程本身决定(Java中,Thread.yield()方法可以让出执行时间,但无法获取执行时间)。线程执行时间由系统进行控制,就不会有一个线程导致整个进程阻塞的问题。这种方案实现起来要复杂很多。

现在基本上大多数的线程调用都会采用抢占式的线程调度策略,关于线程调度的具体介绍可以参考《详解操作系统线程》这篇文章中对线程调度以及调度策略的描述

Java线程调度也是采用抢占式的调度方式,可以通过设置线程的优先级来决定为哪些线程多分配一些时间片,哪些线程少分配一些时间片。

Java语言中一共定义了10个级别的线程优先级:

通过常量定义三个等级的优先级,但是可以通过setPriority()方法设置1-10这十个优先级,当两个线程同时处于ready状态时,优先级越高的线程越容易被系统执行。

public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

public final static int MAX_PRIORITY = 10;

public final void setPriority(int newPriority) {
	……
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
	……
}

用下面抢票的例子可以模拟一下线程优先级的调度。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

public class SellTicketDemo implements Runnable {
    /**
     * 车票
     */
    private int ticket;

    public SellTicketDemo() {
        this.ticket = 1000;
    }

    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        // 线程进入暂时的休眠
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 获取到当前正在执行的程序的名称,打印余票
                    System.out.println(Thread.currentThread().getName()
                                       + ":正在执行操作,余票:" + ticket--);
                }
            }
            Thread.yield();
        }
    }

    public static void main(String[] args) {
        SellTicketDemo demo = new SellTicketDemo();

        Thread thread1 = new Thread(demo,"thread1");
        Thread thread2 = new Thread(demo,"thread2");
        Thread thread3 = new Thread(demo,"thread3");
        Thread thread4 = new Thread(demo,"thread4");
        //priority优先级默认是5,最低1,最高10
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);
        thread4.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/153635.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!