多线程

进程

进程:是系统进行资源分配和调度的一个独立单位。所有运行中的任务通常对应一条进程,并且进程具有一定的独立功能。

进程的特征:

(1)独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

(2)动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。

(3)并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

并发性和并行性是两不同的概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。


线程:线程是进程的执行单元,就像进程在操作系统中的地位一样,线程在程序中是独立的,并发的执行流。一个进程可以有多个线程,但一个线程只能有一个父进程。线程可以有自己的堆栈,自己的程序计数器,自己的局部变量,但不能拥有系统资源,它与父进程的其他线程共享该进程拥有的全部资源。线程可以完成一定的任务,可以其他线程共享父进程中的共享变量及部分环境,相互之间协同完成进程所有的任务。线程的执行是抢占式的,简单的说,当前运行的线程在任何时候都有可能被挂起,让其他线程可以运行。线程的调度和管理是由进程来完成。

多线程:简单的理解就是同一个应用程序运行时,内部可能包含多个顺序执行流,每个顺序执行流就是一个线程。

和进程相比,多线程的优点:

(1)进程间不能共享内存,但线程之间共享内存非常容易。

(2)系统创建进程需要为该进程重新分配系统资源,创建线程的代价就小很多,因此使用多线程来实现多任务并发比多进程的效率高。

(3)Java语言内置多线程功能支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java多线程编程。


继承Thread类创建线程和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Thread类中提供了一个run方法,该方法体就是代表线程需要完成的任务。因此,我们把run方法称为线程的执行体。

格式:   public   class  subThread   extends  Thread{

pubilc  void run{

线程执行体

};

}

线程对象的其他常用的方法

(1)start():该方法是Thread的实例方法,启动调用该方法的线程。

(2)Thread.currentThread():currentThread是Thread类的静态方法,该方法总是返回当前正在运行的现在对象。

(3)getName():该方法是Thread的实例方法,该方法返回调用方法的线程名字。

注意:使用继承Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。


实现Runnable接口创建线程类

使用Runnable接口来创建并启动多线程的步骤:

(1)定义Runnable接口的实现类,并重写该类接口的run方法,该run方法的方法体是该线程的线程执行体。

(2)创建Runnable实现类的实例,并将此实例作为Thread的taget类创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start方法来启动该线程。

经典实例:

public class RunnableTest implements Runnable {

	private int i;
	@Override
	public void run() {
		for (; 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) {
				RunnableTest rt = new RunnableTest();
				//通过new Thread(target,name)方法创建新线程
				new Thread(rt,"线程1").start();
				new Thread(rt,"线程2").start();
			}
		}
	}

}

打印部分结果:

线程1 84
线程1 85
线程1 86
main  94
main  95
main  96
线程2 87
线程2 88
线程2 89
解析:根据打印的结果发现,两条子线程的i变量是连续的,也就是采用Runnable接口的方式创建多条线程可以共享该线程类的实例属性。这是因为这样,程序所创建的Runnable对象只是线程的target,而多态线程可以共享同一个target,所以多条线程可以共享同一个线程类的实例属性。


两种方式创建线程对比

采用实现Runnable接口方式的多线程

(1)线程类只是实现了Runnable接口,还可以继承其他类。

(2)可以多个线程共享同一个target对象。

(3)编程稍微复杂一点,如果要访问当前线程,必须使用Thread.currentThread()方法。

采用继承Thread类的方式创建多线程

(1)线程类已经继承了Thread类,所以不能再继承其他类。

(2)编写简单,如果需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。


线程的生命周期:

线程有五个状态,分别是:新建,就绪,运行,阻塞,死亡五种状态。线程启动以后,不能一直霸占CPU独自运行,CPU需要在多条线程之间切换,所有线程状态也会多次在运行,阻塞之间切换。


新建:当程序使用new关键字创建一个线程之后,该线程就处于新建状态。此时仅仅是有Java虚拟机为其分配了内存,并初始化。

就绪:当线程对象调用了start()方法后,该线程处于就绪状态。虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了,至于什么时候开始运行,取决于虚拟机里线程调度器的调度。

注意:启动线程使用的start方法,而不是run方法,不要调用线程对象的run方法,调用start方法启动线程,系统会把该run方法当成线程执行体来处理。但如果直接调用线程对象的run方法,则run方法立即就会被执行,而且在run方法返回之前其他线程无法并发执行,也就是说系统把线程对象当成了普通的方法对象,而run方法也是一个普通的方法,而不是线程执行体。

运行:处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体。

阻塞:(1)线程调用sleep方法主动放弃所占用的处理器资源。

             (2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

             (3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。

             (4)线程在等待某个通知。

            (5)程序调用了线程的suspend方法将该线程挂起,此方法容易导致死锁,所以不建议使用该方法。

死亡:run方法执行完成,线程正常结束;线程抛出了一个未捕获的Exception或Error;直接调用该线程stop()方法来结束该线程,该方法容易导致死锁,不建议使用。


注意:Java程序运行时默认的主线程,main方法的方法体就是主线程的线程执行体。当主线程结束时,其他线程不受任何影响,并不会随之结束。

不要对处于死亡状态的线程调用start方法,否则会报IllegalThreadStateException异常。表明死亡状态的线程无法再次运行。可以调用isAlive()方法判断线程是否死亡。


控制线程

join线程:Thread提供了让一个线程等待另一个线程完成的方法,join方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。


后台线程

运行在后台,为其他线程提供服务,这样的线程叫后台线程,也叫守护线程,精灵线程等。虚拟机的垃圾回收线程就是典型的后台线程。

后台进程的特征:如果所有的前台线程都死亡了,后台线程会自动死亡。

调用Thread对象的setDaemon(true)方法可以将指定线程设置为后台线程。

Thread类的isDaemon()方法用于判断指定的线程是否为后台线程。

前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令,到它做出响应,需要一定时间。而且需要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemom(true)必须在start()方法之前调用。


线程休眠:sleep

sleep让正在运行的线程暂停一段时间,并进入阻塞状态。

sleep方法有两种重载的形式:

(1)static  void  sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受系统计时器和线程调度器的精度和准确度的影响。

(2)static  void  sleep(long mills,int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态。一般很少调用该方法。

当当前线程调用sleep方法进入阻塞状态后,在其sleep时间内,该线程是不会获得执行的机会,即使系统中没有其他可运行的线程,处于sleep中的线程也不会运行。


线程让步:yield

yield()方法是一个和sleep方法有点相似的方法,它也是一个Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但他不会阻塞该线程,它只是将该线程注入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。

sleep方法和yield方法的区别:

(1)sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同或者更高的线程执行机会。

(2)sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield不会将程序转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield方法暂停之后,立即再次获得处理器资源被执行。

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

(4)sleep方法比yield方法有更好的可移植性,通常不要依靠yield来控制并发线程的执行。


线程的优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。

每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级,因此由main线程创建的子线程也有普通优先级。

Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个1到10整数。

Thread类提供了三个静态常量:

(1)MAX_PRIORITY:值是10。

(2)MIN_PRIORITY:值是1。

(3)NORM_PRIORITY:值是5。


线程同步

Java多线程支持引入同步监视器来解决多个线程同时操作一个文件的问题,使用同步监视器的通用方法就是同步代码块。

同步代码块的语法格式:

synchronized(obj){
需要同步的代码块

}

synchronized后括号中的obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时候只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放对该同步监视器的锁定。

虽然Java程序允许使用任何对象来作为同步监视器,同步监视器的目的是为了阻止多条线程同时对一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。


同步方法

与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字修饰某个方法。则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。

格式:public  synchronized  void  methodName(){

同步代码块

}


释放同步监视器的锁定情况:

(1)当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

(2)当线程在同步代码块,同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。

(3)当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块,该方法异常结束时将释放同步监视器。

(4)当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

在下面两种情况下面,不会释放同步监视器

(1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。

(2)线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监听器。

同步方法或同步代码块使用与竞争资源相关的,隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块机构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。


同步锁(Lock)

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象。

Lock是控制多个线程对共享资源进行访问的工具,通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。在实现线程安全的控制中,通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显示的加锁和释放锁。

Lock对象的代码格式:

public class LockTest {

	// 定义锁对象
	private final ReentrantLock lock = new ReentrantLock();

	public void method() {
		// 加锁
		lock.lock();
		try {
			// 需要保证线程安全的代码
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			lock.unlock();
		}
	}
}


ReentrantLock锁具有重入性,也就说线程可以对它已经加锁的ReentrantLock锁再次加锁。ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock加锁后,必须显示调用unlock来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。


线程通信

线程协调运行

为了让线程协调运行,借助Object类提供的wait()、notify()和notifAll()三个方法。这三个方法不属于Thread类,而属于Object类。但这三个方法必须由监视器对象来调用。

(1)对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。

(2)对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

Object类提供的三个方法解释:

(1)wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。

(2)notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

(3)notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可能执行被唤醒的线程。


使用条件变量控制协调

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait()、notify()和notifAll()三个方法来协调进程的运行。当使用Lock对象来保证同步时,Java提供了一个Condition类保存协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition实例实质上被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象newCondition()方法即可。

Condition类提供如下三个方法:

(1)await():类似于隐式同步监视器上的wait方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。

(2)signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择值唤醒其中一个,选择是任意性的,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

(3)signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。


Callable和Future

可以认为Callable接口是Runnable接口的争强版,Callable接口提供了一个call()方法可以作为线程的执行体,但call方法比run方法更强大,call方法可以有返回值,可以声明抛出异常。

Future接口代表Callable接口中call方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口-----可以作为Thread类的target。

Future接口中定义的几个方法:

(1)boolean  cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。

(2)V  get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束时才会得到返回值。

(3)V  get(long timeout,TimeUnit   unit):返回Callable任务里call()方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间。如果经指定时间后Callable任务依然没有返回值,将抛出TimeoutException异常。

(4)boolean  isCancelled():如果Callable任务正常完成前被取消,则返回true。

(5)boolean  isDone():如果Callable任务已完成,则返回true。

总结创建、并启动有返回值的线程步骤:

(1)创建Callable接口的实现类,并实现call方法。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建,并启动新线程。

(4)调用FutureTask对象的方法来获取子线程执行结果后的返回值。

经典实例:

public class CallableTest {

	public static void main(String[] args) {
		// 创建Callable对象
		CallableThread ct = new CallableThread();
		// 使用FutureTask来包装Callable对象
		FutureTask<Integer> task = new FutureTask<Integer>(ct);
		for (int i = 0; i < 100; i++) {
			System.out
					.println(Thread.currentThread().getName() + "   i的值:" + i);
			if (i == 20) {
				// 实质还是以Callable对象来创建、并启动线程
				new Thread(task, "callable线程").start();
			}
		}
		try {
			// 获取线程返回值
			System.out.println("子线程的返回值:" + task.get());
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

class CallableThread implements Callable<Integer> {

	// 实现call方法,作为线程的执行体
	@Override
	public Integer call() throws Exception {
		// TODO Auto-generated method stub
		int i = 0;
		for (; i < 100; i++) {
			System.out
					.println(Thread.currentThread().getName() + "   i的值:" + i);
		}
		return i;
	}

}

线程池

线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。

使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能急剧下降,甚至导致虚拟机崩溃。

使用Executors工厂类产生线程池,该类的静态方法如下

(1)newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。

(2)newFixedThreadPool(int  nThreads):创建一个可重用、具有固定线程数的线程池。

(3)newSingleThreadExecutor():创建一个只有单线程的线程池,相当于newFixedThreadPool(1)。

(4)newScheduledThreadPool(int   corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中保存的线程数,即使线程是空闲的也被保存在线程池内。

(5)newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。

上面五个方法中前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象和Callable对象所代表的线程。而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。

ExecutorService提供如下三个方法

(1)Future<?>  submit(Runnable  task):将一个Runnable对象提交给指定的线程池。线程池将在有空闲线程时执行Runnable对象代表的任务。

(2)<T>   Future<T>  submit(Runnable task,T  result):将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务,rusult显示指定线程执行结束后的返回值。

(3)<T>   Future<T>  submit(Callable<T>  task):将一个Callable对象提交给指定的线程池。线程池将在有空闲线程时执行Callable对象代表的任务,Future代表Callable对象里call方法的返回值。

ScheduledExecutorService代表可在指定延迟或周期性执行线程任务的线程池。提供如下四个方法:

(1)ScheduledFuture<V>  schedule(Callable<V>  callable,long  delay, TimeUnit  unit):指定callable任务将在delay延迟后执行。

(2)ScheduledFuture<?>  schedule(Runnable command,long  delay, TimeUnit  unit):指定command任务将在delay延迟后执行。

(3)ScheduledFuture<?>  scheduleAtFixedRate(Runnable commed,long  initialDelay,long period,  TimeUnit  unit):指定command任务将在delay延迟后执行,而且以设定频率重复执行,也就是说,在initialDelay后开始执行,依次在initialDelay+period、initialDelay+2*period、.....处重复执行,以此类推。

(4)ScheduledFuture<?>  scheduleWithFixedDelay(Runnable  commed,long initialDelay,long  delay,TimeUnit  unit) :创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一次执行时遇到异常,就会取消后续执行。否则,只能通过程序来显示取消或终止来终止该任务。

当用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池关闭序列,调用了shutdown方法后的线程池不再结束新任务,但会将以前所有已经提交任务执行完成。当线程池中的所有任务都执行完成后,池中所有线程都会死亡;另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

总结使用线程池来执行线程任务步骤:

(1)调用Executors累的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。

(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务。

(3)调用ExecutorService对象的submit方法来提交Runnable实例或Callable实例。

(4)当不想提交任何任务时,调用ExecutorService对象的shutdown方法来关闭线程池。

经典实例:

public class ThreadTest {
	public static void main(String[] args) {
		// 创建一个具有固定线程数6的线程池
		ExecutorService pool = Executors.newFixedThreadPool(6);
		// 向线程池中提交两个线程
		pool.submit(new TestThread());
		pool.submit(new TestThread());
		// 关闭线程池
		pool.shutdown();
	}

}

// Runnale接口来实现一个简单的线程类
class TestThread implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "的i值为=" + i);
		}

	}
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值