Java基础17-多线程

概述

本文主要讲java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的线程函数用法等。
首先让我们来了解下在操作系统中进程和线程的区别:

  • 进程
    进程执行中的程序,例如QQ,微信,每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~N个线程。(进程是资源分配的最小单位)
  • 线程
    同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止(死亡)

创建线程

在java中要想实现多线程有4种手段:
1.继承Thread类,重写run()方法
2.实现Runable接口,实现run()方法
3.实现Callable接口 + FutureTask创建线程,实现call()方法
4.使用线程池创建线程, 常用的是Spring的ThreadPoolTaskExecutor,使用excute(Runable runable)方法
注意:
1.线程的调用是用start()方法而不是run()方法。
2.start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
3.在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实习在就是在操作系统中启动了一个进程。

  • 继承Thread类,重写run()方法

代码示例:

class Thread3 extends Thread {

    private String name;
    public Thread3(String name) {
        this.name = name;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
        }
    }
}
public class CommonTest {
    public static void main(String[] args) throws Exception {
        //创建线程
        Thread3 mTh1=new Thread3("A");
        Thread3 mTh2=new Thread3("B");
        //开启线程
        mTh1.start();
        mTh2.start();
    }
}

执行2次运行结果:

A运行  :  0
B运行  :  0
A运行  :  1
B运行  :  1
A运行  :  2
B运行  :  2
A运行  :  3
B运行  :  3
A运行  :  4
B运行  :  4
A运行  :  0
B运行  :  0
A运行  :  1
A运行  :  2
B运行  :  1
A运行  :  3
B运行  :  2
A运行  :  4
B运行  :  3
B运行  :  4

程序启动运行main时候,Java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mTh1和mTh2的两个对象的start()方法,另外两个线程也启动了,这样整个应用就在多线程下运行。

  • 实现java.lang.Runnable接口

代码示例:

class Thread4 implements Runnable {
    private String name;
    public Thread4(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
        }
    }
}
public class CommonTest {
    public static void main(String[] args) throws Exception {
        Thread4 mTh1 = new Thread4("A");
        Thread4 mTh2 = new Thread4("B");
        new Thread(mTh1).start();
        new Thread(mTh2).start();
    }
}

执行2次运行结果:

B运行  :  0
A运行  :  0
B运行  :  1
B运行  :  2
A运行  :  1
B运行  :  3
A运行  :  2
B运行  :  4
A运行  :  3
A运行  :  4
A运行  :  0
B运行  :  0
A运行  :  1
B运行  :  1
A运行  :  2
B运行  :  2
A运行  :  3
B运行  :  3
A运行  :  4
B运行  :  4

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

线程的状态切换

线程的状态分为五个阶段:创建、就绪、运行、阻塞、终止(死亡)

Java基础17-多线程_第1张图片
线程的状态切换

  • 新建状态(New):新创建了一个线程对象。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
  • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意:sleep不会释放持有的锁
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程调度

  • 线程优先级
    Java线程有优先级,优先级高的线程会获得较多的运行机会
    Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
    static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10。
    static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1。
    static int NORM_PRIORITY:分配给线程的默认优先级,取值为5。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

  • 线程休眠
    Thread.sleep(long millis)方法可以使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后会自动转为就绪(Runnable)状态。sleep()平台移植性好,注意Thread.sleep不会释放锁资源
  • 线程等待
    Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
  • 线程礼让
    Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  • 线程加入
    join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  • 线程唤醒
    Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。
    选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个wait()方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
    类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

常用函数

返回值 方法 描述
static Thread currentThread() 返回对当前正在执行的线程对象的引用
static void dumpStack() 将当前线程的堆栈跟踪打印到标准错误流
long getId() 返回此线程的标识符
String getName() 返回此线程的名称
int getPriority() 返回此线程的优先级
Thread.State getState() 返回此线程的状态
ThreadGroup getThreadGroup() 返回此线程所属的线程组
void interrupt() 中断这个线程
static boolean interrupted() 测试当前线程是否中断
boolean isAlive() 测试这个线程是否活着
boolean isDaemon() 测试这个线程是否是守护线程
boolean isInterrupted() 测试这个线程是否被中断
void join() 等待这个线程死亡
void join(long millis) 等待这个线程死亡最多millis毫秒
void join(long millis, int nanos) 等待最多 millis毫秒加上 nanos纳秒这个线程死亡
void run() 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回
void setContextClassLoader(ClassLoader cl) 设置此线程的上下文ClassLoader
void setDaemon(boolean on) 将此线程标记为daemon线程或用户线程
void setName(String name) 将此线程的名称更改为等于参数 name
void setPriority(int newPriority) 更改此线程的优先级
static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性
static void sleep(long millis, int nanos) 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性
void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法

几个重要函数的说明

  • sleep(long millis)
    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),在线程休眠期间不会释放锁资源,过了休眠时间线程自动唤醒。
  • join()
    join()的作用是:“等待该线程终止”。
    这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法,只有等到子线程结束了才能执行。
    为什么要用join()方法?
    在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
package com.multithread.join;  
class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
        super(name);  
       this.name=name;  
    }  
    public void run() {  
        System.out.println(Thread.currentThread().getName() + " 线程运行开始!");  
        for (int i = 0; i < 5; i++) {  
            System.out.println("子线程"+name + "运行 : " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");  
    }  
}  

运行结果:

main主线程运行开始!
main主线程运行结束!
B 线程运行开始!
子线程B运行 : 0
A 线程运行开始!
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程A运行 : 2
子线程A运行 : 3
子线程A运行 : 4
A 线程运行结束!
子线程B运行 : 2
子线程B运行 : 3
子线程B运行 : 4
B 线程运行结束!

发现主线程比子线程早结束,让我们加上join后再试试:

public class Main {  
  
    public static void main(String[] args) {  
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  
        try {  
            mTh1.join();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        try {  
            mTh2.join();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");  
    }  
}  

运行结果:

main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!

主线程一定会等子线程都结束了才结束

  • yield()
    暂停当前正在执行的线程对象,并执行其他线程。
    Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
    yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
    结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果
  • interrupt()
    不要以为它是中断某个线程!
    它只是向线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
  • Object.wait()
    Obj.wait()**与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁进行操作
    从语法角度来说就Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。
    从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。
    相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。
    Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制
    对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:
    建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:
class MyThreadPrinter implements Runnable {

    private String name;
    private Object prev;
    private Object self;

    private MyThreadPrinter(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (prev) {
                synchronized (self) {
                    System.out.print(name);
                    count--;

                    self.notify();
                }
                try {
                    prev.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static void main(String[] args) throws Exception {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        MyThreadPrinter pa = new MyThreadPrinter("A", c, a);
        MyThreadPrinter pb = new MyThreadPrinter("B", a, b);
        MyThreadPrinter pc = new MyThreadPrinter("C", b, c);


        new Thread(pa).start();
        Thread.sleep(100);  //确保按顺序A、B、C执行
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
        Thread.sleep(100);
    }
}

解释一下其整体思:
从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

wait和sleep区别

  • 共同点:
  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  2. wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
    如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
    需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
  • 不同点:
  1. Thread类的方法:sleep(),yield()等
    Object类的方法:wait()和notify()等
  2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
    所以sleep()和wait()方法的最大区别是:sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

守护线程

在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。

Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) 。

但是有几点需要注意:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
  • 你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
  • 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
  • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了

你可能感兴趣的:(Java基础17-多线程)