Java interview over 4.10 Thread

4.10.1 What is a thread? It is the process What is the difference? Why use multithreading

It refers to a thread execution unit during execution of the program, capable of executing program code.

It refers to the process of executing the program period. And the thread is sometimes called lightweight processes, it is the smallest unit of execution, a process can have multiple threads, memory space is shared between the various program threads (code segments, data segments, and heap space) and a number of processes level of resources (such as opening a file), but each thread has its own stack space.

At the operating system level, the execution of the program are based on the process as a unit, and each process will usually have multiple threads concurrently execute independently of each other, then why do you want to use multiple threads? In fact, the use of multi-threading has brought great convenience to the research and development program, in particular, the content of the following areas:

1) using multiple threads can reduce the response time of the program. In a single-threaded (single-threaded refers to a sequence during program execution only an effective operation, there is a clear order of execution between different operating) in the case of, if an operation is time-consuming, or fall into a long wait (such as waiting for a network response), then the program will not respond to mouse and keyboard operations, the use of multi-threading, can be assigned to this time-consuming to thread a separate thread to execute, so that the program has the better interaction sex.

2) Compared with the process of creating and thread switching overhead is smaller. Due to start a new thread must give this thread allocates a separate address space, the establishment of many data structures to maintain information about threaded code segment, data segment and so on, while running in the same process in the thread shared code segment, data segment, the thread start or switching overhead is much less than the process. Simultaneous multithreading in data sharing efficiency is very high.

3) multi-CPU or multi-core computer itself has the ability to perform multi-threaded, if a single thread, you can not re-use computer resources, resulting in a huge waste of resources. Therefore, the use of multiple threads can improve CPU utilization on multi-CPU computers.

4) the use of multi-threaded program structure can be simplified, so that the program easy to understand and maintain. A very complex process can be divided into multiple threads to execute.

4.10.2 What is the difference between synchronous and asynchronous

In a multithreaded environment, often encounter sharing data, that is, when multiple threads need access to the same resources, they need to somehow order to ensure that the resources can only use one thread at a time, otherwise , the results of the program will be unpredictable, in which case it must be for data synchronization, such as multiple threads simultaneously on the same data is written, that is, when thread a requires the use of a resource, if the resource thread B is being used to make thread a synchronization mechanism will wait forever, until the end of the thread B after the use of the resource, a thread can use this resource, we can see, synchronization mechanisms to ensure the security of resources.

To achieve synchronous operation, it is necessary to obtain a lock every thread object. It can be obtained at the same time to ensure that only one thread can enter the critical section (code block access exclusive resources), and before the lock is released, the other threads can no longer enter the critical section. If there are other threads want to get the lock of the object, can only wait queue waiting to enter. Only when the object that owns the thread exits the critical section of the lock, the lock will be released pending in the queue with the highest priority thread to acquire the lock to enter the share code area.

Java language provides the synchronization mechanism in the language-level support, synchronization can be achieved through the use of synchronized keyword, but the method is not "one size fits all", which is based on a lot of overhead expense, and sometimes may even cause deadlock Therefore, the synchronization control is not possible, try to avoid unnecessary synchronization control. There are two ways to achieve synchronization: one is to use the synchronization code synchronization block; the other is to achieve synchronization using a synchronization method.

Asynchronous and non-blocking similar, because each thread contains its own data or methods needed to run, therefore, during the input and output processing, does not have to be concerned about the status or behavior of other threads, you do not have to wait for the input and output processed before returning . When the application calls the method takes a long time to execute on the object, and do not want the program to wait for the process to return, you should use asynchronous programming, asynchronous can improve the efficiency of the program.


4.10.3 How to implement multi-threaded Java

Java Virtual Machine allows programs to concurrently run multiple threads. In the Java language, multi-threaded generally have the following three methods, the first two of which the most commonly used method.

1) Thread class inheritance, override run () method

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

2)实现 Runnable 接口,并实现该接口的 run()方法

以下是主要步骤:

1)自定义类并实现 Runnable 接口,实现 run()方法。

2)创建 Thread 对象,用实现 Runnable 接口的对象作为参数实例化该 Thread 对象。

3)调用 Thread 的 start()方法。

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

(3)实现 Callable 接口,重写 call()方法

Callable 接口实际是属于 Executor 框架中的功能类,Callable 接口与 Runnable 接口的功能类似,但提供了比 Runnable 更强大的功能,主要表现为以下 3 点:

1)Callable 可以在任务结束后提供一个返回值,Runnable 无法提供这个功能。

2)Callable 中的 call()方法可以抛出异常,而 Runnable 的 run()方法不能抛出异常。

3)运行 Callable 可以拿到一个 Future 对象,Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用 Future 来监视目标线程调用 call()方法的情况,当调用 Fu-ture 的 get()方法以获取结果时,当前线程就会阻塞,直到 call()方法结束返回结果。

 


4.10.4 run()方法与 start()方法有什么区别

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

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

由此可知,只有通过调用线程类的 start()方法才能真正达到多线程的目的。下面通过一个例子来说明说明 run()方法与 start()方法的区别。


4.10.5 多线程同步的实现方法有哪些

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

(1)synchronized 关键字

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

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

1)synchronized 方法。在方法的声明前加入 synchronized 关键字,示例如下:

public synchronized void mutiThreadAccess();

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

2)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()方法获取不到锁,会一直处于阻塞状态,且会忽略 in-terrupt()方法,示例如下:


4.10.6 sleep()方法与 wait()方法有什么区别

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

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

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

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

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

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

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

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

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

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

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

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

常见笔试题:

1.利用 Thread.wait()同步线程,可以设置超时时间吗?

A.可以 B.不可以

答案:A。可以设置超时,函数原型为 wait(long timeout)和 wait(long timeout,int nanos)timeout 代表最长的等待时间,单位为 ms;nanos 代表额外的等待时间,单位为 ns。

2.在一个线程中 sleep(1000)方法,将使该线程在多长时间后获得对 CPU 的控制(假设睡眠过程中不会有其他事件唤醒该线程)?

A.正好 1000ms B.少于 1000ms C.大于等于 1000ms D.不一定

答案:C。sleep()方法指定的时间为线程不会运行的最短时间。当睡眠时间结束后,线程会返回到可运行状态,不是运行状态,还需要等待 CPU 调度执行。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。


4.10.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/O 阻塞时),上面介绍的方法就不可用了。此时可以使用 interrupt()方法来打破阻塞的情况,当 interrupt()方法被调用时,会抛出 InterruptedException 异常,可以通过在 run()方法中捕获这个异常来让线程安全退出,具体实现方式如下:

如果程序因为 I/O 而停滞,进入非运行状态,基本上要等到 I/O 完成才能离开这个状态,在这种情况下,无法使用 interrupt()方法来使程序离开 run()方法。这就需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的 I/O 相关,例如,如果使用 read-Line()方法在等待网络上的一个信息,此时线程处于阻塞状态。让程序离开 run()方法就是使用 close()方法来关闭流,在这种情况下会引发 IOException 异常,run()方法可以通过捕获这个异常来安全地结束线程。


4.10.8 synchronized 与 Lock 有什么异同

Java 语言提供了两种锁机制来实现对某个共享资源的同步:synchronized 和 Lock。其中,synchronized 使用 Object 对象本身的 notify、wait、notityAll 调度机制,而 Lock 可以使用 Condi-tion 进行线程之间的调度,完成 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()方法可以采用非阻塞的方式去获取锁。

在实际使用时,最好不要同时使用两种上锁机制。

常考笔试题:

1.当一个线程进入一个对象的一个 synchronized()方法后,其他线程是否可进入此对象的其他方法?

答案:当一个线程进入一个对象的一个 synchronized()方法后,其他线程是否可进入此对象的其他方法取决于方法本身,如果该方法是非 synchronized()方法,那么是可以访问的,示例如下。从上例可以看出,线程 t1 在调用 sychronized()方法的过程中,线程 t2 仍然可以访问同一对象的非 sychronized()方法。

如果其他方法是静态方法(使用 static 修饰的方法),它用的同步锁是当前类的字节码,与非静态的方法不能同步(因为非静态的方法用的是 this),因此,静态方法可以被调用,示例如下。从上例可以看出,当线程 t1 在调用对象 t 的 sychronized()方法时,线程 t2 仍然可以调用这个对象的静态 sychronized()方法。

如果这个方法内部调用了 wait()方法,那么其他线程就可以访问同一对象的其他 sy-chronized()方法。如果这个方法内部没有调用 wait()方法,并且其他方法都为 sychronized()方法,那么其他线程将无法访问这个对象的其他方法。


4.10.9 什么是守护线程

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

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

在 Java 语言中,守护线程一般具有较低的优先级,它并非只由 JVM 内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用 start()方法启动线程之前调用对象的 setDaemon(true)方法,若将以上参数设置为 false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下:

从运行结果中可以发现,没有输出 Thread-0:end。之所以结果是这样,是在启动线程前将其设置为守护线程了,当程序中只有守护线程存在时,JVM 是可以退出的,也就是说,当 JVM 中只有守护线程运行时,JVM 会自动关闭。因此,当 test3 方法调用结束后,main 线程将退出,此时线程 t1 还处于休眠状态没有运行结束,但是由于此时只有这个守护线程在运行,JVM 将会关闭,因此不会输出「Thread-0:end」。

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

常见笔试题:

1.Java 的 Daemon 线程,setDaemon 设置必须要( )。

A.在调用 start()方法之前 B.调用在 start()方法之后 C.前后都可以

答案:A。见上面讲解。

2.关于守护线程的说法,正确的是( )。

A.所有非守护线程终止,即使存在守护线程,进程运行终止

B.所有守护线程终止,即使存在非守护线程,进程运行终止

C.只要有守护线程或者非守护进程其中之一存在,进程就不会终止

D.只要所有守护线程和非守护线程终止运行之后,进程才会终止

答案:A。见上面讲解。


4.10.10 join()方法的作用是什么

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

===========================================

end

发布了101 篇原创文章 · 获赞 20 · 访问量 6万+

Guess you like

Origin blog.csdn.net/qq_40993412/article/details/104060629