【1】并发编程介绍

161 阅读13分钟

1.1 JUC 简介

在 Java 中,线程部分是一个重点,本篇文章说的 JUC 也是关于线程的。JUC 就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK1.5 开始出现的。 image.png

1.2 进程与线程

进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活 动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

总结来说:

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

1.3 线程的状态

1.3.1 线程状态枚举类

Java 线程的状态可以通过调用相应 Thread实例的 getState方法获取。该方法的返回值类型Thread.State是一个枚举类型(Enum)。Thread.State所定义的线程状态包括以下几种。

Thread.State

public enum State {
/**
* Thread state for a thread which has not yet started.
  */
  NEW,(新建)
  /**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
  */
  RUNNABLE,(准备就绪)
  /**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
  */
  BLOCKED,(阻塞)
  /**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
  */
  WAITING,(不见不散)
  /**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,(过时不候)
/**
* Thread state for a terminated thread.
* The thread has completed execution.
  */
  TERMINATED;(终结)
  }

image.png

  • NEW:一个刚创建而未启动的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程只可能有一次处于该状态。

  • RUNNABLE:该状态可以看成是一个复合的状态。它包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被JVM 的线程调度器 (Scheduler)进行调度而使之处于 RUNNING 状态。后者表示处于该状态的线程正在运行,即相应线程对象的run方法中的代码所对应的指令正在由CPU执行。当Thread 实例的 yield 方法被调用时或者由于线程调度器的原因,相应线程的状态会由RUNNING转换为READY。

  • BLOCKED:一个线程发起一个阻塞式IO (Blocking I/O)操作3后,或者试图去获得一个由其他线程持有的锁时﹐相应的线程会处于该状态。处于该状态的线程并不会占用CPU资源。当相应的IO操作完成后,或者相应的锁被其他线程释放后,该线程的状态又可以转换为RUNNABLE。

  • WAITING:一个线程执行了某些方法调用之后就会处于这种无限等待其他线程执行特定操作的状态。这些方法包括:Object.wait()、Thread.join()和LockSupport.park()。能够使相应线程从 WAITING 转换到 RUNNABLE的相应方法包括:Object.notify().Object.notifyAl1()和 LockSupport.unpark(thread)。

  • TIMED_WAITING:该状态和WAITING类似,差别在于处于该状态的线程并非无限等待其他线程执行特定操作,而是处于带有时间限制的等待状态。当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态自动转换为RUNNABLE。

  • TERMINATED:已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能有一次处于该状态。Thread实例的run方法正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。

总结

从上述描述可知,一个线程在其整个生命周期中,只可能一次处于NEW状态和TERMINATED状态。而一个线程的状态从 RUNNABLE状态转换为BLOCKED、WAITING和TIMED_WAITING 这几个状态中的任何一个状态都意味着上下文切换(Context Switch)的产生。

多线程环境中,当一个线程的状态由 RUNNABLE转换为非 RUNNABLE (BLOCKED,WAITING或者TIMED_WAITING)时,相应线程的上下文信息(即所谓的Context,包括CPU的寄存器和程序计数器在某一时间点的内容等)需要被保存,以便相应线程稍后再次进入RUNNABLE状态时能够在之前的执行进度的基础上继续前进。而一个线程的状态由非RUNNABLE状态进入RUNNABLE状态时可能涉及恢复之前保存的线程上下文信息并在此基础上前进。这个对线程的上下文信息进行保存和恢复的过程就被称为上下文切换

上下文切换会带来额外的开销,这包括保存和恢复线程上下文信息的开销、对线程进行调度的CPU时间开销以及CPU缓存内容失效(即CPU的Ll Cache、L2 Cache等)的开销。

Windows平台下,我们可以使用Windows自带的工具perfmon来监视Java程序运行过程中的上下文切换情况。

1.3.2 wait/sleep 的区别

(1)sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。

(2)sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。

(3)它们都可以被 interrupted 方法中断。

1.4 并发与并行

1.4.1 串行模式

串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。

串行是一次只能取得一个任务,并执行这个任务。

1.4.2 并行模式

并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。

1.4.3 并发

并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,==并发的重点在于它是一种现象==, ==并发描述的是多进程同时运行的现象==。但实际上,对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会。

要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:

  • 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用 队列或类似的数据结构来存放各个小任务的成果
  • 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。
  • 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率

1.4.4 小结(重点)

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点

例子:春运抢票 电商秒杀...

并行:多项工作一起执行,之后再汇总

例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

1.5 管程

管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行

JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁

执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

1.6 用户线程和守护线程

用户线程:平时用到的普通线程,自定义线程

守护线程:运行在后台,是一种特殊的线程,比如垃圾回收

当主线程结束后,用户线程还在运行,JVM 存活

Thread thread =new Thread(()->{
        System.out.println("我很勇的好吧");
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println("线程名:"+Thread.currentThread().getName()+",运行结束了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    },"t1");
    thread.start();
    try {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("main 方法运行结束了");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

image.png

如果没有用户线程,都是守护线程,JVM 结束

Thread thread =new Thread(()->{
    System.out.println("我很勇的好吧");
    try {
        TimeUnit.SECONDS.sleep(5);
        System.out.println("线程名:"+Thread.currentThread().getName()+",运行结束了");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

},"t1");
thread.setDaemon(true);
thread.start();
try {
    TimeUnit.SECONDS.sleep(3);
    System.out.println("main 方法运行结束了");
} catch (InterruptedException e) {
    e.printStackTrace();
}

image.png

  • 守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可退出了。假如当系统只剩下守护线程的时候,java虚拟机会自动退出。
  • setDaemon(true)方法必须在start()之前设置,否则报IIIegalThreadStateException异常

1.7 线程的创建

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;

public class MyThread implements Runnable{
 
	private String threadName;
 
	public MyThread(String name){
		this.threadName = name;
	}
 
	@Override
	public void run() {
		System.out.println("线程" + this.threadName + "开始运行");
	}
 
	@Override
	public String toString() {
		return super.toString();
	}
}
 
 
public class Test {
	public static void main(String[] args) {
		MyThread runnable = new MyThread("一");
 
		Thread thread1 = new Thread(runnable);
 
		thread1.start();
	}
}
  • 通过继承 Thread 类本身;
public class MyThreadExtends extends Thread{
	@Override
	public void run() {
		System.out.println("线程开始运行");
	}
}
public class Test {
	public static void main(String[] args) {
 
		MyThreadExtends myThreadExtends = new MyThreadExtends();
 
		myThreadExtends.start();
 
	}
}

  • 通过 Callable 和 Future 创建线程。

    1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

    2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

    3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

    4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

总结

在Java语言中,一个线程就是一个java.lang.Thread类的实例。因此,在Java语言中创建一个线程就是创建一个Thread类的实例,当然这离不开内存的分配。创建一个 Thread 实例与创建其他类的实例所不同的是,JVM 会为一个 Thread 实例分配两个调用栈(Call Stack)所需的内存空间。这两个调用栈一个用于跟踪Java 代码间的调用关系,另一个用于跟踪Java代码对本地代码(即 Native代码,通常是C代码)的调用关系。

一个 Thread实例通常对应两个线程。一个是JVM中的线程(或称之为Java线程),另一个是与JVM中的线程相对应的依赖于JVM宿主机操作系统的本地(Native)线程。启动一个Java线程只需要调用相应Thread实例的start方法即可。线程启动后,当相应的线程被JVM的线程调度器调度到运行时,相应Thread 实例的run方法会被JVM调用。

1.8 线程启动的说明

1.创建线程

Thread thread =new Thread(()->{
    System.out.println("我很勇的好吧");
});
thread.start();

2.调用start方法线程就启动了

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    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 */
        }
    }
}

解释: 未启动

Thread thread =new Thread(()->{
            System.out.println("我很勇的好吧");
        });
        System.out.println("状态:"+thread.getState());
//        thread.start();

image.png 1.首先判断你的状态是不是未启动状态如果不是则抛出异常 image.png

image.png

image.png 2.将该线程加入到线程组

`group.add(this);`

3.设置初始参数 started 为 false

4.调用start0()方法启动 started为true

5.当start0()启动失败时在最终方法中将该线程设置为启动失败

if (!started) {
    group.threadStartFailed(this);
}

线程组将该线程从线程组中移除

void threadStartFailed(Thread t) {
    synchronized(this) {
        remove(t);
        nUnstartedThreads++;
    }
}

start0()方法

private native void start0();
  • native调用了本地方法,我们可以通过下载官网OpenJDK查看其源码

    • thread.c

    java线程是通过start的方法启动执行的,主要是调用一个native方法start0()来启动一个线程,首先start0()这个方法是在Thread的静态块中来注册的,代码如下

public class Thread implements Runnable {

   /* Make sure registerNatives is the first thing <clinit> does. */

   private static native void registerNatives();

   static {

       registerNatives();

   }

这个registerNatives的作用是注册一些本地方法提供给Thread类来使用,比如start0()、isAlive()、currentThread()、sleep();这些都是大家很熟悉的方法。 registerNatives的本地方法的定义在文件 Thread.cThread.c定义了各个操作系统平台要用的关于线程的公共数据和操作,以下是Thread.c的全部内容

Openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c

start0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。

image.png

  • jvm.cpp

image.png

  • thread.cpp 终于在这里调用了操作系统的线程启动os::start_thread(thread);

image.png

start方法中有一个函数调用: os::start_thread(thread);,调用平台启动线程的方法,最终会调用Thread.cpp文件中的JavaThread::run()方法

总结

  • Java里面创建线程之后必须要调用start方法才能真正的创建一个线程,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run方法进行业务逻辑的处理

至此线程的创建和启动就完毕了

Thread thread =new Thread(()->{
    System.out.println("我很勇的好吧");
});
System.out.println("状态:"+thread.getState());
thread.start();
System.out.println("启动后的状态:"+thread.getState());

image.png