一、理解多线程
多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。
线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。
多个线程的执行是并发的,也就是在逻辑上“同时”,而不管是否是物理上的“同时”。如果系统只有一个CPU,那么真正的“同时”是不可能的,但是由于CPU的速度非常快,用户感觉不到其中的区别,因此我们也不用关心它,只需要设想各个线程是同时执行即可。
多线程和传统的单线程在程序设计上最大的区别在于,由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,由此带来的线程调度,同步等问题,将在以后探讨。
一般而言,进程包含如下3个特征:
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响的。
二、线程的创建和启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表程序流。
1.继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:
(1)定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动多线程。
下面是继承Thread类来创建并启动线程的例子:
public class FirstThread extends Thread{
private int i;
public void run(){
for( ; i<100; i++){
System.out.println(this.getName()+" ..."+i);
}
}
public static void main(String[] args){
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
new FirstThread().start();
new FirstThread().start();
}
}
}
}
2.实现Runnable接口创建线程类
实现Runnable接口来创建并启动多线程的步骤如下:
(1)定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
下面是实现Runnable接口来创建并启动线程的例子:
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for( ; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args){
for(int i=0 ; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondThread st = new SecondThread();
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
3.使用Callable和Future创建线程类
创建并启动有返回值的线程步骤如下:
(1)创建Callble接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。
(2)创建Callble实现类的实例,使用FutureTask类来包装Callble对象,该FutureTask对象封装了该Callble对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
下面程序通过实习Callble接口来实现线程类,并启动该线程:
public class ThirdThread implements Callable<Integer>{
@Override
public Integer call(){
int i=0;
for(; i<100; i++){
System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i);
}
return i;
}
public static void main(String[] args){
ThirdThread rt = new ThirdThread();
FutureTask<Integer> task = new FutureTask<Integer>(rt);
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i);
if(i==20){
new Thread(task, "有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值:"+task.get());
}catch(Exception ex){
ex.printStackTrace();
}
}
}
三、线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程生命周期中,它要经过新建、就绪、运行、阻塞和死亡5种状态。
启动线程使用start()方法,而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其它线程无法并发执行。
当发生如下情况时,线程将会进入阻塞状态:
1.线程调用了sleep()方法主动放弃所占用的处理器资源。
2.线程调用了一个阻塞IO方法,在该方法返回之前,该线程被阻塞。
3.线程视图获得一个同步监视器,但该同步监视器正被其它线程所持有。
4.线程在等待某个通知。
5.程序调用了线程的suspend()方法将该线程挂起。
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
1.调用sleep()方法的线程经过了指定时间。
2.线程调用的阻塞式IO方法已经返回。
3.线程成功的获得了视图取得的同步监视器。
4.线程正在等待某个通知时,其它线程发出了一个通知。
5.处于挂起状态的线程被调用了resume()恢复方法。
线程会以如下3种方式结束,结束后就处于死亡状态。
1.run()或call()方法执行完成,线程正常结束。
2.线程抛出一个未捕获的Exception或Error。
3.直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁。
四、控制线程
1.join线程
Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用了其它线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
如下程序:
public class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
public void run(){
for(int i=0; i<100; i++){
System.out.println(this.getName()+" "+i);
}
}
public static void main(String[] args) throws Exception{
new JoinThread("新线程").start();
for(int i=0; i<100; i++){
if(i==20){
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
2.线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()来实现。
如下程序:
public class SleepTest {
public static void main(String[] args) throws Exception{
for(int i=0; i<10; i++){
System.out.println("当前时间:"+new Date());
Thread.sleep(10000);
}
}
}
3.线程让步:yield
yield()方法可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。
如下程序:
public class YieldTest extends Thread{
public YieldTest(String name){
super(name);
}
public void run(){
for(int i=0; i<50; i++){
System.out.println(this.getName()+" "+i);
if(i==20){
System.out.println(this.getName()+"让步");
Thread.yield();
}
}
}
public static void main(String[] args) throws Exception{
YieldTest yt1 = new YieldTest("高级");
//yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
//yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
4.改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行计划,而优先级低的线程则获得较少的执行机会。
每个线程默认的优先级与创建它的父线程的优先级相同。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其参数范围是1~10之间,也可以是Thread类的3个静态常量。
如下代码片段:
Thread.currentThread().setPriority(6);
五、线程同步
1.同步代码块,语法格式如下:
synchronized(obj)
{
//此处的代码就是同步代码块
}
2.同步方法
与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。语法格式如下:
public synchronized void methodName()
{
//执行语句
}
3.同步锁
class X
{
private final ReentrantLock lock = new ReentrantLock();
public void m()
{
lock.lock();
try{
//需要保证线程安全的代码
}catch(Exception e){
}
finally{
lock.unlock();
}
}
}