1、进程与线程的区别
进程是指一个具有独立功能的程序,是系统进行资源分配和调度的独立单位。线程是指程序执行过程中的一个执行单位,线程是进程中的一个执行控制单元、执行路径。一个进程中至少有一个线程负责控制程序的执行,即执行main方法的主线程。一个进程中如果只有一个执行路径,这个程序称为单线程程序,如果有多个执行路径,这个程序称为多线程程序。
进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。在一个进程中可以包含若干个线程,多个线程可以共享进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的效率。线程上下文切换比进程上下文切换要快得多。
从以上这段话可以总结出三点不同:
1、分工不同:进程是系统进行资源分配的基本单位,线程是独立运行的基本单位。
2、开销不同:同一个进程中的多个线程共享进程资源,线程基本上不拥有系统资源,创建和切换线程花费的开销更小。
3、效率不同:由于线程基本上不拥有系统资源,对于线程间的切换效率更高。
为什么使用多线程?
(1)使用多线程可以减少程序的响应时间。比如在单线程情况下,如果某个操作非常耗时,或者陷入长时间的等待,此时程序将不会响应鼠标和键盘等操作,或者程序将花费很长时间来完成响应。如果使用多线程的话,可以把这个耗时的操作分配到一个单独的线程中去执行,让CPU去执行其他线程,不用一直等待这个耗时的线程,从而使程序具备了更好的交互性。
(2)与进程相比,线程的创建和切换开销更小,同时多线程在数据共享方面效率非常高。
(3)多CPU计算机本身就具有执行多线程的能力,因此在多CPU计算机上使用多线程能提高CPU的利用率。
(4)使用多线程能简化程序的执行结构,使程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。
2、创建线程的几种方式
(1)继承Thread类;
(2)实现Runnable接口;
(3)实现Callable接口通过FutureTask包装器来创建Thread线程;
(4)通过线程池ThreadPoolExecutor或ForkJoinPool来创建线程。
此处只分析前三种方式,第四种使用线程池创建线程的方式在后续文章中再详细分析。
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
public class FirstThreadTest extends Thread{
int i = 0;
//重写run方法,run方法的方法体就是线程执行体
public void run() {
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i = 0;i< 100;i++) {
System.out.println(Thread.currentThread().getName()+" : "+i);
if(i==20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象,getName()方法返回调用该方法的线程的名字。
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
public class RunnableThreadTest implements Runnable{
private int i;
public void run(){
for(i = 0;i <100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args){
for(int i = 0;i < 100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
三、通过Callable和FutureTask创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
public class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(">>> 线程开始工作");
Thread.sleep(1000);
System.out.println(">>> 结束工作开始返回");
return 10;
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Task();
FutureTask task = new FutureTask(callable);
Thread oneThread = new Thread(task);
oneThread.start();
System.out.println(">>> 工作结果 " + task.get().toString());
}
}
创建线程的三种方式对比:
采用实现Runnable、Callable接口的方式创见多线程时,
优势:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时,
优势:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势:线程类已经继承了Thread类,所以不能再继承其他父类。
实现Runnable和Callable接口创建线程的区别:
1、Runnable接口的run()方法没有返回值,而Callable接口的call()方法可以有返回值。
2、Runnable接口的run()方法不可以声明抛出异常,只能在run方法内部处理异常,而Callable接口的call()方法可以声明抛出异常。
3、Runnable可以提交给Thread来包装下,直接启动一个线程来执行,而Callable一般是提交给ExecuteService来执行。
3、如何停止一个正在运行的线程?
(1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
(2)使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
(3)使用interrupt方法中断线程。
当线程终止时,会调用自身的notifyAll方法,唤醒所有等待该线程对象锁的线程去尝试获取锁。
当线程即将终止时,会调用自身的notifyAll方法,不过不是在Java源码中调用的,而是在jdk的native code里调用的。
openjdk 7的源码里有:/jdk7/hotspot/src/os/linux/vm/os_linux.cpp
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
static void *java_start(Thread *thread) {
...
thread->run();
return 0;
}
参数里的thread其实是JavaThread的实例,而在JavaThread的内部实现中,在run方法执行结束之前会调用lock.notify_all(thread)通知所有join等待的线程。源码如下:
/jdk7/hotspot/src/share/vm/runtime/thread.cpp
void JavaThread::run() {
...
thread_main_inner();
}
void JavaThread::thread_main_inner() {
...
this->exit(false);
delete this;
}
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
因此,根据以上源码可知,当线程终止时,会调用自身的notifyAll方法,唤醒所有等待该线程对象锁的线程去尝试获取锁。所以,如果有一个线程A在同步方法或同步代码块中(wait方法只能在同步方法或同步块中调用)调用了另一个线程B(此处说的是线程,而不是普通对象)的wait方法,此时线程A会释放掉持有的线程B对象的锁,并等待线程B执行run方法。当线程B运行结束时,会自动调用自身的notifyAll方法,唤醒所有等待线程B对象锁的线程尝试获取锁,即线程A会被唤醒,尝试获取线程B对象的锁,如果获取成功,则接着wait方法之后的代码继续执行,如果获取失败,则进入线程B对象的锁池阻塞等待,并尝试下一次获取B对象的锁。也就是说不用显式的调用线程B对象的notify或notifyAll方法唤醒处于B对象等待池中的线程A,等到线程B运行结束,会自动唤醒线程A。join方法就是基于这个原理实现的。(对于锁池和等待池在下面的notify和notifyAll部分有所讲解)
代码验证:
package com.tx.study.others.thread;
public class WaitThread {
public static void main(String[] args) {
Thread t = new Thread("线程t"){
@Override
public void run() {
System.out.println(String.format("当前线程[%s]开始执行run方法。",
Thread.currentThread().getName()));
//模拟线程运行耗时
waitTime(1000);
System.out.println(String.format("当前线程[%s]执行run方法结束。",
Thread.currentThread().getName()));
}
};
t.start();
try {
synchronized (t){
System.out.println(String.format("当前线程[%s]进入同步块......",
Thread.currentThread().getName()));
waitTime(10);
t.wait();
System.out.println(String.format("当前线程[%s]退出同步块......",
Thread.currentThread().getName()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("======主线程结束======");
}
private static void waitTime(long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
当前线程[main]进入同步块......
当前线程[线程t]开始执行run方法。
当前线程[线程t]执行run方法结束。
当前线程[main]退出同步块......
======主线程结束======
在上面的代码示例中,在主函数中的同步代码块中调用了线程对象t的wait方法,在其他地方并没有显示调用对象t的notify或notifyAll方法,等到线程t运行结束,主线程main也不在等待,就像自动苏醒一样,继续执行t.wait()之后的代码。
4、Stop、suspend和resume方法为何会被遗弃?
当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止。但是,由于stop方法不安全,它会解除由线程获取的所有锁定,导致数据得不到同步处理,出现数据不一致的问题。另外,调用stop方法会即刻停止run()方法中剩余的全部工作,包括在catch或finally块中的语句,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),有可能使一些清理工作得不到完成,如文件、数据库等的关闭。
suspend()方法的作用是使线程暂停,但不释放锁,容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
resume方法使线程恢复,如果之前没有使用suspend暂停线程,则不起作用。
suspend()和resume()必须要成对出现,否则非常容易发生死锁。
因为suspend方法并不会释放锁,如果调用suspend方法的目标线程对一个重要的系统资源持有锁,那么其他任何线程都不可以使用这个资源,直到调用suspend方法的目标线程被resumed,然后目标线程继续执行。如果其他线程在resume目标线程之前,先尝试获取这个重要系统资源的锁,再去resume目标线程,这两条线程就相互死锁了,也就冻结线程。