Java多线程详解(一)


前言



最近在学习Android的Handler相关知识,涉及到了Java的线程内容,加上之前自己在多线程这块掌握的也不是很好,所以想专门用两篇博客的篇幅来记录一下Java多线程的相关知识和实际应用。本篇介绍的是线程的基本知识、概念以及常用API的介绍,下一篇计划记录的是关于线程同步的相关内容。




一、多任务、多进程与多线程



操作系统的多任务(multitasking)想必大家已经非常熟悉了,即在同一时刻运行多个程序的能力,例如一边玩游戏一边下载电影,实际上是操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉。这里又提到了进程,那么什么是进程,通俗的讲进程就是一个正在运行的程序。再说回来,多任务的处理有两种方式,它们分别是:

1.基于进程

2.基于线程

基于进程的多任务实际上就是操作系统同时运行的多个程序,一个程序就是一个进程,我们打开Windows的任务管理器可以看到:




不难发现,一个进程无非就是一个运行中应用程序,这就是基于进程的多任务。

那么什么又是多线程呢?用Core Java上的话说,多线程在较低的层次上扩展了多任务的概念,即:一个程序同时执行多个任务,通常每一个任务称为一个线程(Thread),可以同时运行一个以上线程的程序称为多线程程序。举个例子,迅雷下载,我们只打开一个迅雷(一个进程),却可以同时执行多个下载任务(多个线程),这就是基于线程的多任务。了解了基本概念,我们再谈谈多线程与多进程在处理多任务中的区别,很明显,多进程需要启动多个应用程序,而多线程只需要启动一个应用程序,在开销方面肯定多进程来的更低,其实它们本质的区别在于:每个进程都拥有自己的一整套变量,而线程则共享数据。从内存角度讲,每个进程都需要分配它们自己独立的地址空间,而每个线程却可以共享相同的地址空间并且共同分享同一个进程的数据。



二、初识Java中的线程



Java本身就是基于多线程的语言,那么运行一个Java程序,进程和线程又是如何提现的?

其实,当我们运行一个Java程序时,调用了java -类名这个命令,将编译好的class文件解释运行,这个动作就是开启了一个Java虚拟机进程,而我们的程序入口是main方法,main方法所在的线程就是主线程,垃圾回收机制是Java的一大特点,它也是一个线程(垃圾回收线程),而且该线程的优先级很高。说了这么多想必大家对Java中的线程已经有了基本的认识,Java是面向对象语言,线程自然也是抽象为一个类,就是Thread类。下面通过代码看看实现线程的两种方式:


1.实现Runnable接口并重写run()方法

package com.xw.blog;

public class MyThreadOne implements Runnable {
	@Override
	public void run() {
		Thread.currentThread().setName("My Thread One"); // 得到当前线程对象,并设置线程Name
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + "--->" + i); // 循环打印当前线程的Name+i
		}
	}
}


测试类:
package com.xw.blog;

public class Test {
	public static void main(String[] args) {
		Thread threadOne=new Thread(new MyThreadOne());  //通过Runnable对象构造一个Thread对象
		threadOne.start();  //启动线程
	}
}


运行之后可以看到:



上面的例子我们通过实现Runnable接口的方式完成了一个线程类,这个线程类的功能是重新设置线程Name,并循环打印5个自然数,在测试类中通过Thread的构造方法Thread(Runnable target)创建了线程对象,并通过start()方法启动线程。下面对上述例子用到的方法做一下总结:


Thread.currentThread()   静态方法,返回当前正在执行的线程对象

Thread.setName(String name)  静态方法,设置当前线程的名字

Thread.getName()  静态方法,返回当前线程的名字

start()  实例方法,启动线程


2.继承Thread类并重写run()方法

package com.xw.blog;

public class MyThreadTwo extends Thread {
	@Override
	public void run() {
		this.setName("My Thread Two"); // 设置当前的线程Name
		for (int i = 0; i < 5; i++) {
			System.out.println(this.getName() + "--->" + i); // 循环打印当前线程的Name+i
		}
	}
}

测试类:

package com.xw.blog;

public class Test {
	public static void main(String[] args) {
		Thread threadOne = new Thread(new MyThreadOne()); // 通过Runnable对象构造一个Thread对象
		Thread threadTwo = new MyThreadTwo(); // 直接通过子类构造一个Thread对象
		threadOne.start(); //启动线程一
		threadTwo.start(); //启动线程二
	}
}


运行之后可以看到:



根据运行结果我们发现,这两种方式都实现了一个线程类,都能正常、完整的打印出所有线程的Name,但是顺序好像有点乱,并不是我们预期的,其实多运行几次就会发现,顺序根本就毫无规律,原因就是我们启动线程调用了start()方法之后,线程并没有立即运行,而是进入了就绪状态,也就是说并没有立即调用run()方法,至于何时调用run()方法,是根据CPU来决定的,CPU会根据当前的占用情况来为每个线程分配时间片,也就是说多个线程并发执行时,谁先谁后是无法精确控制的(但可以通过其它方法控制线程的状态或者通过设置线程优先级来合理的进行线程调度,这一点在后面再讲)。那么我们在启动线程的时候为什么不能调用run()方法呢?因为直接调用run()方法,会执行同一个线程中的任务,而不会启动新的线程,所以应该调用start()方法,这个方法将创建一个执行run()方法的新线程



三、线程的状态及生命周期


线程有如下5种状态:

1.新建(New)

---线程对象被创建,尚未调用start()方法。

2.就绪/可运行(Runnable)

---线程对象的start()方法已被调用,正在等待CPU的使用权。

3.运行中(Running)

---线程占用CPU,正在执行run()方法内部的代码。

4.阻塞(Blocked/waiting/Timed waiting)

---线程对象放弃CPU,暂时停止运行,不会再主动竞争占用CPU。

5.终止(Terminated)

---线程的run()方法已经执行完毕。


看了上面针对每个状态的简单介绍,相信除了阻塞状态之外,其它的几个状态都比较容易理解,下面就阻塞状态结合实例记录一下它的表现形式和作用。比如睡眠就是阻塞线程最常用的方法之一:

Thread.sleep(long millis)  

这是Thread类的静态方法,作用就是让当前线程睡眠(暂停)指定毫秒数。


其实让一个线程阻塞,目的就是给其它线程运行的机会。在上面的例子中,我们在两个线程类中的循环里面都加上一句Thread.sleep(1000),即每次循环睡眠1000毫秒,那么当一个线程睡眠的时候,另一个线程就有机会运行,这样彼此就可以很好的交替轮换执行了,点击运行可以看到结果:




除了sleep()方法,还有一个方法可以使线程阻塞,那就是join()方法。

join()方法的作用是:使当前线程等待其它线程运行完毕再运行。下面通过实例代码来演示,我们修改上面的MyThreadTwo的代码,在其中加了两行:


package com.xw.blog;

public class MyThreadTwo extends Thread {

	private Thread thread;

	public MyThreadTwo(Thread thread) {  //通过构造方法注入其它线程对象
		this.thread = thread;
	}

	public void run() {
		this.setName("My Thread Two");
		try {
			for (int i = 0; i < 10; i++) {
				System.out.println(this.getName() + "--->" + i); // 循环打印当前线程的Name+i
				Thread.sleep(1000);
				if(i==5){  //当前线程运行到i==5时
					this.thread.join();  //调用MyThreadOne的join()方法,为ThreadOne让步,使当前线程等待ThreadOne执行完毕再执行。
				}
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}


相应的测试类中也通过构造方法将MyThreadOne注入到MyThreadTwo中,再调用join方法:

package com.xw.blog;

public class Test {
	public static void main(String[] args) {
		Thread threadOne = new Thread(new MyThreadOne()); // 通过Runnable对象构造一个Thread对象
		Thread threadTwo = new MyThreadTwo(threadOne); // 直接通过子类构造一个Thread对象
		threadOne.start(); // 启动线程一
		threadTwo.start(); // 启动线程二
	}
}


运行之后可以看到效果,MyThreadTwo会在MyThreadOne执行完毕之后再执行:



需要注意的是,处于阻塞状态下的线程对象不会竞争和占用CPU,线程对象结束阻塞状态后将进入就绪状态,而不是运行状态。比如当调用sleep()方法后,线程从运行状态进入阻塞状态,睡眠结束后从阻塞状态进入就绪状态。比如调用join()方法后,当前线程从运行状态进入阻塞状态,结束等待后从阻塞状态进入就绪状态。


还有一个方法可以使运行中的线程改变状态,即从运行中变成就绪状态,这就用到了yield()方法。


yield()是Thread类的静态方法,作用是使当前线程显式出让CPU控制权(让步),即使得当前线程从运行状态变成就绪状态。让步是线程调度中另一个重要的方法,而且不会抛出异常。让步的目的是给其它线程运行的机会,当然前提是优先级相同的线程之间。


一个线程完整的生命周期即从新建到死亡,下面通过一个示意图来看看线程的状态以及生命周期:





四、线程的优先级



上面提到了线程的优先级,下面就作一下具体的介绍,线程的优先级就是在Thread类中定义的常量,源码中是这样的:

   /**
     * The minimum priority that a thread can have. 
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread. 
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have. 
     */
    public final static int MAX_PRIORITY = 10;

可以看到定义了3个级别,分别是MIN(1),NORM(5),MAX(10),如果一个线程我们没有设置优先级,那么默认的就是NORM(5)。关于优先级的两个方法:final void setProperty(int newProperty)final intgetProperty()。那么显而易见,set方法是设置优先级,而get方法是得到当前线程的优先级。具体我们可以参考一下源码:


public final void setPriority(int newPriority) {
        ThreadGroup g;
	checkAccess();
	if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {  //如果优先级大于10或小于1会抛出异常
	    throw new IllegalArgumentException();
	}
	if((g = getThreadGroup()) != null) {
	    if (newPriority > g.getMaxPriority()) {
		newPriority = g.getMaxPriority();  
	    }
	    setPriority0(priority = newPriority); //将参数的优先级的值赋给当前线程
        }
    }

public final int getPriority() {
	return priority;  // 这个很简单,不用说了,直接返回当前线程的优先级
    }

上面两段源码不复杂,源码中关于优先级的处理也很简单,结合注释相信应该都能看懂。最后我们做个测试:

又写了两个简单的线程类,分别是MyThreadThree和MyThreadFour,它们分别打印10遍“线程名+自然数”,我们在运行的时候通过设置优先级来看看结果有何变化。先上代码。


MyThreadThree.java:

package com.xw.blog;

public class MyThreadThree implements Runnable {
	@Override
	public void run() {
		Thread.currentThread().setName("My Thread Three"); // 设置线程Name
		for (int i = 0; i < 10; i++) {
			Thread.yield();
			System.out.println(Thread.currentThread().getName() + "--->" + i); // 循环打印当前线程的Name+i
		}
	}
}


MyThreadFour.java:
package com.xw.blog;

public class MyThreadFour implements Runnable {
	@Override
	public void run() {
		Thread.currentThread().setName("My Thread Four"); // 设置线程Name
		for (int i = 0; i < 10; i++) {
			Thread.yield();
			System.out.println(Thread.currentThread().getName() + "--->" + i); // 循环打印当前线程的Name+i
		}
	}
}


Test.java:

package com.xw.blog;

public class Test {
	public static void main(String[] args) {	
		Runnable r3 = new MyThreadThree();
		Thread thread3 = new Thread(r3);
		thread3.setPriority(Thread.MAX_PRIORITY);  //设置优先级为10
		
		Runnable r4=new MyThreadFour();
		Thread thread4=new Thread(r4);
		thread4.setPriority(Thread.MIN_PRIORITY);  //设置优先级为1
		
		thread3.start();
		thread4.start();
	}
}


下面是运行了10次的效果图:




我们经过对比后可以发现,好像是ThreadThree先执行的概率大一些,没错仅仅是概率,这就是优先级的作用了。它只是宏观的对线程进行调度,但具体谁先谁后也不是优先级能控制的,就像我们看到的,尽管设置为两个极端,还是做不到绝对的谁先谁后。



五、线程调度


上面几次提到线程调度的概念,现在就具体说一下什么是线程调度。概念是这样的:

合理分配多个线程对CPU资源占用,即线程调度。

由于多线程对CPU的大量需求与CPU数量不足产生的矛盾,我们需要线程调度,通过上面的例子我们发现,线程调度也仅仅只能从宏观角度出发,我们只需从大的整体上保证多线程对CPU的占用分配符合需要即可,微观上是无法控制的,就像上面优先级的例子,具体到哪个线程谁先谁后,我们无法控制。线程调度的手段上面都有介绍,即:合理设定优先级、通过sleep()、yield()或join()方法等等



六、总结


本章主要介绍了一些线程的基本概念和基本用法,包括如何新建线程、线程的各个状态和生命周期、线程的优先级以及线程的调度思想、线程的常用方法和API,下一篇将会记录关于线程同步的相关知识,是线程方面真正的重点和难点,也是我个人掌握不太好的地方,我将会花更大的篇幅来做好详尽的记录。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值