Java多线程实践之基础篇

1. 线程的概念

一个程序可能包含多个并发运行的任务,线程是指一个任务从头到尾的执行流。在一个程序中并发的启动多个线程,这些线程可以在多处理器系统或单处理器系统中同时运行。在但CPU中,由操作系统负责调度及分配资源给它们。

在Java中,每个任务都是Runnable接口的一个实例,线程本质上就是一个对象(也可以说是runnable object)。

2. 创建任务和线程

任务就是一个对象。为了创建任务,所以应该定义一个任务类,这个类必须实现Runnable接口。这个接口只有一个run()方法,也是一个线程的入口。一个任务类的模板如下代码:

package zy.thread.demo;

public class TaskClass implements Runnable{

	public TaskClass() {
		// TODO Auto-generated constructor stub
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub		
	}
}
package zy.thread.demo;

public class Client {
	public void someMethod () {
		//创建一个任务实例
		TaskClass task = new TaskClass();
		//任务实例作为参数传入Thread构造函数
		Thread thread = new Thread(task);
		//高速JVM准备运行,调用Runnable接口中的run()方法!
		thread.start();
	}
}
上面给出的样例,就是线程创建和运行的基本流程

注意事项:任务中的run()方法,Java虚拟机会自动调用,无需显示调用,显示调用将会在同一个线程中执行该方法的,新线程不会启动!

另外还有一种创建线程的方法:定义一个Thread的子类,并实现其run()方法。然后在程序中实例化这个类的一个对象,并调用它的start()方法启动线程。例如,ThreadClass继承自Thread类,创建一个对象ThreadClass thread = new ThreadClass ();接着调用thread.start(),这样这个线程就启动了。虽然这个方法比实现Runnable接口的方法少调用了一行代码,但是推荐使用实现Runnable接口的方法,面向接口编程有着很大优势!

3. Thread类

来看看Thread的主要方法,毕竟要跟它打交道,混个脸熟!

Thread()、Thread(Runnable)、start()、isAlive()、setPriority(int)、join()、sleep()、yield()、interrupt()等,这边也列不全,大家自己去看API好了!

注意:stop()、suspend()、resume()具有不安全因素,因此不提倡使用这些方法。stop()方法可以替代为给Thread变量赋值为null。

来几个简单点的例子,下面的代码作用是创建两个线程,一个打印字符、一个打印数字:

package threadDemo;

public class TaskThreadDemo {
	public static void main(String[] args) {
		Runnable printA = new PrintChar('a', 100);
		Runnable print100 = new PrintNum(100);
		
		Thread thread1 = new Thread(printA);
		Thread thread3 = new Thread(print100);
		
		thread1.start();
		thread3.start();
	}
}

class PrintChar implements Runnable {
	private char charToPrint;
	private int times;
	
	public PrintChar(char c, int t) {
		charToPrint = c;
		times = t;
	}
	public void run() {
		for (int i = 0; i < times; i++)
			System.out.print(charToPrint);
	}
}

class PrintNum implements Runnable {
	private int lastNum;
	public PrintNum(int n) {
		lastNum = n;
	}
	public void run() {
		for (int i = 1; i <= lastNum; i++)
			System.out.print(" " + i);
	}
}
这上面的代码可以看到并行运算的效果,交替进行!
1. 我们可以使用yield()方法(只是对线程调度器的建议,完全是选择性的)为其他线程让出CPU,将上述代码中的35~38行修改如下:
public void run() {
		for (int i = 1; i <= lastNum; i++) {
			System.out.print(" " + i);
			Thread.yield();
		}		
	}
因为现在电脑配置越来越好,打印100个字符跟吃饭一样的,所以可以适当提高打印字符的个数、减少打印数字的个数,效果可能会好!

这样每次打印一个数字(也可能是几个,因为实在速度快)后,就会紧跟着一些字符。在多线程中,很难做到“精准”控制,操作系统有自己的节奏!

2. 休眠sleep()

同样是35~38行,替换为:

public void run() {
		try {
			for (int i = 1; i <= lastNum; i++) {
				System.out.print(" " + i);
				if (i >= 50) Thread.sleep(1);
			}
		} catch (InterruptedException e) {
		}		
	}
与yield()不同,sleep()可以确保其他线程的执行,参数是毫秒,意思是:我先休息一会,你们先干活!

在这里为什么要用try catch块?因为睡眠中的线程interrupt()方法被调用时,就会产生异常,由于异常不能跨线程的传播,因此只能本地默默的处理,虽然这种情况很少碰到,但是Java强制捕获必检的异常。如果在一个循环中调用了sleep()方法,那应该将循环放入try-catch块中,否则可能会继续执行【这段来自Java基础编程第8版】

3. join()

同样是35~38行,替换为:

public void run() {
		Thread thread = new Thread(new PrintChar('c', 40));
		thread.start();
		try {
			for (int i = 1; i <= lastNum; i++) {
				System.out.print(" " + i);
				if (i == 50) thread.join();
			}
		} catch (InterruptedException e) {
		}		
	}
在thread3中创建了thread,打印字符40次。在thread结束之后,thread3打印50~100的数字。

在分析这个过程之前,我们来看看JVM的线程运行机制:

Java给每个线程指定一个优先级,默认情况下,线程继承生成它的那个线程的优先级。可以用setPriority()方法来设置线程的优先级,getPriority()方法获得优先级。Thread类有三个int型的优先级常量,分别是MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),主线程的优先级是NORM_PRIORITY(5)。

JVM总是选择当前优先级最高的可运行线程,如果所有的线程具有相同的优先级,那将会用循环队列给它们分配相同的CPU份额,即循环调度!在这个例子里面,thread1、thread3、thread优先级都是5,因此按照启动顺序,thread1先加入队列、再者是thread3、最后thread,但是结果可能并不如我们所料,因为执行次数太少,很难看出来调度的过程,因为还没来得及调用,就已经执行完成了!可以将上面代码修改一下,将start()方法写在判断语句里,则可以严格控制等c字符输出40次之后再输出50之后的数字,下面是代码:

public void run() {
		Thread thread = new Thread(new PrintChar('c', 40));	
		try {
			for (int i = 1; i <= lastNum; i++) {
				System.out.print(" " + i);
				if (i == 50) {
					thread.start();
					thread.join();
				}
			}
		} catch (InterruptedException e) {
		}		
	}
注意:如果总有一个优先级较高的线程在运行,或者有一个相同优先级的线程不退出,那么其他线程可能永远没有运行的机会,这种情况称之为资源竞争或缺乏状态。为避免这种现象,优先级高德线程必须定时的调用sleep()或yield()【只是建议,并非强制】方法,让出一个运行的机会!

4. 线程池

线程池是管理并发执行任务个数的理想方法。Java提供Executor接口来执行线程池中的任务,提供ExecutorService接口来管理(创建、销毁)和控制任务。为了创建Executor对象,可以使用Executors类中的静态方法,在线程池中创建一定数量的线程,上代码:

package threadDemo;

import java.util.concurrent.*;

public class ExcutorDemo {

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(3);
		
		executorService.execute(new PrintChar('a', 100));
		executorService.execute(new PrintChar('b', 100));
		executorService.execute(new PrintNum(100));
		
		executorService.shutdown();
	}

}
如果将第8行,替换成:
ExecutorService executorService = Executors.newFixedThreadPool(1);
则会顺序执行3个线程。如果将8行换成:
ExecutorService executorService = Executors.newCachedThreadPool();
则会并发的执行3个线程,因为当有任务在等待时,而当前线程池中所有线程都不是空闲时,newCachedThreadPool()方法就会创建一个新线程。

执行shutdown()方法之后,不能接受新的任务,但现有任务将继续执行直至完成。

注意:如果只有一个线程,那么推荐使用Thread类,多个任务,则使用线程池。

5. 从线程中产生返回值

Runnable是一个独立任务,不会返回任何值,如果需要返回值,则可以实现Callable接口。它是一种具有类型参数的泛型,类型参数表示的是从call()(原来的是run()方法)方法返回的值,并且必须使用ExecutorService.submit()方法调用它,简单的示例:

package zy.thread.demo;

import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class TaskWithResult implements Callable<String>{

	private int id;
	public TaskWithResult(int id) {
		this.id = id;
	}
	@Override
	public String call() throws Exception {
		return "Result of TaskWithResult " + id;
	}

}

public class CallableDemo {
	public static void main(String[] args) {
		ExecutorService executor = Executors.newCachedThreadPool();
		ArrayList<Future<String>> results = 
				new ArrayList<Future<String>>();
		for (int i = 0; i < 10; i++) {
			results.add(executor.submit(new TaskWithResult(i)));
		}
		for (Future<String> future : results) {
			try {
				System.out.println(future.get());
			} catch (InterruptedException e) {
				System.out.println(e);
				return;
			} catch (ExecutionException e) {
				System.out.println(e);
			} finally {
				executor.shutdown();
			}
		}
	}
}
可以调用isDone() 方法来查询Future是否已经完成,不调用直接使用get()方法,那么将会阻塞,直到结果准备就绪。因此上述代码多了InterruptedException。

先写到这里吧,今天写了基本的概念,明天写同步的一些用法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值