以前学习线程时只用过一种创建方式,由于面试常问,现在来熟悉下创建线程的三种方式(说法不一,这里介绍3种)。
一.继承Thread类
1.首先在线程实现类中继承Thread类,使线程实现类成为Thread的子类。然后重写Thread类中的run方法,run方法实际上就是执行体,方法中的代码实现就是线程所要完成的任务。
2.创建子类的对象,实际上也就是创建了Thread类的对象。
3.通过对象调用Thread类中的run方法,启动线程。
//继承Thread
public class Demo extends Thread{
//执行体
public void run(){
//重写run方法
System.out.println("执行重写之后的run方法");
}
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建并启动线程
Demo demo=new Demo();
demo.start();
}
}
二.继承Runnable接口
1.在接口实现类中继承Runnable接口,重写Runnable接口中的run方法,这时的run方法也是执行体。
2.创建Runnable实现类的实例对象,这时的对象并不是真正的线程对象,所以要把这时的对象传给Thread来创建对象,这时的Thread对象才是真正的线程对象。
3.通过Thread对象调用run方法启动线程。
//继承Runnable接口
public class Demo1 implements Runnable{
//重写接口中的run方法
public void run(){
System.out.println("执行重写之后的run方法");
}
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建实现接口类的对象
Demo1 demo1=new Demo1();
//创建Thread的对象,并分配新的Thread对象
Thread thread=new Thread(demo1);
//调用Thread里面的start方法启动线程
thread.start();
}
}
三.继承callable接口和利用Future接口实现
这种实现方式与前两种相比,执行体发生了改变,但显得比前两种更为强大,该方式的执行体为Callable接口中的call方法,此方法可以有返回值,并且可以利用Future接口中的get方法接收线程结束之后的返回值,并且Future接口有一实现类Futuretask,这个实现类既继承了Future接口,又继承了Runnable接口,因此该类的对象可以作为Thread对象的target来创建Thread对象.(下面以返回值类型为int型为例)
1.在接口实现类中继承Callable接口,重写接口中的call方法,这时call方法才是线程的执行体,并且有返回值。
2.创建接口实现类的对象,并且将该对象传递给Futuretask类来创建Futuretask对象,再讲Futuretask对象传递给Thread来创建Thread对象。
3.用Thread对象调用start方法启动线程。
4.在线程结束之后,利用Futuretask对象调用get方法得到call方法的返回值(get方法需要抛出异常)。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable和Future实现
public class Demo2 implements Callable<Integer>{
public static void main(String[] args) throws InterruptedException, ExecutionException{
//实例化接口实现类的对象
Demo2 demo2=new Demo2();
//创建FutureTask对象封装Demo2对象
FutureTask<Integer> future=new FutureTask<>(demo2);
//创建Thread类的对象封装FutureTask对象
Thread thread =new Thread(future);
//启动线程(调用了重写之后的call方法)
thread.start();
//获得call方法的返回值
int c=future.get();
System.out.println("c= "+c);
}
@Override
//执行体
public Integer call() throws Exception {
// TODO Auto-generated method stub
System.out.println("调用了重写之后的call方法");
int a=3;
return a;
}
}
小结:
1.三种方式相比,不太建议使用第一种,因为java中是只能继承单个类的(除Object外),如果继承了Thread类就无法继承其他的类,所以就显得这种方式比较笨拙。而接口是可以多继承的,所以建议一般使用继承接口来实现线程。
2.实现Runnable和Callable方式基本相同,但不同的是二者的执行体,后者的执行体有返回值。
3.线程的启动都是通过Thread类中的start方法,所以这3种方式都是最后都需要去创建Thread类的对象。
4.使用继承Thread类和继承Runnable接口类实现创建线程还有一点不同就是继承Thread类不适合实现多线程资源共享,而继承Runnable接口则可以做到多个线程资源共享。
续:
下面详细说明上述小结第四点,为什么继承Runnable接口可以做到多个线程资源共享而继承Thread类却做不到。
这里我们引入一个卖票的问题,试想一个线程控制一个售票窗口,然后各个窗口共享票数这个数据,在售票时每个线程都对剩余票数进行改变,就能实现资源共享,下面贴上继承Runnable接口创建线程的代码。
public class Ticket implements Runnable{
//定义票
private int ti=5;
//重写接口中的run方法
public void run(){
for(int i=0;i<5;i++){
if(ti>0)
//打印卖票信息
System.out.println(Thread.currentThread().getName()+" 剩余票数为"+this.ti--);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建接口实现类的对象
Ticket ticket=new Ticket();
//将该对象传入线程Thread并创建Thread对象
Thread thread1=new Thread(ticket);
thread1.setName("售票窗口1");
Thread thread2=new Thread(ticket);
thread2.setName("售票窗口2");
Thread thread3=new Thread(ticket);
thread3.setName("售票窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
这样的话,之所以会发生资源共享,是因为线程共享了存在堆里面的ticket对象,相当于把这一个对象交给3个线程来完成,把改变ti这个属性的任务交给3个线程来完成。运行结果如下:
结果一:
售票窗口1 剩余票数为5
售票窗口1 剩余票数为4
售票窗口1 剩余票数为3
售票窗口1 剩余票数为2
售票窗口1 剩余票数为1
结果二:
售票窗口2 剩余票数为5
售票窗口1 剩余票数为4
售票窗口1 剩余票数为1
售票窗口3 剩余票数为3
售票窗口2 剩余票数为2
但是这样的话,我们会发现打印的结果并没有像我们想象中那样按照票的顺序来打印,但是在共享的同时就发生了线程并发,线程互相争夺资源,而并没有对线程做并发控制,这样就会发生各种奇怪的结果。因为线程在执行的时候就是在抢CPU的,CPU是随机执行线程,所以就会出现错乱的现象。所以这里需要对线程进行并发控制,这里我采用synchronized关键字锁定代码块。下面贴上改良之后的代码:
public class Ticket implements Runnable{
//定义票
private int ti=5;
//重写接口中的run方法
public void run(){
for(int i=0;i<5;i++){
synchronized (this) {
if(ti>0){
//打印卖票信息
System.out.println(Thread.currentThread().getName()+" 剩余票数为"+this.ti--);
}
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建接口实现类的对象
Ticket ticket=new Ticket();
//将该对象传入线程Thread并创建Thread对象
Thread thread1=new Thread(ticket);
thread1.setName("售票窗口1");
Thread thread2=new Thread(ticket);
thread2.setName("售票窗口2");
Thread thread3=new Thread(ticket);
thread3.setName("售票窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
运行之后的结果如下:
售票窗口1 剩余票数为5
售票窗口3 剩余票数为4
售票窗口2 剩余票数为3
售票窗口3 剩余票数为2
售票窗口3 剩余票数为1
这样就实现了线程共享资源。
如果是继承Thread类的话,就没有一个共享的存在堆里的对象ticket,所以实现就会比较麻烦。下面就直接贴上继承Thread类的代码。
public class Ticket extends Thread{
//定义票
private int ti=5;
//重写run方法
public void run(){
for(int i=0;i<5;i++){
if(ti>0){
//打印卖票信息
System.out.println(Thread.currentThread().getName()+" 剩余票数为"+this.ti--);
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket ticket=new Ticket();
ticket.setName("售票窗口1");
Ticket ticket1=new Ticket();
ticket1.setName("售票窗口2");
Ticket ticket2=new Ticket();
ticket2.setName("售票窗口3");
ticket.start();
ticket1.start();
ticket2.start();
}
}
运行结果如下:
售票窗口1 剩余票数为5
售票窗口3 剩余票数为5
售票窗口3 剩余票数为4
售票窗口3 剩余票数为3
售票窗口2 剩余票数为5
售票窗口3 剩余票数为2
售票窗口1 剩余票数为4
售票窗口3 剩余票数为1
售票窗口2 剩余票数为4
售票窗口1 剩余票数为3
售票窗口2 剩余票数为3
售票窗口1 剩余票数为2
售票窗口1 剩余票数为1
售票窗口2 剩余票数为2
售票窗口2 剩余票数为1
从结果来看,会发现这里的每个线程都完成了一次卖票的任务,每个线程执行的任务不是同一个任务,所以并没有资源共享。