问题及答案来源自《Java程序员面试笔试宝典》第四章 Java基础知识 4.10多线程
1、什么是线程?它与进程有什么区别?为什么要使用多线程?
线程:指程序在执行过程中,能够执行程序代码的一个执行单元
进程:指一段正在执行的程序,线程有时候也被称为轻量级进程
线程和进程:
一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),
但是各个线程拥有自己的栈空间
为什么要使用多线程(多线程的好处):
- 使用多线程可以减少程序的响应时间
- 与进程相比,线程的创建和切换开销更小
- 多CPU或多核计算机本事就具有执行多线程的能力(充分利用计算机资源)
- 使用多线程能简化程序的结构,使程序便于理解和维护
2、同步和异步有什么区别?
同步机制保证资源的安全性:
在多线程的环境中,经常会遇到数据共享问题,即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一
时刻只能被一个线程使用,否则程序的运行结果将会是不可预料的,在这种情况下就必须对数据进行同步,例如多个线程同时对同一
数据进行写操作,即当线程A需要使用某个资源时,如果这个资源正在被B使用,同步机制就会让A一直等待下去,直到B结束对该
资源的使用后A才能使用这个资源
同步机制的实现:
要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有一个线程能够进入临界区(访问互斥资源
的代码块),并且在这个锁被释放之前其他线程不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列
等待。只有当拥有该对象锁的线程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区
Java对同步机制的支持:
Java中可以通过使用synchronized关键字来实现同步,但是该方法并非万金油,它是以很大的系统开销作为代价,有时候甚至可能
造成死锁,所以同步控制不是越多越好,要尽量避免无谓的同步控制
异步:
类似非阻塞,由于每个线程都包含了运行时自身所需要的数据或方法,由此在进行输入输出处理时不必关心其他线程的状态和行为,
也不必等到输入输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待
方法的返回时就应该使用异步编程,异步能够提高程序的效率
同步和异步的区别:
同步就是我喊你去吃饭,如果听到了我就和你一起去吃饭,如果没听到你就不停的喊直到我告诉你听到了我们一起去吃饭;而异步
就是你喊我然后自己去吃饭我得到消息后可能立即走和你一起去吃饭也有可能下班了才去吃饭
3、如何实现Java多线程?
Java多线程的实现有以下三种方法:
- 继承Thread类,重写run方法
- 实现Runnable接口并实现该接口的run方法
- 实现Callable接口并重写call方法
(1)继承Thread类,重写run方法
1 class MyThread extends Thread{ // 创建线程类 2 public void run(){ 3 System.out.println("Thread body"); 4 } 5 } 6 7 public class CreateThreadingDemo { 8 public static void main(String[] args) { 9 MyThread thread = new MyThread(); 10 thread.start(); 11 } 12 }
(2)实现Runnable接口并实现该接口的run方法
1 class MyThread implements Runnable{ // 创建线程类 2 public void run(){ 3 System.out.println("Thread body"); 4 } 5 } 6 7 public class CreateThreadingDemo { 8 public static void main(String[] args) { 9 MyThread thread = new MyThread(); 10 Thread t = new Thread(thread); 11 t.start(); 12 } 13 }
(3)实现Callable接口并重写call方法
Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口中的功能类似,但是提供了比Runnable更强大的功能:
- Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能
- Callable中的call方法可以抛出异常,而Runnable中的run方法不能抛出异常
- 运行Callable可以拿到一个Future对象(表示异步计算的结果),可以监视目标线程调用call方法的情况
1 import java.util.concurrent.*; 2 3 public class CallableAndFuture { 4 // 创建线程类 5 public static class CallableTest implements Callable<String>{ 6 7 public String call() throws Exception { 8 return "Hello World!"; 9 } 10 11 } 12 13 public static void main(String[] args) { 14 ExecutorService threadPool = Executors.newSingleThreadExecutor(); 15 // 启动线程 16 Future<String> future = threadPool.submit(new CallableTest()); 17 try{ 18 System.out.println("waiting thread to finish"); 19 System.out.println(future.get()); // 等待线程结束并获取返回结果 20 } catch (Exception e) { 21 e.printStackTrace(); 22 } 23 } 24 }
4、run()方法和start()方法有什么区别?
通常系统通过调用线程类的start方法来启动一个线程,此时线程处于就绪状态而非运行状态,也就意味着这个线程可以被JVM来
调度执行。在调度过程中,JVM通过调用线程类的run方法来完成实际的操作,当run方法结束后此线程就会终止
如果直接调用线程类的run方法,这会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说start方法能够
异步地调用run方法,但是直接调用run方法却是同步的,因此也无法达到多线程的目的
总结 - run方法和start方法的区别:
只有通过线程类的start方法才能真正达到多线程的目的,通过run方法不能达到多线程的目的(run方法是同步的)
5、多线程同步的实现方法有哪些?
当使用多线程访问同一个资源时,非常容易出现线程安全的问题(例如,当多个线程同时对一个数据进行修改时,会导致某些线程
对数据的修改丢失) ,因此需要采用同步机制来解决这个问题。Java提供了三种实现同步机制的方法:
- synchronized关键字
- wait()方法和notify()方法
- Lock
(1)synchronized关键字
1 // synchronized方法:在方法的声明前加上synchronized关键字 2 public synchronized void mutiThreadAccess(); 3 // 只要把多个线程对类需要被同步的资源的操作放到mutiThreadAccess()方法中,就能保证 4 // 这个方法体在同一时刻只能被一个线程访问,从而保证多线程访问的安全性 5 6 // synchronized块:把任意的代码段申明为synchronized,也可以指定上锁对象,有非常高的灵活性: 7 synchronized(syncObject){ 8 // 访问syncObject的代码 9 }
(2)wait()方法和notify()方法
当使用synchronized来修饰某个共享资源时,如果线程A1在执行synchronized代码,另一个线程A2也要同时执行同一对象的同一
synchronized代码时,线程A2将要等到线程A1执行完成后才能执行,在这种情况下我们可以使用wait方法和notify方法
在synchronized代码块被执行时,线程可以调用wait方法释放对象锁,进入等待状态,并且可以调用notify方法或notifyAll方法
通知正在等待的其他的线程。notify和notifyAll的区别是:notify方法唤醒一个线程(等待队列中的第一个线程)并允许它获得锁,
而notifyAll方法唤醒所有线程并允许它们获得锁(让它们竞争锁)
(3)Lock
JDK1.5新增了Lock接口及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步,具体而言它提供了以下
方法来实现多线程的同步:
- lock():以阻塞的方式获得锁(有就返回,没有就等待至有)
- tryLock():以非阻塞的方式获得锁(有返回true,没有返回false)
- tryLock(long timeout, TimeUnit unit):获取锁返回true(可以等待一段时间),开始和等待一段时间均没有获取锁返回false
- lockInterruptibly():获取锁就返回,没有就休眠直到获得锁或当前线程被其他线程中断
6、sleep()方法与wait()方法有什么区别?
sleep方法是使线程暂停执行一段时间的方法,wait方法也是一种使线程暂停执行的方法
两者的区别主要体现在以下几个方面:
- 原理不同
- 对锁的处理机制不同
- 使用区域不同
(1)原理不同
sleep方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,
等到计时时间一到,此线程就会自动苏醒,例如当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法
前面加一个sleep方法,以便让自己每隔1s执行一次,该过程如闹钟一样
wait方法是Object类的方法,用于线程的通信,这个方法会使当前拥有该对象锁的线程等待,直到其他线程调用notify方法(notifyAll)
时才醒过来,当然也可以指定一个时间让它自动醒过来
(2)对锁的处理机制不同
sleep方法作用只是让线程的执行暂停一段时间,时间一到立刻恢复,不涉及线程间的通信,调用sleep方法并不会释放锁
而wait方法调用后,线程会释放它占用的锁,从而使线程所在对象中的同步资源供其他线程使用
(3)使用区域不同
wait方法必须放在同步控制方法或者同步语句块中使用,sleep方法可以在任何地方使用
sleep方法必须捕获异常(InterruptedException),而wait方法、notify方法、notifyAll方法不需要捕获异常
由于sleep不会释放锁,有可能会导致死锁问题,因此一般情况下不推荐使用sleep方法,推荐使用wait方法
引申 - sleep方法和yield方法有什么区别?
线程的优先级:sleep方法不考虑线程的优先级(给低优先级的线程运行机会),yield方法只给相同优先级或更高优先级的线程运行机会
执行:sleep方法执行后线程转入阻塞状态(在指定的时间内不会被执行),yield方法是使当前线程重新回到可执行状态,有可能在进入
到可执行状态后马上又被执行
异常:sleep方法声明抛出InterruptedException,yield方法没有声明任何异常
移植性:sleep方法比yield方法(跟操作系统相关)具有更好的可移植性
7、终止线程的方法有哪些?
stop与suspend:
当用Thread.stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象
处于一个不一致的状态,其他线程将会看到这个不一致的状态,这可能会导致程序执行的不正确性,这很难被定位
用suspend方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中因争夺资源造成的一种互相等待的现象,
如果没有外力作用,它们都将无法推进),由于调用suspend方法不会释放锁,这就会导致一个问题:当用suspend挂起
一个有锁的线程,那么在锁恢复之前将不会被释放,当线程试图取得被挂起的锁时此时就造成了死锁
鉴于以上两种方法的不安全性,Java已经不建议使用以上两种方法来终止线程了
比较推荐的方法:
那么如何才能终止线程呢?一般建议让线程自行结束进入Dead状态,一个线程进入Dead状态即执行完run方法,也就是
说想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run方法的执行,实现如下:
1 public class MyThread implements Runnable{ 2 private static Boolean flag = true; 3 public void stop(){ 4 flag = false; 5 } 6 7 public void run(){ 8 while(flag){ 9 // do something 10 } 11 } 12 }
真正应该使用的方法:
上面通过调用MyThread的stop方法虽然能终止线程,但同样存在问题:当线程处于非运行状态时(sleep方法被调用或wait方法被
调用或当被IO阻塞时) ,上面的方法就不可用了,此时可以用interrupt方法来打破阻塞的情况,具体实现如下:
1 // 终止线程的正确方法 2 public class StopThreadingDemo { 3 public static void main(String[] args) { 4 Thread thread = new Thread(new Runnable() { 5 6 public void run() { 7 System.out.println("thread go to sleep"); 8 try { 9 // 用休眠模拟线程被阻塞 10 Thread.sleep(5000); 11 System.out.println("thread finish"); 12 } catch (InterruptedException e) { 13 System.out.println("thread is interrupted!"); 14 } 15 } 16 }); 17 thread.start(); 18 thread.interrupt(); 19 // 运行结果为: 20 // thread go to sleep 21 // thread is interrupted! 22 } 23 }
8、synchronized与Lock有什么异同?
Java语言提供了两种锁机制来实现对某个共享资源的同步:synchronized和Lock。其中synchronized使用Object对象本身
的notify、wait、notityAll调度机制,而Lock可以使用Condition进行线程之间的调度,完成synchronized实现的所有功能
具体而已,二者的主要区别主要体现在以下几个方面的内容:
- 用法不一样
- 性能不一样
- 锁机制不一样
(1)用法不一样
synchronized既可以加在方法上,也可以加在特定代码块中;而Lock需要显示地指定起始位置和终止位置
synchronized是托管给JVM执行的;而Lock的锁定是通过代码实现的,有比synchronized更精确的线程语义
(2)性能不一样
在JDK5中增加了一个Lock接口的实现类ReentrantLock,它不仅拥有和synchronized相同的并发性和内存语义,
还多了锁投票、定时锁、等候和中断锁等。
关于它们的性能:在资源竞争不是很激烈的情况下,synchronized的性能要优于ReentrantLock;
在资源竞争很激烈的情况下,synchronized的性能会下降得非常快,而ReentrantLock得性能基本保持不变
(3)锁机制不一样
synchronized获得锁和释放锁的方式都是在块结构中,当获取多个锁时必须以相反的顺序释放,并且是自动解锁,
不会因为出了异常而导致锁没有被释放从而引发死锁;
Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生
另外Lock还提供了更强大的功能,它的tryLock方法可以采用非阻塞的方式去获取锁
9、什么是守护线程?
Java提供了两种线程:守护线程和用户线程
守护线程:
又称"服务进程"或"后台进程",是指在程序运行时在后台提供一种通用服务的线程,这种线程不属于程序不可或缺的部分
通俗的讲任何一个守护线程都是整个JVM中所有非守护线程的保姆
用户线程:
几乎和守护线程一样,唯一不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了
在Java中设置守护线程:
1 public class DaemonThreadDemo { 2 3 public static class ThreadDemo extends Thread{ 4 public void run(){ 5 System.out.println(Thread.currentThread().getName() + ":begin"); 6 try{ 7 Thread.sleep(1000); 8 } catch(InterruptedException e){ 9 e.printStackTrace(); 10 } 11 System.out.println(Thread.currentThread().getName() + ":end"); 12 } 13 } 14 15 public static void main(String[] args) { 16 System.out.println("begin"); 17 Thread t1 = new ThreadDemo(); 18 t1.setDaemon(true); // 设置守护线程 括号里如果为false表示设置用户线程 19 t1.start(); 20 System.out.println("end"); 21 } 22 }
从输出结果中可以发现,没有输出"Thread -0: end",之所以这样是在启动线程前将其设置为守护线程,当程序中只有守护线程时,
JVM是可以退出的,也就是说当JVM中只有守护线程在运行时JVM会自动关闭。因此当t1.start()之后,main线程将退出,此时线程
t1还处于休眠状态没有运行结束,但是由于此时只有这个守护线程在运行,JVM将会关闭,因此不会输出"Thread -0: end"
10、join()方法的作用是什么?
在Java中,join方法的作用是让调用该方法的线程在执行完run方法后,再执行join方法后面的代码,
就是将两个线程合并(实现同步),具体而言,可以通过线程A的join()方法或join(2000)方法来等待线程A的结束
示例如下:
1 public class JoinDemo { 2 3 public static class ThreadImp implements Runnable{ 4 public void run(){ 5 try{ 6 System.out.println("Begin ThreadImp"); 7 Thread.sleep(5000); 8 System.out.println("End ThreadImp"); 9 } catch(InterruptedException e){ 10 e.printStackTrace(); 11 } 12 } 13 } 14 15 public static void main(String[] args) { 16 Thread t = new Thread(new ThreadImp()); 17 t.start(); 18 try{ 19 t.join(1000); // 主线程等待t结束,只等待1s 20 if(t.isAlive()){ 21 // t没有结束 22 System.out.println("t has not finished"); 23 } else{ 24 System.out.println("t has finished"); 25 } 26 System.out.println("joinFinish"); 27 } catch(InterruptedException e){ 28 e.printStackTrace(); 29 } 30 } 31 32 }