面试准备系列——Java基础技术篇(10)/Java并发编程

1.什么是线程?他与进程的区别?为什么要用多线程

线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有4种状态:运行、就绪、挂起和结束
进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间( 代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的对比关系如下图所示:
在这里插入图片描述

在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了巨大的便利,具体而言,有以下几个方面的内容:

  • 1)使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
  • 2)与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
  • 3)多CPU或多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
  • 4)使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。

2.同步和异步有什么区别

在多线程的环境中,经常会碰到数据的共享问题,即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用, 否则,程序的运行结果将会是不可预料的,在这种情况下就必须对数据进行同步,例如多个线程同时对同一数据进行写操作,即当线程A需要使用某个资源时,如果这个资源正在被线程B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用后,线程A才能使用这个资源,由此可见,同步机制能够保证资源的安全。

要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

Java语言在同步机制中提供了语言级的支持,可以通过使用synchronized关键字来实现同步,但该方法并非“万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一种是利用同步代码块来实现同步;另一种是利用同步方法来实现同步。

异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等到输入输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。

举个生活中的简单例子就可以区分同步与异步了。同步就是你喊我去吃饭,如果听到了,我就和你去吃饭;如果我没有听到,你就不停地喊,直到我告诉你听到了,我们才一起去吃饭。异步就是你喊(发消息)我,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

3.如何实现Java多线程

Java虚拟机允许应用程序并发地运行多个线程。在Java语言中,多线程的实现一般有以下3种方法,其中前两种为最常用的方法。

  • (1)继承Thread类,重写run( )方法
    Thread本质上也是实现了Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方 法就是通过Thread 类的start( )方法。start( )方法是一个native (本地)方法,它将启动一个新线程,并执行run( )方法(Thread 中提供的run( )方法是-一个空方法)。这种方式通过自定义直接extend Thread,并重写run()方法,就可以启动新线程并执行自己定义的run()方法。需要注意的是,调用start( )方法后并不是立即执行多线程代码,而是使得该线程变为可运行态( Runnable),什么时候运行多线程代码是由操作系统决定的。下例给出了Thread的使用方法:
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Thread body");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  • (2)实现Runnable接口,并实现该接口的run()方法
    以下是主要步骤:
  • 1)自定义类并实现Runnable接口,实现run( )方法。
  • 2)]创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象。
  • 3)调用Thread的start( )方法。
@Override
    public void run() {
        System.out.println("Thread body");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t = new Thread(myThread);
        t.start();
    }

其实,不管是通过继承Thread类还是通过使用Runnable接口来实现多线程的方法,最终还是通过Thread的对象的API来控制线程的。

  • (3)实现Callable接口,重写call( )方法
    Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为以下3点:
  • 1)Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。
  • 2)Callable 中的call()方法可以抛出异常,而Runnable的run( )方法不能抛出异常。el弥
  • 3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监视目标线程调用call( )方法的情况,当调用Future的get( )方法以获取结果时,当前线程就会阻塞,直到call( )方法结束返回结果。
    在这里插入图片描述
    以上3种方式中,前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。当需要实现多线程时,一般推荐实现Runnable接口的方式,其原因是:首先,Thread类定义了多种方法可以被派生类使用或重写。但是只有run( )方法是必须被重写的,在run( )方法中实现这个线程的主要功能。这当然是实现Runnable接口所需的方法。其次,很多Java开发人员认为,一个类仅在他们需要被加强或修改时才会被继承。因此,如果没有必要重写Thread类中的其他方法,那么通过继承Thread的实现方式与实现Runnable接口的效果相同,在这种情况下最好通过实现Runnable接口的方式来创建线程。

引申:一个类是否可以同时继承Thread与实现Runnable接口?
答案:可以。为了说明这个问题,首先给出如下示例:

在这里插入图片描述
从上例中可以看出,, Test 类实现了Runnable 接口,但是并没有实现接口的run()方法,可能有些读者会认为这会导致编译错误,但实际它是能够编译通过并运行的,因为Test类从Thread类中继承了run( )方法,这个继承的run( )方法可以被当作对Runnable接口的实现,因此这段代码能够编译通过。当然也可以不使用继承的run()方法,而是需要通过在Test类中重写run( )方法来实现Runnable接口中的run( )方法,示例如下:
在这里插入图片描述

4.(重点)run方法与start方法有什么区别

通常,系统通过调用线程类的start( )方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run( )方法来完成实际的操作,当run( )方法结束后,此线程就会终止。

如果直接调用线程类的run()方法,这会被当作-个普通的丽数调用,程序中仍然只有主线程这一个线程,也就是说,start 方法( )能够异步地调用run()方法,但是直接调用run( )方法却是同步的,因此也就无法达到多线程的目的。

由此可知,只有通过调用线程类的start( )方法才能真正达到多线程的目的。下面通过一个例子来说明说明run( )方法与start( )方法的区别。
在这里插入图片描述
从test1的运行结果可以看出,线程t1是在testl方法结束后才执行的( System. out. printhn(" testl :end" )语句不需要等待tl. start( )运行结果就可以执行),因此,在test1中调用start()方法是异步的,所以main线程与t1线程是异步执行的。从test2的运行结果可以看出,调用t1. run( )是同步的调用方法,因为System. out. println( " test2 :end" )只有等t1. run( )调用结束后才能执行。

5.(重点)多线程同步实现的方法有哪些?

当使用多线程访问同一个资源时,非常容易出现线程安全的问题(例如,当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失)。因此,需要采用同步机制来解决这种问题。Java 主要提供了3种实现同步机制的方法:

(1) synchronized 关键字

在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段 synchronized代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后,释放锁。

synehronized关键字主要有两种用法( synchronized方法和synchronized块),此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。

  1. synchronized 方法。在方法的声明前加入synchronized关键字,示例如下:
public synchronized void mutiThreadAccess( ) ;

只要把多个线程对类需要被同步的资源的操作放到mutiThreadAccess()方法中,就能保证这个方法在同一时刻只能被一个线程访问, 从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高程序的效率,Java 提供了synchronized 块。

  1. synchronized 块。synchronized 块既可以把任意的代码段声明为synchronized, 也可以指定上锁的对象,有非常高的灵活性。其用法如下:
    在这里插入图片描述

(2) wait( )方法与notify()方法

当使用synchronized来修饰某个共享资源时,如果线程A1在执行synchronized代码,另外一个线程A2也要同时执行同一对象的同一synchronized 代码时,线程A2将要等到线程A1执行完成后,才能继续执行。在这种情况下可以使用wait( )方法和notify( )方法。

在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify( )方法或notifyAll( )方法通知正在等待的其他线程。notify( )方法仅唤醒一个线程 (等待队列中的第一个线程) 并允许它去获得锁,notifyAll( )方法唤醒所有等待这个对象的线程并允许它们去获得锁( 并不是让所有唤醒线程都获取到锁,而是让它们去竞争)。
线程各种运行状态如下图所示:
在这里插入图片描述

(3) Lock

JDK 5新增加了Lock接口以及它的一个实现类ReentrantLock ( 重入锁),Lock也可以用来:实现多线程的同步,具体而言,它提供了如下一些方法来实现多线程的同步:

  1. lock()。 以阻塞的方式获取锁,也就是说,如果获取到了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获取锁后返回。
  2. tryLock( )。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,立即返回false。
  3. tryLock( long timeout, TimeUnit unit)。如果获取了锁,立即返回true,否则会等待参数给定的时间单元,在等待的过程中,如果获取了锁,就返回true,如果等待超时,返回false。
  4. lockInterruptibly()。 如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断(会收到InterruptedException异常)。它与lock( )方法最大的区别在于如果lock( )方法获取不到锁,会-直处于阻塞状态,且会忽略interrupt( )方法,示例如下:
    在这里插入图片描述如果把lock. locklnterruptibly( )替换为lock. lock(),编译器将会提示lock. lock( )catch代码块无效,这是因为lock. lock( )不会抛出异常,由此可见lock( )方法会忽略interrupt( )引发的异常。

6.sleep方法与wait方法的区别

可以看看这个图对比理解:
在这里插入图片描述
下边的这个图也是一样道理的:
在这里插入图片描述
sleep( )是使线程暂停执行一段时间的方法。wait( )也是一种使线程暂停执行的方法,例如,当线程交互时,如果线程对一个同步对象x发出一个wait( )调用请求,那么该线程会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间超时。

具体而言,sleep()方法与wait( )方法的区别主要表现在以下几个方面:

  • 1)原理不同。sleep( )方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动“苏醒”,例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加上-个sleep( )方法,以便让自己每隔1s执行一次,该过程如同闹钟一样。而wait()方法是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法( 或notifyAll方法)时才“醒”来,不过开发人员也可以给它指定一个时间,自动“醒”. 来。与wait( )方法配套的方法还有notify( )方法和notifyAll( )方法。

  • 2)对锁的处理机制不同。由于sleep( )方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用sleep( )方法并不会释放锁。而wait( )方法则不同,当调用wait( )方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。举个简单例子,如果小吴拿遥控器的期间,可以用自己的sleep( )方法每隔10 min调异常一次频道,而在这10min里,遥控器还在他的手上。

  • 3)使用区域不同。由于wait()方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而sleep( )方法则可以放在任何地方使用。

sleep( )方法必须捕获异常,而wait()、notify( )以及notifyall( )不需要捕获异常。在sleep的过程中,有可能被其他对象调用它的interrupt( ),产生InterruptedException 异常。

由于sleep不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用sleep( )方法,而推荐使用wait( )方法。

引申: sleep( )方法与yield( )方法有什么区别?

首先,Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

sleep( )方法与yield( )方法的区别主要表现在以下几个方面:

  • 1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而yield()方法只会给相同优先级或更高优先级的线程以运行的机会

  • 2)线程执行sleep()方法后会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行

  • 3)sleep( )方法声明抛出InterruptedException,而yield( )方法没有声明任何异常。

  • 4)sleep( )方法比yield( )方法(跟操作系统相关)具有更好的可移植性。

常见题型:
在这里插入图片描述

7.终止线程的方法都有哪些

在Java语言中,可以使用stop( )方法与suspend( )方法来终止线程的执行。当用**Thread.stop()**来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。调用suspend()方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进)。由于调用suspend( )方法不会释放锁,这就会导致一个问题:如果用一个suspend挂起一个有锁的线程,那么在锁恢复之前将不会被释放。如果调用suspend( )方法,线程将试图取得相同的锁,程序就会发生死锁,例如,线程A已经获取到了互斥资源M的锁,此时线程A通过suspend( )方法挂起线程A的执行,接着线程B也去访问互斥资源M,这时候就造成了死锁。鉴于以上两种方法的不安全性,Java 语言已经不建议使用以上两种方法来终止线程了。

那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,即执行完run( )方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run( )方法的执行。在实现时,可以通过设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run( )方法从而终止线程。下例给出了结束线程的方法:
在这里插入图片描述
上例中,通过调用MyThread的stop( )方法虽然能够终止线程,但同样也存在问题:当线程处于非运行状态时(当sleep( )方法被调用或当wait( )方法被调用或当被I/0阻塞时),上面介绍的方法就不可用了。此时可以使用interrupt( )方法来打破阻塞的情况,当interrupt( )方法被调用时,会抛出InterruptedException 异常,可以通过在run( )方法中捕获这个异常来让线程安全退出,具体实现方式如下:
在这里插入图片描述
如果程序因为I/O而停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态,在这种情况下,无法使用interrupt( )方法来使程序离开run()方法。这就需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的I/O相关,例如,如果使用readLine( )方法在等待网络上的一个信息,此时线程处于阻塞状态。让程序离开run( )方法就是使用close( )方法来关闭流,在这种情况下会引发IOException异常,run( )方法可以通过捕获这个异常来安全地结束线程。

8.synchronized与Lock有什么异同

Java语言提供了两种锁机制来实现对某个共享资源的同步:synchronized和Lock。其中,synchronized使用Object对象本身的notify、wait、 notityAll 调度机制,而Lock可以使用Condition进行线程之间的调度,完成synchronized实现的所有功能。

具体而言,二者的主要区别主要表现在以下几个方面的内容:

  • 1)用法不一样。在需要同步的对象中加入synchronized控制,synchronized 既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而Lock需要显式地指定起始位置和终止位置。synchronized 是托管给JVM执行的,而Lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。
  • 2)性能不一样。在JDK 5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。它们的性能在不同的情况下会有所不同:在资源竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,synchronized 的性能会下降得非常快,而
    ReetrantLock的性能基本保持不变。
  • 3)锁机制不一样。synchronized 获得锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。此外,Lock 还提供了更强大的功能,它的tryLock( )方法可以采用非阻塞的方式去获取锁。

虽然synchronized与Lock都可以用来实现多线程的同步,但是,最好不要同时使用这两种同步机制,因为ReetrantLock 与synchronized 所使用的机制不同,所以它们的运行是独立的,相当于两种类型的锁,在使用时互不影响,示例如下:
在这里插入图片描述
程序运行结果:
在这里插入图片描述
当然,上例中,并不是每次运行的结果都是相同的,与前-一个例子对比可以发现,上例中的输出结果value的值并不是连续的,这就是因两种上锁方法采用了不同的机制而造成的,因此在实际使用时,最好不要同时使用两种上锁机制。

常见题目:
在这里插入图片描述

在这里插入图片描述
从上例可以看出,线程t1在调用sychronized( )方法的过程中,线程t2仍然可以访问同一对象的非sychronized( )方法。

如果其他方法是静态方法(使用static 修饰的方法),它用的同步锁是当前类的字节码,与非静态的方法不能同步(因为非静态的方法用的是this),因此,静态方法可以被调用,示例如下。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.在这里插入图片描述

9.什么是守护线程

Java提供了两种线程:守护线程与用户线程。守护线程又被称为“ 服务进程”“精灵线程”或“后台线程”,是指在程序运行时在后台提供一种通 用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。

用户线程和守护线程几乎一样, 唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。

在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start( )方法启动线程之前调用对象的setDaemon( true)方法,若将以上参数设置为false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下:
在这里插入图片描述
从运行结果中可以发现,没有输出Thread -0: end。之所以结果是这样,是在启动线程前将其设置为守护线程了,当程序中只有守护线程存在时,JVM是可以退出的,也就是说,当JVM中只有守护线程运行时,JVM会自动关闭。因此,当test3方法调用结束后,main 线程将退出,此时线程t1还处于休眠状态没有运行结束,但是由于此时只有这个守护线程在运行,JVM将会关闭,因此不会输出“Thread-0:end"。

守护线程的一个典型的例子就是垃圾回收器。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。

常见题型:
在这里插入图片描述

10.join()方法的作用是什么

在Java语言中,join( )方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。简单点说,就是将两个线程合并,用于实现同步功能。具体而言,可以通过线程A的join ()方法来等待线程A的结束,或者使用线程A的join (2000) 方法来等待线程A的结束,但最多只等待2s,示例如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值