文章目录
如何停止一个线程
-
stop方法, 非常不安全, 不应该使用
此方法会立即释放此线程拥有的所有的锁, 并且停止run方法中所有正在工作的线程,可能导致操作一些数据还没有完全同步就关闭了停止了,其他线程就会拿到不安全的数据.
-
使用interrupt两阶段终止模式停止线程
其他线程里面interrupt需要停止的线程, 对这个线程打一个中断标记
**这个线程的run方法里面会由一个判断打断标记是否为true的判断, 如果为真, 不要抛出, 就在判断语句中处理需要执行的善后工作.**
i++的线程安全问题
i++的流程, 此时i为静态变量, 才能多线程共享 , i++与i--需要在主存与工作内存中进行数据切换
java
static int i;
i++;
i++的流程
etstatic i // 获取静态变量i的值
-----------以下是工作线程-----------
iconst_1 // 准备常量1
iadd // 自增
-----------以上是工作线程,以下是写入主内存-----------
putstatic i // 将修改后的值存入静态变量i
i++为临界区,就是一个代码块包含多线程的读与写操作
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
这四步多线程情况下按照顺序执行没有问题
但是如果在将工作线程中的数据写入到主内存之前,cpu时间片发送切换,上下文切换, 那么此时读取到的数据就不是实时的数据,之后再进行数据写入,就会操作数据覆盖
共享变量线程安全的解决问题
方法1 :使用synchronized,lock锁的方法阻塞式解决
synchroniezd可以完成互斥和同步
互斥就是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步就是由于线程执行的先后顺序不同,需要一个线程待定其他线程运行到某个点
方法2 :使用原子变量
synchronized
基础概念
synchronized实际上是用**对象锁保证了临界区内代码的原子性。**
需要锁住的临界区必须是对同一个对象加锁,同时多线程操作临界区时,不能一个线程加锁,一个不加,不然无法实现,临界区内的代码对外是不可分割的,不会被线程切换打断。
synchronized只能锁对象,如果加在方法上, 锁的就是this对象
加在静态方法上,锁住的是当前类的对象
java
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
java
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
java
优化, 不加锁,使用面向对象的方式完成原子性的操作
package org.example.multiThread;
import lombok.extern.slf4j.Slf4j;
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment(); //对象的操作是原子性的
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}", room.get());
}
}
java对象头
一个64位,8个字节的普通对象有32bit,就是4个字节的Mark Word
和 32bit
,4个字节的Klass Word
.
Mark Word包含了很多的信息,包括了hashcode,GC的年龄,加锁的情况等等信息。
通过Klass World找到对象的Class,就是是一个什么类型的对象。
数据对象一个96bit, 12个字节,多了32位的数据长度
一个int的基本类型,占4个字节
**一个Integer对象,对象头8个字节+int的值4个字节=12个字节**
Monitor
Monitor是操作系统提供的,每个Java对象都可以关联一个Monitor对象 ,如果使用synchronized给对象上锁之后,该对象头的MarkWord就会指向Monitor对象的指针,Monitor里面包含WaitSet, EntryLsit,Owner
- 刚开始monitor中的Owner为null。
- 当线程1执行synchronized(obj)时,这时候根据这个obj对象找到对应的Monitor,就会将Monitor的Owner置为线程1,一个Monitor只 能有一个Owner.
- 当线程1获取锁后,其他线程也进入synchronized(obj),根据obj找到对应的,就会进入EntryList,就是阻塞队列
- 线程1执行完同步代码块中的内容后, 唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
- WaitSet是之前获得过锁,但是条件不满足,进入了WAITING状态的线程。
优化
轻量级锁
线程栈中锁记录作为的轻量级锁。
轻量级锁是为了优化锁的性能的,虽然一个对象有多线程访问, 但是访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化,减少了传统的重量级锁使用操作系统的互斥量产生的性能消耗。
java
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
轻量级锁的流程
- 线程执行到synchronized时,先在线程中创建锁记录对象(Lock Record).
Lock Record有一个两位数的地址和一个对象指针组成。
对象指针指向锁对象
两位数的地址用于交换锁对象内的Mark Word
- 让锁记录中的对象地址指向锁对象, 并且尝试使用cas替换锁对象的Mark Word, 将Mark Word的值存入锁记录。
正常无锁状态的两位数字为 01,轻量级锁为 00
cas交换后, 栈帧中的锁记录地址为 01 , 锁对象的Mark Word的锁就为00,表示加上了轻量级锁
- cas也可能失败
(1)比如其他线程已经持有了该锁对象的轻量级锁, 表示有了锁的竞争, 进入锁膨胀过程
(2)==如果自己执行了synchroniezd锁重入,那么再添加一条Lock Record作为重入的计数,==这个新加的Lock Record里面会记录一个null值,这话null值只是一个标记,方便自己锁重入计数
- 当退出synchroniezd代码块时,就是解锁时,如果有取值为null的锁记录,表示有锁重入,就把重入计数-1
当解锁时,锁的记录不为null值,使用cas将Mark Word的值恢复给对象头
此时可能失败,就是锁对象的Mark Word不是00 了,说明锁膨胀了或者升级为重量级锁了,这时就要进入重量级锁的解锁流程
锁膨胀
如果尝试加上轻量级锁的过程中, CAS操作无法成功, 这时就是有其他线程为此锁对象加上了轻量级锁,说明存在了竞争了,这时需要进行锁膨胀, 把轻量级锁升级为重量级锁。
流程
- 当线程1尝试对锁对象加锁时,发现锁对象的Mark Word已经为00了,就是轻量级锁,此时进入锁膨胀过程
- 线程1先为锁对象申请一个Monitor锁, 让锁对象指向Monitor的锁地址
然后自己进入Monitor的EntryList,成为Blocked状态
- 当已经获取锁的线程退出同步代码块解锁时,使用cas将Mark Word的值恢复给锁的对象的Mark Word, 失败,这时进入重量级锁的解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList的阻塞线程
自旋优化
重量级锁竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,就是持锁线程退出了synchronized代码块,释放了锁,这时候可以避免线程阻塞,防止因为阻塞带来的上下文切换
**
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。
所以引入了偏向级锁进行优化。
只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不需要重新CAS。
其他线程竞争时,就会撤销轻量级锁
- 默认是开启偏量级锁的 ,对象创建后,
markword
的最后三位为101。此时它的thread, epoch, age
都为0. - 偏量级锁默认是延迟的,不会在程序启动后立即生效,需要等个两三秒生效,如果想要避免延迟,可以加VM参数
-XX:BiasedLockingStartupDelay=0 来禁止延迟 - 如果没有开启偏向锁,那么对象创建后,markword的最后三位值为001,这时他的hashcode, age都为0.第一次用到hashcode是才会赋值, 且偏量级锁会失效.
以下64位的操作系统
线程结束后, 偏量级锁的线程id仍然会保留
前54位就是操作系统分配的线程ID, 后10位就是对象的信息,thread,epoch,age,锁
偏向锁适合在没有多线程竞争的情况下,多次重入一个锁,优化轻量级锁.
多个线程会竞争锁的情况下, 这时需要关闭偏向锁已提升性能.
-XX:-UseBiasedLocking 禁用参数
UseBiasedLocking前面的 "-" 号会禁用
即使开启了偏量级锁,调用hashcode的时候,也会变成一个normal对象.因为hashcode只有第一次调用才会生成,生成之后就会导致markword的的其他空间被hashcode占用,变为nornaml对象.
重量级锁的hashcode和线程ID都存在monitor中,不存在markword被占用的情况
偏量级锁的撤销
-
调用
hashcode
方法,hashcode
会占用markword
的其他空间,导致可偏量变成不可偏向对象,锁也会升级为轻量级锁 -
其他线程使用偏量锁对象时,会将偏量锁升级位轻量级锁。
偏量级锁升级为轻量级锁必须要在线程错开执行的情况下,就是一个线程解锁之后,另一个线程再去获取锁。
如果在持锁过程中竞争锁, 就会升级为重量级锁。
偏量级锁的批量重定向
多线程情况下,没有竞争的获取锁,而是错开时间获取锁,一旦偏量级锁被撤销成为不可偏向锁时,就会导致接下来一直使用线程栈中的锁记录作为轻量级锁,性能低下。
当偏向锁的撤销超过20次之后, 就可以重新把不可偏向的锁以当前线程id重定向为偏量级锁,而且使当前线程接下来的所有锁都进行重定向。
偏量级锁的批量撤销
当偏向锁的撤销超过40次之后,JVM就会把所有对象都变为不可偏向的状态