Thread类的核心方法较多,读者应该着重掌握如下关键技术点:
□线程的启动
□如果使线程暂停
□如何使线程停止
□线程的优先级
□线程安全相关的问题
1.1 进程与多线程的概念及线程的优点
本节主要介绍在Java语言中使用多线程技术。但是讲到多线程不得不提到进程这个概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,
是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早
期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代
面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据
及其组织形式的描述,进程是程序的实体。
将一个正在操作系统中运行的exe程序理解成一个进程!通过查看任务管理器中的列表,完全可以将运行在内存中的exe文件理解成进程,进程是受操作系统管理的基本运行单元。
那什么是线程呢?线程可以理解成是进程中独立运行的子任务。比如,QQ.exe运行时就有很多的子任务在同时运行。再如,好友视频线程,下载文件线程,传输数据线程等等,这些不同的任务或者说功能都可以同时运行,其中每一项任务完全可以理解成是线程在工作。使用多任务操作系统后,可以最大限度地利用CPU的空闲时间来处理其它的任务,比如一边打印,一边使用word编辑。而CPU在这些任务之间不停地切换,由于切换速度非常快,给使用者的感觉就是这些任务似乎在同时运行。所以在使用多线程技术后,可以在同一时间内运行更多不同种类的任务。
1.2 使用多线程
一个进程正在运行时至少会有一个线程在运行,这种情况在java中也是存在的。这些线程在后台默默地执行:
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());// main
}
在控制台输出的main其实就是一个名称叫做main的线程在执行main方法中的代码。另外需要说明一下,在控制台输出的main和main方法没有任何的关系,仅仅是名字相同而已。
1.2.1 继承Thread类
实现多线程编程的方式有两种,一种是继承Thread类,另一种是实现Runnable接口。先来看看Thread类的结构:
public class Thread implements Runnable
从源代码可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。其实,使用继承Thread类的方式创建线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单根继承,所以为了支持多继承,完全可以实现Runnable接口的方式,一边实现一边继承。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("My Thread!");
}
public static void main(String[] args) {
MyThread my = new MyThread();
my.start();
System.out.println("运行结束");// 运行结束/n My Thread!
}
}
以上代码的打印顺序可以看出来,run方法执行的时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序无关。
上面代码介绍了线程的调用的随机性,下面将演示线程的随机性。
public class MyThread extends Thread {
@Override
public void run() {
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("run="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread my = new MyThread();
my.setName("myThread");
my.start();
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("main="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在代码中,为了展现出线程具有随机特性,所以使用随机数的形式来使线程得到挂起的效果,从而表现出CPU执行哪个线程具有不确定性。Thread.java类中的start方法通过‘线程规划器‘此线程已经准备就绪,等待调用线程对象的run方法。这个过程其实就是让系统安排一个时间来调用Thread中的run方法,也就是线程得到运行,启动线程,具有异步执行的效果。如果直接调用代码thread.run就不是异步执行。 而是同步,那么此线程对象并不会交给"线程规划器"来进行处理,而是由main主线程来调用run方法,也就是必须等run方法中的代码执行完成后才能执行后面的代码。(看下面代码中的注释)
public static void main(String[] args) {
MyThread my = new MyThread();
my.setName("myThread");
my.run();// 由start替换成run,运行结果发生变化,先执行完线程里面的,然后再执行main方法里面的
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("main="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
另外还需要注意一点,执行start()方法的顺序不代表线程启动的顺序.
1.2.2 实现Runnable接口
如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这样的情况.
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("运行中!");
}
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结束");// 运行结束/n 运行中!
}
}
使用继承Thread类的方式来开发多线程应用程序在设计上是有局限性的,因为Java是单根继承,不支持多继承.另外需要说明的是,Thread类也实现了Runnable接口,那也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用.
1.2.3 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点.
(1)不共享数据的情况
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while(count > 0) {
count --;
System.out.println("由"+this.currentThread().getName()+"计算,count=" + count);
}
}
public static void main(String[] args) {
Thread t1 = new MyThread("A");
Thread t2 = new MyThread("B");
Thread t3 = new MyThread("C");
t1.start();
t2.start();
t3.start();
}
}
打印结果为:
由B计算,count=4
由B计算,count=3
由B计算,count=2
由B计算,count=1
由B计算,count=0
由C计算,count=4
由A计算,count=4
由A计算,count=3
由C计算,count=3
由A计算,count=2
由A计算,count=1
由A计算,count=0
由C计算,count=2
由C计算,count=1
由C计算,count=0
(1)共享数据的情况,意思就是多个线程访问同一个变量
public class MyThread extends Thread {
private int count = 5;
public MyThread() {
super();
}
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
count--;
System.out.println("由" + this.currentThread().getName() + "计算,count=" + count);
}
public static void main(String[] args) {
Thread thread = new MyThread();
Thread t1 = new Thread(thread, "A");
Thread t2 = new Thread(thread, "B");
Thread t3 = new Thread(thread, "C");
Thread t4 = new Thread(thread, "D");
Thread t5 = new Thread(thread, "E");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t4.start();
t5.start();
}
}
结果如下:由此可以得知产生了"非线程安全"问题.
由B计算,count=3
由C计算,count=2
由A计算,count=3
由E计算,count=1
由D计算,count=0
如果要结果正确,代码改动如下(新添加一个synchronize关键字)
@Override
public synchronized void run() {
super.run();
count--;
System.out.println("由" + this.currentThread().getName() + "计算,count=" + count);
}
通过在run方法前加入synchronize关键字,使多个线程在执行run方法时,以排队的方式进行处理.当一个线程调用run前,先判断run方法有没有上锁,如果上锁,说明有其它线程正在调用run方法,必须等待其它线程对run方法调用结束后才可以执行run方法.这样也就实现了排队调用run方法的目的,synchronize可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区".
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能拿到这把锁,那么这个线程就可以执行synchronize里面的代码.如果不能拿到这把锁,那么这个线程就会不断地去长度拿这把锁,知道能拿到为止.而且是多线程同时去争抢这把锁.
1.3 currentThread()方法
currentThread()方法可返回代码正在被那个线程调用的信息.
public class CurrentThreadTest extends Thread{
public CurrentThreadTest() {
System.out.println("constructor is :" + currentThread().getName());
}
@Override
public void run() {
super.run();
System.out.println("run is :" + currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new CurrentThreadTest();
thread.start();// run is :thread-0
// thread.run(); // run is :main
}
}
稍微复杂一点的:
public class CountOperate extends Thread {
public CountOperate() {
System.out.println("CountOperate--begin");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("this.getName()="+this.getName());
System.out.println("CountOperate--end");
}
@Override
public void run() {
System.out.println("run--begin");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("this.getName()="+this.getName());
System.out.println("run--end");
}
public static void main(String[] args) {
Thread thread = new Thread(new CountOperate());
thread.setName("Demo");
thread.start();
}
}
打印结果如下:
CountOperate--begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
CountOperate--end
run--begin
Thread.currentThread().getName()=Demo
this.getName()=Thread-0
run--end
1.4 isAlive()
isAlive()的功能是判断该线程是否处于活动状态.活动状态就是线程未停止.当线程处于正在运行或者准备开始运行的状态,就认为线程是存活的.
public class IsAliveTest extends Thread{
@Override
public void run() {
System.out.println("run: isAlive=" + this.isAlive());// true
}
public static void main(String[] args) {
Thread thread = new IsAliveTest();
System.out.println("begin: isAlive=" + thread.isAlive());// false
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end: isAlive=" + thread.isAlive());// false
}
}
1.5 sleep()方法
sleep()的作用是在指定的毫秒数内让当前"正在执行的线程"休眠(暂停执行).这个"正在执行的线程"是指this.currentThread()返回的线程.
public class MyThread1 extends Thread{
@Override
public void run() {
try {
System.out.println("run threadName = " + this.currentThread().getName() + " begin");// 1
Thread.sleep(2000);
System.out.println("run threadName = " + this.currentThread().getName() + " end");// 2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
System.out.println("begin = " + System.currentTimeMillis());// 3
// myThread.start();// 顺序 3,4,1,2
myThread.run();// 顺序 3,1,2,4
System.out.println("end = " + System.currentTimeMillis());// 4
}
}
1.6 getId()方法
getId()方法的作用是取得线程的唯一标识.