java中的多线程(1)

概述:

       多线程是Java编程中很重要的一个模块,相信每一个Java程序员都曾与多线程有着难以消解的爱恨情仇。本系列文章将对Java中多线程的知识进行总结,方便后续开发参考学习。文章将首先说明线程、进程、并发的概念以及为什么要使用多线程,然后理清整个线程的生命周期,接着以示例的方式展示如何创建使用多线程,接着总结有关线程同步的相关知识,包括锁机制,继而总结线程中一些重要的方法,最后再通过示例总结有关线程池的知识。

一、线程的概念

(1)什么是进程:

       首先我们了解一下什么是进程。简单的说在多任务系统中,每个独立执行的程序我们称之为进程,也就是说“正在进行的程序”。我们所使用的操作系统一般都是多任务的,即能同时操作多个应用程序。在Windows系统的电脑中我们通过任务管理器可以查看系统正在运行的进程。在Linux系统中我们可以通过ps命令查看系统中的进程状态。

(2)什么是线程:

       一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

(3)什么是并发:

       从专业的角度来说,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发

       当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

       直观上来说,当多个用户同时对同一个系统进行同一个请求时,系统内部将通过多个线程分别处理每个用户的请求,但是每个线程的执行逻辑是相同的,这种编程的场景我们可以看成是并发。

(4)为什么使用多线程编程

        使用多线程无非是为了提高cpu的利用率,提高系统的执行效率。再者就是为了处理多用户并发访问的这种场景。我们可以实际的模拟一种场景:比如我们要上传一些资料到系统中,所有的资料的处理逻辑是一样的,但是比较繁琐,需要比较长的时间进行处理,当用户在点击上传第一篇文档之后,文档还没有上传完成,后台还在执行的时候上传第二篇第三篇文档,这种场景我们就可以在后台采用多线程的方式,为每一次上传创建一个线程,更佳的是后台创建线程池,对多个线程进行管理。

二、线程的生命周期

     线程的执行是一个动态执行的过程,有一个从生产到死亡的过程,下图展示了整个线程的生命周期

    

Java线程具有五种基本状态

第一种:新建状态(New)

       当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

第二种:就绪状态(Runnable)

       当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

第三种:运行状态(Running)

       当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

第四种:阻塞状态(Blocked)

        处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

        1、等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

        2、同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

        3、其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

第五种:死亡状态(Dead)

       线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、创建并使用线程

Java中线程的创建方式有三种

第一种:通过继承Thread类创建线程

第二种:通过实现Runnable接口创建线程

第三种:通过Callable和Future创建线程

1、通过继承Thread类来创建使用线程

package com.jwang.thread;

/**
 * 描述:通过继承Thread类创建线程
 * 
 * @author jwang
 *
 */
public class MySecondThread extends Thread 
{
	private Thread thread;

	private String threadName;

	public MySecondThread(String threadName)
	{
		this.threadName = threadName;
		System.out.println("creating thread" + threadName);
	}

	/**
	 * run方法中是线程中执行的逻辑
	 */
	public void run() 
	{
		System.out.println("Running " + threadName);
		try
		{
			for (int i = 4; i > 0; i--)
			{
				System.out.println("Thread: " + threadName + ", " + i);
				// 让线程睡眠一会
				Thread.sleep(50);
			}
		} 
		catch (InterruptedException e) 
		{
			System.out.println("Thread " + threadName + " interrupted.");
		}
		System.out.println("Thread " + threadName + " exiting.");
	}

	/**
	 * 复写start方法
	 */
	public void start() 
	{
		System.out.println("Starting " + threadName);
		if (thread == null)
		{
			thread = new Thread(this, threadName);
			thread.start();
		}
	}
	
	public static void main(String[] args)
	{
		Thread thread1 = new MySecondThread("thread-1");
		thread1.start();
		Thread thread2 = new MySecondThread("thread-2");
		thread2.start();
	}
}

2、通过实现Runnable接口创建线程

package com.jwang.thread;

/**
 * 描述:通过实现Runnable接口创建线程
 * @author jwang
 *
 */
public class MyFirstThread implements Runnable
{
	private Thread thread;

	private String threadName;

	public MyFirstThread(String threadName)
	{
		this.threadName = threadName;
		System.out.println("creating thread" + threadName);
	}

	/**
	 * run方法中是线程中执行的逻辑
	 */
	@Override
	public void run() 
	{
		System.out.println("Running " + threadName);
		try
		{
			for (int i = 4; i > 0; i--)
			{
				System.out.println("Thread: " + threadName + ", " + i);
				// 让线程睡眠一会
				Thread.sleep(50);
			}
		} 
		catch (InterruptedException e) 
		{
			System.out.println("Thread " + threadName + " interrupted.");
		}
		System.out.println("Thread " + threadName + " exiting.");
	}
	
	public void start() 
	{
		System.out.println("Starting " + threadName);
		if (thread == null)
		{
			thread = new Thread(this, threadName);
			thread.start();
		}
	}
	
	public static void main(String[] args)
	{
		MyFirstThread thread1 = new MyFirstThread("thread-1");
		thread1.start();
		MyFirstThread thread2 = new MyFirstThread("thread-2");
		thread2.start();
		
	}
	
}

两种方式的运行结果基本相同,如下图所示:

当我们使用new关键字创建线程时,线程处于创建状态,当线程对象调用start()方法时线程处于就绪状态,等待cpu的调度。当cpu调度该线程时即开始执行run()方法中代码。两种创建线程的方式比较而言通过实现Runnable接口创建线程的方式更加的灵活。因为一个类在继承Thread类后是不能再继承其它类的,这是Java单继承的特性,但是一个类在实现Runnable接口的同时还可以实现其它的接口。

3、通过Callable和Future创建线程

     首先创建Callable接口的实现类,并实现call()方法,然后使用FutureTask类来包装Callable实现类的对象,最后将此Future Task对象作为Thread对象的target来创建线程。

下面我们来看具体的示例:

package com.jwang.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 描述:通过Callable和FutureTask创建并使用线程
 * @author jwang
 *
 */

public class TestCallable
{
	public static void main(String[] args)
	{
		//创建Callable实现类的对象
		Callable<Integer> callable = new CallableTest();
		
		//创建Future实现类的对象,并将Callable实现类的对象封装
		FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
		
		for(int i=0; i<100; i++)
		{
			System.out.println(Thread.currentThread().getName()+":"+i);
			if(i == 40)
			{
				new Thread(futureTask).start();
			}
		}
		System.out.println("主线程for循环执行完毕..");
		
		try 
		{
			int sum = futureTask.get();
			System.out.println("子线程运行的结果为:"+sum);
		} 
		catch (InterruptedException e) 
		{
			e.printStackTrace();
		}
		catch (ExecutionException e)
		{
			e.printStackTrace();
		}
	}
	
}

class CallableTest implements Callable<Integer>
{
	/**
	 * call方法中编写需要线程执行的核心代码
	 */
	@Override
	public Integer call() throws Exception 
	{
		int sum = 0;
		for(int i=0;i<100;i++)
		{
			System.out.println(Thread.currentThread().getName() + ":" + i);
			sum += i;
		}
		return sum;
	}
}

       我们发现在Callable接口是一个泛型接口,之所是使用泛型是因为线程执行的结果类型不确定。在Callable实现类中我们实现的call()方法,而不是run()方法,多线程执行的逻辑写在call()方法中。同时该方法是具有返回值的。在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。之所以能够这样是因为FutureTask既实现了Future接口,具有返回执行结果的功能,又实现了Runnable接口,可以作为Thread对象的target。所以FutureTask具有这两个接口的双重特性

       多次执行此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕..”这句话则很可能是在子线程循环中间输出,也就是说主线程可能已经执行完毕,子线程还没有执行完毕。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?原因在于通过futureTssk.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,futureTask.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

猜你喜欢

转载自blog.csdn.net/m0_38045882/article/details/83143370