——- android培训、java培训、期待与您交流! ———-
19th July 2015
1.多线程简介
定义:多线程就是让程序具有多个并发的执行线索,类似于团队协作,多人合作完成一个任务,这样一来就可以大大提高效率,提高资源的利用率。
ps.每一个程序中的main方法就是一个线程,它一般被称为主线程。在主线程中可以启动多个子线程来执行。
2 定义线程和创建线程对象
定义线程有两种方法,和定义线程对应的创建线程对象也有两种方法。两种定义线程方法各自有优缺点。
2.1继承Thread类定义线程
定义一个线程可以通过继承Thread类来实现,继承的类就具有了线程的能力,这是一种相对简单的定义线程的方法。采用这种方法来实现继承最重要的一点就是在定义的线程中需要重写Thread中的run方法,可以定义该线程所要执行的语句。当线程启动时,run方法中的线程就成为一条独立的执行线程。
public class xiancheng21 extends Thread
{
public void run()
{
System.out.println("通过继承Thread定义线程");
}
}
代码解析
该程序是无法运行的,因为没有main方法,也就是没有启动线程的方法。
通过继承Thread类创建线程,是很容易创建线程对象的。在这种定义线程的方法中,创建线程对象和创建普通对象是一样的,这只是对于使用Thread类创建线程的方法来说。
Xicheng21 xc = new xiancheng21();
2.2 实现Runnable接口定义线程
在Runnable接口中具有一个抽象的run方法,在实现Runnable接口时,需要实现该run方法。该run方法就会作为一个执行线程的方法。对于实现Runnable接口的类,其自身并不是一个线程,只是在该类中通过实现run方法指出了线程所需要完成的任务。
public class XiCheng22 implements Runnable
{
public void run()
{
System.out.println("通过实现Runnable接口定义线程");
}
}
不管该方法是通过重写父类方法,还是实现接口方法,这两种方法都需要定义一个run方法,run方法是一个线程的入口,是线程必须具备的。
通过实现Runnable接口定义线程,想要创建线程对象就不是很容易做到的。因为直接创建类对象,创建的并不是一个线程对象。想要创建线程对象,必须要借助thread类。
方法如下:
XiCheng22 xc = new XianCheng22();
Thread t = new Thread(xc);
3 运行线程
3.1 启动线程
错误: 调用run方法 并不等于 启动线程
以下演示一个错误的启动线程的程序
class MyRunnable implements Runnable
{
//使用Runnable接口 重写run线程方法
public void run()
{
System.out.println("这是一个错误的启动线程的程序");
}
}
public class XianCheng31
{
public static void main(String args[])
{
MyRunnable mr = new MyRunnable();
mr.run(); //调用run方法
}
}
代码解析:
run方法是可以通过方法调用来执行的,但是这并不代表创建了一个新线程。
这是一个错误的启动线程方法。
如果想正确地启动一个线程,需要调用线程对象的start方法。下面演示一个正确启动线程的程序
class MyRunnable implements Runnable
{
//定义一个run线程方法
public void run()
{
System.out.println("这是一个正确启动线程的程序");
}
}
public class XianCheng312
{
public static void main(String args[])
{
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start(); //启动线程
}
}
代码解析:
初学者可能会奇怪,为什么调用的是start方法而执行的是run方法,这是Java对多线程的设计。
调用start方法后,就启动了线程,该线程是和main方法并列执行的线程。这样该程序就变成一个多线程程序
Tip: 线程只能被启动一次,也就是只能调用一次start方法。当多次启动线程,也就是多次调用start方法时,程序是能够正常编译的,但是运行时,就会发生异常。第一次启动线程能运行run方法。但第二次启动线程就会发生IllegalThreadStateException异常。
3.2 同时运行多个线程
Java中在线程启动后可以保证的只是每个线程都启动并且会执行结果,但是同时运行结果不是唯一的,因为有很多不确定的因素,首先执行哪一个线程是不确定的,线程间交替也是不确定的。唯一能确定的是每一个线程都将启动,每一个线程都执行结束。
下面演示一个执行多线程的程序
class MyRunnable1 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++){
System.out.print("@");
}
}
class MyRunnable2 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++)
{
System.out.print("$");
}
}
}
public class DuoXianCheng
{
public static void main(String args[])
{
MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
t1.start();
t2.start();
}
}
运行结果
@@@@@@@@@@@@@@@@@@@@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$@@@@@@@@@@@@@@@@@@@@@@@@$$$$$
从运行结果中可以看出,不同的符号是交替显示的。
4 线程生命周期
线程是存在生命周期的,线程的生命周期分为5种不同的状态,分别是新建状态,准备状态,运行状态,等待/阻塞状态和死亡状态。
4.1 新建状态
新建状态是生命周期的第一个状态。当一个线程对象被创建后,线程就处于新建状态。处于新建状态中的线程被调用start方法后就会进入准备状态。
* 从新建状态只能进入准备状态,并且不能从其他状态进入新建状态。
4.2 准备状态
处于新建状态的线程被调用start方法就进入准备状态,等待被调度执行。处于准备状态下的线程随时都可能被系统选择进入运行状态,从而执行线程。可能同时又多个线程处于准备状态,与对哪一个线程将进入运行状态是不确定的。 线程从新建状态进入到准备状态后是不可能再进入新建状态的。在等待/阻塞状态中的线程被解除等待和阻塞后将不直接进入运行状态,而是首先进入准备状态,让系统来选择哪一个线程进入运行状态。
4.3 运行状态
处于准备状态中的线程一旦被系统选中,线程获取CPU时间,进入运行状态。在运行状态中将执行线程类run方法中的程序语句。线程进入运行状态后不是一下执行结束的,线程在运行状态下随时可能被调度程序调度回准备状态。在运行状态下,还可以让线程进入到等待/阻塞状态。
通常在单核CPU中,在同一时刻只有一个线程处于运行状态。在多核的CPU中,就可能两个线程或更多的线程同时处于运行状态,这也是多核CPU运行速度快的原因。
4.4 等待/阻塞状态
Java中定义了许多线程调度的方法,包括睡眠,阻塞,挂起和等待等等。使用这些方法会将处于运行状态的线程调度到等待/阻塞状态。处于等待/阻塞状态的线程被接触后,不会立即回到运行状态,而是首先进入准备状态,等待系统的调度。
- 睡眠:正在执行的线程如果被调用了sleep方法将进入睡眠状态
- 阻塞:正在运行的线程如果调用了阻塞方法,而正好又满足阻塞的情况,则会停止执行进入阻塞状态,知道阻塞条件解除再进入准备状态、
- 挂起:正在执行的线程如果被调用了其suspend方法,就会处于挂起状态,直到调用了其resume方法才会回到准备状态。
- 等待:正在运行的线程由于逻辑条件不满足,自己调用wait方法进入等待状态,知道收到通知的消息才回到准备状态,等待执行。
4.5 死亡状态
当线程中的run方法执行结束后,或者程序发生异常终止运行后,线程会进入死亡状态。处于死亡状态的线程不能再使用start方法启动线程。但这不代表处于死亡状态的线程不能再被使用,它也是可以再被使用的,只是将被作为普通的类来使用。
Tips线程生命周期的问题在后面的学习中会经常使用到,只有充分了解线程的生命周期,才能更好地理解深层的机制和后面的学习。
5 线程的调度
通过系统的自动调度,线程的执行顺序是没有保障的。Java中定义了一些线程调度的方法,使用这些方法在一定程度上对线程进行调度,只是给线程一个调度的建议,具体是否能够成功,也是没有保障的。
线程调度的方法有几个,包括睡眠方法,设置优先级,让步方法等。
5.1 睡眠方法
当线程处于运行状态时,调用sleep睡眠方法将使线程从运行状态进入等待/阻塞状态,从而使程勋停止运行,这是使正在运行的线程让出CPU的最简单方法之一。
*然后当线程调用睡眠方法后,想要回到运行状态,需要的时间要比指定的睡眠时间长。
Sleep方法被重载,存在两种形式,基本语法如下
public static void sleep(long millis)throws InterruptedException;
public static void sleep(long millis. int nanos)throws InterruptedException;
mills参数表示线程睡眠的毫秒数
nanos参数表示线程睡眠的纳秒数
要想让某一个线程进入睡眠状态,并不是让该线程调用sleep方法,而只是让线程执行sleep方法。
*线程是可能发生捕获异常的
以下演示一个使用sleep方法调度线程的程序
class MyRunnable1 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++)
{
System.out.print("@");
try
{
Thread.sleep(50);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
}
class MyRunnable2 implements Runnable
{
for(int i=0; i<100; i++)
{
System.out.print("$");
try
{
Thread.sleep(50);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
public class XianChengSleep
{
public static void main(String args[])
{
MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
t1.start();
t2.start();
}
}
运行结果
@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@
$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$
@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@
$@$@$@$@$@$@$@$@$@$@$@$@
程序解析:
该线程调用sleep方法,使线程进入了睡眠状态50毫秒,从而将CPU时间让给另外一个线程,这样就出现了交替的显示符号的运行结果。
*
sleep方法的只是给线程调度一个建议,是否调度成功是不能确定的。当程序中存在多个线程的时候,运行结果就可能发生变化,甚至出现意外的结果。
5.2 线程优先级
Java中的优先级才用数字1到10来表示,数字越大表示优先级越高。如果没有为线程设置优先级,则线程的优先级为5,这又是线程的默认值。
对于子线程来说,它的优先级是和其父线程优先级相同的。
通过setPriority方法来设置线程优先级 语法如下
public final void setPriority(int i);
除了用数字表示优先级以外,Java还在Thread类中定义了3个表示优先级的常量,MAX_PRIORITY, NORM_PRIORITY, MIN_PRIORITY.
以下演示一个通过设置线程优先级来对线程进行调度的程序
class MyRunnable1 implements Runnable
{
public void run()
{
for(int i=0; i<00; i++)
{
System.out.print("@");
}
}
}
class MyRunnable2 implements Runnable
{
public void run()
{
for(int i=0, i<100; i++)
{
System.out.print("$");
}
}
}
public class XianChengPriority
{
public static void main(String args[])
{
MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
Thread t1 = Thread(mr1);
Thread t2 = Thread(mr2);
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
运行结果:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$
线程t1获得最高级的优先权,使t1获得更多CPU时间
线程t2获得最低级的优先权
Java中有两种线程让步方法
5.3 Yield让步方法
Yield让步方法是可以使当前进程正在运行的线程让出当前CPU资源,使线程返回准备状态,让其他线程有进入运行状态的机会。而将CPU让给哪个线程是不确定的,有系统来进行选择。
*yield让步操作是可能不成功的。因为在线程中使用yield方法,使该线程进入准备状态。但是系统是有可能再次选择该线程,是该线程进入运行状态的。
yield让步方法的基本语法格式
public static void yield();
下面演示一个用yield让步方法对线程进行调度的程序
class MyRunnable1 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++)
{
System.out.print("@");
Thread.yield();//调用yield方法让当前进程让步
}
}
}
class MyRunnable2 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++)
{
System.out.print("$");
Thread.yield(); //调用yield方法让当前进程让步
}
}
}
public class Yield
{
public static void main(String args[])
{
MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
t1.start();
t2.start();
}
}
运行结果如下
@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@
$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$
@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$
5.4 Join让步方法
使用Join让步方法,可以将当前线程的CPU让给指定线程。
Join的三种语法形式
public final void join()thorws InterruptedException;
public final void join(long mills)throws InterruptedException;
public final void join(long mills, int nanos)throws InterruptedException;
*没有参数的表示指定线程执行完成后在执行其他线程
有参数的表示在参数的时间内执行让步给的执行线程
*join方法是可能发生捕获异常的,所以在使用时要进行异常处理
以下演示一个使用join方法调度线程的程序
class MyRunnable1 implements Runnable
{
public void run()
{
for(int i=0; i<100; i++)
{
System.out.print("@");
}
}
}
public class XianChengJoin
{
public static void main(String args[])
{
MyRunnable1 mr1 = new MyRunnable1();
Thread t1 = new Thread(mr1);
t1.start();
for(int i=0; i<100; i++)
{
if(i==10)
{
try
{
t1.join()
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.print("$");
}
}
}
运行结果
&&&&&&&&&&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
代码解析
代码在运行10次for循环后打印$符号后,当I=10时,t1线程用join方法调度,使其获得优先权,完成t1线程后返回主线程。
*值得注意的是,该程序是包含两个线程,一个即MyRunnable1, 另一个是main程序的线程。
晋级key words:
同步多线程,死锁,线程安全,死循环,全局变量进程间通讯,线程池,epoll,mfc,synchronized。