线程的概念
线程:一个程序同时执行多个任务。通常,每一个任务就称为一个线程。与进程相比较,线程更"轻量级",创建、撤销一个线程比启动一个新进程开销要小的多。没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
多进程与多线程区别:本质区别在于,每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使得线程之间的通信比进程之间通信更有效、更方便。
那么,多线程表现在哪里呢?
在实际应用中,多线程非常有用。例如,一个浏览器应用可以同时下载多个图片、音乐;一个Web服务器需要同时处理多个并发的请求。这些都是多线程的应用。
- 高并发:访问的线程量非常非常高。
- 高并发带来的问题:服务器内存不够用,无法处理新的请求。
线程的状态
关于线程的定义,或者线程与进程的区别参考Linux系统编程部分,下面看看线程状态图:
这是一幅线程状态图,如何实现多线程呢?
实现多线程
一、Thread类实现多线程
java.lang.Thread是一个线程操作的核心类。新建一个线程最简单的方法就是直接继承Thread类,而后覆写该类中的run()方法(就相当于主类中的main方法),是每个线程的入口!
下面摘了一段start方法的源码,不难看出,如果线程一旦被start,那么threadStatus 就会变成非0,一个标识线程状态的数字,所以启动已经启动了的线程的时候便会抛出IllegalThreadStateException:
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {}
}
}
private native void start0();
这个start0()是个本地方法,Thread 类有个 registerNatives 本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如 start0(), stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的。
这个方法放在一个 static 语句块中,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。而本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,它定义了各个操作系统平台都要用到的关于线程的公用数据和操作,我们看看JNI代码
JNIEXPORT void JNICALL
Java_Java_lang_Thread_registerNatives (JNIEnv \*env, jclass cls){ //registerNatives
(\*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods)); }
static JNINativeMethod methods[] = {
{"start0", "()V",(void \*)&JVM_StartThread}, //start0 方法
{"stop0", "(" OBJ ")V", (void \*)&JVM_StopThread},
{"isAlive","()Z",(void \*)&JVM_IsThreadAlive},
{"suspend0","()V",(void \*)&JVM_SuspendThread},
{"resume0","()V",(void \*)&JVM_ResumeThread},
{"setPriority0","(I)V",(void \*)&JVM_SetThreadPriority},
{"yield", "()V",(void \*)&JVM_Yield},
{"sleep","(J)V",(void \*)&JVM_Sleep},
{"currentThread","()" THD,(void \*)&JVM_CurrentThread},
{"countStackFrames","()I",(void \*)&JVM_CountStackFrames},
{"interrupt0","()V",(void \*)&JVM_Interrupt},
{"isInterrupted","(Z)Z",(void \*)&JVM_IsInterrupted},
{"holdsLock","(" OBJ ")Z",(void \*)&JVM_HoldsLock},
{"getThreads","()[" THD,(void \*)&JVM_GetAllThreads},
{"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads},
可以容易的看出 Java 线程调用 start->start0 的方法,实际上会调用到 JVM_StartThread 方法,那这个方法又是怎么处理的呢?该方法最终要调用 Java 线程的 run 方法,事实的确也是这样的。在 jvm.cpp 中,有如下代码段:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
...
native_thread = new JavaThread(&thread_entry, sz);
...
}
这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 thread_entry,如下:
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,obj,
KlassHandle(THREAD,SystemDictionary::Thread_klass()),
vmSymbolHandles::run_method_name(),
vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到调用了 vmSymbolHandles::run_method_name 方法,而run_method_name是在 vmSymbols.hpp 用宏定义的:
class vmSymbolHandles: AllStatic {
...
template(run_method_name,"run") //这里决定了调用的方法名称是 “run”!
...
}
通过上图来看就非常清晰明了,我们调用了start(),是虚拟机创建完本地操作系统级线程后回调了run方法,不可手动去调用run方法(否则就是普通方法)!
二、Runnable()接口实现多线程
Runnable()接口实现多线程是一种推荐使用的,避免单继承的局限性,Thread类实现了Runnable接口,下面是的Thread的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
当子类实现Runnable接口,此时子类与Thread就是典型的代理设计模式,子类负责真实线程业务操作,Thread类负责资源调度与创建线程来辅助真实业务!
Thread与Runnable区别
- 明显使用Runnable实现多线程要比继承Thread类要好,因为可以避免但继承局限。
- 使用Runnable实现多线程可以更好地体现程序共享的概念
在多线程的处理上使用的就是代理设计模式。除了以上的关系之外,实际上在开发之中使用Runnable还有一个特点:使用Runnable实现的多线程的程序类可以更好的描述出程序共享的概念(并不是说Thread不能)
范例:使用Thread实现数据共享(产生若干线程进行同一数据的处理操作)
class MyShell implements Runnable{
private volatile int ticket = 30;
@Override
public void run() {
while(ticket > 0){
System.out.println(ticket--);
}
}
}
public class Demo {
public static void main(String[] args) {
Runnable my = new MyShell();
new Thread(my).start();
new Thread(my).start();
new Thread(my).start();
}
}
三、Callable实现多线程(since JDK1.5)
从JDK1.5开始追加了新的开发包:java.uti.concurrent。这个开发包主要是进行高并发编程使用的,包含很多在高并发操作中会使用的类。在这个包里定义有一个新的接口Callable
- 核心方法call
- 有返回值
Runnable中的run()方法没有返回值,它的设计也遵循了主方法的设计原则:线程开始了就别回头。但是很多时候需要一些返回值,例如某些线程执行完成后可能带来一些返回结果,这种情况下就只能利用Callable来实现多线程!
不管何种情况。如果要想启动多线程只有Thread类中的start()方法。
class MyThread implements Callable{
private int ticket = 10;
@Override
public Object call() throws Exception {
while(this.ticket > 0){
System.out.println("剩下票数:"+ticket--);
}
return "票卖完了";
}
}
public class Demo2 {
public static void main(String[] args) {
FutureTask task = new FutureTask<>(new MyThread());
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
线程常用方法
线程命名与取得
取得线程名称:Thread.currentThread()
设置线程名称:public Thread(Runnable target, String name)
多线程的运行状态是不确定的,所以对于多线程操作必须有一个明确标识出线程对象的信息,这个信息往往通过名称来描述。在Thread类中提供有如下的线程名称方法:
class MyuThread implements Runnable{
@Override
public void run() {
//打印当前线程名称
System.out.println(Thread.currentThread());
}
}
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(new MyuThread());
//不启动线程,就相当于普通的调用方法
t.run();
}
}
通过以上程序我们发现,主方法本身就是一个线程,所有的线程都是通过主线程创建并启动的。实际上每当使用了java命令去解释程序的时候,都表示启动了一个新的JVM进程。而main方法只是这个进程上的一个线程而已。
线程休眠
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象
- 线程休眠并非同时休眠,同一时刻,JVM只会运行一个线程
- 线程休眠会导致线程由运行态由运行态--->阻塞态,休眠结束后返回就绪态,什么时候再执行由系统调度,无法人为干预
- 线程中断异常为受查异常,必须强制处理
线程让步
暂停当前正在执行的线程对象,并执行其他线程。
意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
join()方法
等待该线程终止后继续执行本线程
主线程调用join(),会让主线程休眠,让调用该方法的线程执行完毕再继续执行主线程:
class MyuThread implements Runnable{
@Override
public void run() {
//打印当前线程名称
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(new MyuThread());
System.out.println("主线程开始");
//不启动线程,就相当于普通的调用方法
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束");
}
}
线程停止
- 设置标志位,让线程正常退出(推荐使用)
- 使用stop()强制停止线程,JDK9中取消此方法(极度不推荐)
- 使用Thread类中的interrupt()中断线程
stop存在数据不安全的问题,设置标志位是为了让一次循环或者一个代码块结束,但是stop是直接让线程退出,于是很容易出现数据的不完整性,所以极度不推荐使用!
正常执行模式下(除了wait、sleep、join)使用Thread.interrupt()会将线程的状态置为打断状态,根据此状态人为进行线程终止。
如果线程中出现wait、sleep、join再调用Thread.interrupt(),会抛出线程中断异常,然后在catch中人为进行线程终止!
class MyuThread2 implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){
System.out.println("非阻塞状态下停止线程"+ interrupted);
break;
}
}
}
void setFlag(boolean flag){
this.flag = flag;
}
}
public class Demo2 {
public static void main(String[] args) {
MyuThread2 thread = new MyuThread2();
Thread t = new Thread(thread);
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
System.out.println("代码结束");
}
}
class MyuThread2 implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
try {
Thread.sleep(1000);
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){
System.out.println("非阻塞状态下停止线程"+ interrupted);
break;
}
} catch (InterruptedException e) {
System.out.println("线程中断");
boolean interrupted = Thread.currentThread().isInterrupted();
System.out.println(interrupted);
break;
}
}
}
void setFlag(boolean flag){
this.flag = flag;
}
}
public class Demo2 {
public static void main(String[] args) {
MyuThread2 thread = new MyuThread2();
Thread t = new Thread(thread);
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
System.out.println("代码结束");
}
}
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。
然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
- 如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;
- 如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,
- 而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true之后,一旦线程调用了wait、 jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被程序会自动清除,重新设置为false。
通过上面的分析,我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
线程优先级
线程的优先级指的是,线程的优先级越高越有可能先执行,但仅仅是有可能而已。
//设置优先级:
public final void setPriority(int newPriority)
//取得优先级:
public final int getPriority()
Thread类提供的三个常量:
1、最高优先级:public final static int MAX_PRIORITY = 10;
2、中等优先级:public final static int NORM_PRIORITY = 5;
3、最低优先级:public final static int MIN_PRIORITY = 1;
主方法只是一个中等优先级,线程具有继承性,线程是有继承关系的,比如当A线程中启动B线程,那么B和A的优先级将是一样的。
守护线程
守护线程是一种特殊的线程,它属于是一种陪伴线程,只要当前进程中任意存在一个非守护线程没有结束,守护线程就不会停止,只有当最后一个非守护线程结束时,守护线程会随着JVM一起退出,比如:垃圾回收线程
注意:Java中默认为用户线程,主线程也是用户线程!
判断线程是否为守护线程:
public final boolean isDaemon();
设置线程为守护线程必须在线程启动之前:
public final void setDaemon();
class A implements Runnable{
private int i;
@Override
public void run() {
try{
while(true){
i++;
System.out.println("线程名称:"+Thread.currentThread().getName()
+" ,i="+i+", 是否为守护线程:"+Thread.currentThread().isDaemon());
Thread.sleep(1000);
}
}catch (InterruptedException e){
System.out.println("线程名称:"+Thread.currentThread().getName()
+"线程中断了");
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new A(),"子线程A");
// 设置线程A为守护线程,此语句必须在start方法之前执行
thread1.setDaemon(true);
thread1.start();
Thread thread2 = new Thread(new A(),"子线程B"); thread2.start();
Thread.sleep(3000);
// 中断非守护线程
thread2.interrupt();
Thread.sleep(10000);
System.out.println("代码结束");
}
}