写点什么

最详细的多线程讲解!

用户头像
愚者
关注
发布于: 1 小时前

 线程、进程、并发、并行

1、线程与进程

  • 线程也被称作轻量级进程,线程是进程的执行单元;

  • 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程;

2、并发与并行

  • 并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果;

  • 并行:指在同一时刻,有多条指令在多个处理器上同时执行;


创建线程的两种方式

1、继承 Thread 类创建线程类

通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 定义 Thread 类的子类,并重写该类的 run()方法。把 run()方法称为线程执行体;

  2. 创建 Thread 子类的实例,即创建线程对象;

  3. 调用线程对象的 start()方法来启动该线程;

​​

public class Test7 extends Thread{    @Override    public void run() {        //当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名。        System.out.println(getName());    }
public static void main(String[] args) { //调用Thread的currentThread方法获取当前线程 System.out.println(Thread.currentThread().getName()); new Thread(new Test7()).start(); }}//控制台打印mainThread-0复制代码
复制代码



2、实现 Runnable 接口创建线程类

通过实现 Runnable 接口来创建并启动多线程的步骤如下:

1.定义 Runnable 接口的实现类,并重写该接口的 run()方法;


2.创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;


3.调用线程对象的 start()方法来启动线程;


​​

public class Test7 implements Runnable{    @Override    public void run() {        System.out.println(Thread.currentThread().getName());    }
public static void main(String[] args) { //调用Thread的currentThread方法获取当前线程 System.out.println(Thread.currentThread().getName()); new Thread(new Test7()).start(); //设定线程名字 //new Thread(new Test7(),"新线程").start(); }}//控制台打印mainThread-0复制代码
复制代码


3、创建线程的两种方式对比

采用实现 Runnable 接口的方式:


  • 还可以继承其他类;


  • 这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况;


  • 如需访问当前线程,则必须使用 Thread.currentThread()方法;

采用继承 Thread 类的方式:


  • 已经继承了 Thread 类,所以不能再继承别的类;


  • 如需访问当前线程,直接使用 this 即可获得当前线程;

推荐使用接口的形式;


4、对同一线程多次调用 start()方法会怎样?

public class Test implements Runnable {
@Override public void run() { System.out.println("Test"); }
public static void main(String[] args) { Thread thread = new Thread(new Test()); thread.start(); thread.start(); }}复制代码
复制代码


运行控制台会报错:


​ 这是因为 start()方法会在调用开始前检查当前线程的状态。线程创建时默认状态为 0,当调用 start()方法后,线程状态被修改,所以再次调用 start()方法会报错;

​​​

    /* Java thread status for tools,     * initialized to indicate thread 'not yet started'     */    private volatile int threadStatus = 0;        if (threadStatus != 0)            throw new IllegalThreadStateException();复制代码
复制代码


5、容易混淆的创建线程的几种方式?


网上对于创建线程的几种方式各有不同,有说两种、四种、三种的?那么到底是几种呢?

​​​

    /* What will be run. */    private Runnable target;
@Override public void run() { if (target != null) { target.run(); } }复制代码
复制代码


我们查看 Thread 源代码可知,其实本质上就两种,一种是继承 Thread,重写 run 方法的话,那么 Thread 原本的 run 方法则不再存在,我们调用的就是我们重写后的方法。

还有一种就是实现 Runnable 并重写 run 方法,此种方式的话,我们传入了 Runnable 对象,如上代码进行判断,target 不为空,则执行了 target 的 run 方法;

查看网上其它的创建方式,其实本质上都是调用的我们最基本的两种创建方式,只是对他们进行了封装!


6、如果同时使用两种创建方式运行多线程会出现什么结果?

​​

public class Test{    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println("我来自Runnable");            }        }){            @Override            public void run() {                System.out.println("我来自Thread");            }        }.start();    }}复制代码
复制代码


控制台打印:我来自 Thread;

这是为什么呢?这是因为我们重写了 Thread 的 run()方法,所以即使我们传入了 Runnable 对象,但是下面的代码已经不存在了。最终是直接执行我们所重写的 run()方法

​​

    @Override    public void run() {        if (target != null) {            target.run();        }    }复制代码
复制代码


详细分析 Java 中实现多线程的方法有几种?(从本质上出发)

线程的生命周期

java.lang.Thread 定义了一个内部枚举 State,如下:

​​

public enum State {    NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;}复制代码
复制代码


可以知道 Java 线程有六个状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)、TERMINATED(终止);

线程状态之间的转换关系如下:


当程序使用 new 关键字创建了一个线程之后,该线程就处于 NEW(新建)状态,此时它和其他的 Java 对象一样,仅仅由 Java 虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体;

当线程对象调用了 start()方法后,该线程处于 RUNNABLE(可运行)状态,Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于 JVM 里线程调度器的调度;


​ 调用线程对象的 start()方法之后,该线程立即进入 RUNNABLE 状态-----RUNNABLE 状态相当于等待执行,但该线程并未真正进入运行状态;


1、NEW、RUNNABLE、TERMINATED 状态演示

​​

public class Test implements Runnable {
@Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i); }
}
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Test()); System.out.println(thread.getState());
//调用start()方法转为RUNNABLE状态,此状态并不会立马执行,至于该线程何时开始运行,取决于JVM里线程调度器的调度 thread.start(); System.out.println(thread.getState());
//休眠10毫秒让程序正常走完,线程执行完为TERMINATED状态 Thread.sleep(10); System.out.println(thread.getState()); }}
//控制台打印NEWRUNNABLE01...99TERMINATED复制代码
复制代码


2、WAITING、TIMED_WAITING、BLOCKED 状态演示

​​

public class Test implements Runnable {
@Override public void run() { sync(); }
private synchronized void sync(){ try { Thread.sleep(1000); wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
public static void main(String[] args) throws InterruptedException { Test runnable = new Test(); Thread thread1 = new Thread(runnable); thread1.start(); Thread thread2 = new Thread(runnable); thread2.start();
//避免主线程执行太快,休眠50毫秒再打印状态 Thread.sleep(50); System.out.println(thread1.getState()); System.out.println(thread2.getState());
//同步方法里子线程休眠1000毫秒,那么这里主线程需要休眠超过1000毫秒,等待子线程代码走到wait()再打印状态 Thread.sleep(1500); System.out.println(thread1.getState());
}}//控制台打印TIMED_WAITINGBLOCKEDWAITING复制代码
复制代码


一般习惯而言,我们通常会把 WAITING、TIMED_WAITING、BLOCKED 统称为阻塞状态!

interrupt()、interrupted()、isInterrupted()


1、interrupt()

​​

public void interrupt()复制代码
复制代码


中断线程,将会设置该线程的中断状态位,即设置为 true。如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出 SecurityException。


如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。


2、interrupted()

​​

public static boolean interrupted()复制代码
复制代码


测试当前线程是否已经中断。线程的中断状态由该方法清除。


换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。


3、isInterrupted()

​​

public boolean isInterrupted()复制代码
复制代码


测试线程是否已经中断。线程的 中断状态 不受该方法的影响。

wait()、notify()、notifyAll()



1、使用 notify 唤醒等待线程

​​

public class Test{
private static Object object = new Object();
public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (object){ System.out.println(Thread.currentThread().getName()+"获取到了锁"); try { //释放对象锁,释放cpu资源并进入等待 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"被唤醒"); } }).start(); //主线程休眠50毫秒,保证上面的线程先执行 Thread.sleep(50);
new Thread(() -> { synchronized (object){ System.out.println(Thread.currentThread().getName()+"获取到了锁"); object.notify(); } }).start();
}}//控制台打印Thread-0获取到了锁Thread-1获取到了锁Thread-0被唤醒复制代码
复制代码


2、notify 与 notifyAll 的区别

​​

public class Test2 implements Runnable{
private static Object object = new Object();
@Override public void run() { synchronized (object){ System.out.println(Thread.currentThread().getName()+"获取到锁"); try { //释放对象锁,释放cpu资源并进入等待 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"被唤醒"); } }
public static void main(String[] args) throws InterruptedException { new Thread(new Test2()).start(); new Thread(new Test2()).start();
//主线程休眠50毫秒,保证上面的线程先执行 Thread.sleep(50);
new Thread(() -> { synchronized (object){ //object.notify(); object.notifyAll(); } }).start(); }
}//控制台打印Thread-0获取到锁Thread-1获取到锁Thread-1被唤醒Thread-0被唤醒复制代码
复制代码


使用 notify()的话,多次运行会发现,唤醒的线程是随机的!


3、wait 只释放当前锁

​​

public class Test3{    private static volatile Object objectA = new Object();    private static volatile Object objectB = new Object();
public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (objectA){ System.out.println(Thread.currentThread().getName()+"获取到objectA锁"); synchronized (objectB){ System.out.println(Thread.currentThread().getName()+"获取到objectB锁"); try { objectA.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start();
Thread.sleep(1000);
new Thread(() -> { synchronized (objectA){ System.out.println(Thread.currentThread().getName()+"获取到objectB锁"); synchronized (objectB){ System.out.println(Thread.currentThread().getName()+"获取到objectA锁"); } }
}).start(); }}//控制台打印Thread-0获取到objectA锁Thread-0获取到objectB锁Thread-1获取到objectB锁复制代码
复制代码

4、为什么 wait()、notify()、notifyAll()必须在同步方法/代码块中调用?


对于对象的同步方法/代码块来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们;


wait()方法强制当前线程释放对象锁,这意味着在调用某对象的 wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的 wait()方法;


当某线程调用某对象的 notify()或 notifyAll()方法时,任意一个(对于 notify())或者所有(对于 notifyAll())在该对象的等待队列中的线程,将被转移到该对象的入口队列。接着这些队列(可能只有一个)将竞争该对象的锁,最终获得锁的线程继续执行。如果没有线程在该对象的等待队列中等待获得锁,那么 notify()和 notifyAll()将不起任何作用。在调用对象的 notify()和 notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的 notify()或 notifyAll()方法。

join()、sleep()、yield()


1、join()


Thread 提供了让一个线程等待另一个线程完成的方法---join()方法。当在某个程序执行流中调用其他线程的 join()方法时,调用线程将被阻塞,直到被 join()方法加入的 join()线程执行完成为止。


​​

public class JoinThread extends Thread{    @Override    public void run()    {        for (int i = 0; i < 100 ; i++ )        {            System.out.println(getName() + "  " + i);        }    }    public static void main(String[] args) throws Exception    {        for (int i = 0; i < 100 ; i++ )        {            if (i == 20)            {                JoinThread jt = new JoinThread();                jt.start();                //main线程调用了jt线程的join方法,main线程                //必须等jt执行结束才会向下执行                jt.join();            }            System.out.println(Thread.currentThread().getName() + "  " + i);        }    }}复制代码
复制代码


控制台打印:



​ 主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了 t.join()方法,此时主线程需要等待,等待子线程结束了,才能继续执行 t.join()之后的代码块;


2、sleep()


如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用 Thread 类的静态 sleep()方法来实现:


  • static void sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态;


  • static void sleep(long millis,int nanos):让当前正在执行的线程暂停 millis 毫秒加 nanos 毫微秒;


当当前线程调用 sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会释放锁,也不会获得执行的机会,即使系统中没有其他可执行的线程,处于 sleep()中的线程也不会执行,因此 sleep()方法常用来暂停程序执行;

​​

public class TestSleep{    public static void main(String[] args) throws Exception    {		for (int i = 0; i < 10 ; i++ )		{			System.out.println("当前时间: " + new Date());			//调用sleep方法让当前线程暂停1s。			Thread.sleep(1000);		}    }}复制代码
复制代码


sleep(long mills):让出 CPU 资源,但是不会释放锁资源(包括 synchronized 和 lock)! wait():让出 CPU 资源和锁资源!


2.1、sleep 不释放 synchronized 锁

​​

public class Test4 implements Runnable{
@Override public void run() { sync(); }
public synchronized void sync(){ System.out.println(Thread.currentThread().getName()+"获取锁"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"释放锁"); } public static void main(String[] args) { Test4 runnable = new Test4(); new Thread(runnable).start(); new Thread(runnable).start(); }}//控制台打印Thread-1获取锁Thread-1释放锁Thread-0获取锁Thread-0释放锁复制代码
复制代码


2.2、sleep 不释放 lock 锁

​​

public class Test5 implements Runnable{    private static final Lock LOCK = new ReentrantLock();
@Override public void run() { try { LOCK.lock(); System.out.println(Thread.currentThread().getName()+"获取锁"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); }finally { LOCK.unlock(); System.out.println(Thread.currentThread().getName()+"释放锁"); } }
public static void main(String[] args) { Test5 runnable = new Test5(); new Thread(runnable).start(); new Thread(runnable).start(); }}//控制台打印Thread-0获取锁Thread-0释放锁Thread-1获取锁Thread-1释放锁复制代码
复制代码

总结:sleep 方法可以让线程进入 Waiting 状态,并且不占用 CPU 资源,但是不释放锁,直到规定时间后再运行,休眠期间如果被中断,会抛出异常并清除中断状态


3、yield()


yield()方法是一个和 sleep()方法有点相似的方法,它也是 Thread 类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了 yield()方法暂停之后,线程调度器又将其调度出来重新执行;


实际上,当某个线程调用了 yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会;


​​

public class TestYield extends Thread{	public TestYield()	{	}	public TestYield(String name)	{		super(name);	}	//定义run方法作为线程执行体	public void run()	{		for (int i = 0; i < 50 ; i++ )		{			System.out.println(getName() + "  " + i);			//当i等于20时,使用yield方法让当前线程让步			if (i == 20)			{				Thread.yield();			}		}	}    public static void main(String[] args) throws Exception    {		//启动两条并发线程		TestYield ty1 = new TestYield("高级");		//将ty1线程设置成最高优先级		ty1.setPriority(Thread.MAX_PRIORITY);		ty1.start();		TestYield ty2 = new TestYield("低级");		//将ty1线程设置成最低优先级		ty2.setPriority(Thread.MIN_PRIORITY);		ty2.start();	    }}复制代码
复制代码



resume()、suspend()、stop()


这三个方法分别是恢复线程、暂停线程、终止线程,这三个方法已经弃用;

suspend()可能出现死锁,stop()是线程不安全的;

线程属性:线程 ID、线程名字、守护线程、优先级


1、线程 ID


每个线程都有自己的 ID,用于标识不同的线程;

​​

​​

public class Test7 implements Runnable{    @Override    public void run() {        System.out.println("子线程ID为:"+Thread.currentThread().getId());    }
public static void main(String[] args) { new Thread(new Test7()).start(); System.out.println("主线程ID为:"+Thread.currentThread().getId()); }}//控制台打印主线程ID为:1子线程ID为:12复制代码
复制代码


查看源码可知,线程 ID 每次都是自增 1,并且是从 1 开始的!

​​

tid = nextThreadID();
private static long threadSeqNumber;
private static synchronized long nextThreadID() { return ++threadSeqNumber;}
public long getId() { return tid;}复制代码
复制代码


那么为什么上述程序打印主线程 ID 为 1,而子线程 ID 为 12 了呢?

这是因为 JVM 启动的时候还有很多后台进程同步启动:



2、线程名字


让用户或者程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等;

​​

public class Test7 implements Runnable{    @Override    public void run() {        System.out.println("子线程名称为:"+Thread.currentThread().getName());    }
public static void main(String[] args) { new Thread(new Test7(),"新建子线程").start(); new Thread(new Test7()).start(); System.out.println("主线程名称为:"+Thread.currentThread().getName()); }}//控制台打印:主线程名称为:main子线程名称为:新建子线程子线程名称为:Thread-0复制代码
复制代码


查看源码可知,当我们没有传入名称时,会自动生成一个默认名字,因为++写在后面,所以是从 0 开始的:

​​

public Thread() {    init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(Runnable target) {    init(null, target, "Thread-" + nextThreadNum(), 0);}private static int threadInitNumber;private static synchronized int nextThreadNum() {    return threadInitNumber++;}复制代码
复制代码


3、守护线程


有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”。JVM 的垃圾回收线程就是典型的后台线程;


后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡;调用 Thread 对象的 setDaemon(true)方法可将指定线程设置成后台线程:

​​

public class DaemonThread extends Thread{    //定义后台线程的线程执行体与普通线程没有任何区别    @Override    public void run()    {        System.out.println("守护线程");    }
public static void main(String[] args) throws InterruptedException { DaemonThread t = new DaemonThread();
//前台线程创建的子线程默认也是前台线程 System.out.println(t.isDaemon());
//将此线程设置成后台线程 t.setDaemon(true); //启动后台线程 t.start();
//因为前台线程结束的话,后台线程也随之结束,所以在这休眠50毫秒以让后台线程可以执行完毕 Thread.sleep(50);
//isDaemon()方法用于判断指定线程是否为后台线程 System.out.println(t.isDaemon());
System.out.println(Thread.currentThread().getName()); //------程序执行到此处,前台线程(main线程)结束------ //后台线程也应该随之结束 }}//控制台打印false守护线程truemain复制代码
复制代码


从以上程序可以看出,主线程默认是前台线程,t 线程默认也是前台线程。并不是所有的线程默认都是前台线程,有些线程默认就是后台线程-----前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。



4、线程优先级


每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会;


每个线程默认的优先级都与创建它的父线程的优先级相同,在默认的情况下,main 线程具有普通优先级,由 main 线程创建的子进程也具有普通优先级;


Thread 类提供了 setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。其中 setPriority()方法的参数可以是一个整数,范围是 1~10 之间,也可以是 Thread 类的如下三个静态常量:


  • MAX_PRIORITY:其值是 10;


  • MIN_PRIORITY:其值是 1;


  • NORM_PRIORITY:其值是 5;

线程异常

public class Test7 implements Runnable{    @Override    public void run() {        throw new RuntimeException();    }
public static void main(String[] args) throws InterruptedException { new Thread(new Test7()).start(); Thread.sleep(1000); System.out.println("执行结束"); }}//控制台打印Exception in thread "Thread-0" java.lang.RuntimeException at com.example.demo.Test7.run(Test7.java:11) at java.lang.Thread.run(Thread.java:748)执行结束复制代码
复制代码


由上代码运行结果可知:子线程的异常不会影响到主线程的执行,即使子线程有异常抛出,并且打印了异常信息,主线程依然能够正常运行!


使用 try/catch 包住线程语句:

​​

​​

try{    new Thread(new Test7()).start();}catch (Exception e){    System.out.println("捕获异常");}复制代码
复制代码


运行结果与上述仍然一样,可知:子线程异常无法用传统方法进行捕获!

使用 try/catch 包住线程执行体内代码:

​​

@Overridepublic void run() {    try{        throw new RuntimeException();    }catch (RuntimeException e){        System.out.println("捕获异常");    }}复制代码
复制代码


运行程序控制台打印捕获异常,可知:try/catch 只能捕获对应线程内的异常;


但是,如果我们手动在每个 run 方法里面都使用 try/catch 进行捕获的话,那工作量不仅大、重复性工作还麻烦,代码也很冗余。


所以我们可以使用 UncaughtExceptionHandler,这个接口可以检测出由于未捕获异常而终止的情况,并且对此进行处理;

​​

@FunctionalInterfacepublic interface UncaughtExceptionHandler {    void uncaughtException(Thread t, Throwable e);}复制代码
复制代码


编写我们自己的全局异常处理器:

​​

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {    @Override    public void uncaughtException(Thread t, Throwable e) {        System.out.println("线程名称为:"+t.getName()+" 出现异常,异常为:"+e.getMessage());    }}复制代码
复制代码


设置子线程的全局异常处理器:

​​

public class Test7 implements Runnable{    @Override    public void run() {        throw new RuntimeException();    }
public static void main(String[] args){ Thread thread = new Thread(new Test7());
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
thread.start();
}}复制代码
复制代码


运行程序,控制台打印:线程名称为:Thread-0 出现异常,异常为:null

用户头像

愚者

关注

还未添加个人签名 2021.07.22 加入

还未添加个人简介

评论

发布
暂无评论
最详细的多线程讲解!