认识Java中的线程
前言:最近在看Java中的并发,做了笔记,但是还是觉得记录一下比较好,加深理解。同时这个模块可能有很多篇文章更新,笔者会抽时间更新,如果文章中有错误,欢迎指正!!
在进入正文前,,我们先来讲解一下基本的概念,线程是什么?进程又是啥?
一、线程与进程
1、进程是程序运行资源分配的最小单位
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。
2、线程是CPU调度的最小单位,必须依赖进程而存在
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
二、线程中的生命周期
1、生命周期中的5个状态
线程中生命周期主要为5个状态: new、runnable、running、blocked、dead。
(1)、新建(new Thread):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动),也就是说处于新生状态的线程有自己的内存空间,但该线程并没有运行。
(2)、就绪(runnable):线程已经被启动,正等待被分配给CPU时间片。
(3)、运行(running):线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
(4)、阻塞(blocked):由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入阻塞状态。
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5)、死亡(dead):当线程执行完毕或被其他线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行run()方法后终止。
- 异常终止:调用stop()方法让一个线程终止运行。
2、线程状态对应的常用方法
void run():创建该类的子类时必须实现的方法。
void start():开启线程的方法。
static void sleep(long t) / static void sleep(long millis, int nanos):释放CPU的执行权,不释放锁。当前线程睡眠/millis的时间(millis指定睡眠时间是其最小的不执行时间,因为sleep(millis)休眠到达后,无法保证会被JVM立即调度),sleep()是一个静态方法(static method),所以它不会停止其他的线程也处于休眠状态。线程sleep()时不会失去拥有的对象锁。作用是:保持对象锁,让出CPU,调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留一定的时间给其他线程执行的机会。
final void wait():释放CPU的执行权,释放锁。当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池( Waiting Pool)中,同时失去了对象的机锁——暂时的,wait后还要返还对象锁。当前线程必须拥有当前对象的锁,如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常,所以 wait()必须在 synchronized block 中调用。
final void notify()/notifyAll(): 唤醒在当前对象等待池中等待的第一个线程/所有线程。notify()/notifyAll()也必须拥有相同对象锁,否则也会抛出IllegalMonitorStateException异常。
static void yied(): 可以对当前线程进行临时暂停,让出CPU的使用权,给其他线程执行机会、让同等优先权的线程运行(但并不保证当前线程会被JVM再次调度、使该线程重新进入Running状态),如果没有同等优先权的线程,那么yield()方法将不会起作用。
三、线程的创建
创建线程的三种方法(继承Thread、实现Runable中run方法、实现Callable中的call方法结合Future接口来实现)
1、继承Thread,覆写run方法。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
继承Thread线程类,然后覆盖run方法即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
执行Main类的main方法,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
这种方法的缺点是:一个Java类只能继承一个父类。
执行结果:
- 1
- 2
其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
2、实现Runable中run方法。
- 1
- 2
- 3
- 4
- 5
- 6
该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
还是在上面的Main类中的main方法中执行,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
结果如下:
- 1
- 2
- 3
3、实现Callable中的call方法结合FutureTask类来实现。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
还是在main方法中执行,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
这里主要使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。
执行结果如下:
- 1
- 2
- 3
- 4
- 5
我们来看一下FutureTask相关源码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在这里FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。再run的实现方法中我们可以发现调用call()方法的调用。
上面就是创建线程的主要三种方法,需要特别注意的是:不能对同一线程对象两次调用start()方法,ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值,就是说主线程获取到值的,只有主线程才会执行下面的代码。
四、线程中的中断机制
1、调用Thread.stop(),该方法强迫停止一个线程,并抛出一个新创建的ThreadDeath对象作为异常。但是这个方法不是安全的,在JAVA中不建议使用了。
2、利用Thread.interrupt()方法和机制。
Java中断机制是一种协作机制,也就是通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
Thread中提供了三个中断方法:
- Thread.interrupted(): 测试当前线程是否已经中断,线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将送回false (在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)
- Thread.interrupt(): 中断线程,但是没有返回结果。是唯一能将中断状态设置为true的方法。
- Thread.isInterrupted(): 检测线程是否中断,不会影响中断状态
我们可以自己设置中断变量实现,比如前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。
代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
我们也可以使用Thread自带中的中断方法,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
其实interrupt还可以处理一些更为复杂的逻辑,当外部线程对某线程调用Thread.interrupt()方法后,Java语言的处理机制是这样的:如果该线程处在可中断状态下(调用了Thread.wait()或者Thread.sleep()等特定会发生阻塞的api),那么该线程会立即被唤醒,同时会受到一个InterruptedException,同时,如果是阻塞在IO上,对应的资源会被关闭。如果该线程接下来不执行Thread.interrupted()方法(不是interrupt).那么该线程处理任何IO资源的时候,都会导致这些资源关闭。当然,解决的办法就是调用一下interrupted(),不过这取需要程序员自行根椐代码的逻辑来设定,根据自己的需求确认是否可以直接忽略该中断,还是应该马上退出。
简单的异常处理:
- 1
- 2
- 3
- 4
- 5
五、当前线程副本:ThreadLocal
1、当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,目标变量就像是线程的本地变量,这也是类名中Local所要表达的意思。
2、ThreadLocal类提供的4个方法:
- void set(Tvalue),设置当前线程的线程局部变量的值。
- public T get(),该方法返回当前线程所对应的线程局部变量。
- public void remove(),将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0新增的法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected T initialValue(),返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
我们简单使用一下,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
运行程序,打印结果如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
虽然三个线程共享一个实例,但是相互之间并没有干扰。这是因为ThreadLocal为每个线程都提供了一个副本。
我们可以查看一下ThreadLocal的相关源码,如图所示:
我们在ThreadLocal源码中set()方法中可以发现,getMap(t)获取一个当前线程相关的ThreadLocalMap,然后将变量的值存储到ThreadLocalMap对象中,如果获取的ThreadLocalMap的对象为空,就调用createMap创建。
接着我们查看一下createMap相关代码,
初始值方法和get()方法,
可以发现ThreadLocalMap是一个关键,而ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都存一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变影方问在不同线程中的隔离。因为每个线程的变都是自己特有的,完全不会有并发错误。还有一点就是, ThreadLocalMap存储的键值对
中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
这下我们就明白了ThreadLocal的原理了,其实ThreadLocal在处理线程的局部变量的时候比synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
注意:使用ThrcadLocal,—般都是声明在静态变量中,如果不断地创建ThreadLocal而且没冇调用其remove方法,将会异致内存泄露,特别是在高并发的Web容器当中这么做的时候。
六、线程中异常处理
run()方法不允许 throw exception ,所有的异常必须在run()方法内进行处理。
在Java多线程程序中,所有线程都不允许抛出未捕获的 checked exception ,也就足说各个线程需要自己把自己的checked exception 处理掉。这一点是通过 java.lang.Runnable.run( )方法声明(因为此方法声明上没有throw exception 部分)进行了约束。但是线程依然有可能抛出 unchecked exception,当抛出此类异常时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常,也是说完全无法 catch 到这个异常。
关于checked exception和unchecked exception,可以查看下面图:
(将派生于Error或者RuntimeException的异常称为unchecked异常,所有其他的异常为checked异常。如果出现了RuntimeException,就一定是程序员自身的问题。)
在thread里面,对于checked exception直接使用try/catch块来捕获。
而对于unchecked exception,主要分三步:
- 定义实现UncaughtExceptionHandler接口的对异常处理的逻辑和步骤。
- 定义线程执行结构和逻辑。这一步和普通线程定义一样。
- 使用Thread里面的setUncaughtExceptionHandler(UncaughtExceptionHandler)来处理。该方法是在thread.start()语句前调用。(thread.setUncaughtExceptionHandler(new MyThreadExceptionHandler()))
我们还是来一个实例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
查看控制台,可以发现打印如下:
到此Java中线程的基本知识就介绍到这里,后面再更新Java并发相关的其他知识。:)
参考
- 详解Java中的checked异常和unchecked异常
- 《Java并发编程从入门到精通》