多线程的概念
什么是多线程
操作系统在运行程序时,就会为其创建一个进程,我们可以把进程理解为“运行中的程序”,它拥有独立的地址空间,包括文本区域(text region)、数据区域(data region)和堆栈(stack region);而一个进程又能够创建多个线程,这些线程拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
多线程的好处
Java作为在服务端广泛使用的语言,内置了对多线程的支持。相对于我们熟悉的串行执行的单线程程序,多线程程序可能因为多线程竞争发生意想不到的错误,所以需要我们对多线程有一个深入了解。通过使用多线程,我们可以把计算任务分配给多个处理器,提高效率;也可以提高程序的响应时间,比如最近我在做一个项目设计到TCP传输消息给服务器,服务器端通过校验解析后将信息推送给客户端并保存到数据库,然后把响应消息通过TCP连接返回,如果使用单线程必须要等到消息推送和保存完毕之后才能够返回响应信息,响应时间很长,通过多线程在校验完消息后就可以直接返回响应消息,通过另外一个线程进行推送保存,这样就缩短了响应时间。
创建线程
有两种创建线程的方式:实现接口以及继承Thread类。
实现接口
通过实现Runnable接口重写run方法。
public class StartThread {
public static void main(String[] args) {
new Thread(new ToyThread()).start();
}
private static class ToyThread implements Runnable {
@Override
public void run() {
System.out.println("this is ToyThread");
}
}
}
继承Thread类
public class StartThread {
public static void main(String[] args) {
new ToyThread().start();
}
private static class ToyThread extends Thread {
@Override
public void run() {
System.out.println("this is ToyThread");
}
}
}
不管是实现Runnable接口还是继承Thread类,在新建了线程对象了通过调用start方法,线程处于就绪状态等待操作系统调用。
线程状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程对象被构建,但还没有调用start()方法 |
RUNNABLE | 运行状态,线程可能正在等待操作系统调用,也可能正在运行 |
BLOCKED | 阻塞状态,表示线程等待获取锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要其他线程做出特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,可以在等待指定时间后自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕,或者产生异常结束 |
线程状态转换:
synchronized同步锁
为了防止多个线程对共享资源进行访问读写时引发的线程安全问题(线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的),如果不是线程安全的,我们就需要对共享变量进行互斥同步,可以使用基于JVM实现的synchronized同步锁。
synchronized是Java关键字,是基于JVM实现的同步锁,能够实现对共享资源的互斥同步。synchronized实现同步的基础:Java中的每一个对象都可以作为锁,表现在以下三种形式:
- 对于普通的以synchronized修饰的同步方法,锁是当前的实例对象。
- 对于synchronized修饰的静态方法,锁是当前类的Class对象。
- 对于同步代码块,锁是synchronized括号配置的对象。
当一个线程试图访问同步代码块时,首先必须要得到锁,退出或抛出异常时必须要释放锁;Java的内置锁是一种互斥锁,如果有其他线程先于当前线程获得锁,那么当前线程就处于Blocked状态,等待其他线程把锁释放了才能访问同步代码块。为什么说synchronized通过对象获取锁?下面将介绍在HotSpot虚拟机中对象的对象头。
对象头
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),synchronized用的锁存在Java对象头,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果是非数组对象,则用2字宽存储对象头,在32位虚拟机中,一字宽等于4字节,即32bit,我们重点关注对象头与同步锁密切相关的Mark Word:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 |
32/64bit | Array | 数组的长度(如果当前对象是数组) |
32位JVM的Mark Word的默认存储结构:
为了尽可能多的存储信息,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。从图中可以看到,锁一共有四种状态,从级别由低到高一次是:无锁态、偏向锁状态、轻量级锁状态和重量级锁状态,这几种状态随着竞争状况逐渐升级。这是JDK1.6为了减少获得锁和释放锁带来的性能消耗对同步锁进行了优化。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来枷锁和解锁,只需要测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1:如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
volatile
volatile是Java关键字,它在多线程程序中保证了共享变量的“可见性”,即当一个线程修改了共享变量,另外一个线程能读到这个修改的值。Java并发模型如何实现共享变量的可见性呢?下面将介绍Java内存模型:
Java内存模型JMM
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意如图所示:
线程A和线程B都有主内存中共享变量的副本,线程A在执行过程中修改了共享变量副本的值,当线程A需要把修改后的值让其他线程也收到,就需要把修改后的值刷新到主内存,随后线程B到主内存读取修改后的值,这是线程B读取的共享变量的值就与线程A一致了。
那么将贡献变量副本的值刷新到主内存以及强制到主内存读取刷新后的共享变量的值就是volatile的功效了。volatile的功能实现与内存屏障(memory barrier,是一个CPU指令能确保一些特定操作执行的顺序以及影响一些数据的可见性)如果字段被volatile修饰,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
总体而言,volatile具有下列特性:
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但对于类似volatile++这种符合操作不具有原子性。
参考资料:
Java并发编程的艺术