Java并发总结(基础)

一.并发基础

1.什么是进程,什么是线程?进程与线程之间的区别。

进程:操作系统中一个程序的执行周期称为一个进程。
线程:一个程序同时执行多个任务。通常,每个任务就称为一个线程。没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。

多进程与线程的区别:
(1)根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
(2)线程依赖于进程而存在,一个线程只能属于一个进程,一个进程可以有一个线程或者多个线程。
(3)进程间不会相互影响;而一个线程挂掉将导致整个进程挂掉。
(4)通信:进程间通信较为复杂,同一台计算机的进程通信称为IPC,不用计算机之间的进程通信,需要通过网络并遵守共同的协议来进行。
线程间通信相对简单,因为它们共享进程内的内存,可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
(5)调度和切换:线程上下文切换比进程上下文切换要快得多。
(6)内存:进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量)。

2.多线程的实现方式有哪些?

  • 继承Thread类
    步骤:
    (1)创建一个Thread线程类的子类(子线程),同时重写Thread类的run方法;
    (2)创建稿子类的实例对象,并通过调用start()方法启动线程。
  • 实现Runnable接口
    ———通过继承Thread类的方式实现了多线程,但这种方式有一定局限性,因为Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑实现Runnable()方法来实现多线程。
    步骤:
    1)创建一个Runnable接口的实现类,同时重写接口中的run()方法;
    2)创建Runnable接口的实现类对象;
    3)使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;
    4)调用线程实例的start()方法启动线程。
  • Callable实现多线程
    1)通过创建一个Callable接口的实现类,同时重写Callable接口的call()方法;
    2)创建Callable接口的实现类对象;
    3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
    4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
    5)调用线程实例的start()方法启动线程。
    Runnnable中的run()方法没有返回值。它的设计也遵循了主方法的设计原则;线程一开始就不回头。但是很多时间需要一些返回值,例如某些线程执行完成后可能带来一些返回结果,这种情况下就只能利用Callable来实现多线程。

3.小结

3.1继承Thread类和实现接口比较

  • 接口更适合多个相同的程序代码的线程去共享同一个资源。
  • 接口可以避免java中的单继承的局限性。
  • 接口代码可以被多个线程共享,代码和线程独立。
  • 线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类。
    扩充:
    在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

3.2实现.Runnable接口和实现Callable接口比较

  • 相同点:
    • 两者都是接口;
    • 两者都可用来编写多线程程序;
    • 两者都需要调用Thread.start()启动线程;
  • 不同点:
    • 实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果;
    • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常;
    • 实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能
  • 注意点:
    Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

4.线程的生命周期

当Thread对象创建完成时,线程的生命周期便开始了。当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常或者错误时,线程的生命周期便会结束。
Java中线程的状态可以分为6个状态,分别是NEW(新建状态)、RUNNABLE(可运行状态)、BLOCKED(阻塞状态)、WAITING(等待状态)、TIMEDWAITING(定时等待状态)、TERMINATED(终止状态)。

(1).NEW(新建状态)

创建一个线程对象后(new关键字创建了一个线程之后,该线程就处于新建状态),该线程对象就处于新建状态,此时它不能运行,和其它Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。

(2).RUNNABLE(可运行状态)

当新建状态下的线程对象调用了start()方法,此时就会从新建状态进入可运行状态。在RUNNABLE状态内部又可细分为两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。
就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行;
运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。

(3).BLOCKED(阻塞状态)

处于运行状态的线程可能会由于某些原因失去CPU的执行权,暂时停止运行进入阻塞状态。此时JVM不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转换到运行状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
线程一般会在以下两种情况下进入阻塞状态:
1>当线程A运行过程中,试图获取同步锁时,却被线程B获取,此时JVM把当前线程A存到对象的锁池中,线程A就会进入阻塞状态;
2>当线程运行过程中,发出I/O请求时,此时线程也会进入阻塞状态。

(4).WAITING(等待状态)

当处于运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会将当前运行中的线程转换为等待状态。
处于等待状态中的线程不能立即争夺CPU使用权,必须等待其它线程执行特定的操作后,才有机会再次争夺CPU使用权,将等待状态的线程转换为运行状态。例如,调用wait()方法而处于等待状态中的线程,必须等待其它线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;调用join()方法而处于等待状态中的线程,必须等待其它加入的线程终止。

(5).TIMEDWAITING(定时等待状态)

当运行状态中的线程转换为定时等待状态中的线程与转换为等待状态中的线程操作类似,只是运行线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法。
处于定时等待状态中的线程也不能立即争夺CPU的使用权,必须等待其它相关线程执行完特定的操作或者限时结束后,才有机会再次争夺CPU的使用权,将定时等待状态的线程转换为运行状态。例如,调用了wait(long timeout)方法而处于等待状态中的线程,需要通过其它线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时结束后也可以进行状态转换。

(6).TERMINATED(终止状态)

线程中的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常、错误,线程就进入终止状态。一旦进入终止状态,线程将不再拥有运行的资格,也不能转换到其它状态,生命周期结束。

线程状态转换图:在这里插入图片描述

二.多线程中常用方法的辨析

1.Thread类中start()方法和run()方法有何区别?

当调用 run ()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start ()方法才会启动新线程。

2.sleep()和yield()方法有何区别?

都是Thread类中的静态方法。

  • sleep()
    • 调用sleep方法会让当前线程从Running状态进入Timed Waiting状态(阻塞)。
    • 睡眠结束后的线程未必会立即执行。
  • yield()
    • 调用yield方法会让当前线程从Running状态进入Runnable就绪状态。
      *具体的调度依赖于操作系统。

3.notify 和 notifyAll有什么区别?

notify()方法随机唤醒某个等待的的线程。
  notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

4.为什么wait, notify 和 notifyAll这些方法不在thread类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

5.Java中interrupted 和 isInterruptedd方法的区别?

interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

6.为什么wait和notify方法要在同步块中调用?

主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。

7.Java中如何停止一个线程?

Java提供了很丰富的API但没有为停止线程提供API。

1. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
  • 为什么弃用stop?
    • 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
      调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
2.设置标记位。在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { … } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。
3.调用线程的interrupt方法,它有两个作用:
  • 1、如果此线程处于阻塞状态(比如调用了wait方法,io等待),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。
    2、如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自己是否被中断,并做退出操作。

    注意:
    如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。

    如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。

8.一个线程运行时发生异常会怎样?

这是我在一次面试中遇到的一个很刁钻的Java面试题, 简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

9.如何在两个线程间共享数据?

你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。这篇教程《Java线程间通信》(涉及到在两个线程间共享对象)用wait和notify方法实现了生产者消费者模型。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值