JAVA多线程(machao)
2021-04-27 18:03:53 0 举报
AI智能生成
java多线程, synchronized原理, ThreadLocal原理
作者其他创作
大纲/内容
synchronized
synchronized作用于「方法」或者「代码块」,保证被修饰的代码在同一时间只能被一个线程访问。
使用
thread.wait() -- 线程从运行态转换为阻塞态,同时还会释放线程的同步锁
Thread.sleep()
thread.join() -- 线程强制运行,线程强制运行期间,其他线程无法运行
Thread.yield() -- 当前线程从运行状态 回到 就绪状态
thread..notify()/thread.notifyAll() -- 唤醒阻塞态的线程
基于Monitor实现
(重量级锁/悲观锁)
(重量级锁/悲观锁)
synchronized修饰代码块时,JVM编译代码时采用「monitorenter、monitorexit」两个指令来实现代码块的同步
synchronized修饰同步方法时,JVM编译代码时采用「ACC_SYNCHRONIZED」标记符来实现方法旳同步
原理
1. Java实例对象里有对象头
Header
Mark Word
主要是保存对象运行时的关键数据(当前锁机制的记录信息)
Klass Word
指针区域指向元数据区中(JDK1.8)该对象所代表的类
Length
Data
Padding
2. 对象头里面有Mark Word,Mark Word指针指向了「monitor」对象
a. Monitor对象由C++实现
b. 每个java对象都会有一个Monitor对象
c. Monitor对象会随着java对象一同创建和销毁
3. monitor对象, 其中_count、_recursions、_owner、_WaitSet、 _EntryList体现了monitor的工作原理
变量
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
工作原理
monitorenter
1. 当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中
2. 当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。
3. 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
4. 如果其他线程调用 notify() / notifyAll() ,会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。
monitorexit
5. monitor的拥有者线程才能执行 monitorexit指令。
6. 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
总结 -- 对象里有对象头, 对象头里面有Mark Word, Mark Word指针指向了monitor
锁优化
事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。一个重量级锁,为啥还要经常使用它呢? 从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略。
自旋锁
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
为何需要自旋锁?
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒显然对CPU来说苦不吭言。其实很多时候,锁状态只持续很短一段时间,为了这段短暂的光阴,频繁去阻塞和唤醒线程肯定不值得。因此自旋锁应运而生。
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒显然对CPU来说苦不吭言。其实很多时候,锁状态只持续很短一段时间,为了这段短暂的光阴,频繁去阻塞和唤醒线程肯定不值得。因此自旋锁应运而生。
锁消除
锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
public void add(String str1){
StringBuffer sb = new StringBuffer("hello");
sb.append(str1);
}
众所周知,【StringBuffer】是线程安全的,因为内部的关键方法都是被synchronized修饰过的,
但是上述代码中,sb是局部变量,不存在竞争共享资源的现象,此时JVM会自动需要【StringBuffer】中的锁。
StringBuffer sb = new StringBuffer("hello");
sb.append(str1);
}
众所周知,【StringBuffer】是线程安全的,因为内部的关键方法都是被synchronized修饰过的,
但是上述代码中,sb是局部变量,不存在竞争共享资源的现象,此时JVM会自动需要【StringBuffer】中的锁。
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
为何需要锁租化?
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
由于【StringBuffer】是在内部方法实现的【synchronized】加锁,我们无法把锁提取到循环体外,如果没有锁粗化,此处要进行100次加锁。
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
由于【StringBuffer】是在内部方法实现的【synchronized】加锁,我们无法把锁提取到循环体外,如果没有锁粗化,此处要进行100次加锁。
锁状态
锁的升级——锁的升级是单向的、也就是说只能从低到高升级,不会出现锁的降级。
无锁
偏向锁
偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。(大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得)
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。(大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得)
步骤
1. CAS操作: 线程A的ID记录到对象Mark Word中,同时置偏向标志位1
2. 以后线程A在进入和退出同步块时不需要进行CAS操作来加锁和解锁(只需要比较对象头的Mark Word里是否存储着指向当前线程的ID)
3. 因为偏向锁不会主动释放,当线程B去尝试获取这个对象时,可以看到对象是偏向状态
4. 线程B检测对象锁偏向旳线程(线程A)是否还存活
如果挂了,则可以将对象变为无锁状态,然后偏向自己(线程B)
如果依然存活,则偏向锁升级为轻量级锁
总结: 在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。
轻量级锁
经验依据:对绝大部分的锁,在整个同步周期内都不存在竞争
步骤
虚拟机会在当前线程的栈帧中创建一个名为锁记录的空间,用于存储锁对象目前的MarkWord的拷贝
当前线程偏向锁升级
1. 当前持有偏向锁的线程A, 尝试获得偏向锁的线程B
2. 由于线程A存活, 且未退出同步代码块, 因此JVM会把偏向锁升级为轻量级锁
3. 升级过程:
a. 线程A暂停执行同步代码, 并在线程栈中分配“锁记录”内存空间
b. 线程A复制锁对象的markword到“锁记录”内存空间
c. CAS操作: 将锁对象的markword更新为指向“锁记录”的指针及markword对象的锁标志
d. 线程A获得轻量级锁
a. 线程A暂停执行同步代码, 并在线程栈中分配“锁记录”内存空间
b. 线程A复制锁对象的markword到“锁记录”内存空间
c. CAS操作: 将锁对象的markword更新为指向“锁记录”的指针及markword对象的锁标志
d. 线程A获得轻量级锁
4. 唤醒线程A, 线程B
5. 线程A继续执行同步代码, 线程B则自旋等待(自旋一定次数后锁升级为重量级锁)
多个线程争夺锁
1. 线程A, 线程B尝试获得偏向锁
2. 线程A, 线程B复制锁对象的markword到“锁记录”内存空间
3. CAS操作:将对象的Mark Word更新为指向“锁记录”的指针
如果成功,则该线程已经获取了对象的轻量级锁
如果失败,检查对象的Mark Word是否指向当前线程
如果指向当前线程,则进入同步代码块
如果没有,则自旋等待
释放锁
5. 虚拟机会检查对象的Mark Word是否还在指向当前线程的锁记录
(有可能被升级为重量级锁, Markword为ObjectMonitor的指针)
(有可能被升级为重量级锁, Markword为ObjectMonitor的指针)
6. 如果是,那么就用CAS操作把对象当前的Mark Word和线程中复制的Mark Word替换
如果替换成功,那么整个同步代码块执行完了,也就是锁已经释放了。
如果替换失败,那么就说明对象锁已升级为重量级锁(markword指向ObjectMonitor),那么就要在释放锁的同时,唤醒被挂起的其它线程。
重量级锁
基于Monitor实现
锁升级流程图
ThreadLocal
ThreadLocal只是一个简单的工具类, 它提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
作用
1. 管理生命周期与线程相同的实例对象, 例如jdbc的Connection
2. 同一个线程中使参数可以隐式传递
3. 在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式
实现的原理
1. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
2. 每个Thread维护了一个ThreadLocalMap的成员变量,这是实现ThreadLocal的核心
3. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
源码实现
threadLocal.set():向当前线程的ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
threadLocal.get(): 从当前线程的ThreadLocalMap获取值,key是ThreadLocal对象
避免内存泄露
原因
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
避免
1. 使用完 ThreadLocal ,最好手动调用 remove() 方法
2. 不使用static关键字修饰ThreadLocal变量
(不然除非持有ThreadLocal变量的类被回收, 否则ThreadLocal变量永远不会被回收)
(不然除非持有ThreadLocal变量的类被回收, 否则ThreadLocal变量永远不会被回收)
应用
- PageHelper 的PageInfo
- Hibernate 的 ThreadLocal模式 Session
- Dubbo的RpcConetxt
- 日志的MDC
- spring的声明式事务
- spring的RequestContextHolder
优化
对静态SimpleDateFormat变量的优化
使用ThreadLocal<SimpleDateFormat>变量
使用ThreadLocal<SimpleDateFormat>变量
0 条评论
下一页