Java基础 - 多线程01

1.什么是线程和进程?

(1)何为进程?

      进程是程序执行一次的过程,是系统运行程序的基本单位,因此,进程是动态的。系统运行一个程序,就是一个进程从创建、运行到销毁的过程。

      在Java中,启动main函数时,实际就启动了一个JVM的进程,而main函数所在的线程就是进程中的一个线程,称为主线程。

      进程是资源分配的最小单位。

(2)何为线程?

      线程与进程相似,但线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

      线程是进程的一部分,是CPU调度的最小单位。

2.线程和进程的关系、区别及优缺点

      从上图可以看到,一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、本地方法栈、虚拟机栈。

总结:线程是进程划分成更小的运行单位。线程与进程最大的不同在于,基本上各个进程之间是相互独立的,而各个线程之间不一定独立,同一进程中的线程之间可能会相互影响。线程执行开销小,但不利于资源管理和包含,进程与之相反。

3.并行和并发的区别

(1)并行:单位时间内,多个任务同时执行。(两个人吃两个馒头)

(2)并发:同一时间段,多个任务都在执行(单位时间内,不一定同时执行)。也就是多线程。(一个人吃两个馒头)

3.1 并发编程的三个重要特性

(1)原子性 

        一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。 synchronized 可以保证代码片段的原子性。

(2)可见性

        当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。 volatile 关键字可以保证共享变量的可见性。

(3)有序性

        代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。 volatile 关键字可以禁止指令进行重排序优化。

4.使用多线程可能带来什么问题?

      并发编程的目的是为了提高程序的执行效率、提高程序的运行效率,但是并发编程并不是总能提高执行效率,也会带来一些问题,如内存泄漏、上下文切换、死锁,以及受限于硬件和软件的资源闲置问题。      

(1)内存泄漏

        程序中已经动态分配的堆内存,由于某种原因程序未释放或者无法释放内存,造成系统内部的浪费,导致程序运行速度减缓甚至系统崩溃等严重结果。内存泄漏的堆积终将导致内存溢出。

        内存溢出:没有足够的内存提供申请者使用。

(2)上下文切换  

      无论是单双核处理器都支持多线程。多线程编程时,CPU通过给每个线程分配CPU时间片,在这个时间片内执行这个线程,因为时间片非常短,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

  如何减少上下文切换:无锁并发编程、CAS算法、使用最少线程、使用协程。

(3)死锁

        见Java基础 - 多线程02。

5.创建线程有几种方式?

      创建线程有四种方式:继承Thread类、实现Runnable接口、实现Callable接口、线程池。

(1)继承Thread类,重写run()方法

        定义Thread类的子类,并重写该类的run()方法,该run方法的方法体就代表了线程要完成的任务,因此把run( ) 方法称为方法体;定义子类的实例,即线程对象;调用线程对象的start()方法,启动线程。

(2)实现Runnable接口

        01 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体是该线程的线程执行体。

        02 创建 Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

        03 调用线程对象的start()方法来启动该线程。

(3)实现Callable接口结合Future

        01 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例。该call()方法将作为线程执行体,并且有返回值;

        02 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。 【注】FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。

        03 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

        04 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值.

(4)线程池

采用实现Runnable、Callable接口的方式创见多线程时,优势是:

(1)线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

(2)在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

(1)编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时优势是:

(1)编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

(1)线程类已经继承了Thread类,所以不能再继承其他父类。

6.run()方法和start()方法有什么区别?

      run方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start方法用来启动线程。

     new 一个线程,线程进入了新建状态;调用 start() 方法,会启动线程并使线程进入就绪状 态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

7.线程可以重复启动吗?会有什么后果?

      不可以重复启动,即不能重复调用start()方法。只能对处于新建状态的线程调用start()方法,否则会引发 IllegalThreadStateException 异常。

      当程序使用new关键字创建一个线程之后,该线程就处于新建状态。

8.介绍一下线程的生命周期

      在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。特别是线程启动之后,它不可能一直霸占着CPU,CPU需要在多条线程之间相互切换,所以线程状态也会多次在运行、就绪之间切换。

(1)新建状态(New)

      当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由 Java 虚拟机(JVM)为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。此时仅仅是个对象。

(2)就绪状态(Ready)

      当线程对象调用了 start() 方法后,该线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器。处于就绪状态的线程并没有开始运行,只是表示该线程可以运行了。至于什么时候开始运行,取决于 JVM 里线程调度器的调度。

(3)运行状态(Running)

      如果处于 Ready 的线程获得了CPU,开始执行 run 方法的线程执行体,那么该线程就处于运行状态。线程的执行是由底层平台控制的,具有一定的随机性。

      为了让线程切换后,能够恢复到正确的执行位置,每个线程都有一个独立的线程计数器,各条线程之间的计数器互不影响,独立存储。

(4)阻塞状态(Blocked)

      一个线程运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是为了让其他线程获得执行的机会,线程调度细节取决于底层所采用的策略。当发生如下状况时,线程会进入阻塞状态

    (a)线程调用 sleep 方法主动放弃所占用的处理器资源;

    (b)线程调用了一个 IO 式阻塞方法,在该方法返回之前,线程被阻塞;

    (c)线程试图获得一个同步监视器,但该同步监视器正在被其他线程占用;

    (d)线程在等待某个通知(nofity);

    (e)程序调用了线程的 suspend 方法将该线程挂起。但该方法容易导致死锁,不推荐使用。

针对上面几种情况,当发生如下特定情况时,可以解除阻塞,让线程进入就绪状态:

   (a)调用 sleep 方法的线程经过了指定时间;

   (b)线程调用的 IO 式阻塞方法已经被返回;

   (c)线程成功获得了试图获取的同步监视器;

   (d)线程在等待通知时,其他线程发出了通知;

   (e)处于挂起状态的线程调用了 resume 恢复方法。

(5)死亡状态(Dead)

      线程会以如下三种方式结束,结束后就处于死亡状态,

        (a)run 方法或 call 方法执行完成,线程正常结束;

        (b)线程抛出一个未捕获的异常 Exception 或错误 Error;

        (c)直接调用线程的 stop 方法来结束线程。但该方法容易导致死锁,不推荐使用。

9.如何实现线程同步?

   线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,处于等待状态,直到该线程完成操作, 其他线程才能对该内存地址进行操作;

   线程安全:指保护一个线程访问共享资源时,其他线程不能访问共享资源,等当前线程访问完后,其他线程才可以访问。

        线程同步是实现线程安全的一种手段
 

(1)同步方法

      同步方法,即有synchronized关键字修饰的方法,由于Java的每个对象都有一个内置锁,当使用该关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要调用内置锁,否则会处于阻塞状态。

(2)同步代码块

      即用synchronized关键字修饰的语句块。被该关键字修饰的语句块会被自动加上内置锁。但是,值得注意的是,同步是一个高开销的操作,应该减少同步的内容。

(3)ReentrantLock

      Java5新增了java.util.concurrent包来支持同步,其中ReentrantLock是可重入、互斥、实现了Lock接口的锁,它与使用synchronized的方法和代码块具有相同的基本行为和语义,并且扩展了其能力,但需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于它会大幅度降低程序运行效率,一般不推荐使用。

(4)volatile

      volatile 关键字可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

      volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此,每次使用该域就要重新计算,而不是使用寄存器中的值。

      volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

(5)原子变量

      在java.util.concurrent.atomic包中提供了创建原子类型变量的工具类,使用该类可以简化线程同步。

10.Java多线程之间的通信方式

       线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

      线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。

      线程通信的方式主要可以分为4种:volatile、等待 / 通知机制、join方式、threadLocal。

(1)volatile

         volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。volatile语义保证线程可见性有两个原则保证,

        01 所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存。

        02 所有volatile修饰的变量在使用之前必须重新读取主内存的值。

(2)等待 / 通知机制

        等待通知机制是基于 wait 和 notify 方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。

     如果线程之间采用synchronized关键字来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。

     wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()方法用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便当前线程释放锁后竞争锁,进而得到CPU的执行。

    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列里面存储了已就绪(将要竞争锁)的线程,阻塞队列里面存储了被阻塞的线程。当一个阻塞线程被唤醒后,会进入就绪队列,进而等待CPU的调度。反之,一个线程被wait之后,就会进入阻塞队列,等待被唤醒。

(3)join方式

        join 其实可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序。

        但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。

(4)thraedLocal

      threadLocal方式的线程通信,不像以上三种方式是多个线程之间的通信,它更像是一个线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。

11.说一说Java同步机制中的wait和notify

      wait()、notify()、notifyAll()并不是Thread类所声明的方法,而是Object类中的方法。wait()方法可以让当前线程释放对象锁,并进入阻塞队列。notify()方法可以唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放对象锁后竞争锁,进而获得CPU的执行。notifyAll()方法用于唤醒所有正在等待相应对象锁的线程,使它们进行就绪队列,以便当前线程释放对象锁后竞争锁,进而获得CPU的执行。

      每个对象锁都有两个队列:就绪队列和阻塞队列。就绪队列存储了已就绪(要竞争锁)的线程,阻塞队列存储了被阻塞的队列。当一个线程被唤醒后,就会进入就绪队列,进而等待CPU的调度。当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

12.说一说sleep()和wait()的区别

(1)两者都可以暂停线程的执行。wait 通常用于线程间通信,sleep 常用于暂停执行;

(2)sleep()可以使用在任何地方,wait()只能在同步方法或同步代码块中使用;

(3)sleep()不用释放锁,wait()需要释放锁;

(4)sleep 方法执行完后,线程会自动苏醒。wait 方法被调用后,线程不会自动苏醒,需要别的线程调用同一对象上的 notify 或 notifyAll 方法。

13.说一说notify()和notifyAll()的区别

(1)notify()

      用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行;

(2)notifyAll()

      用于唤醒所有正在等待响应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值