Java—多线程基础

1,686 阅读7分钟

基本概念

进程

所谓进程就是运行在操作系统的一个任务,进程是计算机任务调度的一个单位,操作系统在启动一个程序的时候,会为其创建一个进程,JVM就是一个进程。进程与进程之间是相互隔离的,每个进程都有独立的内存空间。

计算机实现并发的原理是:CPU分时间片,交替执行,宏观并行,微观串行。同理,在进程的基础上分出更小的任务调度单元就是线程,我们所谓的多线程就是一个进程并发多个线程。

线程

在上面我们提到,一个进程可以并发出多个线程,而线程就是最小的任务执行单元,具体来说,一个程序顺序执行的流程就是一个线程,我们常见的main就是一个线程(主线程)。

线程的组成

想要拥有一个线程,有这样的一些不可或缺的部分,主要有:CPU时间片,数据存储空间,代码。

CPU时间片都是有操作系统进行分配的,数据存储空间就是我们常说的堆空间和栈空间,在线程之间,堆空间是多线程共享的,栈空间是互相独立的,这样做的好处不仅在于方便,也减少了很多资源的浪费。代码就不做过多解释了,没有代码搞个毛的多线程。

线程的创建和启动
传统创建线程有两种方式
  1. 继承Thread类,覆盖run方法

  2. 实现Runnable接口,覆盖run方法

Runnable并不是线程对象,而是一个任务对象。那么Runnable和Thread有什么样的关系呢?

通过查阅API,我们发现创建一个线程除了使用Thread的无参构造方法以外有一个有参构造方法是这样 :Thread(Runnable target),通过这个方法会分配一个新的Thread 对象。

其中的参数是一个类型为Runnable的target属性。

Runnable接口最大的作用就是为非Thread子类的类提供了一种实现线程的方式,只需要实现Runnable接口就可以借助Thread创建一个线程;另一方面,如果只想重写run方法,不想得到其他的Thread的方法,实现Runnable是一个好的选择。

JDK1.5

线程池

ExecutorService(线程池 interface)

//通过工具类中的方法能够新建一个线程池,用ExecutorService接受
ExecutorService es = Executors.newFixedThreadPool(2);

Callable对象

类似于Runnable(描述任务的interface)。

//创建一个Callable的实现类
Callable<Integer> task1 = new Callable<Integer>(){
  public Integer call() throws Exception{
    int result = 0;
    for(int i=2;i<=100;i+=2){
      result += i;
      Thread.sleep;
    }
      return result;
  }
  
}

//用Future对象接收fask1的返回值  将任务提交给线程池
Future<Integer> f = es.submit(task1);
//通过get方法获取Future中的值 在这个时候主线程主动的调取get  如果分支线程还没有结束,主线程会在这里阻塞
int result = f1.get();
//关闭线程池
es.shutdown();

从以上这段代码我们可以看到很多不一样的地方,首先在Callable对象中是可以抛出异常的,其次有返回值,在这个基础上也就引出了一个新的问题,如果接收该线程的对象?JDK1.5中也给出了解决的方法是Future对象.

启动线程

在这里我们需要明白,上面两种方式并不会让我们得到真正的线程,只是得到了线程对象,只有启动线程,才算得到了真正的线程。

通过执行start()方法能够启动一个线程,但是启动线程并不是立即执行,成功启动的线程会处于就绪状态,什么时候执行需要等到拿到时间片之后。

线程的分类

用户线程和守护(Daemon)线程。

守护线程:守护线程会一直运行,直到其他非守护线程都结束的时候,才会结束。有一个典型的守护线程就是:垃圾回收线程,和虚拟机共存亡,直到虚拟机中没有任何线程的时候虚拟机关闭的时候才会终止,简单说就是虚拟机在,它就在,虚拟机亡便亡。

线程的状态

线程的状态

上面我们提到过,一个线程在启动之后不会立马执行,而是处于就绪状态(Ready),就绪状态就是线程的状态的一种,处于这种状态的线程意味着一切准备就绪, 需要等待系统分配到时间片。为什么没有立马运行呢,因为同一时间只有一个线程能够拿到时间片运行,新线程启动的时候让它启动的线程(主线程)正在运行,只有等主线程结束,它才有机会拿到时间片运行。

**线程的状态:**初始状态(New),就绪状态(Ready),运行状态(Running)(特别说明:在语法的定义中,就绪状态和运行状态是一个状态Runable),等待状态(Waitering),终止状态(Terminated)

RUNNABLE),等待状态(Waitering),终止状态(Terminated)

初始状态(New)

线程对象被创建出来,便是初始状态,这时候线程对象只是一个普通的对象,并不是一个线程。

Runable

**就绪状态(Ready):**执行start方法之后,进入就绪状态,等待被分配到时间片。

**运行状态(Running):**拿到CPU的线程开始执行。处于运行时间的线程并不是永久的持有CPU直到运行结束,很可能没有执行完毕时间片到期,就被收回CPU的使用权了,之后将会处于等待状态。

等待状态(Waiting)

等待状态分为有限期等待和无限期等待,所谓有限期等待是线程使用sleep方法主动进入休眠,有一定的时间限制,时间到期就重新进入就绪状态,再次等待被CPU选中。

而无限期等待就有些不同了,无限期并不是指永远的等待下去,而是指没有时间限制,可能等待一秒也可能很多秒。至于进入等待的原因也不尽相同,可能是因为CPU时间片到期,也可能是因为一个比较耗时的操作(数据库),或者主动的调用join方法。

wait和sleep的区别

wait sleep
wait()方法是Object类里的方法 sleep()是Thread类的static(静态)的方法
wait()睡眠时,释放对象锁 sleep()睡眠时,保持对象锁,仍然占有该锁
常用于线程间通信 常用于暂停执行
wait和notify/notifyAll是成对出现的, 必须在synchronize块中被调用
阻塞状态(Blocked)

在我看来,阻塞状态实际上是一种比较特殊的等待状态,处于其他等待状态的线程是在等着别的线程执行结束,等着拿CPU的使用权;而处于阻塞状态的线程等待的不仅仅是CPU的使用权,主要是锁标记,没有拿到锁标记,即便是CPU有空也没有办法执行。(关于锁见下节:线程同步)

等待和阻塞的区别

等待 阻塞
已经拿到锁对象,或者说不存在拿不到执行不了的情况 等待拿到锁对象
等待被唤醒 等待拿到锁对象
终止线程(Terminated)

已经终止的线程会处于该种状态。

总结

总体上来说,作为一个线程挺倒霉的,首先,不会知道自己什么时候被选中;其次在执行过程中随时可能被打断让出CPU,最后碰到数据库等耗时的操作也要让出CPU去等待,并且就算数据准备好了, 仍然需要等着被挑选。