一、多线程基本操作与实现

一、多线程基本操作与实现_第1张图片
在这里插入图片描述

线程的概念

线程:一个程序同时执行多个任务。通常,每一个任务就称为一个线程。与进程相比较,线程更"轻量级",创建、撤销一个线程比启动一个新进程开销要小的多。没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。

多进程与多线程区别:本质区别在于,每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使得线程之间的通信比进程之间通信更有效、更方便。

那么,多线程表现在哪里呢?
在实际应用中,多线程非常有用。例如,一个浏览器应用可以同时下载多个图片、音乐;一个Web服务器需要同时处理多个并发的请求。这些都是多线程的应用。

  • 高并发:访问的线程量非常非常高。
  • 高并发带来的问题:服务器内存不够用,无法处理新的请求。

线程的状态

关于线程的定义,或者线程与进程的区别参考Linux系统编程部分,下面看看线程状态图:


一、多线程基本操作与实现_第2张图片
在这里插入图片描述

这是一幅线程状态图,如何实现多线程呢?

实现多线程

一、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”! 
    ...
}

一、多线程基本操作与实现_第3张图片
在这里插入图片描述

通过上图来看就非常清晰明了,我们调用了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实现多线程可以更好地体现程序共享的概念
一、多线程基本操作与实现_第4张图片
在这里插入图片描述

在多线程的处理上使用的就是代理设计模式。除了以上的关系之外,实际上在开发之中使用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()方法。


一、多线程基本操作与实现_第5张图片
在这里插入图片描述
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方法只是这个进程上的一个线程而已。


一、多线程基本操作与实现_第6张图片
在这里插入图片描述

线程休眠

线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象

一、多线程基本操作与实现_第7张图片
在这里插入图片描述

  • 线程休眠并非同时休眠,同一时刻,JVM只会运行一个线程
  • 线程休眠会导致线程由运行态由运行态--->阻塞态,休眠结束后返回就绪态,什么时候再执行由系统调度,无法人为干预
  • 线程中断异常为受查异常,必须强制处理

线程让步

暂停当前正在执行的线程对象,并执行其他线程。
意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

一、多线程基本操作与实现_第8张图片
在这里插入图片描述

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("代码结束");
    }
}

你可能感兴趣的:(一、多线程基本操作与实现)