多线程是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 callable = new CallableTest();
//创建Future实现类的对象,并将Callable实现类的对象封装
FutureTask futureTask = new FutureTask(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
{
/**
* 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()方法执行完毕才能取到返回值。