进程与线程的区别,以及创建线程的几种方式

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();
			}
		}
	}
}

 

三、通过CallableFutureTask创建线程

(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、Stopsuspendresume方法为何会被遗弃

当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止。但是,由于stop方法不安全,它会解除由线程获取的所有锁定,导致数据得不到同步处理,出现数据不一致的问题。另外,调用stop方法会即刻停止run()方法中剩余的全部工作,包括在catch或finally块中的语句,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),有可能使一些清理工作得不到完成,如文件、数据库等的关闭。

suspend()方法的作用是使线程暂停,但不释放锁,容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。

resume方法使线程恢复,如果之前没有使用suspend暂停线程,则不起作用。

suspend()和resume()必须要成对出现,否则非常容易发生死锁。 
因为suspend方法并不会释放锁,如果调用suspend方法的目标线程对一个重要的系统资源持有锁,那么其他任何线程都不可以使用这个资源,直到调用suspend方法的目标线程被resumed,然后目标线程继续执行。如果其他线程在resume目标线程之前,先尝试获取这个重要系统资源的锁,再去resume目标线程,这两条线程就相互死锁了,也就冻结线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值