多线程-线程概念

概念

线程与进程(process、Thread)

**进程:**是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

**线程:**是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

* 进程与线程的区别
    进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
    线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
复制代码

线程调度:

1. 分时调度
	所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
2. 抢占式调度
	优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
复制代码

Java使用的为抢占式调度。

很多多线程是模拟出来的,真正的多线程是值多个CPU,即多核,如服务器。

如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,以为切换的很快,所以就有同时执行的错觉

  • 线程就是独立的执行单元
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程。如:main线程、GC线程
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器始于操作系统紧密相关的,先后顺序时不能人为的干预
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度时间(线程的上下文切换),并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程状态

​ 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的**生命周期(一个对象从创建到销毁期间一系列的轨迹)**中,有多个状态。

状态有

  1. 创建(new)
  2. 就绪(runnable)
  3. 运行(running)
  4. 阻塞(blocked)
  5. 计时等待(time waiting)
  6. 无限等待(waiting)
  7. 消亡(dead、Teminated)
线程状态 详解
New(新建) 线程刚被创建,但是并未启动。还没调用start方法。 MyThread myThread = new MyThread()只有线程对象,没有线程特征
Runnable(可运行) 线程可以再java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用thread.start() 方法:就绪状态
Running(运行) 线程正在运行
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程将编程Runnable状态
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
TimedWaiting(计时等待) 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep()、Object.Wait()
Teminated(被终止) 因为run()方法正常退出而死亡,或者因为没有捕获的异常终止了run() 方法而死亡。

​ 当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

​ 当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

​ 线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

阻塞与等待的区别

  • 阻塞:当一个线程试图获取对象锁(非java.util.concurrent库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断
  • 等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、Thread.join()以及等待Lock或Condition。

  需要强调的是虽然synchronized和JUC里的Lock都实现锁的功能,但线程进入的状态是不一样的。synchronized会让线程进入阻塞态,而JUC里的Lock是用LockSupport.park()/unpark()来实现阻塞/唤醒的,会让线程进入等待态。但话又说回来,虽然等锁时进入的状态不一样,但被唤醒后又都进入runnable态,从行为效果来看又是一样的。

上下文切换

​ 对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

  由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。

  因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

  说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行

程序计数器(PC寄存器):

​ 记录线程执行到的字节码行数(运行的位置),当线程上下文切换时使用

Thread类

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    // Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字
    private volatile String name;
    // 线程的优先级(最大值为10,最小值为1,默认值为5)
    private int priority;
    private Thread threadQ;
    private long eetop;

    // 是否单步执行此线程
    private boolean     single_step;

    // 线程是否是守护线程
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    // 要执行的任务
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    private AccessControlContext inheritedAccessControlContext;

    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
}
复制代码

主要方法

start()

​ start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

新启一个线程执行其run()方法,一个线程只能start一次。主要是通过调用native start0()来实现。

 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) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }


private native void start0();
复制代码

run()

​ run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

sleep()

线程休眠sleep()方法有两个重载版本:

sleep(long millis)   //参数为毫秒
sleep(long millis,int nanoseconds)  //第一参数为毫秒,第二个参数为纳秒
复制代码

​ sleep()相当于让线程睡眠,交出CPU,让CPU去执行其他的任务,但是sleep方法**不会释放锁**。也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:

public class Test {
     
    private int i = 10;
    private Object object = new Object();
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);
                try {
                    System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {
                    // TODO: handle exception
                }
                System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}
// 输出结果:
i:11
线程Thread-0进入睡眠状态
线程Thread-0睡眠结束
i:12
i:13
线程Thread-1进入睡眠状态
线程Thread-1睡眠结束
i:14
复制代码

从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

  注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

总结:

  1. sleep(参数) 指定当前线程阻塞的毫秒数
  2. sleep()存在异常InterruptedException
  3. sleep时间达到后,线程进入就绪状态(Runnable)
  4. sleep()可以模拟网络延时,倒计时等
  5. 每一个对象都有一个锁,sleep()只会让出CPU的执行权,但不会释放锁。

yield()

线程礼让、退让。yield()只是假装是否锁,这个线程还是会去争抢锁(类似于篮球争球一样,自己也参加)。

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

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

public class TestYield {

    public static void main(String[] args) throws IOException  {
        TestYield test = new TestYield();
        MyThread thread1 = test.new MyThread();
        thread1.setName("a");
        thread1.start();

        MyThread thread2 = test.new MyThread();
        thread2.setName("b");
        thread2.start();
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(currentThread().getName()+"---start");
            Thread.yield();
            System.out.println(currentThread().getName()+"---end");
        }
    }
}
// 执行结果1
a---start
a---end
b---start
b---end
// 执行结果2
a---start
b---start
a---end
b---end
复制代码

可以看出yield()方法不一定礼让成功,线程从Running进入Runnable后,有可能会重新获得CPU的执行权

总结

  1. 礼让线程,让当前正在执行的线程重回就绪状态(Runnable),该方法不会让线程进入阻塞状态(与sleep()的区别)。
  2. yield()会让出CPU重新调度,礼让不一定成功,看下次那个线程抢到CPU执行权

join()

线程强制执行 (插队)join

join合并线程,带此线程执行完成后,再执行其他线程,其他线程阻塞 。【类似于**插队**】

join方法有三个重载版本:

join()
join(long millis)  //参数为毫秒
join(long millis, int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
复制代码

假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。

  看下面一个例子:

public class TestJoin {

    public static void main(String[] args) throws IOException  {
        System.out.println("进入线程"+Thread.currentThread().getName());
        TestJoin test = new TestJoin();
        MyThread thread1 = test.new MyThread();
        thread1.start();
        try {
            System.out.println("线程"+Thread.currentThread().getName()+"等待");
            thread1.join();
            System.out.println("线程"+Thread.currentThread().getName()+"继续执行");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("进入线程"+Thread.currentThread().getName());
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                // TODO: handle exception
            }
            System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");
        }
    }
}
// 执行结果:
进入线程main
线程main等待
进入线程Thread-0
线程Thread-0执行完毕
线程main继续执行
复制代码

可以看出,当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。

实际上调用join方法是调用了Object的wait方法,这个可以通过查看join()源码得知:

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;
            }
        }
    }
复制代码

​ wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。

 由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。具体的wait方法使用在后面文章中给出。

interrupt()

​ interrupt,顾名思义,即**中断**的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。

此操作会将线程的中断标志位置位,至于线程作何动作那要看线程了。

  • 如果线程sleep()、wait()、join()等处于阻塞状态,那么线程会定时检查中断状态位如果发现中断状态位为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断状态位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
  • 如果线程正在运行、争用synchronized、lock()等,那么是不可中断的,他们会忽略。

可以通过以下三种方式来判断中断:

1)isInterrupted()

此方法只会读取线程的中断标志位,并不会重置。

2)interrupted()

此方法读取线程的中断标志位,并会重置。

3)throw InterruptException

抛出该异常的同时,会重置中断标志位。

public class TestInterrupt {

    public static void main(String[] args) throws IOException  {
        TestInterrupt test = new TestInterrupt();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            try {
                System.out.println("进入睡眠状态");
                Thread.currentThread().sleep(10000);
                System.out.println("睡眠完毕");
            } catch (InterruptedException e) {
                System.out.println("得到中断异常");
            }
            System.out.println("run方法执行完毕");
        }
    }
}
// 执行结果:
进入睡眠状态
得到中断异常
run方法执行完毕
复制代码
public class TestInterrupt2 {

    public static void main(String[] args) throws IOException  {
        TestInterrupt2 test = new TestInterrupt2();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(i<Integer.MAX_VALUE){
                System.out.println(i+" while循环");
                i++;
            }
        }
    }
}
复制代码

​ 运行该程序会发现,while循环会一直运行直到变量i的值超出Integer.MAX_VALUE。所以说直接调用interrupt方法不能中断正在运行中的线程。

​ 但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。比如下面这段代码:


/**
 * interrupt方法配合isInterrupted()能够中断正在运行的线程中断处于
 */
public class TestInterrupt3 {
    public static void main(String[] args) throws IOException  {
        TestInterrupt3 test = new TestInterrupt3();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(!isInterrupted() && i<Integer.MAX_VALUE){
                System.out.println(i+" while循环");
                i++;
            }
        }
    }
}
复制代码

运行会发现,打印若干个值之后,while循环就停止打印了。

  但是一般情况下不建议通过这种方式来中断线程,一般会在MyThread类中增加一个属性 isStop来标志是否结束while循环,然后再在while循环中判断isStop的值。

public class TestInterrupt4 {
    public static void main(String[] args) throws IOException  {
        TestInterrupt3 test = new TestInterrupt3();
        TestInterrupt3.MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    }

    class MyThread extends Thread{
        private volatile boolean isStop = false;
        @Override
        public void run() {
            int i = 0;
            while(!isStop){
                i++;
            }
        }

        public void setStop(boolean stop){
            this.isStop = stop;
        }
    }
}
复制代码

如此就可以在外面通过调用setStop方法来终止while循环。

stop()和 destroy()

stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。

destroy方法也是废弃的方法。基本不会被使用到。

JDK不推荐使用stop()和destroy()来停止线程。

一般让线程自己停止下来,或者使用标志位进行终止变量,当标志位为false时,线程停止。

/**
 * 使用标志位进行终止变量,当标志位为false时,线程停止。
 */
public class TestStop implements Runnable{

    // 1.线程中定义线程体中使用的标识
    private boolean flag = true;

    @Override
    public void run() {
        // 2.线程体使用该标识
        while (true){
            System.out.println("业务逻辑");
        }
    }

    // 对外提供方法,改变标识
    public void stop() {
        this.flag = false;
    }
}
复制代码

suspend()/resume()

挂起线程,直到被resume,才会苏醒。

但调用suspend()的线程和调用resume()的线程,可能会因为争锁的问题而发生死锁,所以JDK 7开始已经不推荐使用了。

其他方法

以下是关系到线程属性的几个方法:

  1. getId

  用来得到线程ID

  2. getName和setName

  用来得到或者设置线程名称。

  3. getPriority和setPriority

  用来获取和设置线程优先级。

​ 注:优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了。线程的执行与否,取决于CPU的调度

  4. setDaemon和isDaemon

  用来设置线程是否成为守护线程和判断线程是否是守护线程。

  守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

  Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。

参考

  1. Thread详解
  2. Java并发编程:Thread类的使用
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享