如何应用多线程?
在java中,我们有有多种方式来实现多线程。如继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现带有返回值的多线程。
-
继承Thread类创建线程
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()是一个native方法,它会启动一个新的线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extends(继承) Thread类,并overread(重写)run()方法,就可以启动新的线程并执行自己定义的run()方法。
public class MyThread extends Thread{ public void run(){ System.out.println("MyThread start"); } public static void main(String[] args){ MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); myThread1.start(); myThread2.start(); } }
-
实现Runnable接口创建线程
如果我们自己的类已经extends(继承)了另一个类,就无法直接extends(继承)Thread类,这个时候,我们可以实现Runnable接口
public class MyThread extends MyOrderClass implements Runnable{ public void run(){ System.out.println("MyThread start"); } public static void main(String[] args){ MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); myThread1.run(); myThread2.run(); } }
-
实现Callable接口通过FutureTask包装器来创建Thread线程
有的时候,我们可能需要让一步执行的线程在执行完成以后,提供一个返回值给当前的主线程主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程。在javaAPI中也提供了这种的实现方式
pubilc class CallbaleDemo implements Callable<String>{ public static void main(String[] args) throws ExecutionException,InterruptedException{ //创建线程池 ExecutionService executionService = Executor.newFixedThreadPool(10); CallableDemo callableDemo = new CallableDemo(); //执行线程并返回 future对象 Futrue<String> futrue = executorService.submit(callableDemo); //获取call的返回值 String call = future.get(); System.out.println(call); //关闭线程池 executorService.shutdown(); } @Override public String call()throws Exception{ int a= 1; int b= 2; System.out.println(a+b); return "执行结果"+(a+b); } }
多线程的实际应用场景
说到多线程的应用场景,其实大家在工作中应该很少有场景能够应用到多线程了,因为基于业务开发来说,很多使用异步的场景我们都通过分布式消息队列来做了。但是并不是说多线程不会被用到,如果有看过一些源码,会发现线程的使用无处不在。
之前我应用的比较多的场景是在做银行对账,我们有一个定时任务去拿到数据后然后通过线程去处理。
之前看zookeeper源码的时候看到一个比较有意思的异步
责任链模式
public class Request{ private String name;//名字 public String getName(){ return name; } public void setName(String name){ this.name = name; } @Override public String toString(){ return "Request{" +"name='"+name+'\" +"}"; } }
public interface RequestProcessor{ void process(Request request); }
public class PrintProcessor extends Threads implements RequestProcessor{ LinkBlockQueue<Request> reuqests = new LinkBlockQueue<Request>(); private final RequestPorcessor requestProcessor; private volatile boolean isFinish = false; /** * 对外提供一个关闭线程的方法 * **/ public void shutdown(){ isFinish = true; } public PrintProcessor(RequestProcessor nextProcessor){ //有参构造方法赋值 this. requestProcessor = nextProcessor; } @Override public void run(){ while(!isFinish){ try{ Reuqest request = requests.take;//阻塞式获取数据 System.out.println("printdata:"+request.getName()); //判断如果下一条链不为空 则交给下一条链进行处理 if(null != requestProcessor){ requestProcessor.process(reqeust); } }catch(InterruptedException e){ e.printStackTrace(); } } } //处理请求 public void processRequest(Requst request){ requests.add(request); } }
public calss SaveProcessor extends Threads implements RequestProcessor{ LinkBlockQueue<Request> requests = new LinkBlockQueue<Request>(); private ReuqestProcessor requestProcessor; //是否开启线程 默认是开启状态 private volatile boolean isFinish= false; //对外提供一个可以关闭的方法 public void shutdown(){ isFinish = true; } @Override public void run(){ while(!isFinish){ try{ Reqeust reqeust = requests.take();//阻塞式获取数据 System.out.println("SaveProcessor:"+request.getName()); //判断是否存在下一条链如果存在继续向下传递 if(null != requestProcessor){ requestProcessor.process(request); } }catch(InterruptedException e){ e.printStackTrace(); } } } @Override public void process(Request request){ //TODO 根据一些实际需求去做一些处理请求 requests.add(request); } }
Main
public class Main{ PrintProcessor printProcessor; protected Main(){ SaveProcessor saveProessor = new SaveProcessor(); saveProcessor.start(); printProcessor = new PrintProcessor(saveProcessor); printProcessor.start(); } private void doTest(Request request){ printProcessor.process(request); } public static void main(String[] args){ Request request = new Request(); request.setName("hello world"); new Main().doTest(reqeust); } }
Java 并发编程的基础
基本应用搞清楚以后我们再基于Java线程的基础切入,来逐步去深入挖掘线程的整体模型。
-
线程的生命周期
Java线程既然能够创建,那么也势必回被销毁,所以线程式存在生命周期的,那么我们接下来从线程的生命周期奥克斯hi去了解线程。
线程一共有六种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)
-
NEW(初始化状态):线程被构建,但是还没有调用start方法
-
RUNNABLED(运行状态):Java线程把操作系统中的就绪和运行两种状态统一称为“运行中”
-
BOLKED(阻塞状态):表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
- 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
- 同步阻塞:运行的线程在获取对象的同步锁时,如果该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
- 其他阻塞:当我们调用sleep或者join方法或者发出了I/O请求时,jvm会把当前线程设置为阻塞状态,等到sleep结束、join终止、io处理完成后才会恢复线程
-
TIME_WAITING(超时等待状态):超时以后自动返回
-
TIMEINATED(终止状态):表示当前线程已经执行完毕
-
[外链图片转存失败(img-XBYeZnpF-1566398463415)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566267851350.png)]
通过代码演示线程的状态
实例如下:
public class ThreadStatus{
public static void main(String[] args){
//TIME_WAITING
new Thread(()->{
while(true){
try{
TimeUnit.SECONDS.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
},"timewaiting").start();
new Thread(()->{
while(true){
try{
ThreadStatus.class.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
},"WAITING").start();
//线程在TreadStatus加锁后 不会释放锁
new Thread(new BlockDemo(),"BlockDemo-01").start();
new Thread(new BlockDemo(),"BlockDemo-02").start();
}
static class BlockDemo extends Thread{
@Override
public void run(){
synchronized(BlockDemo.class){
while(true){
try{
TimeUnit.SECOND.sleep(100);
}catch(InterruptedException e){
e.printstackTrace();
}
}
}
}
}
}
启动一个线程前,最好为这个线程设置一个线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给我们提供一些提示
-
显示线程的状态
- 运行该实例,打开终端或者命令提示符,键入”jps“,(JDK1.5提供的一个显示当前所有java进程pid的命令)
- 根据上一步骤获得的pid,继续输入jstack pid(jstack是java虚拟机自带的一种堆栈跟踪工具,jstack用于打印给定的java进程ID 或 core file或远程调试服务的java堆栈信息)
通过上面的分析我们了解到了线程的生命周期,现在在整个生命周期中并不是固定的处于某个状态,而是随着代码的执行在不同的状态之间进行切换线程的启动。
- 运行该实例,打开终端或者命令提示符,键入”jps“,(JDK1.5提供的一个显示当前所有java进程pid的命令)
-
线程的启动
前面我们通过一些案例演示了线程的启动,也就是调用start()方法去启动一个线程,当run方法中的代码执行完毕后,线程的生命周期也将终止,调用start方法的语义是当前线程告诉JVM,启动嗲用start方法的线程。
线程的启动原理
刚开始我们开始学习多线程的时候会充满疑惑,启动一个线程为什么是调用start()方法,而不是run()方法? 这做一个简单的分析,先简单看一下start方法的定义
[外链图片转存失败(img-CGZ1B03V-1566398463416)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566286860601.png)]
[外链图片转存失败(img-DHC13agd-1566398463417)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566286901381.png)]
可以看到 start()方法其实是调用一个native方法start0()来启动一个线程,首先start0()这个方法是在Thread的静态块中来注册的。代码如下:
[外链图片转存失败(img-0yVstLpD-1566398463417)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566287131573.png)]
registerNatives的本地方法的定义在文件Thread.c,Thread.c定义了各个操作系统平台要用的关于线程的公共数据和操作,以下是Thread.c的全部内容
- open jdk查看地址
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/lang/Thread.c
[外链图片转存失败(img-9eMnLmfH-1566398463417)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566294913341.png)]
从这段代码中可以看出 start0()方法实际上会执行JVM_StartThread方法,这个方法是做什么的呢?从名字上来看,似乎是在JVM层面去启动一个线程,如果真的是这样,那么在JVM层面,一定会调用我们Java中定义的run方法。下面我去翻阅一下Hotspot源码去看一下这一块的调用
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
// We cannot hold the Threads_lock when we throw an exception,
// due to rank ordering issues. Example: we might need to grab the
// Heap_lock while we construct the exception.
bool throw_illegal_thread_state = false;
// We must release the Threads_lock before we can post a jvmti event
// in Thread::start.
{
// Ensure that the C++ Thread and OSThread structures aren't freed before
// we operate.
MutexLocker mu(Threads_lock);
// Since JDK 5 the java.lang.Thread threadStatus is used to prevent
// re-starting an already started thread, so we should usually find
// that the JavaThread is null. However for a JNI attached thread
// there is a small window between the Thread object being created
// (with its JavaThread set) and the update to its threadStatus, so we
// have to check for this
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
// We could also check the stillborn flag to see if this thread was already stopped, but
// for historical reasons we let the thread detect that itself when it starts running
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
// Allocate the C++ Thread structure and create the native thread. The
// stack size retrieved from java is signed, but the constructor takes
// size_t (an unsigned type), so avoid passing negative values which would
// result in really large stacks.
size_t sz = size > 0 ? (size_t) size : 0;
native_thread = new JavaThread(&thread_entry, sz);
// At this point it may be possible that no osthread was created for the
// JavaThread due to lack of memory. Check for this situation and throw
// an exception if necessary. Eventually we may want to change this so
// that we only grab the lock if the thread was created successfully -
// then we can also do this check and throw the exception in the
// JavaThread constructor.
if (native_thread->osthread() != NULL) {
// Note: the current thread is not being used within "prepare".
native_thread->prepare(jthread);
}
}
}
JVM_ENTRY是用来定义JVM_StartThread函数的,在这个函数里面创建了一个真正和平台有关的本地线程,奔着一条道走到黑的原则,继续看一下new JavaThread(&thread_entry, sz)这个方法做了什么事情,继续寻找JavaThread的定义。下面我们继续看
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
#if INCLUDE_ALL_GCS
, _satb_mark_queue(&_satb_mark_queue_set),
_dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
if (TraceThreadEvents) {
tty->print_cr("creating thread %p", this);
}
initialize();
_jni_attach_state = _not_attaching_via_jni;
set_entry_point(entry_point);
// Create the native thread itself.
// %note runtime_23
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
os::java_thread;
os::create_thread(this, thr_type, stack_sz);
_safepoint_visible = false;
// The _osthread may be NULL here because we ran out of memory (too many threads active).
// We need to throw and OutOfMemoryError - however we cannot do this here because the caller
// may hold a lock and all locks must be unlocked before throwing the exception (throwing
// the exception consists of creating the exception object & initializing it, initialization
// will leave the VM via a JavaCall and then all locks must be unlocked).
//
// The thread is still suspended when we reach here. Thread must be explicit started
// by creator! Furthermore, the thread must also explicitly be added to the Threads list
// by calling Threads:add. The reason why this is not done here, is because the thread
// object must be fully initialized (take a look at JVM_Start)
}
这个方法有两个参数,第一个是函数名称,线程创建成功之后会根据这个函数名称调用对应的函数;第二个是当前进程内已经有的线程数量。最后我们重点关注一下os::create_thread方法,实际就是调用平台创建线程的方法来创建线程。接下来就是线程的启动,会调用Thread.cpp文件中的Thread::start(Thread*thread)方法,代码如下:
void Thread::start(Thread* thread) {
trace("start", thread);
// Start is different from resume in that its safety is guaranteed by context or
// being called from a Java method synchronized on the Thread object.
if (!DisableStartThread) {
if (thread->is_Java_thread()) {
// Initialize the thread state to RUNNABLE before starting this thread.
// Can not set it after the thread started because we do not know the
// exact thread state at that time. It could be in MONITOR_WAIT or
// in SLEEPING or some other state.
java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
java_lang_Thread::RUNNABLE);
}
os::start_thread(thread);
}
}
start方法中有一个函数调用os::start_thread(thread);调用平台启动线程的方法,最终会调用Thread.cpp文件中的JavaThread::run()方法。
线程的终止
线程的终止,并不是简单的调用stop命令,虽然api仍然可以调用,但是和其他的线程控制方法如suspend、resume一样都是过期了的不建议使用,就拿stop来说,stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。
如果我们要去中断一个线程,在线程中提供了一个interrupt方法
interrupt 方法
当其他线程通过调用当前线程interrupt方法,表示相当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
线程通过检查资深是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。
通过下面这个例子,来实现了线程终止的逻辑
public class InterruptDemo{
private static int i;
public static void main(String[] args)throws InterrupedException{
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
//默认情况下isInterrupted 返回 false、通过thread.interrupt变成了true
i++;
}
System.out.println("Num:"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//加和不加的效果
}
}
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断的将线程停止,因此这种终止线程的做法显得更加安全和优雅。
Thread.interrupted
上面的案例中,通过interrupt,设置了一个标识告诉线程可以终止了,线程中还提供了静态方法,Thread.interrupted()对设置中断标识的线程复位。比如在上面的案例中,外面的线程调用thread.interrupt来设置中断标识,而在线程里面,有通过Thread.interrupted把线程的标识有进行了复位
public class InterruptedDemo{
public static void main(String[] args){
Thread thread = new Thread(()->{
while(true){//死循环
if(Thread.currentThread().isInterrupted()){
System.out.println("Before: "+Thread.currentThread().isInterrupted());
Thread.interrupted();//对线程调用复位 true 变成 false
System.out.println("After: "+Thread.currentThread().isInterrupted());
}
}
},"InterruptedStatus");
thread.start();//启动线程
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//终止线程
}
}
其他的线程复位
除了通过Thread.interrupted方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出InterruptedException 异常的方法,在InterruptedExceptio抛出之前,JVM会先把线的中断标识位清楚,然后才会抛出Interrupted方法,将会返回false分别通过下面两个demo来演示复位的效果
public class InterruptDemo{
private static int i;
private static void main(String[] args) throws InterrruptedException{
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Num:"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println(thread.isInterrupted());
}
}
对比以下代码:
public class InterruptDemo{
private static int i;
public static void main(String[] args){
Thread thread= new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printstackTrace();
}
System.out.println("Num:"+i);
}
},"interruptedDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}
为什么要复位
Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经等到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是false,这就是复位的原因。
线程的终止原理
我们来看一下 thread.interrupt()方法做了什么事情
[外链图片转存失败(img-0GfJ8Mdb-1566398463418)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1566307994709.png)]
这个方法里面调用了interrupt0()这个方法 ,在前面我们先分析了start方法的时候见过,是个native方法,同样我们要找到jvm.cpp文件,找到JVM_Interrupt的定义,代码如下:
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");
// Ensure that the C++ Thread and OSThread structures aren't freed before we operate
oop java_thread = JNIHandles::resolve_non_null(jthread);
MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
// We need to re-resolve the java_thread, since a GC might have happened during the
// acquire of the lock
JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
if (thr != NULL) {
Thread::interrupt(thr);
}
JVM_END
这个方法比较简单没直接调用了Thread::interrupt(thr)这个方法,这个方法的定义在Thread.cpp文件中,代码如下:
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}
Thread::interrupt方法调用了os::interruptf方法,这个是调用平台的interrupt方法,这个方法的实现是在os_*.cpp文件中其中星号代表的是不同平台,因为jvm是跨平台的,所以对于不同的操作平台,线程的调度方式都是不一样的。我们以os_linux.cpp为例
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
//获取本地线程对象
OSThread* osthread = thread->osthread();
if (!osthread->interrupted()) {//判断本地线程对象是否为中断
osthread->set_interrupted(true);//设置中断状态为true
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we execute unpark().
//这里是内存屏障 内存屏障的目的是使得interrupted状态对其他线程理解生效
OrderAccess::fence();
//_SleepEvent相当于Thread。sleep,表示如果线程调用了sleep方法,则通过unpark唤醒
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
//_ParkEvent用于synchronized同步块和Object.wait(),这里相当于也是通过unPark进行唤醒
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
set_interrupted(true)实际上就是调用osThread.hpp中的set_inttupted()方法,在osThread中定义了一个成员属性volatile jint_interrupted;
通过上面的代码分析可以知道,thread.interrupt()方法实际就是设置一个interrupted状态标识为true、并且通过ParkEvent的unpark方法来唤醒线程。
- 对于synchronized阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被park
- 再调用ParkEvent的park方法之前,会先判断线程的中断状态,如果为true,会清除当前线程的中断标识
- Object.wait、Thread.sleep、Thread.join、会抛出InterruptedException异常
-
为什么Object.wait、Thread.sleep和Thread.join 都会抛出InterruptedException?
共同点:这几个方法都是属于阻塞的方法,而阻塞方法的释放会取决于一些外部的事件名单时阻塞方法可能因为等不到一个外部的触发事件而导致无法终止,所以它允许一个线程请求自己来停止它正在做的事情。当一个方法抛出InterruptedException时,它是再告诉调用者如果执行该方法的线程被中断,它会尝试停止正在做的事情并且通过抛出InterruptedException表示提前返回。所以,这个异常的意思就是表示一个阻塞被其他线程中断了。然后由于线程调用了interrupt()中断 方法,那么Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException。
注意: 抛出InterruptedException异常并不是说线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理要看线程本身,比如:
-
直接捕获异常不做任何处理
-
将异常往外抛出
-
停止当前线程,并打印异常信息
为了更好的理解上面的这段话,我以Thread.sleep为例直接从jdk的源码中找到中断标识的清除以及异常抛出的方法代码,找到is_interrupted()方法,linux平台中的实现再os_linux.cpp文件中,代码如下:
-
bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();
bool interrupted = osthread->interrupted();//获取线程的中断标识
if (interrupted && clear_interrupted) {//如果中断标识为true
osthread->set_interrupted(false);//设置中断标识为false
// consider thread->_SleepEvent->reset() ... optional optimization
}
return interrupted;
}
找到Thread.sleep这个操作再jdk中的源码体现,怎么找?下面我们来看一下以下一段代码:
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
JVMWrapper("JVM_Sleep");
if (millis < 0) {
THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
}
//判断并清除线程中断状态,如果中断状态为true,抛出中断异常
if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}
// Save current thread state and restore it at the end of this block.
// And set new thread state to SLEEPING.
JavaThreadSleepState jtss(thread);
注意: 上面加了中文注释的地方的代码,先判断is_interrupted的状态,然后抛出一个InterruptedException异常。
未完待续...