多线程基础-线程创建、线程方法、线程状态等

多线程高并发

内容简介

多线程高并发目前已经基本成为大厂面试必备,如果你想要去BATJ等一线互联网大厂,或者去中小厂在同级别的码农中获得更高薪水,那么多线程高并发将会使你与他人拉开差距的一个很好的桥段。

本文会从多个维度,尽可能覆盖更全面,由浅入深,希望把复杂难懂的知识点尽可能形象的表达讲述出来,以帮助诸位更好的学习和进步,若有错误,请不吝赐教,以助及时改正,谢谢


What?多线程是什么

Process 进程

在谈多线程之前,我们首先来认识三个名词的概念:进程、线程、协程(纤程)

进程是什么呢?我想如果你平时使用计算机的时候,难免会遇到一些卡顿的情况发生,可能你不得不打开任务管理器,去关闭卡死的程序,那么在你打开任务管理器去关闭这个程序的时候,不知道你有没有发现,你所关闭的那个窗口,就是进程窗口

任务管理器中的进程

那么你可以看到,在任务管理器中,有我们目前在Windows中启动的各种应用程序,比如IDEA、Chrome,这一个个程序,本来是静态的,当我们点击它之后将他运行起来,就可以把它叫做进程。进程相对于程序来说是活的,是动态的。

Thread 线程

线程是基于进程的东西,线程是进程里最小的一个执行单元,是一个进程中不同的执行路径,举个例子,你打开Chrome浏览器,这是打开一个进程,你点开2个窗口,一个在播放电影,一个播放电视剧,这2个窗口工作起来是相互不受彼此影响的,那么我们可以把它们成为2个线程,而这2个线程就是多线程的缩影

Coroutine 协程/纤程

比线程更加轻量级的存在!一个线程也可以拥有多个协程。但是在Java之中没有对协程有很好的应用,可以在Lua/Go/Python中很好的体现,感兴趣的可以去看一下。

当然,对于这种概念只需要大体能说,了解即可,不必过分深究,如果你是在校大学生,建议好好学习计算机操作系统这一节课,会详细讲到

How?多线程如何编写?

extends Thread

继承thread类,重写run方法,调用start方法进行启动

package com.jiang.th3read01;

/**
 * @Title: 继承Thread
 * @author: JiangPeng
 */
public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 启动新线程
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

这就是我们第一个多线程的编写,ok当然你可以看到,在这个demo中,我的MyThread类继承了Thread,然后重写run方法。

关于Run方法:我们可以点进Thread类看看它的源码,当点进去可以看到Thread类实现了Runnable,当然Runnable我们等会也要见到

class Thread implements Runnable

继续看看Runnable是什么东西。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable里可以看到有一个abstract 即抽象类的run方法,所以我们Thread类既然实现了Runnable,就必须重写run方法。

implements Runnable

我们在上面讲到,Thread类是实现了Runnable,然后我们又继承的Thread类,那么我们能不能直接实现Runnable,不去继承Thread类呢?

当然是可以的。

/**
 * @Title: 实现Runnable
 * @author: JiangPeng
 */
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

大同小异,都是要重写Run方法,然后在run方法中编写你需要输出的代码。

很明显,实现Runnable要比继承Thread类要好,因为只能单继承,而实现接口可以多个。

State?method?

线程方法

睡眠 sleep

package com.jiang.thread01;

/**
 * @Title:
 * @author: JiangPeng
 */
public class BlockedSleep {

    public static void main(String[] args) {
        Web12306 web = new Web12306();
        new Thread(web,"xiaowang").start();
        new Thread(web,"xiaochen").start();
        new Thread(web,"xiaomeng").start();
    }
    static class Web12306 implements Runnable {
    private int piao = 100;

    @Override
    public void run() {
        while (true) {
            if (piao < 0) {
                break;
            }
            try{
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ">>" + piao--);
        }
    }
}
}

通过 Thread.sleep(200);使的进程之间暂停200秒后进行下一步的操作
sleep模拟了网络延时,增大了出现问题的可能性

sleep:意思就是睡眠 ,当前线程暂停一段时间让给别的线程去运行,sleep是由睡眠时间而定,等规定的时间到了自动复活

  • JDK源码中是这么定义sleep

    public static void sleep(long millis, int nanos)
        throws InterruptedException {
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (nanos < 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }
    
            if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
                millis++;
            }
    
            sleep(millis);
        }
    

礼让 yield

让当前正在执行的线程暂停,不是阻塞线程,而是将线程从运行状态转入就绪状态,让cpu重新调度

Copypackage com.jiang.thread01;

/**
 * @Title:
 * @author: JiangPeng
 */
public class Yield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }
}

class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+">>start");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+">>end");
    }
}

其实就是怕防止某个进程一直使用。比如抢票 不能让某个黄牛一直都能抢得到

yield 就是当前线程正在执行的时候停止下来进入等待队列 ,回到等待队列里在系统的调度算法里呢 还是依然可能把你刚回去的这个线程拿回来继续执行,意思就是我让出来一下CPU,后面调度能不能抢到就不管了。

  • JDK源码中是这么定义yield的:public static native void yield();

插队 join

合并线程,插队线程

Copypackage com.jiang.thread01;

/**
 * @Title:
 * @author: JiangPeng
 */
public class BlockedJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i=0;i<100;i++){
                System.out.println("lambda..."+i);
            }
        });
        t.start();

        for (int i=0;i<100;i++){
            if(i==20){
                t.join();//插队,main被阻塞了
            }
            System.out.println("mian..."+i);
        }
    }
}

join:意思就是在自己当前线程加入你调用的线程(),本线程等待。等调用的线程执行完 自己再去执行。例如:ti和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2上去运行 ,t1等t2运行完后再去运行自己的t1(自己 jion 自己 没意义)

  • Join在JDK源码实现:

     public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
    
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }
    

线程状态

New

当我们new一个线程时,还没调用start() 该线程 处于新建状态

MyThread thread = new MyThread();
System.out.println(thread.getState());

此时我们打印出来的thread.getState就是NEW

Runnable

线程调用start() 方法时 它会被线程调度器执行,也就是交给操作系统执行,这整个状态就叫Runnable,就绪状态

Ruuable的内部有两种状态:

  • Ready就绪状态
    • 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    • 调用线程的start()方法,此线程进入就绪状态。
    • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  • Running运行状态
    • 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

BLOCKED

这个状态,一般是线程等待获取一个锁,来继续执行下一步的操作,比较经典的就是synchronized关键字,这个关键字修饰的代码块或者方法,均需要获取到对应的锁,在未获取之前,其线程的状态就一直未BLOCKED,获取之后则进入Runnable就绪状态。如果线程长时间处于这种状态下,我们就是当心看是否出现死锁的问题了

Code case

public class MyThread extends Thread {
    private byte[] lock = new byte[0];

    public MyThread(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("done");

        }
    }
}

WAITING

一个线程会进入这个状态,一定是执行了如下的一些代码,例如

  • Object.wait()
  • Thread.join()
  • LockSupport.park()

当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()
或者一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。
当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。
当该线程处于这种等待的时候,其状态即为WAITING。
需要关注的是,这边的等待是没有时间限制的,当发现有这种状态的线程的时候,若其长时间处于这种状态,也需要关注下程序内部有无逻辑异常!

即执行wait()、Join()、LockSupport.park()会进入WAITING等待状态

执行notify()、notifyAll()、LockSupport.unpark()的时候会进入Runnable就绪状态

TIMED_WAITING

这个状态和WAITING状态的区别就是,这个状态的等待是有一定时效的,即可以理解为WAITING状态等待的时间是永久的,即必须等到某个条件符合才能继续往下走,否则线程不会被唤醒。但是TIMED_WAITING,等待一段时间之后,会唤醒线程去重新获取锁。当执行如下代码的时候,对应的线程会进入到TIMED_WAITING状态

  • Thread.sleep(long)
  • Object.wait(long)
  • Thread.join(long)
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()

当时间结束后,会自动进入到Runnable就绪状态

TERMINATED

已经执行结束的线程处于这种状态。 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

线程终止

终止线程:

1.线程正常结束--》次数
​ 2.外部干涉 --》加入标识

不要使用stop/destory方法,不安全。

Copypackage com.jiang.thread01;

/**
 * @Title:
 * @author: JiangPeng
 */


public class TerminateThread implements Runnable {
    //加入标识,标记线程体是否可以运行。
    private boolean flag = true;
    private String name;

    public TerminateThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        int i = 0;
        //关联标识,true->运行,false->停止。
        while(flag){
            System.out.println(name+"-->"+i++);
        }
    }
    //对外提供方法,改变标识
    public void terminate(){
        this.flag=false;
    }
    public static void main(String[] args) {
        TerminateThread terminateThread = new TerminateThread("jack");
        new Thread(terminateThread).start();

        for (int i=0;i<=99;i++){
            if(i==88){
                terminateThread.terminate();//线程终止
                System.out.println("线程手动终止");
            }
            System.out.println("main->"+i);
        }
    }
}

线程优先级

我们可以为线程通过setPriority方法设置优先级,优先级的范围为1-10。下面是几个优先级的常量:

  • MIN_PRIORITY:最小优先级,值为1
  • NORM_PRIORITY:正常优先级,线程的默认优先级,值为5
  • MAX_PRIORITY:最大优先级,值为10

注意,优先级并不能代表先后顺序,只能导致执行的概率不同。优先级大的线程执行的概率会更大。

原文地址:https://www.cnblogs.com/pengcode/p/12944257.html