java并发——四种创建线程方式

并发概念


并发用来提高运行在单处理器上的程序的性能。
这听起来有些违背直觉。如果有多个CPU处理器,那么我们让不同CPU并发处理程序一定会让速度变快。但是我们只有一个处理器,并发看起来只会增加上下文切换的开销时间。真的是这样吗?让这个问题的答案反转的是:阻塞。如果我们在执行一段代码中,有一处发生了阻塞,我们只能将整个程序停下来。如果我们采用并发的方式,即使这一处发生了阻塞,其他的任务还可以继续执行,直到程序结束,最后的情况只是一处阻塞,结果还不算太坏。

事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器上使用并发就没有任何意义。 ——Bruce Eckel

实现并发最直接的方式就是我们的操作系统做的那样,使用进程。在多任务操作系统中可以通过周期性将CPU从一个进程切换到另一个进程,来实现同时运行多个进程的效果。尽管这样会让进程看起来执行的停停歇歇,但是互不干扰的进程还是非常吸引人。不同的,JAVA中使用线程来实现并发的概念。不同的线程会共享一个进程下的资源和I/O设备,这使得如何控制访问的同步变成了重要的课题。

在单CPU处理器上使用并发程序在任何时刻都只是执行一项工作,因此从理论上讲,肯定可以不用任何任务而编写出相同的程序。但是并发提供了一个重要的组织结构上的好处:类似仿真等程序的设计可以极大地简化。最浅显的例子,比如我们做超级马里奥的小游戏,采用多线程来控制马里奥和怪物的行为就比不使用方便的多。如果在游戏中还有类似菜单的按钮,我们不可能每段代码都去检查这个按钮是否被点击,这个时候使用一个线程就显得方便简洁。

线程状态


在Thread中的内部嵌套类State中规定,线程一共有6种状态。

6种状态

New 新创建的线程

这里的创建新的线程真的是仅仅new了一个线程。创建新的线程,是指刚new出来的线程,这个线程没有通过start的方法来启动

Runnable 可运行

一旦我们调用了start方法,这个线程开始工作并处于可运行状态。可运行状态不只包含线程运行,线程中断也被算为可运行状态。一个可运行状态的线程可能在运行也可能没在运行,不要因线程在可运行的状态下没运行而急躁,很有可能这个线程的终止只是为了让其他的线程获得机会。

Blocked 被阻塞

一个线程试图去获得一个内部锁时,但这个内部锁被其他的线程持有,这个时候,为了等待去使用这个内部锁,这个线程将会暂时处在被阻塞的状态。当其他线程释放锁的时候,这个线程获得了内部锁,并且从阻塞状态转变为非阻塞状态。

Wait 等待

一个线程等待另一个线程通知调度器一个条件(condition),这个线程自己进入等待状态。等待状态和阻塞状态很类似,但是他们是存在本质区别的。如果另一个线程通知调度器结束,那么这个线程进行工作,等待状态也随之结束。

Timed waiting 计时等待

计时等待和等待是比较相似的,计时等待相比较等待多了一个超时参数。调用他们导致线程会进入计时等待。这个状态将一直保持到超时期满或者接收到适当的通知。相比较直接的等待,变得更加的安全。

Terminated 终止

线程终止。线程run方法执行方法体中最后一条语句后,正常退出而自然死亡。或者,出现了在方法中没有捕获的异常,此时终止run方法意外死亡。

创建线程


1.Thread创建线程

第一种创建线程的方式是从Java.lang.Thread类派生一个新的线程类,重载它的run()方法。ExtThread类是实现了Thread的一个子类,它重写了run方法。NewThread类是主方法,创建了一个ExtThread的实例,并且通过调用start方法启动了该线程,自动调用了run方法。我们不需要手动调用run方法,而应该调用start方法来让它自动调用run方法。在JAVA的API中,start是这样定义的:

public void start( )
使该线程开始执行;
Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

如果我们直接调用run方法,得到的将是在main函数的主线程中调用的run方法,这样没有开启新的线程。

run方法通常会以某种形式的循环来进行,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件(或者直接从run方法返回)。通常run方法被写成无限循环的形式,这样就意味着,除非某个条件使得run终止,否则他将永远运行下去。

NewThread类:

package AllThread;

/**
 * 
 * @author QuinnNorris
 * 
 *         通过Thread创建新线程
 */
public class NewThread {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Thread t1 = new ExtThread();
        // 创建一个ExtThread对象
        t1.start();
        // 调用start方法,运行新的线程,即运行的是t1中的run方法
    }

}

ExtThread类:

package AllThread;

/**
 * 
 * @author QuinnNorris
 * 
 *         Thread的一个实现类
 */
public class ExtThread extends Thread {

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("new thread");
    }

}

2.实现Runnable接口创建线程

在JAVA中类仅支持单继承。这样,如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性

NewRunable类:

package AllThread;

/**
 * 
 * @author QuinnNorris
 * 
 *         通过Runnable接口创建新线程
 */
public class NewRunable {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Runnable r = new ImplRunnable();
        // 创建一个Runnable实例类的对象
        Thread t1 = new Thread(r);
        // 由r作为构造器的参数创建一个Thread对象
        // 将这个Runnable子类对象作为参数传入Thread中
        t1.start();
        // 调用start方法,运行新的线程,即运行的是t1中的run方法
    }

}

ImplRunnable类:

package AllThread;

/**
 * 
 * @author QuinnNorris
 * 
 *         Runnable的一个实现类
 */
public class ImplRunnable implements Runnable {

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("new thread");
    }

}
多线程资源共享

我们可以利用实现Runnable接口的方式实现多线程的资源共享:把资源保存在Runnable接口中,只创建一份实现了Runnable接口的类的实例,多个Thread对象用同一个Runnable接口实例为参数实例化。但是要注意的是,资源的共享会涉及到同步的问题,如果处理不当,那么数据的谬误、脏数据的出现是必然的事情。每当涉及到资源共享时都要小心谨慎。而且这种资源共享的方法也不是必须的,只要我们能确保最后所有的线程都指向同一个资源,那么他的存放位置不需要被严格规定。

3.使用执行器(Executor)创建线程池(thread pool)

使用线程池是比前两种相对少见的创建线程做法。从JAVA SE5开始,java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。如果我们的程序需要用到很多生命周期比较短的线程,那么应该使用线程池,线程池中包含了很多空闲线程,而且这些线程的生命周期不需要我们操心。另一个使用线程池的原因是:如果你的代码需要大量的线程,那么最好使用一个线程池来规定总线程数的上线,防止虚拟机崩溃。这样可以限制最大的并发数量。

静态方法创建线程池实例

正如Collection类的静态方法都在Collections中一样,执行器Executor创建线程池的静态方法全部在Executors类中:

public static ExecutorService newCachedThreadPool()

创建一个新的线程池。如果需要线程而线程池中无空闲线程时,创建一个新的线程。空闲线程会被保留60秒。

public static ExecutorService newFixedThreadPool(int nThreads)

根据参数值创建一个固定数量线程的线程池。如果所需线程超过池中线程数则会发生等待。空闲线程会被一直保留。

public static ExecutorService newSingleThreadExecutor()

创建一个仅有一个线程的线程池,顺序执行每一个提交的任务。(和第二种方法参数为1时效果相同)

提交Runnable任务到线程池中

Executor作为一个祖先接口,提供了一个也仅有一个提交线程的方法:

void execute(Runnable command)

在Executor的子类中,有很多子类提供了具有返回值的提交方法,返回提交的结果。

public Future<?> submit(Runnable task)

比如这种submit方法,提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。调用get方法就能得到提交的结果。

关闭线程池

在线程使用结束后,为了保证程序的安全,我们有必要手动调用关闭线程池的方法:

public void shutdown()

ThreadPool类:

package AllThread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 
 * @author QuinnNorris
 * 
 *         创建线程池
 */
public class ThreadPool {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        ExecutorService es = Executors.newFixedThreadPool(5);
        // 我们调用静态方法创建了包含五个线程的线程池

        for (int i = 0; i < 5; i++)
            es.submit(new ImplRunnable());

        es.shutdown();
        // 在使用结束之后,一定要关闭线程池
    }

}

ImplRunnable类:

package AllThread;

/**
 * 
 * @author QuinnNorris
 * 
 *         Runnable的一个实现类
 */
public class ImplRunnable implements Runnable {

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("new thread");
    }

}

4.使用Callable与Future创建线程并获取返回值

我们使用Runnable封装了一个异步运行的任务,我们可以把它想象成一个没有参数和返回值的异步方法,Callable与Runnable相似,但是Callable具有返回值,可以从线程中返回数据

Callable

我们从jdk中找到了Callable<V>的源代码,去掉一些无用的部分:

package java.util.concurrent;

public interface Callable<V> {

    V call() throws Exception;
}

可以看出Callable接口只是将run方法换成了call方法,其他并没有太多的改动。

Future

Future类负责保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后我们去做其他的事情,Future对象的所有者在结果计算好之后就可以调用get方法获得它。我们在上面线程池的submit方法中也提到过。

V get() throws InterruptedException,ExecutionException

如有必要,等待计算完成,然后通过get方法获取其结果。

FutureTask包装器

我们虽然有了Callable和Future类,但是我们仍然需要一种方法将他们结合起来使用。而且还存在的问题是,Callable的出现替代了Runnable。我们需要一种手段让Thread类能够接受Callable做参数。在这里我们使用非常好用的FutureTask包装器。它可以将Callable转换成Futrue和Runnable,因为他同时实现了Runnable和Future<V>两个接口

CallablePool类:

package AllThread;

import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * 
 * @author QuinnNorris
 * 
 *         用线程池实现Callable创建线程
 */
public class CallablePool {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        ExecutorService es = Executors.newFixedThreadPool(5);
        // 创建一个5个线程大小的线程池

        ArrayList<Future<Integer>> results = new ArrayList<Future<Integer>>(5);
        // 创建一个Future<Integer>类型的数组

        FutureTask<Integer> ft = null;

        for (int i = 0; i < 5; i++) {
            ft = new FutureTask<Integer>(new ImplCallable(i));
            // 将Callable类型转化成FutureTask类型

            es.submit(ft);
            // 提交线程

            results.add(ft);
            // 将返回的结果提交,因为FutureTask同时也可变为Future类型,所以这里不需要其他类型转化

        }
        for (int i = 0; i < 5; i++)
            try {
                System.out.println(results.get(i).get());
                // 打印结果,发现数组中为0到4五个数字,成功。

            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    }

}

ImplCallable类:

package AllThread;

import java.util.concurrent.Callable;

/**
 * 
 * @author QuinnNorris
 * 
 *         Callable的实现类
 */
public class ImplCallable implements Callable<Integer> {

    private int index;

    ImplCallable(int index) {
        this.index = index;
    }

    @Override
    public Integer call() throws Exception {
        // TODO Auto-generated method stub
        return index;

    }

}

上面采用了线程池的方法来表现Callable和Future的使用方法,如果是简单实用Thread道理也是相同的,我们需要把:

ExecutorService es = Executors.newFixedThreadPool(5);
es.submit(ft);

这两句去掉,在for循环中替换成下面两句。创建Thread实例,开启新的线程。

Thread th = new Thread(ft);
th.start();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值