一前言
这里先来一个小菜,说说进程和线程的区别:
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
关系:一个程序至少一个进程,一个进程至少一个线程。
二、线程的创建方式
我们先列出创建线程的三种方式,脑子里先有个故事大纲:
1.通过继承Thread类创建线程类,重写run函数
2.通过实现Runnable接口创建线程类,重写run函数
3.通过Callable和Futrure接口创建线程,重写call函数
从上述三项可以看出,从实现方式上可以分为两类,一是继承Thread类,二是实现Runnable和Future接口。既然存在这两类实现方式,那他们的优缺点又是什么呢?我们这里也先列一下:
1.从实现复杂度讲。继承Thread类较简单,实现Runnable和Futrue接口与前者比较稍微复杂。
2.从可扩展性讲。java类是单继承的,第一类失去了再继承其它类的权利,第二类还可以继承其它类
3.从共享资源来讲。第一类方式实现的多线程,是不能做到的共享同一资源的,第二类方式实现的多线程可以共享同一资源。(比较关键的区别)
4.是否需要线程执行后的返回结果。如果需要获取到线程执行后的结果,只能使用Callable和Future这种方式的创建的线程。其余的两种都不具备线程执行完后返回结果的能力。
如果看到这里,您一点都不懵的话,就可以关掉该页面了,慢走,不送
如果您没有关掉页面的话,继续往下看,还是能学到些东西。。。。
下面针对三种创建方式进行具体分析:
2.1 通过继承Thread类创建线程类
这种方法的具体实现步骤是:
1)自定义一个类继承Thread类,并且重写父类的run()方法。其中run()方法中的方法体中的逻辑,就是该线程需要做的事
2)实例化上述自定义类对象,该对象就是线程的对象
3)调用线程对象的start()方法,开始启动线程,使线程进入就绪状态
示例:
//第一步:自定义类继承Thread类
public class ExtendThread extends Thread{
private int resource = 0;
public static void main(String []args){
for(int i=1;i<=10;i++){
//打印线程的名字
System.out.println(Thread.currentThread().getName()+":"+i);
//第二步:实例化对象
Thread thread =new ExtendThread();
//设置线程的名字
thread.setName("thread"+i);
//第三步:启动线程,使线程进入就绪状态
thread.start();
}
}
//重写run方法,而且run()方法返回类型是void,所以没有返回值
@Override
public void run(){
//进入该方法,表示线程进入运行状态
System.out.println(this.getName()+":resource="+resource);
resource++;
}
}
代码中已经做了详细注释,可供参考,下面我们看下运行结果:
运行结果分析:
1.线程之间不能共享资源变量。每次线程执行完方法后,都对资源变量进行了加一操作,从结果中可以看出每次资源变量输出都还是为0。即这种实现的线程,资源变量resource都是单独的,线程之间不能共享。
2.start()只是进入线程就绪状态,并不是进入运行状态,谁先运行,需要看cpu调度。如thread2虽然先创建,但是thread3却先打印出信息。
2.2通过实现Runnable接口创建线程类,重写run函数
这种方法的具体实现步骤是:
1)自定义一个实现Runnable接口的类,并且重写接口的run()方法。其中run()方法中的方法体中的逻辑,就是该线程需要做的事
2)实例化上述自定义类对象runnable。
3)将runnable作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;(与第一种比较就是多了这一步,其实也不复杂)
4)调用线程对象的start()方法,开始启动线程,使线程进入就绪状态
示例:
//第一步:自定义类实现Runnable接口
public class ImpleRunnable implements Runnable {
private int resource=0;
@Override
public void run(){
//进入该方法,表示线程进入运行状态
System.out.println(Thread.currentThread().getName()+":resource="+resource);
resource++;
}
public static void main(String []args){
//第二步:实例化对象
ImpleRunnable runnable = new ImpleRunnable();
for(int i=1;i<=10;i++){
//打印线程的名字
System.out.println(Thread.currentThread().getName()+":"+i);
//第三步:创建线程对象
Thread thread = new Thread(runnable);
//设置线程的名字
thread.setName("thread"+i);
//第四步:启动线程,是线程进入就绪状态
thread.start();
}
}
}
运行结果分析:
1.线程之间可以共享资源变量。每次线程执行完方法后,都对资源变量进行了加一操作,从结果中可以看出每次资源变量输出都是在上一次线程上进行的累加。最后两个线程打印的结果都为8,这里就是出现了点击的并发,导致线程不安全。
2.start()只是进入线程就绪状态,并不是进入运行状态,谁先运行,需要看cpu调度。如thread2虽然先创建,但是thread3却先打印出信息。
2.3 通过Callable和Futrure接口创建线程
由上面可知,前两种的线程的执行提都是run()方法,但是通过Callable创建的线程的执行提是Callable中的call()方法,call()方法可以返回结果,可以跑出异常
这种方法的具体实现步骤是:
1)自定义一个实现Callable接口的实现类,并且重写call方法,call方法就是将要创建的线程的执行体,并且可以返回结果
2)实例化一个上述类的对象,并且用FutureTask进行包装为future对象。
3)将future对象作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
4)调用线程对象的start()方法,开始启动线程,使线程进入就绪状态
5)要想获得返回值,使用future对象的get()方法获取返回值
示例:
//第一步:自定义一个实现类,实现Callable接口,重写Callable方法
class CallableThread implements Callable<Integer> {
int resource =0;
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+":resource="+resource);
return ++resource;
}
}
public class FutureCreateThread {
public static void main(String []args) throws ExecutionException, InterruptedException {
//第二步:实例化CallableThread对象
CallableThread callable = new CallableThread();
for(int i=1;i<=10;i++){
//打印线程的名字
System.out.println(Thread.currentThread().getName()+":"+i);
//第三步:对callable对象进行封装
FutureTask<Integer> futureTask = new FutureTask(callable);
//第四步:实例化构造参数为futureTask的线程对象
Thread thread = new Thread(futureTask);
//设置线程的名字
thread.setName("thread"+i);
//第五步:启动线程,是线程进入就绪状态
thread.start();
//第六步:获取结果
System.out.println(futureTask.get());
}
}
}
运行结果分析:
1.线程之间可以共享资源变量。每次线程执行完方法后,都对资源变量进行了加一操作,从结果中可以看出每次资源变量输出都是在上一次线程上进行的累加。结果打印顺序是按照顺序来的主要原因是get()方法会导致线程阻塞,所以只有当上一个线程执行完返回结果时,主线程才会继续运行创建线程
2.call()方法自带向外抛出异常。主要是因为futureTask不仅包括get()方法,也包括cancel()方法,此方法可以取消正在运行的线程任务