stop方法, 非常不安全, 不应该使用
此方法会立即释放此线程拥有的所有的锁, 并且停止run方法中所有正在工作的线程,可能导致操作一些数据还没有完全同步就关闭了停止了,其他线程就会拿到不安全的数据.
使用interrupt两阶段终止模式停止线程
其他线程里面interrupt需要停止的线程, 对这个线程打一个中断标记
**这个线程的run方法里面会由一个判断打断标记是否为true的判断, 如果为真, 不要抛出, 就在判断语句中处理需要执行的善后工作.**
i++的流程, 此时i为静态变量, 才能多线程共享 , i++与i–需要在主存与工作内存中进行数据切换
static int i;
i++;
i++的流程
etstatic i // 获取静态变量i的值
-----------以下是工作线程-----------
iconst_1 // 准备常量1
iadd // 自增
-----------以上是工作线程,以下是写入主内存-----------
putstatic i // 将修改后的值存入静态变量i
i++为临界区,就是一个代码块包含多线程的读与写操作
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
这四步多线程情况下按照顺序执行没有问题
但是如果在将工作线程中的数据写入到主内存之前,cpu时间片发送切换,上下文切换, 那么此时读取到的数据就不是实时的数据,之后再进行数据写入,就会操作数据覆盖
方法1 :使用synchronized,lock锁的方法阻塞式解决
synchroniezd可以完成互斥和同步
互斥就是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步就是由于线程执行的先后顺序不同,需要一个线程待定其他线程运行到某个点
方法2 :使用原子变量
synchronized实际上是用**对象锁保证了临界区内代码的原子性。**
需要锁住的临界区必须是对同一个对象加锁,同时多线程操作临界区时,不能一个线程加锁,一个不加,不然无法实现,临界区内的代码对外是不可分割的,不会被线程切换打断。
synchronized只能锁对象,如果加在方法上, 锁的就是this对象
加在静态方法上,锁住的是当前类的对象
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) {
}
}
}
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
优化, 不加锁,使用面向对象的方式完成原子性的操作
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());
}
}
一个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是操作系统提供的,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的MarkWord就会指向Monitor对象的指针,Monitor里面包含WaitSet, EntryLsit,Owner
线程栈中锁记录作为的轻量级锁。
轻量级锁是为了优化锁的性能的,虽然一个对象有多线程访问, 但是访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化,减少了传统的重量级锁使用操作系统的互斥量产生的性能消耗。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
轻量级锁的流程
正常无锁状态的两位数字为 01,轻量级锁为 00
cas交换后, 栈帧中的锁记录地址为 01 , 锁对象的Mark Word的锁就为00,表示加上了轻量级锁
如果尝试加上轻量级锁的过程中, CAS操作无法成功, 这时就是有其他线程为此锁对象加上了轻量级锁,说明存在了竞争了,这时需要进行锁膨胀, 把轻量级锁升级为重量级锁。
流程
重量级锁竞争时,还可以使用自旋来进行优化,如果当前线程自旋成功,就是持锁线程退出了synchronized代码块,释放了锁,这时候可以避免线程阻塞,防止因为阻塞带来的上下文切换
**
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。
所以引入了偏向级锁进行优化。
只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不需要重新CAS。
其他线程竞争时,就会撤销轻量级锁
markword
的最后三位为101。此时它的thread, epoch, age
都为0.以下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就会把所有对象都变为不可偏向的状态