zoukankan      html  css  js  c++  java
  • 面试必备——Java多线程与并发(一)

    1.进程和线程

    (1)由来

    1)串行

    最初的计算机只能接受一些特定的指令,用户输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。显然这样效率低下,在很多时候,计算机都处在等待状态。

    2)批处理

    提高计算机的效率,不用等待用户的输入,把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机,计算机不断读取指令进行相应的操作,就这样,批处理操作系统诞生了。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
    存在问题:
    当有两个任务A和B,任务A执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能等任务A读取完数据再能继续进行,这样就白白浪费了CPU资源。于是人们就想,能否在任务A读取数据的过程中,让任务B去执行,当任务A读取完数据之后,暂停任务B,让任务A继续执行?
    这时候又出现了几个问题:内存中始终都只有一个程序在运行,而想要解决上述问题,必然要在内存中装入多个程序,如何处理呢?多个程序使用的数据如何辨别?当一个程序暂停后,随后怎么恢复到它之前执行的状态呢?此时,进程应运而生.

    3)进程

    用进程来对应一个程序,每个进程来对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程之间互不干扰。保存了程序每个时刻的运行状态,为进程切换提供了可能。当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行。进程让操作体统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。之所以造成用户在宏观上的假象,CPU分配给单一任务的时间片很短,切换频次高。
    进程出现之后,操作系统的性能得到了大大的提升。虽然解决了操作系统的并发问题,随着计算机的发展,人们并不满足这样,逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一个事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,子任务之间往往不存在顺序上的依赖,是可以并发执行的,既然CPU可以按照时间片的方式轮流切换进程,能不能给子任务打上标签,按照更细的时间片去执行?

    4)线程

    答案是肯定的,由于子任务共享进程的内存等资源,相互间切换更快速,人们发明了线程,让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。
    进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

    (2)区别

    进程是资源分配的最小单位(进程之间互不干扰),线程是CPU调度的最小单位(线程间互相切换)。
    • 所有与进程相关的资源,都被记录在PCB(进程控制块)中
    • 进程是抢占处理器的调度单位;线程属于某个进程,共享其资源

    • 线程只由堆栈寄存器、程序计数器和TCB组成

    总结

    • 线程不能看做独立应用,而进程可看做独立应用
    • 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
    • 进程数据分开,共享复杂,同步简单;线程共享简单,同步复杂
    • 线程没有独立的地址空间,多进程的程序比多线程程序健壮(进程出现问题不会影响其他进程,可靠高;一个线程挂掉,整个进程也会挂掉,可靠低)
    • 进程的切换比线程的切换开销大(进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小)

    (3)JAVA进程和线程的关系

    • Java对操作系统提供的功能进行封装,包括进程和线程
    • 运行一个程序会产生一个进程,进程包含至少一个线程
    • 每个进程对应一个JVM实例,多个线程共享JVM里的堆,JVM是多线程的
    • Java采用单线程编程模型,程序会自动创建主线程
    • 主线程可以创建子线程,原则上要后于子线程完成执行,因为要执行各种关闭动作

    2.线程的start和run的区别

    • 调用start()方法会创建一个新的子线程并启动
    • run()方法只是Thread的一个普通方法的调用

    3.Thread和Runnable是什么关系

    • Thread是实现了Runnable接口的类,通过Thread的start()方法可以给Runnable的run()方法附上多线程的特性
    • 因Java类的单一继承原则,推荐多使用Runnable接口(为了提升系统的可拓展性,通过使业务类实现Runnable接口,将业务逻辑封装在run方法里,后续可以给普通类附上多线程的特性)

    4.如何实现处理线程的返回值

    (1)主线程等待法:实现简单,我们可以通过实现循环等待的逻辑;缺点是变量多的时候会显得臃肿,无法精准控制

     1 public class CycleWait implements Runnable {
     2  
     3   private String value;
     4  
     5   @Override
     6   public void run() {
     7     try {
     8       Thread.currentThread().sleep(5000);
     9     } catch (InterruptedException e) {
    10       e.printStackTrace();
    11     }
    12     value = "we have date now";
    13   }
    14   
    15   public static void main(String[] args) throws InterruptedException {
    16     CycleWait cw = new CycleWait();
    17     Thread t = new Thread(cw);
    18     t.start();
    19     //当值为null的时候一直循环,直到有值时才退出循环
    20 
    21     while (cw.value == null) {
    22 
    23         Thread.currentThread().sleep(100);
    24 
    25     }
    26     System.out.println("value:" + cw.value); // 没有前面的循环,可能取出的值为null
    27   }
    28 }
    View Code

    (2)使用Thread类的join()阻塞当前线程以等待子线程处理完毕:实现更简单,缺点是力度不够细,无法精确控制

    1 public static void main(String[] args) throws InterruptedException {
    2     CycleWait cw = new CycleWait();
    3     Thread t = new Thread(cw);
    4     t.start();
    5     t.join();
    6     System.out.println("value:" + cw.value); 
    7   }
    View Code

    (3)通过Callable接口实现:JDK5.0新增的,具体可以通过FutureTask或线程池获取

     1 public class myCallable implements Callable {
     2   @Override
     3   public String call() throws Exception {
     4     String value = "test";
     5     System.out.println("Ready to work");
     6     Thread.currentThread().sleep(3000);
     7     System.out.println("task done");
     8     return value;
     9   }  
    10 }
    View Code
    • FutureTask
    1 public static void main(String[] args) throws ExecutionException, InterruptedException {
    2    FutureTask<String> ft = new FutureTask<String>(new myCallable());
    3    new Thread(ft).start();
    4    if (!ft.isDone()) {
    5       System.out.println("task has not finished, please wait!");
    6    }
    7    System.out.println("task reture:" + ft.get());
    8 }
    View Code
    • 线程池
     1 public static void main(String[] args) {
     2    ExecutorService executorService = Executors.newCachedThreadPool();
     3    Future<String> future = executorService.submit(new myCallable());
     4    if (!future.isDone()) {
     5       System.out.println("task has not finished, please wait!");
     6    }
     7    try {
     8       System.out.println("task reture:" + future.get());
     9    } catch (InterruptedException e) {
    10       e.printStackTrace();
    11    } catch (ExecutionException e) {
    12       e.printStackTrace();
    13    } finally {
    14       executorService.shutdown();
    15    }
    16 }
    View Code

    运行结果

    1 task has not finished, please wait!
    2 Ready to work
    3 task done
    4 task reture:test

    5.线程的状态(6个)

    (1)新建(New):创建后尚未启动的线程的状态

    (2)运行(Runnable):包含Running和Ready(Running:正在执行;Ready:等待CPU分配执行时间)

    (3)无限期等待(Waiting):不会被分配CPU执行时间,需要显示被唤醒

    • 没有设置Timeout参数的Object.wait()方法
    • 没有设置Timeout参数的Thread.join()方法
    • LockSupport.park()方法

    (4)限期等待(Timed Waiting):在一定时间后由系统自动唤醒

    • Thread.sleep()方法
    • 设置了Timeout参数的Object.wait()方法
    • 设置了Timeout参数的Thread.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUntil()方法

    (5)阻塞(Blocked):等待获取排它锁

    (6)结束(Terminated):已终止线程的状态,线程已经结束执行

    6.sleep和wait的区别

    (1)基本差别

    • sleep是Thread类的方法,wait是Object类中定义的方法
    • sleep()方法可以在任何地方使用
    • wait()方法只能在synchronized方法或synchronized块中使用

    (2)最主要的本质区别

    • Thread.sleep只会让出CPU,不会释放锁
    • Object.wait不仅会让出CPU,还会释放锁(这个方法要写在synchronized里面,因为要获得锁,才能释放锁)

     7.notify和notifyall的区别

    (1)需要先了解的两个概念

    • 锁池EntryList
    假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池
    • 等待池WaitSet
    假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待锁中,进入到等待池中的线程不会去竞争该对象的锁

    (2)区别

    • notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
    • notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

    8.yield相关

    当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示,该方法不释放锁。

     1 public static void main(String[] args) {
     2    Runnable yieldTask = () -> {
     3       for (int i = 0; i <= 10; i++) {
     4          System.out.println(Thread.currentThread().getName() + i);
     5          if (i == 5){
     6             Thread.yield(); // 暗示线程调度器愿意让出CPU使用,但最终决定权还是在线程调度器中
     7          }
     8       }
     9    };
    10    Thread thread1 = new Thread(yieldTask,"A");
    11    Thread thread2 = new Thread(yieldTask,"B");
    12    thread1.start();
    13    thread2.start();
    14 }
    View Code

    9.interrupt相关

    (1)设计理念

    一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,是一种比较温柔的做法,它更类似一个标志位。其作用不是中断线程,而是通知线程应该中断了,具体到底中断还是继续运行,应该由被通知的线程自己处理。

    (2)如何中断线程

    1)已经被抛弃的方法

    • 通过调用stop()方法停止线程
    它可以是由一个线程去停止另外一个线程,太过暴力,而且不安全。
    比如说,线程A去停止线程B,但是不知道线程B执行的情况,突然停止,会导致线程B的清理工作无法完成,还有其他情况,执行stop方法后,线程B会马上释放锁,可能引发数据不同步问题
    • 通过调用suspend()和resume()方法

    2)目前使用的方法

    调用interrupt(),通知线程应该中断了
    • 如果线程处于被阻塞状态,那么线程立即退出被阻塞状态,并抛出一个InterruptedException异常
    • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响
    因此,interrupt并不能真正中断线程,需要被调用的线程配合去中断
    • 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
    • 调用Thread.interrupted() 方法后线程恢复非中断状态,即Thread.currentThread().isInterrupted()是false
  • 相关阅读:
    两个简单的画验证码图形程序
    Cisco路由技术基础知识详解
    网络管理中的常用命令
    网络管理中的常用命令
    基于SNMP的MIB库访问实现
    SNMP编程基础
    SNMP编程基础
    Cisco路由技术基础知识详解
    两个简单的画验证码图形程序
    模版方法
  • 原文地址:https://www.cnblogs.com/huozhonghun/p/duoxiancheng.html
Copyright © 2011-2022 走看看