线程作为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的映射关系
创建线程
当Thread对象调用start0()本地方法时,会去调用JVM的JVM_StartThread()方法进行线程的创建的和启动,而在该方法中,会调用navite_thread = new JavaThread(&thread_entry,sz)
进行线程的创建。
在该方法中,会去调用操作系统的线程创建的方法,以X86的linux系统为例,会去调用create_thread()方法,而在该方法中又去调用pthread_create(),这个方法才是去真正的创建一个线程。
线程创建完成之后,一直处于初始化的状态,所以会一直进行阻塞,直到被唤醒
上面创建线程的过程都是在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状态
在Linux中最后真正调用的是pd_start_thread(Thread* thread)
方法,该方法会去唤醒前面创建线程后一直处于阻塞状态的线程,最后调用JVM中的JavaThread的run()方法
在JavaThread::run
会去调用JavaThread::thread_main_inner
,在thread_main_inner()方法中,会去执行this->entry_point()(this,this)
,最后调用到thread_entry()方法,在该方法中,会根据前面JVM的JavaThread与Java的Thread对象的绑定关系,去调用Theaad对象的run()方法,至此一个线程就完全创建完成并开始执行业务了。
注:从上面线程创建的流程中可以看出,Java的线程属于内核级线程,完全基于操作系统线程模型来实现,JVM与操作系统之间采用一对一的线程模型实现。
2.3 协程
Coroutines是一种基于线程之上,但是比线程更轻量级的存在,协程不被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对操作系统内核不可见的特性。这样带来的好处就是性能得到了极大的提升,不会像线程切换那样消耗CPU资源
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