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。
先写到这里吧,今天写了基本的概念,明天写同步的一些用法!