多线程01
线程的状态图
-
新建(NEW):新创建了一个线程对象。
-
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
-
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
-
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。 (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。 (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
-
死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
run方法是thread里面的一个普通的方法,所以我们直接调用run方法,这个时候它是会运行在我们的主线程中的,因为这个时候我们的程序中只有主线程一个线程,所以如果有两个线程,都是直接调用的run方法,那么他们的执行顺序一定是顺序执行
启动线程方式
- 继承Thread
- 实现Runable接口
- Executors.newCachedTread(线程池,本质还是上面两种方式其中一种去实现线程)
- 使用Lamda表达式(本质还是继承Thread)
- 实现Callable接口
synchronized
synchronized概述
synchronized是Java的内建锁,用来确保线程安全,是解决并发问题的一种重要手段,synchronized可以保证在多线程状态下,每次仅有一个线程访问共享资源
synchronized的作用主要有以下三个:
- 原子性:线程互斥的访问同步代码块,可以将小原子合成大原子
- 可见性:synchronized解锁之前,必须将工作内存中的数据同步到主内存,其它线程操作该变量时每次都可以看到被修改后的值。
- 有序性:一个线程的加锁,必须等到其它线程将锁释放;一个线程要释放锁,首先要加锁。
synchronized同步原理
synchronized仅是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。
synchronized修饰代码块
public class Test implements Runnable {
@Override
public void run() {
// 加锁操作
synchronized (this) {
System.out.println("hello");
}
}
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
}
}
javap查看相应的class文件:
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。
为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
synchronized修饰方法
public class Test implements Runnable {
@Override
public synchronized void run() {
System.out.println("hello again");
}
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(test);
thread.start();
}
}
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
锁优化
JDK1.6之前,synchronized是一个重量级锁,何谓重量级锁?就是多个线程竞争同一把锁,未获得锁的线程都会被阻塞,等到持有锁的线程将锁释放之后,这些线程又被唤醒。其中线程的阻塞和唤醒都与操作系统有关,是一个极其耗费CPU资源的过程。因此为了提高synchronized的性能特地在JDK1.6做了优化
Java对象内存模型
一个Java对象由,对象标记,类型指针,真实数据,内存对齐四部分组成。
- 对象标记也称Mark Word字段,存储当前对象的一些运行时数据。
- 类型指针,JVM根据该指针确定该对象是哪个类的实例化对象。
- 真实数据自然是对象的属性值。
- 内存补齐,是当数据不是对齐数的整数倍的时候,进行调整,使得对象的整体大小是对齐数的整数倍方便寻址。典型的以空间换时间的思想。
其中对象标记和类型指针统称为Java对象头。
Mark Word字段
Mark Word用于存储对象自身运行时的数据,如hashcode,GC分代年龄,锁状态标志位,线程持有的锁,偏向线程ID,等等。
为社么Java的任意对象都可以作为锁?
在Java对象头中,存在一个monitor对象,每个对象自创建之后在对象头中就含有monitor对象,monitor是线程私有的,不同的对象monitor自然也是不同的,因此对象作为锁的本质是对象头中的monitor对象作为了锁。这便是为什么Java的任意对象都可以作为锁的原因。
当对象在无锁的状态下进行了hashcode的计算(equals方法等)时,无法进入到偏向锁的状态(因为偏向锁会在前56位中记录线程id,但是如果有hashcode时无法记录id,如内存图所示)
优化手段
偏向锁:
偏向锁针对的是锁不存在竞争,每次仅有一个线程来获取该锁,为了提高获取锁的效率,因此将该锁偏向该线程。提升性能。
偏向锁的获取:
1.首先检测是否为可偏向状态(锁标识是否设置成1,锁标志位是否为01).
2.如果处于可偏向状态,测试Mark Word中的线程ID是否指向自己,如果是,不需要再次获取锁,直接执行同步代码。
3.如果线程Id,不是自己的线程Id,通过CAS获取锁,获取成功表明当前偏向锁不存在竞争,获取失败,则说明当前偏向锁存在 锁竞争,偏向锁膨胀为轻量级锁。
偏向锁的撤销:
偏向锁只有当出现竞争时,才会出现锁撤销。
1。等待一个全局安全点,此时所有的线程都是暂停的,检查持有锁的线程状态,如果能找到说明当前线程还存活,说明还在执 行同步块中的代码,首相将该线程阻塞,然后进行锁升级,升级到轻量级锁,唤醒该线程继续执行代同步码。
2.如果持有偏向锁的线程未存活,将对象头中的线程置null,然后直接锁升级。
轻量级锁:
偏向锁考虑的是不存在多个线程竞争同一把锁,而轻量级锁考虑的是,多个线程不会在同一时刻来竞争同一把锁。
轻量级锁的获取:
1.在线程的栈帧中创建用于存储锁记录得空间,
2.并将Mark Word复制到锁记录中,(这一步不论是否存在竞争都可以执行)。
3.尝试使用CAS将对象头中得Mark word字段变成指向锁记录得指针。
4 操作成功,不存在锁竞争,执行同步代码。
5操作失败,锁已经被其它线程抢占了,这时轻量级锁膨胀为重量级锁。
轻量级锁得释放:
反替换,使用CAS将栈帧中得锁录空间替换到对象头,成功没有锁竞争,锁得以释放,失败说明存在竞争,那块指向锁记录得指针有别的线程在用,因此锁膨胀升级为重量级锁。
重量级锁:
重量级锁描述同一时刻有多个线程竞争同一把锁。
当多个线程共同竞争同一把锁时,竞争失败得锁会被阻塞,等到持有锁的线程将锁释放后再次唤醒阻塞的线程,因为线程的唤醒和阻塞是一个很耗费CPU资源的操作,因此此处采取自适应自旋来获取重量级锁来获取重量级锁。
锁的升级
无锁 – > 偏向锁 -----> 轻量级锁 ---- > 重量级锁
其它优化
自旋锁:
线程未获得锁后,不是一昧的阻塞,而是让线程不断尝试获取锁。
缺点:若线程占用锁时间过长,导致CPU资源白白浪费。
解决方式:当尝试次数(一般是10次,具体看JVM的实现)达到每个值得时候,线程挂起。
自适应自旋锁:
自旋得次数由上一次获取锁的自旋次数决定,次数稍微延长一点点。
锁消除
对于线程的私有变量,不存在并发问题,没有必要加锁,即使加锁编译后,也会去掉。
锁粗化
当一个循环中存在加锁操作时,可以将加锁操作提到循环外面执行,一次加锁代替多次加锁,提升性能。
执行时间短(加锁代码),线程数少,偏向用自旋锁
执行时间长,线程数多,用系统重量级