文档章节

Jdk源码之Thread

村长杨京京
 村长杨京京
发布于 2016/09/13 21:51
字数 2498
阅读 143
收藏 2

本文通过几个问题入手,主要通过案例讲解Thread类的使用,以及注意事项,最后站在源码的角度分析问题。

1.多线程一定快吗?

多线程不一定快!

多线程只是极大限度的利用CPU的空闲时间来处理其他的任务,这样才会缩短多个任务的执行时间。如果在没有CPU空闲的情况下,多线程之间的上下文切换(保存线程的运行环境,还原线程的运行环境)会产生开销,执行时间会慢于单线程。

2.区分单任务操作系统与多任务操作系统

单任务操作系统:同步执行,排队。只有当前任务执行完了才可以执行下一个任务。例如cmd中执行完一条指令之后才可以执行另一条指令

多任务操作系统:异步执行,通过时间片的快速切换实现多个任务响应执行。

3.创建Thread类的两种形式,及其差别

(1)继承Thread类,重写run( )方法

package com.feng.example;

public class MyThread extends Thread {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		System.out.println("继承Thread类");
	}	
}

启动线程:

package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyThread myThread = new MyThread();
		myThread.start();

	}

}

(2)创建一个Runnable接口的实现类A,将A的实例对象作为Thread类的参数传入

package com.feng.example;

public class MyRunnable implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("通过实现Runnable接口重写run");
	}

}

启动线程:

package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Runnable myRunnable = new MyRunnable();
		Thread myThread = new Thread(myRunnable);
		myThread.start();

	}

}

由于在Thread类也是实现Runnable接口的,因此Thread的对象也可以作为第二种形式的参数

public class Thread implements Runnable {} //这里只是贴出源码中Thread类的定义

将第一种方式创建的MyThread类对象作为第二种方式的输入参数

package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建MyThread类对象
		Runnable myRunnable = new MyThread();
		Thread myThread = new Thread(myRunnable);
		myThread.start();

	}

}

两种方式的区别:两种方式没有本质的区别,一般都会使用第二种形式,主要解决java中类不能多继承的缺陷。

比如Cat类继承Animal,同时又想将Cat定义为线程类,因为Cat不能同时继承Animal,Thread类,但是Cat可以继承Animal,实现Runnable接口

 

4.通过线程的start( )方法运行run( )方法中的代码与直接调用run( )方法的区别

回顾一下操作系统课中线程的创建过程:线程的创建包括两部分

(1)创建线程,相当于MyThread  myThread = new MyThread( );

(2)将线程放到就绪队列中,如果仅仅只是创建了线程却没有将线程放到就绪队列中,线程调度器是没法找到线程的。因此myThread.start( );将线程放到就绪队列中。等待分配cpu时间片,当获取了cpu之后就可以执行线程的run( )方法

通过start( )方法:是新开辟了一个线程,run( )方法是由线程规划器来调用的,是异步运行

直接调用run( )方法:调用者是Thread子类的一个对象,属于同步执行,没有开辟新的线程。

package com.feng.example;

public class MyThread extends Thread {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		System.out.println("线程为:"+Thread.currentThread().getName());
	}	
}
package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建MyThread类对象
		MyThread myThread = new MyThread();
		myThread.start();
		
		myThread.run();  //同步调用run方法  

	}

}

程序运行结果:

 

5.两种实现方式一块使用,会调用那种方式的run( )方法,通过源码分析

首先查看Thread类源码中是如何重写Runnable接口中的run( )方法的。

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

接下来看一下这个target又是什么?

/* What will be run. */
    private Runnable target;

由源码可以看出,在Thread类的内部,存在一个类型为Runnable的成员变量,此成员变量便是接收第二种创建线程类的方法中的输入参数的。再看Thread类的run方法,只是判断有此target存不存在,如果存在便执行target的run( )方法。

如果不存在怎么办?

如果不存在则是使用继承的方式来实现的线程类,那么run( )方法已经在Thread子类中进行了重写,覆盖了父类的run( )方法

下面通过一个题目来消化这个知识点:

Runnable接口的实现类如下:

package com.feng.example;

public class MyRunnable implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("通过Runnable接口实现线程类");
	}

}

Thread的子类实现如下:

package com.feng.example;

public class MyThread extends Thread {

	public MyThread(Runnable target)
	{
		super(target);
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("通过继承实现线程类");
	}	
}

测试代码如下:

package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建Runnable接口实现类
		Runnable myRunnable = new MyRunnable();
		//创建MyThread类对象
		MyThread myThread = new MyThread(myRunnable);
		
		//到底输出的是谁的run方法????
		myThread.start();
		

	}

}

分析: 首先start的是MyThread线程,因为会执行MyThread类的run( )方法。虽然传入了Runnable类型的参数,但是在MyThread类中的run( )方法中并没有super.run( );因此程序只输出MyThread类中的run( )方法中的输出。

输出结果如图:

修改MyThread类中的run( )方法:

package com.feng.example;

public class MyThread extends Thread {

	public MyThread(Runnable target)
	{
		super(target);
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		System.out.println("通过继承实现线程类");
	}	
}

分析:

首先start的是MyThread线程,因为会执行MyThread类的run( )方法。因为在构造MyThread时传入了Runnable类型的参数,因此在执行MyThread类中的run( )方法时,先执行super.run( ); 执行Thread类的run( ); 根据源代码先检查target存不存,这里target就是存入的myRunnable, 所以执行myRunnable里面的run( );执行完super.run( )方法后,在输出后面的语句。

输入结果如下图:

在有些地方可能会以匿名类的形式来进行考察:

package com.feng.example;

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		
		new Thread(new Runnable(){

			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("Runnable...");
			}
			
		}){
			public void run()
			{
				//super.run();
				System.out.println("Thread...");
			}
			
		}.start();

	}

}

看到此代码不要惊慌,其实与我最初定义的三个类文件的考察方式是一样的,只不过这里使用了匿名类,如果忘记了回去翻看匿名类的知识,此题注意注释的那一句super.run( ); 看一下注释不注释有什么区别。

只要理解Thread源码中的run( )方法,这个题目就显得很简单了。

浅析join的用法

一、在研究join的用法之前,先明确两件事情。

1.join方法定义在Thread类中,则调用者必须是一个线程,

例如:

Thread t = new CustomThread();//这里一般是自定义的线程类

t.start();//线程起动

t.join();//此处会抛出InterruptedException异常

2.上面的两行代码也是在一个线程里面执行的

以上出现了两个线程,一个是我们自定义的线程类,我们实现了run方法,做一些我们需要的工作;另外一个线程,生成我们自定义线程类的对象,然后执行

customThread.start();

customThread.join();

在这种情况下,两个线程的关系是一个线程由另外一个线程生成并起动,所以我们暂且认为第一个线程叫做“子线程”,另外一个线程叫做“主线程”。

二、为什么要用join()方法

主线程生成并起动了子线程,而子线程里要进行大量的耗时的运算(这里可以借鉴下线程的作用),当主线程处理完其他的事务后,需要用到子线程的处理结果,这个时候就要用到join();方法了。

三、join方法的作用

在网上看到有人说“将两个线程合并”。这样解释我觉得理解起来还更麻烦。不如就借鉴下API里的说法:

“等待该线程终止。”

解释一下,是主线程(我在“一”里已经命名过了)等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。(Waits for this thread to die.)

进入源码

    public final void join() throws InterruptedException {
    join(0);
    }

    public final synchronized void join(long millis) 
    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {    //进入这个分支 
        while (isAlive()) { //进入这个分支 
        wait(0);//阻塞 
        }
    } else {
        while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {
            break;
        }
        wait(delay);
        now = System.currentTimeMillis() - base;
        }
    }
    }

Thread --- lock/sychronized

从实现角度来说

        sychronied 关键字是JVM层实现的,由其提供内置的锁。而lock方式必须被显示的创建

从代码角度来说

        lock方式,代码缺乏优雅性。sychronized代码更加简洁,代码量少。

从使用角度来说

        lock方式更加灵活,但是只有在解决特殊问题的时候才会使用。

        用sychronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些必须使用lock方式。

        使用sychronized关键字时,如果某些事务失败了,那么就会抛出异常,由JVM自动释放线程资源,但是你无法也没有机会去做任何的清理工作,以维护系统使其处于良好的状态。使用显示的Lock方式,你就可以使用finally子句将系统维护在正确的状态了。 

        显示的Lock对象在加锁和释放锁方面,相对于内建的sychronized锁来说,还赋予了你耕细粒度的控制力。

从性能角度来说

     在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态

 

 

© 著作权归作者所有

共有 人打赏支持
上一篇: 可行性分析报告
下一篇: Jdk源码之ArrayList
村长杨京京
粉丝 161
博文 876
码字总数 904789
作品 0
杭州
程序员
私信 提问
谈谈那个听起来挺高大上的ThreadLocal

估计很多人都没听过,因为说实话平时写代码很少用到,我第一次看到代码里用是在的源码,的源码中有下面这样的一个定义: 定义 大家可以百度看,都说的很高大上,普通的解释是:当使用ThreadLocal维护...

Anderson大码渣
2017/10/11
0
0
JDK1.8 不一样的HashMap

前言 HashMap想必大家都很熟悉,JDK1.8 的 HashMap 随便一搜都是一大片一大片的,那为什么还要写呢,我会把它精简一下,一方面有利于自己的学习,另一方面希望让大家更好理解核心内容。本篇主...

小坏怎么被用了
2018/01/05
0
0
Java Thread.join()方法

join方法只有在继承了Thread类的线程中才有。 线程必须要start() 后再join才能起作用。 1. 使用方式 join是Thread类的一个方法,启动线程后直接调用。 2. 为什么使用join方法 在很多情况下,...

牧师-Panda
2016/09/17
13
0
Xcode debug Hotspot(一)——创建Xcode项目

概述 前面安装gdb调试hotspot里面,我记录了自己安装gdb调试hotspot的过程。后来我发现,使用gdb的时候,一般都是和eclipse结合在一起使用。而我作为一个有洁癖的开发,我实在不想自己的电脑...

flycash
2018/12/05
0
0
java多线程 --run()和Start()的区别

Thread类包含start()和run()方法,它们的区别是什么?本章将对此作出解答。本章内容包括: start() 和 run()的区别说明 start() 和 run()的区别示例 start() 和 run()相关源码(基于JDK1.7.0_...

Jasonisoft
2016/05/30
0
0

没有更多内容

加载失败,请刷新页面

加载更多

dockerfile 镜像构建(1)

通用dockerfile 利用已经编译好的.jar 来构建镜像。要构建的目录如下: [root@iZuf61quxhnlk9m2tkx16cZ demo_jar]# docker build -t demo:1 . 运行镜像: [root@iZuf61quxhnlk9m2tkx16cZ de...

Canaan_
58分钟前
2
0
Redis radix tree源码解析

Redis实现了不定长压缩前缀的radix tree,用在集群模式下存储slot对应的的所有key信息。本文将详述在Redis中如何实现radix tree。 核心数据结构 raxNode是radix tree的核心数据结构,其结构体...

阿里云云栖社区
今天
12
0
vue import 传入变量

在做动态添加component的时候,传入变量就会报错,出现以下错误信息: vue-router.esm.js?fe87:1921 Error: Cannot find module '@/components/index'. at eval (eval at ./src/components ......

朝如青丝暮成雪
今天
3
0
Flutter开发 Dio拦截器实现token验证过期的功能

前言: 之前分享过在Android中使用Retrofit实现token失效刷新的处理方案,现在Flutter项目也有“token验证过期”的需求,所以接下来我简单总结一下在Flutter项目中如何实现自动刷新token并重...

EmilyWu
今天
9
0
final Map可以修改内容,final 常量不能修改

1.final Map 可以put元素,但是不可以重新赋值 如: final Map map = new HashMap(); map = new HashMap();//不可以 因为栈中变量map引用地址不能修改 2.final str = “aa”; str = "bb";/......

qimh
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部