1、什么是线程?它与进程有什么区别?为什么要使用多线程
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有4种状态:运行、就绪、挂起和结束。
进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的对比关系如图4-15所示。
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了巨大的便利,具体而言,有以下几个方面的内容:
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的使用方法:
(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()方法的情况,当调用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的运行结果可以看出,线程tl是在testl方法结束后才执行的(System.out.println("testl:end")语句不需要等待tl.start()运行结果就可以执行),因此,在testl中调用start()方法是异步的,所以main线程与tl线程是异步执行的。从test2的运行结果可以看出,调用tl.run()是同步的调用方法,因为System.out.println("test2:end”)只有等tl.run()调用结束后才能执行。
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 JDK5新增加了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.lockInterruptibly()替换为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()方法每隔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()方法(跟操作系统相关)具有更好的可移植性。
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()方法被调用或当被IVO阻塞时),上面介绍的方法就不可用了。此时可以使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会抛出InterruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出,具体实现方式如下:
如果程序因为I/O而停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态,在这种情况下,无法使用interrupt()方法来使程序离开run()方法。这就需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的I/O相关,例如,如果使用readLine()方法在等待网络上的一个信息,此时线程处于阻塞状态。让程序离开run()方法就是使用close()方法来关闭流,在这种情况下会引发I0Exception异常,run()方法可以通过捕获这个异常来安全地结束线程。
8、synchronized与Lock有什么异同
Java 语言提供了两种锁机制来实现对某个共享资源的同步:synchronized和Lock。其中,synchronized使用Object 对象本身的notify、wait、notityAll 调度机制,而Lock可以使用Condition 进行线程之间的调度,完成synchronized实现的所有功能。
具体而言,二者的主要区别主要表现在以下几个方面的内容:
1)用法不一样。
在需要同步的对象中加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而Lock需要显式地指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。
2)性能不一样。
在JDK5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义,还多了锁投票、定时锁、等候和中断锁等。它们的性能在不同的情况下会有所不同:在资源竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降得非常快,而ReetrantLock的性能基本保持不变。
3)锁机制不一样。
synchronized获得锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。此外,Lock还提供了更强大的功能,它的tryLock()方法可以采用非阻塞的方式去获取锁。
虽然synchronized与Lock都可以用来实现多线程的同步,但是,最好不要同时使用这两种同步机制,因为ReetrantLock与synchronized所使用的机制不同,所以它们的运行是独立的,相当于两种类型的锁,在使用时互不影响,示例如下:
当然,上例中,并不是每次运行的结果都是相同的,与前一个例子对比可以发现,上例中的输出结果value的值并不是连续的,这就是因两种上锁方法采用了不同的机制而造成的,因此在实际使用时,最好不要同时使用两种上锁机制。
9、什么是守护线程
Java提供了两种线程:守护线程与用户线程。守护线程又被称为“服务进程”“精灵线程”或“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。
在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下:
从运行结果中可以发现,没有输出Thread-0:end。之所以结果是这样,是在启动线程前将其设置为守护线程了,当程序中只有守护线程存在时,JVM是可以退出的,也就是说,当JVM中只有守护线程运行时,JVM会自动关闭。因此,当test3方法调用结束后,main线程将退出,此时线程tl还处于休眠状态没有运行结束,但是由于此时只有这个守护线程在运行,JVM将会关闭,因此不会输出“Thread-0:end”。
守护线程的一个典型的例子就是垃圾回收器。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
10、join()方法的作用是什么
在Java语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。简单点说,就是将两个线程合并,用于实现同步功能。具体而言,可以通过线程A的join()方法来等待线程A的结束,或者使用线程A的join(2000)方法来等待线程A的结束,但最多只等待2s,示例如下: