1.线程的生命周期
线程具有生命周期,其中包含5种状态,分为为出生状态、就绪状态、运行状态、暂停状态(包括休眠、等待和阻塞等)和死亡状态,出生状态就是线程被创建时的状态:当线程对象调用start()方法后,线程处于就绪状态(又称为可执行状态):当线程得到系统资源后就进入了运行状态。
一旦线程进入运行状态,它会在就绪与运行状态下转换,同时也有可能进入暂停状态或死亡状态,当处于运行状态下的线程调用sleep()方法、wait()方法或者发生阻塞时,会进入暂停状态:当在休眠结束、调用notify()方法或notifyAll()方法、或者阻塞接触时,线程会重新进入就绪状态:当线程的run()方法执行完毕,或者线程发生错误、异常时。线程进入死亡状态。
线程生命周期中的各种状态如下图:
2.操作线程的方法
操作线程有很多方法,这些方法可以使线程从某一种状态过渡到另一种状态,下面将讲解如何对线程执行休眠、加入、和中断等操作。
2.1线程的休眠
能控制线程行为的方法之一是调用sleep()方法,sleep()方法可以指定线程休眠的时间,线程休眠时间以毫秒为单位,sleep()的使用方法如下:
try {
Thread.sleep(1000); // 使线程休眠一秒
}catch (Exception e) {
e.printStackTrace();
}
上述代码会使线程在一秒之内不会进入就绪状态,由于sleep()方法有可能抛出InterruptedException异常,所以将sleep()方法放在try-catch块中,虽然使用了sleep()方法的线程在一段时间内会醒来,但是并不能保证它醒来后就会进入运行状态,只能保证它进入就绪状态。
下面一个实例来讲解sleep用法,代码如下:
public class Demo {
public static void main(String[] args) {
Thread thA = new ThreadA();
thA.start();
Thread thB = new ThreadB();
thB.start();
}
}
class ThreadA extends Thread{
public void run() {
for(int i = 1;i<=10;i++) {
System.out.println("线程A:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadB extends Thread{
public void run() {
for(int i = 1;i<=10;i++) {
System.out.println("线程B:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
返回结果:
2.2线程的加入
假如当前程序为多线程程序,并且存在一个线程A,现在需要插入线程B,并要求线程B执行完毕后,再继续执行A,此时可以使用Thread类中的join()方法来实现,这就好比A正在看电视,突然B上门收水费,A必须付完款后才能继续看电视。
下面我们来看join()方法的例子,代码如下:
public class Demo {
ThreadB thB = new ThreadB();
ThreadA thA = new ThreadA();
public void thA() {
thA.start();
}
//创建内部类线程B
class ThreadB extends Thread{
public void run() {
for(int i = 0;i<=5;i++) {
System.out.println("线程B运行第" + i + "次");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建内部类线程A
class ThreadA extends Thread{
public void run() {
int count = 0;
while(count <= 5) {
System.out.println("线程A运行第" + count + "次");
if(count == 3) {
System.out.println("--------线程B加入----------------");
try {
//启动线程B
thB.start();
//将线程B加入到线程A
thB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}
public static void main(String[] args) {
Demo start = new Demo();
start.thA();
}
}
返回结果:
在上面实例中可以看出,线程A和线程B分别执行6次(0~5),当线程A执行第4次的时候,线程B加入到线程A中执行6次,当线程B都执行完毕后,线程A才接着执行。
2.3线程的中断
以往会使用stop()方法停止线程,但JDK早已废除了stop()方法,不建议使用stop()方法来停止线程,现在提倡在run()方法中使用无限循环的形式,然后使用一个布尔类型标记控制循环的停止。
还有一种方法时Thread类中的interrupt()方法使线程离开run()方法,同时结束线程,但此时程序会抛出InterruptedException异常,可以在处理该异常时完成线程的中断业务,如终止while循环,下面讲解一下interrupt()方法,代码如下:
public class Demo extends Thread{
public void run() {
int count = 0;
while(true) {
System.out.println(count);
try {
if(count == 5) {
//终止线程
interrupt();
}
sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程被终止");
break;
}
count++;
}
}
public static void main(String[] args) {
Demo th = new Demo();
th.start();
}
}
返回结果:
3.线程的同步
在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,如果使用多线程程序,就会发生两个线程抢占资源的问题,例如两个人以相反方向同时过同一个独木桥,为此,java提供了线程同步机制来防止多线程编程中抢占资源的问题。
3.1线程安全
实际开发中,使用多线程程序的情况很多,如银行排好业务、火车站售票系统等,这种多线程的程序通常会发生问题,以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行把火车票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行并完成判断是否有票的操作,并得出结论票数大于程序安全问题,实质上,线程安全问题来源于两个线程同时操作单一对象的数据,
例如,在项目中创建ThreadSafeTest类,该类实现了Runnable接口,在未考虑到线程安全问题的基础上,模拟火车站的售票系统,代码如下:
public class Demo implements Runnable {
int num = 10; // 设置当前总票数
public void run() {
while (true) { // 设置无限循环
if (num > 0) { // 判断当前票数是否大于0
try {
Thread.sleep(100); // 使当前线程休眠100毫秒
} catch (Exception e) {
e.printStackTrace();
}
// 票数减1
System.out.println(Thread.currentThread().getName() + "——票数" + num--);
}
}
}
public static void main(String[] args) {
// 实例化类对象
Demo t = new Demo();
// 以该类对象分别实例化4个线程
Thread tA = new Thread(t, "线程一");
Thread tB = new Thread(t, "线程二");
Thread tC = new Thread(t, "线程三");
Thread tD = new Thread(t, "线程四");
tA.start();// 分别启动线程
tB.start();
tC.start();
tD.start();
}
}
返回结果:
从返回结果可以看出,最后打印剩下的票为负数,这样就出现了问题,这是由于同时创建了4个线程,这4个线程执行run()方法,在num变量为1时,线程一、线程二、线程三、线程四都对num变量有存储功能,当线程一执行run()方法时,还没有来得及做递减操作,就调用sleep()方法进入就绪状态,这时线程二、线程三、线程四也都进入了run()方法,发现num变量依然大于0,但此时线程一休眠时间已经到了,将num变量值递减,同时线程二、线程三、线程四也都对num变量进行递减操作,从而产生了负值
3.2线程同步机制
该如何解决资源共享的问题呢?基本上,所有解决多线程资源冲突问题的方法都是在给定时间只允许一个线程访问资源,这时就需要给共享资源上一道锁,这就好比一个人上洗手间,他进入洗手间后会将门锁上,出来时在将锁打开,然后其他人才可以进入。
在java中提供了同步机制,可以有效地防止资源冲突,同步机制使用synchronized关键字,使用该关键字的代码块称为同步块,也称为临界区,语法如下:
synchronized (Object) {
}
通常将共享资源的操作放置在synchronized定义的区域内,这样其他线程获取到这个锁是,就必须等待锁被释放后才可以进入该区域,Object为任意一个对象,每个对象都存在一个标识位,并具有两个值,分别为0和1,一个线程运行到同步块是首先检查该对象的标识位,如果为0状态,表明此同步块内存在其他线程上,这时当前线程处于就绪状态,知道处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位设置为1,当前线程才能开始执行同步块中的代码,并将Object对象的标识位设置为0,以防止其他线程执行同步块中的代码。
还是以上面的例子为例,代码如下:
public class Demo implements Runnable {
int num = 10; // 设置当前总票数
public void run() {
while (true) { // 设置无限循环
synchronized (this) {//设置同步代码块
if (num > 0) { // 判断当前票数是否大于0
try {
Thread.sleep(100); // 使当前线程休眠100毫秒
} catch (Exception e) {
e.printStackTrace();
}
// 票数减1
System.out.println(Thread.currentThread().getName() + "——票数" + num--);
}else {
System.out.println("进程终止");
//退出控制台
System.exit(0);
}
}
}
}
public static void main(String[] args) {
// 实例化类对象
Demo t = new Demo();
// 以该类对象分别实例化4个线程
Thread tA = new Thread(t, "线程一");
Thread tB = new Thread(t, "线程二");
Thread tC = new Thread(t, "线程三");
Thread tD = new Thread(t, "线程四");
tA.start();// 分别启动线程
tB.start();
tC.start();
tD.start();
}
}
返回结果:
从返回结果可以看出,打印到最后票数没有出现负数,这时因为将共享资源放置在了同步块中,而且票的数量是有序的,这就是锁的机制。
3.3同步方法
同步方法就是被synchronized关键字修饰的方法,其语法如下;
public synchronized void doit() {
}
当某个对象调用了同步方法时,该对象的其他同步方法必须等待该同步方法执行完毕后才能被执行,必须将每个能访问共享资源的方法都修饰为synchronized,否则就会出错,
修改上面案例,将共享资源的操作放置在一个同步方法中,代码如下:
public void run() {
doit();
}
public synchronized void doit() {
while (true) { // 设置无限循环
if (num > 0) { // 判断当前票数是否大于0
try {
Thread.sleep(100); // 使当前线程休眠100毫秒
} catch (Exception e) {
e.printStackTrace();
}
// 票数减1
System.out.println(Thread.currentThread().getName() + "——票数" + num--);
}else {
System.out.println("进程终止");
//退出控制台
System.exit(0);
}
}
}
运行结果与使用同步块的结果是一致的