文章目录
- 什么是线程和进程?
- 请简要描述线程与进程的关系,区别及优缺点?
- 并发和并行的区别?
- 为什么要使用多线程呢?
- 使用多线程可能带来什么问题?
- 线程的生命周期和状态
- 什么是上下文切换?
- 什么是线程死锁?如何避免死锁?
- sleep() 方法和wait() 方法区别和共同点
- 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法?
- synchronized关键字
- synchronized关键字最主要的三种使用方式
- synchronized底层原理
- JDK1.6 之后的synchronized 关键字底层做了哪些优化
- synchronized和ReentrantLock 的区别
- volatile关键字
- 并发编程的三个重要特性
- synchronized 关键字和volatile 关键字的区别
- ThreadLocal
- 线程池
- Atomic 原子类
- AQS
什么是线程和进程?
何为进程?
进程是程序的⼀次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。
何为线程?
线程与进程相似,但线程是⼀个比进程更小的执⾏单位。⼀个进程在其执行的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产生⼀个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序天⽣就是多线程程序,我们可以通过 JMX 来看⼀下⼀个普通的 Java 程序有哪些线程,代码
如下。
请简要描述线程与进程的关系,区别及优缺点?
从 JVM ⻆度说进程和线程之间的关系
图解进程和线程的关系
下图是 Java 内存区域,通过下图我们从 JVM 的⻆度来说⼀下线程和进程之间的关系。
从上图可以看出:⼀个进程中可以有多个线程,多个线程共享进程的堆和⽅法区 (JDK1.8 之后的元空间)资源,但是每个线程有⾃⼰的程序计数器、虚拟机栈 和 本地⽅法栈。
总结: 线程是进程 划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反
扩展内容
下⾯来思考这样⼀个问题:为什么程序计数器、虚拟机栈和本地⽅法栈是线程私有的呢?为什么堆和⽅法区是线程共享的呢?
程序计数器为什么是私有的?
程序计数器主要有下⾯两个作⽤:
- 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、 选择、循环、异常处理。
- 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能 够知道该线程上次运⾏到哪⼉了。
需要注意的是,如果执⾏的是 native ⽅法,那么程序计数器记录的是 undefined 地址,只有执⾏的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置。
虚拟机栈和本地⽅法栈为什么是私有的?
- 虚拟机栈: 每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法
(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。
⼀句话简单了解堆和⽅法区
堆和⽅法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并发和并行的区别?
- 并发: 同⼀时间段,多个任务都在执行 (单位时间内不⼀定同时执行);
- 并行: 单位时间内,多个任务同时执行。
- 关键在于是否同时
为什么要使用多线程呢?
先从总体上来说:
-
从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销。
-
从当代互联⽹发展趋势来说: 现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
再深⼊到计算机底层来探讨:
- 单核时代: 在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在理想情况下达到
100%了。 - 多核时代: 多核时代多线程主要是为了提⾼ CPU 利⽤率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。
使用多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题。
线程的生命周期和状态
线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调⽤start()
⽅法后开始运⾏,线程这时候处于READY(可运⾏) 状态。可运⾏状态的线程获得了 cpu 时间⽚(timeslice)后就处于 RUNNING(运⾏) 状态。当线程执⾏ wait()
⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到运⾏状态,⽽TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,⽐如通过 sleep(long millis)
⽅法或 wait(long millis)
⽅法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞) 状态。线程在执⾏Runnable 的 run()
⽅法之后将会进⼊到TERMINATED(终⽌) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运⾏中) 状态 。
什么是上下文切换?
多线程编程中⼀般线程的个数都大于CPU核心的个数,而⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的 CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。
Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下文切换和模式切换的时间消耗非常少。
什么是线程死锁?如何避免死锁?
认识线程死锁
线程死锁描述的是这样⼀种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终⽌。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。
死锁代码实现
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resources1");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resources2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resources2");
}
}
}, "线程1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resources2");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resources1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resources1");
}
}
}, "线程2").start();
}
}
输出:
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 得到执⾏然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对⽅的资源,然后这两个线程就会陷⼊互相等待的状态,这也就产⽣了死锁。上⾯的例⼦符合产⽣死锁的四个必要条件。
死锁必须具备四个条件
- 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
- 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
- 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
如何避免线程死锁?
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了
- 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资
源需要互斥访问)。 - 破坏请求与保持条件 :⼀次性申请所有的资源。
- 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放
它占有的资源。 - 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破
坏循环等待条件。
我们对线程 2 的代码修改就不会产⽣死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resources1");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resources2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resources2");
}
}
}, "线程1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resources2");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resources1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resources1");
}
}
}, "线程2").start();
}
}
上⾯的代码为什么避免了死锁的发⽣?
线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁。
sleep() 方法和wait() 方法区别和共同点
- 共同点: 两者都可以暂停线程的执行。
- 两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁 。
- 两者都可以暂停线程的执⾏。
- Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
- wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者notifyAll() ⽅法。sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout)超时后线程会⾃动苏醒。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法?
new ⼀个 Thread,线程进⼊了新建状态;调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏run() ⽅法的内容,这是真正的多线程⼯作。 ⽽直接执⾏ run() ⽅法,会把 run ⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调⽤ start ⽅法⽅可启动线程并使线程进⼊就绪状态,⽽ run ⽅法只是 thread 的⼀个普通⽅法调⽤,还是在主线程⾥执⾏。
synchronized关键字
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转换需要相对⽐较⻓的时间,时间成本相对较⾼,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官⽅对从 JVM 层⾯对synchronized 较⼤优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引⼊了⼤量的优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized关键字最主要的三种使用方式
- 修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁
- 修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份)。所以如果⼀个线程A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程B需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态
synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。 - 修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态⽅法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
synchronized 关键字的具体使⽤
手写单例模式并解释⼀下双重检验锁方式实现单例模式的原理
双重校验锁实现对象单例(线程安全)
public class Singleton {
private violate static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
// 先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (instance == null) {
// 类对象加锁
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
另外,需要注意 instance采⽤ volatile 关键字修饰也是很有必要。
instance采⽤ volatile 关键字修饰也是很有必要的, instance= new Singleton();
这段代码其实是分为三步执⾏:
- 为 instance分配内存空间
- 初始化 instance
- 将 instance指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1→3→2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和3,此时 T2 调⽤ getInstance() 后发现 instance不为空,因此返回instance,但此时 instance还未被初始化。
使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
synchronized底层原理
synchronized 关键字底层原理属于 JVM 层⾯。
① synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK ⾃带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:⾸先切换到类的对应⽬录
执⾏ javac SynchronizedDemo.java
命令⽣成编译后的 .class ⽂件,然后执⾏ javap -c -s -v -l SynchronizedDemo.class
从上⾯我们可以看出:
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
② synchronized 修饰⽅法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized方法");
}
}
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。
JDK1.6 之后的synchronized 关键字底层做了哪些优化
JDK1.6 对锁的实现引⼊了⼤量的优化,如偏向锁、轻量级锁、⾃旋锁、适应性⾃旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。注意锁可以升级不可降级,这种策略是为了提⾼获得锁和释放锁的效率。
synchronized和ReentrantLock 的区别
① 两者都是可重⼊锁
两者都是可重⼊锁。“可重⼊锁”概念是:自己可以再次获取⾃⼰的内部锁。⽐如⼀个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重⼊的话,就会造成死锁。同⼀个线程每次获取锁,锁的计数器都⾃增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM ⽽ ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前⾯我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进⾏了很多优化,但是这些优化都是在虚拟机层⾯实现的,并没有直接暴露给我们。
ReentrantLock 是 JDK 层⾯实现的(也就是 API 层⾯,需要 lock() 和 unlock() ⽅法配合try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReentrantLock ⽐ synchronized 增加了⼀些⾼级功能
相⽐synchronized,ReentrantLock增加了⼀些⾼级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- ReentrantLock提供了⼀种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实
现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是⾮公平的,可以通过ReentrantLock类的
ReentrantLock(boolean fair)
构造⽅法来制定是否是公平的。 - synchronized关键字与wait()和notify()/notifyAll()⽅法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接⼝与newCondition() ⽅法。
Condition是JDK1.5之后才有的,它具有很好的灵活性,⽐如可以实现多路通知功能也就是在⼀
个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的
Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,⽤ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能⾮常重要,⽽且是Condition接⼝默认提供的。⽽synchronized关键字就相当于整个Lock对象中只有⼀个Condition实例,所有的线程都注册在它⼀个身上。如果执⾏notifyAll()⽅法的话就会通知所有处于等待状态的线程这样会造成很⼤的效率问题,⽽Condition实例的signalAll()⽅法只会唤醒注册在该Condition实例中的所有等待线程。如果你想使⽤上述功能,那么选择ReentrantLock是⼀个不错的选择。
④ 性能已不是选择标准
volatile关键字
Java内存模型
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。
要解决这个问题,就需要把变量声明为volatile
,这就指示 JVM,这个变量是不稳定的,每次使⽤它都到主存中进⾏读取。说⽩了, volatile 关键字的主要作⽤就是①保证变量的可⻅性②不保证原子性③防⽌指令重排序。
volitale可见性验证demo
/**
* @program: Test
* @description:
* @author: GrandNovice
* @create: 2020-12-19 19:43
**/
class MyData {
volatile int number = 0;
public void add() {
this.number = 60;
}
}
/**
* 1. 验证volatile的可见性
* 1.1 假如int number = 0; number变量之前根本没有添加volatile关键字修饰
* 1.2 添加了volatile关键字之后才能穿过while循环,表明volatile使得number对main线程可见
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData(); // 资源类
// 第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 暂停3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
System.out.println(Thread.currentThread().getName() + "\t updated number value: " + myData.number);
}, "线程1").start();
// 第二个线程就是我们的main线程
while (myData.number == 0) {
// main线程就一直在这里等待循环,直到number值不等于0
// 如果触发volatile可见性,则main线程可以读取到number值的变化,则可以穿过这个循环
}
System.out.println(Thread.currentThread().getName() + "\t mission is over! main get number value: " + myData.number);
}
}
volatile不保证原子性
import java.util.concurrent.TimeUnit;
/**
* @program: Test
* @description:
* @author: GrandNovice
* @create: 2020-12-19 19:43
**/
class MyData {
volatile int number = 0;
// volatile不保证原子性
public void addPlus() {
number++; // number++在多线程下是不安全的
}
}
/**
* 2. 验证volatile不保证原子性
* 2.1 原子性含义——不可分割,完整性;某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整
*
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData(); // 资源类
// 20个线程
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
}
}, "线程" + i).start();
}
// 需要等待上面20个线程全部计算完成后,再用main线程读取number的值
// 暂停等待
while (Thread.activeCount() > 2) { // 如果线程存活数目大于2(main 和 GC),说明还没有计算完
Thread.yield(); // 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
// 只有当20个线程全部执行完main线程才能穿过这个while循环
}
// 输出结果不是20000,即证明volatile不保证原子性
System.out.println(Thread.currentThread().getName() + "\t finished and number value: " + myData.number);
}
}
如何解决volatile不保证原子性的问题?
- 加synchronized
- 使用juc下的AtomicInteger
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: Test
* @description:
* @author: GrandNovice
* @create: 2020-12-19 19:43
**/
class MyData {
volatile int number = 0;
// volatile不保证原子性
public void addPlus() {
number++; // number++在多线程下是不安全的
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement(); // i++;
}
}
/**
* 2. 验证volatile不保证原子性
* 2.1 原子性含义——不可分割,完整性;某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整
*
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData(); // 资源类
// 20个线程
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
myData.addAtomic();
}
}, "线程" + i).start();
}
// 需要等待上面20个线程全部计算完成后,再用main线程读取number的值
// 暂停等待
while (Thread.activeCount() > 2) { // 如果线程存活数目大于2(main 和 GC),说明还没有计算完
Thread.yield(); // 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
// 只有当20个线程全部执行完main线程才能穿过这个while循环
}
// 输出结果不是20000,即证明volatile不保证原子性
System.out.println(Thread.currentThread().getName() + "\t int finished and number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t AtomicInteger finished and number value: " + myData.atomicInteger);
}
}
输出:
main int finished and number value: 13428
main AtomicInteger finished and number value: 20000
并发编程的三个重要特性
- 原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的⼲扰⽽中断,要么所有的操作都执行,要么都不执行。 synchronized 可以保证代码⽚段的原⼦性。
- 可⻅性 :当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可⻅性。 - 有序性 :代码在执⾏的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执⾏顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁⽌指令进⾏重排序优化。
synchronized 关键字和volatile 关键字的区别
synchronized关键字和volatile关键字⽐较
-
volatile
关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字要好。但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块。
synchronized关键字在Java1.6之后进⾏了主要包括为了减少获得锁和释放锁带来的性能消耗⽽引⼊的偏向锁和轻量级锁以及其它各种优化之后执⾏效率有了显著提升,实际开发中使⽤synchronized 关键字的场景还是更多⼀些。 -
多线程访问volatile关键字不会发⽣阻塞,⽽synchronized关键字可能会发⽣阻塞
-
volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都能保证。
-
volatile关键字主要⽤于解决变量在多个线程之间的可见性,⽽synchronized关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
ThreadLocal简介
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。
ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果你创建了⼀个 ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get()
和 set()
⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
再举个简单的例⼦:
⽐如有两个⼈去宝屋收集宝物,这两个共⽤⼀个袋⼦的话肯定会产⽣争执,但是给他们两个⼈每个⼈分配⼀个袋⼦的话就不会出现这样的问题。如果把这两个⼈⽐作线程的话,那么ThreadLocal就是⽤来避免这两个线程竞争的。
- ThreadLocal示例
相信看了上⾯的解释,⼤家已经搞懂 ThreadLocal 类是个什么东⻄了。
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable {
// SimpleDateFormat 不是线程安全的,所以每个线程都要有⾃⼰独⽴的副本
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(obj, "" + i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter = " + formatter.get().toPattern());
}
}
从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化
值相同,其他线程也⼀样。
ThreadLocal原理
从 Thread
类 源代码可以看出 Thread
类中有⼀个 threadLocals
和 ⼀个inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,我们可以把ThreadLocalMap
理解为 ThreadLocal
类实现的定制化的 HashMap
。默认情况下这两个变量都是null,只有当前线程调⽤ ThreadLocal
类的 set
或 get
⽅法时才创建它们,实际上调⽤这两个⽅法的时候,我们调⽤的是 ThreadLocalMap
类对应的 get()
、 set()
⽅法。
通过ThreadLocal
类的 set()
⽅法,我们可以得出结论:最终的变量是放在了当前线程的ThreadLocalMap
中,并不是存在 ThreadLocal
上, ThreadLocal
可以理解为只是 ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过 Thread.currentThread()
获取到当前线程对象后,直接通过 getMap(Thread t)
可以访问到该线程的 ThreadLocalMap
对象。
ThreadLocal
内部维护的是⼀个类似 Map
的 ThreadLocalMap
数据结构, key
为当前对象的Thread
对象,值为 Object
对象。
⽐如我们在同⼀个线程中声明了两个 ThreadLocal
对象的话,会使⽤ Thread
内部都是使⽤仅有那个 ThreadLocalMap
存放数据的, ThreadLocalMap
的 key
就是 ThreadLocal
对象,value
就是 ThreadLocal
对象调⽤ set
⽅法设置的值。
ThreadLocalMap
是 ThreadLocal
的静态内部类。、
ThreadLocal 内存泄露问题
ThreadLocalMap
中使⽤的 key 为 ThreadLocal
的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal
没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap
中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove()
⽅法的时候,会清理掉 key 为 null 的记录。使⽤完ThreadLocal
⽅法后 最好⼿动调⽤ remove()
⽅法。
弱引⽤介绍:
如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。弱引⽤与软引⽤的区别在于:只具 有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾 回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。
弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。
线程池
为什么要⽤线程池?
池化技术相⽐⼤家已经屡⻅不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应
⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。
线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。
使⽤线程池的好处:
- 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
- 提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
- 提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系 统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。
实现Runnable接⼝和Callable接⼝的区别
Runnable
⾃Java 1.0以来⼀直存在,但 Callable
仅在Java 1.5中引⼊,⽬的就是为了来处理 Runnable
不⽀持的⽤例。 Runnable 接⼝不会返回结果或抛出检查异常,但是 Callable
接⼝可以。所以,如果任务不需要返回结果或抛出异常推荐使⽤Runnable
接口。
⼯具类 Executors
可以实现 Runnable
对象和Callable
对象之间的相互转换。
执⾏execute()⽅法和submit()⽅法的区别是什么呢?
execute()
⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与
否;submit()
⽅法⽤于提交需要返回值的任务。线程池会返回⼀个Future
类型的对象,通过这
个Future
对象可以判断任务是否执⾏成功,并且可以通过Future
的get()
⽅法来获取
返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤get(long timeout,TimeUnit unit)
⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
如何创建线程池
⽅式⼀:通过构造⽅法实现
⽅式⼆:通过Executor 框架的⼯具类Executors来实现
ThreadPoolExecutor 类分析
⼀个简单的线程池Demo: Runnable + ThreadPoolExecutor
线程池原理分析