Java多线程-基础知识

时间:2022-04-07 04:59:45

一. 进程是执行中的程序,程序是静态的(我们写完以后不运行就一直放在那里),进程是执行中的程序,是动态概念的。一个进程可以有多个线程。

二. 多线程包含两个或两个以上并发运行的部分,把程序中每个这样并发运行的部分称为线程。

  1. 基于进程的多任务处理是指:允许你的计算机同时运行两个或更多的程序。

  2. 基于线程的多任务处理是指:一个程序可以执行两个或者更多的任务。

  由于每个线程只有获取到计算机CPU的时间片才能运行,通过分配给各个线程的时间片,从宏观上可以感觉是实现了多线程间的不停切换,但是从微观上来说,还是单核的串行处理。所以后来有了多核CPU,多线程的优点如下:

  1. 在多核 CPU 中,利用多线程可以实现真正意义上的并行执行;

  2. 通过对不同任务创建不同的线程去处理,可以提升程序处理的效率;

三. Java线程的实现方式:

  1. extends Thread并重写run()方法;

  2. implements Runnable接口,把实现Runnable接口的对象作为参数传递给new Thread(new Runnable(){public void run() {}});

  3. 使用 ExecutorService、Callable、 Future 实现带返回结果的多线程

四. 线程的启动和终止

  线程的启动是调用start()方法,不是直接跑run()方法,可以看下Thread类的start()方法,它里面调用了start0()的native方法。

Java多线程-基础知识

  而native方法又是通过静态代码块中的registerNatives()方法来注册的。

Java多线程-基础知识

   registerNatives 的本地方法的定义在文件 Thread.c中,从Thread.c中可以看到start0()实际会执行JVM_StartThread()。

Java多线程-基础知识

  JVM_StartThread方法从名字上来看,似乎是在 JVM 层面去启动一个线程,如果真的是这样,那么在 JVM 层面一定会调用 Java 中定义的 run 方法。那接下来继续去找找答案。我们找到 jvm.cpp 这个文件;

Java多线程-基础知识

  JVM_ENTRY 是用来定义 JVM_StartThread 函数的,在这个函数里面创建了一个真正和平台有关的本地线程。本着打破砂锅查到底的原则,继续看看 newJavaThread 做了什么事情。  Java多线程-基础知识

  我们重点关注与一下 os::create_thread,实际就是调用平台创建线程的方法来创建线程。

  接下来就是线程的启动,会调用 Thread.cpp 文件中的 Thread::start(Thread* thread)方法,代码如下

Java多线程-基础知识

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

  线程的终止不要用stop,suspend,resume之类的Deprecated方法了,就拿 stop()来说,stop()在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态(相当于kill -9)。正确的方法是用一个volatile的boolean标识控制是否运行。

五. 线程的生命周期:

线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、 WAITING、TIME_WAITING、TERMINATED)

Java多线程-基础知识

  1. 新建状态(New):新创建了一个线程对象,但不为其分配资源

  2. 可运行状态(Runnable):JAVA 线程把操作系统中的就绪和运行两种状态统一称为“Runnable” ,线程对象创建后其他线程调用了该对象的start()方法。为其分配资源,该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 阻塞状态(BLOCKED):表示线程进入等待状态,也就是线程 因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况

  ➢  等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列

  ➢  同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程放入到锁池中

  ➢  其他阻塞:运行的线程执行 Thread.sleep 或者 join 方法或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复

  4. 等待和超时等待(WAITING和TIME_WAITING):超时等待状态,超时以后自动返回。

  5. 终止状态(TERMINATED):终止状态,表示当前线程执行完毕。

六. interrupt方法:

一开始这个东西我一直不是很理解,后来慢慢理解了这里做个整理

在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。

  • 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
  • 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
  • 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

中断的相关方法

  • public void interrupt() :将调用者线程的中断状态设为true。
  • public static boolean interrupted():判断当前线程是否被设置中断标记位。 它会做两步操作:1)返回当前线程的中断状态;2)将当前线程的中断状态设为true;
  • private native boolean isInterrupted(boolean ClearInterrupted):Tests if some Thread has been interrupted. The interrupted state is reset or not based on the value of ClearInterrupted that is passed.
如何安全的停止线程:

stop函数停止线程过于暴力,它会立即停止线程,不给任何资源释放的余地,下面介绍两种安全停止线程的方法。

1. 循环标记变量:自定义一个共享的boolean类型变量,表示当前线程是否需要中断。

  • 中断标识:volatile boolean interrupted = false;
  • 任务执行函数
Thread t1 = new Thread( new Runnable(){
public void run(){
while(!interrupted){
// 正常任务代码……
}
// 中断处理代码……
// 可以在这里进行资源的释放等操作……
}
} );
  • 中断函数
Thread t2 = new Thread( new Runnable(){
public void run(){
interrupted = true;
}
} );

2. 循环中断状态

  • 中断标识:由线程对象提供,无需自己定义。
  • 任务执行函数
Thread t1 = new Thread( new Runnable(){
public void run(){
while(!Thread.currentThread.isInterrupted()){
// 正常任务代码……
}
// 中断处理代码……
// 可以在这里进行资源的释放等操作……
}
} );
  • 中断函数
t1.interrupt();

  上述两种方法本质一样,都是通过循环查看一个共享标记为来判断线程是否需要中断,他们的区别在于:第一种方法的标识位是我们自己设定的,而第二种方法的标识位是Java提供的。除此之外,他们的实现方法是一样的。

  上述两种方法之所以较为安全,是因为一条线程发出终止信号后,接收线程并不会立即停止,而是将本次循环的任务执行完,再跳出循环停止线程。此外,程序员又可以在跳出循环后添加额外的代码进行收尾工作。

七、深入分析Sychronized的实现原理

  记得刚刚学习Java的时候,Sychronized是我们百试不爽的杀手锏,一有并发问题第一个想到的就是sychronized,这个确实能够解决并发问题,然而sychronized真的那么好吗(虽然Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了)?

  Synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

  Java中每个对象都可以作为锁对象,这是Sychronized实现同步的基础。这个对象有些地方也叫做monitor。

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象

  当一个线程访问同步代码块时,它需要首先得到锁才能执行同步代码块,当退出或者抛出异常是必须要释放锁。同步代码块是使用monitorenter和monitorexit指令来实现的,同步方法会在方法的修饰符上access_flags字段中的synchronized标志位置1实现的。

  对象头:synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

  Mark Word存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。32位虚拟机的mark word如下

Java多线程-基础知识

  锁主要存在四中状态依次是:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。之所以引入偏向锁,轻量级锁(包括自旋锁),目的是为了避免使用重量级锁,避免挂起线程,造成线程切换(从内核态切换到用户态)。

  1. 偏向锁:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。只需要检查对象头的mark word是否为偏向锁、锁标识为以及ThreadID即可

获取锁:

  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01。
  2. 若为可偏向状态,则测试线程id是否是当前线程id,如果是则执行步骤(5),否则执行步骤(3)。
  3. 如果线程id不为当前线程id,则通过CAS操作竞争锁,如果竞争成功则将Mark Word的线程id替换为当前线程id,否则执行线程(4)。
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
  5. 执行同步代码块。

释放锁:偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
  2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态;

Java多线程-基础知识

  2. 轻量级锁:引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁

获取锁:

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
  3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;

释放锁 轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

下图是轻量级锁的获取和释放过程:

Java多线程-基础知识

  3. 重量级锁:重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

推荐阅读:http://www.jianshu.com/p/40d4c7aebd66