Java基础知识之线程和线程池及相关面试题

一.并发和并行是即相似又有区别(微观概念):
并行:指两个或多个事件在同一时刻点发生;
并发:指两个或多个事件在同一时间段内发生。

在操作系统中,在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单CPU系统中,每一时刻却仅能有一道程序执行(时间片),故微观上这些程序只能是分时地交替执行。
倘若计算机系统中有多个CPU,则这些可以并发执行的程序便可被分配到多个处理器上,实现多任务并行执行,即利用每个处理器来处理一个可并发执行的程序,这样,多个程序便可以同时执行,因为是微观的,所以大家在使用电脑的时候感觉就是多个程序是同时执行的。
所以,大家买电脑的时候喜欢买“核”多的,其原因就是“多核处理器”电脑可以同时并行地处理多个程序,从而提高了电脑的运行效率。
单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。
同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
时间片即CPU分配给各个程序的运行时间(很小的概念).

二进程和线程:
进程是指一个内存中运行中的应用程序。每个进程都有自己独立的一块内存空间,一个应用程序可以同时启动多个进程。比如在Windows系统中,一个运行的abc.exe就是一个进程。
那么我们此时就可以处理同时玩游戏和听音乐的问题了,我们可以设计成两个程序,一个专门负责玩游戏,一个专门负责听音乐。
但是问题来了,要是要设计一个植物大战僵尸游戏,我得开N个进程才能完成多个功能,这样的设计显然是不合理的。
更何况大多数操作系统都不需要一个进程访问其他进程的内存空间,也就是说进程之间的通信很不方便。
此时我们得引入“线程”这门技术,来解决这个问题。
线程是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程,如:多线程下载软件。
多任务系统,该系统可以运行多个进程,一个进程也可以执行多个任务,一个进程可以包含多个线程.
一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个执行任务,即多线程。
多进程:操作系统中同时运行的多个程序。
多线程:在同一个进程中同时运行的多个任务。
我们查看Windows环境下的任务管理器:
在操作系统中允许多个任务,每一个任务就是一个进程,每一个进程也可以同时执行多个任务,每一个任务就是线程。

进程与线程的区别:
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的,又称为轻型进程或进程元。
因为一个进程中的多个线程是并发运行的,那么从微观角度上考虑也是有先后顺序的,那么哪个线程执行完全取决于CPU调度器(JVM来调度),程序员是控制不了的。
我们可以把多线程并发性看作是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。
Java程序的进程(Java的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程)。
线程调度:
计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令.
所谓多进程并发运行,从宏观上看,其实是各个进程轮流获得CPU的使用权,分别执行各自的任务.
那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度.
JVM采用的是**抢占式调度,**没有采用分时调度,因此可以能造成多线程执行结果的的随机性。

线程的状态
1.新建状态(New):
2.就绪状态(Runnable)
3.运行状态(Running
4. 阻塞状态(Blocked)
5. 死亡状态(Dead)
在这里插入图片描述
三.多线程优势:
多线程作为一种多任务、并发的工作方式,当然有其存在优势:
① 进程之间不能共享内存,而线程之间共享内存(堆内存)则很简单。
② 系统创建进程时需要为该进程重新分配系统资源,创建线程则代价小很多,因此实现多任务并发时,多线程效率更高.
③ Java语言本身内置多线程功能的支持,而不是单纯第作为底层系统的调度方式,从而简化了多线程编程.

多线程下载:可以理解为一个线程就是一个文件的下载通道,多线程也就是同时开起好几个下载通道。当服务器提供下载服务时,使用下载者是共享带宽的,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。
不难理解,如果你线程多的话,那下载的越快。现流行的下载软件都支持多线程。
多线程是为了同步完成多项任务,不是为了提供程序运行效率,而是通过提高资源使用效率来提高系统的效率。
所以现在大家买电脑的时候,也应该看看CPU的线程数。

四.在Java代码中如何去运行一个进程

//运行一个进程
public class ProcessDemo {
    public static void main(String[] args) throws IOException {
        //方式1:Runtime类的exec方法:
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("notepad");//打开一个记事本
        //方式2:ProcessBuilder的start方法:
        ProcessBuilder pb = new ProcessBuilder("notepad");//打开一个记事本
        pb.start();
    }
}

五.创建线程的方式
①继承Thread类创建
 通过继承Thread并且重写其run(),run方法中即线程执行任务。创建后的子类通过调用 start() 方法即可执行线程方法。
 通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量。(需要创建不同Thread对象,自然不共享)

public class ThreadTest extends Thread {
    private int i = 0;

    @Override
    public void run() {
        for (; i < 50; i++) {
          System.out.println(Thread.currentThread().getName() + " is running " + i);
        }
    }

    public static void main(String[] args) {
        for (int j = 0; j < 50; j++) {
            if (j == 20) {
                new ThreadTest().start();
            }
          System.out.println(Thread.currentThread().getName() + " is running " + j);
        }
    }
}

②通过Runnable接口创建线程类
该方法需要先 定义一个类实现Runnable接口,并重写该接口的 run() 方法,run方法是线程执行体。创建 Runnable实现类的对象,作为创建Thread对象的参数target,此Thread对象才是真正的线程对象。通过实现Runnable接口的线程类,是互相共享资源的。

public class RunnableTest implements Runnable {
    private int i ;
    @Override
    public void run() {
        for(;i<50;i++){
            System.out.println(Thread.currentThread().getName() + " -- " + i);
        }
    }
    public static void main(String[] args) {
        for(int i=0;i<100;i++){
            if(i==20){
                RunnableTest runnableTest = new RunnableTest() ;
                new Thread(runnableTest,"线程1").start() ;
            }
            System.out.println(Thread.currentThread().getName() + " is running " + i);
        }
    }
}

③使用Callable和Future创建线程
  从继承Thread类和实现Runnable接口可以看出,上述两种方法都不能有返回值,且不能声明抛出异常。而Callable接口则实现了此两点,Callable接口如同Runable接口的升级版,其提供的call()方法将作为线程的执行体,同时允许有返回值。
  但是Callable对象不能直接作为Thread对象的target,因为Callable接口是 Java 5 新增的接口,不是Runnable接口的子接口。对于这个问题的解决方案,就引入 Future接口,此接口可以接受call() 的返回值,RunnableFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target 。并且, Future 接口提供了一个实现类:FutureTask 。
  FutureTask实现了RunnableFuture接口,可以作为 Thread对象的target。

public class CallableTest {
    public static void main(String[] args) {
        CallableTest callableTest = new CallableTest() ;
        //因为Callable接口是函数式接口,可以使用Lambda表达式
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int i = 0 ;
            for(;i<100;i++){
                System.out.println(Thread.currentThread().getName() + "的循环变量i的值 :" + i);
            }
            return i;
        });
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" 的循环变量i :" + i);
            if(i==20){
                new Thread(task,"有返回值的线程").start();
            }
        }
        try{
            System.out.println("子线程返回值 : " + task.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

④使用线程池例如用Executor框架
1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。

Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线 程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较

newCachedThreadPool() 缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中缓存型池子通常用于执行一些生存期很短的异步型任务因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。能reuse的线程,必须是timeoutDLE内的池中线程,缺省timeout是60s,超过这个DLE时长,线程实例将被终止及移出池。注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止
newFixedThreadPool(int) newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TC或UDPDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:fixed池线程数固定,并且是0秒IDLE(无
newScheduledThreadPool(int) 调度型线程池这个池子里的线程可以按schedule依次delay执行,或周期执行
SingleThreadExecutor() 单例线程,任意时间池中只能有一个线程用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。
Executor执行Runnable任务
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上

public class TestCachedThreadPool {
    public static void main(String[] args){
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++){
            executorService.execute(new TestRunnable());
            System.out.println("************* a" + i + " *************");
        }
        executorService.shutdown();
    }
}

class TestRunnable implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName() + "线程被调用了。");
    }
}

六.面试题
①线程和进程有什么区别?
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

②Thread 类中的 start() 和 run() 方法有什么区别?
调用 start() 方法才会启动新线程;如果直接调用 Thread 的 run() 方法,它的行为就会和普通的方法一样;为了在新的线程中执行我们的代码,必须使用 Thread.start() 方法。

③用 Runnable 还是 Thread ?
我们都知道可以通过继承 Thread 类或者调用 Runnable 接口来实现线程,问题是,创建线程哪种方式更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口更好了。

④Runnable 和 Callable 有什么不同?
Runnable 和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。

⑤什么是 Executor 框架?
Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。

⑥Executors 类是什么?
Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。

发布了99 篇原创文章 · 获赞 2 · 访问量 2619

猜你喜欢

转载自blog.csdn.net/weixin_41588751/article/details/105104603
今日推荐