多线程学习 线程安全 synchronized volatile

Posted *^O^*—*^O^*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程学习 线程安全 synchronized volatile相关的知识,希望对你有一定的参考价值。

线程安全

代码层面上:多个线程对同一个共享数据的操作(读,写),如果有写操作,就存在线程安全问题
底层的原因:原子性,可见性,有序性

原子性

一系列的操作(多行指令之间),需要具有不可分割的特性,即为原子性
如果可以被分割,指令之间,可以有其他线程并发执行的操作,对同一个共享变量操作就会产生影响
特殊的非原子性操作:n++,n–,++n,–n
1.从主存把数据读到CPU (读)
2.进行数据更新 (改)
3.把数据写回到CPU主存 (写回)
一行new对象的代码,分解为:
1.分配内存空间(Java虚拟机)
2.执行构造方法(字节码层面的构造方法:收集成员变量,实例代码块,Java代码中的构造方法)
3.把new的对象赋值给变量

可见性

线程对主存数据的操作,需要先加载到线程私有的工作内存,操作完再写回到主存线程之间的工作内存,是互相不可见的
在这里插入图片描述
共享变量,存储于主存(线程共享变量区域),线程对变量的操作:
1.加载到工作内存
2.操作(赋值,修改)
3.写回主存

重排序

多行指令(字节码指令在Java虚拟机执行,及机器码指令在CPU执行)在执行时,可能进行优化(目的式提高执行效率),只是不能重排序有关联的指令

解决方法

导致原因:是由于多个线程对共享变量操作,并发并行执行的结果;这种共享变量,称为临界资源,这种代码行,称为临界区
解决方法思路:临界区代码执行时,先申请jvm加锁,然后再执行,申请锁,是需要同一把锁来保证线程安全,申请失败,线程则需要等待(可以是阻塞式的,也可以非阻塞式)
在这里插入图片描述
这种操作,在多个线程执行临界区都申请同一把锁的情况下,多个线程运行的结果,就表现为:多个线程,依次执行临界区代码

synchronized

同步关键字
作用:基于对象头加锁的方式,只要是申请同一把锁的线程,都有同步互斥的作用
某个对象加锁:本质上是对象在内存中的对象头加锁
申请同一把锁:是否为同一个对象加锁
同步互斥:多个线程之间执行synchronized作用域范围的临界区代码,是一个个依次执行的

语法:
1)静态同步方法:使用的锁为当前的类对象
2)实例同步方法:使用的锁为当前调用的实例
3)同步代码块:使用的锁,为小括号中的对象

    //加锁:在Class<当前类>对象头加锁:使用同样锁的线程,需要依次执行
    private static synchronized void increment1() {//同步方法
        COUNT++;
    }
    private static void increment2() {
        synchronized (SynchronizedSafe.class) {//同步代码块:使用类对象加锁
            COUNT++;
        }
    }
    private static Object o = new Object();
    private static void increment3() {
        synchronized (o) {//同步代码块:使用new对象加锁
            COUNT++;
        }
    }

语法上对应申请锁——加锁——释放锁的流程
synchronized代码行:线程申请某个对象锁
synchronized作用域结束(花括号结束):自动的释放对象锁

特别注意的事项:
多线程只有申请对同一个对象加锁,才具有同同步互斥的效果
不是对同一个对象加锁,并发并行的执行(部分A加同一把锁,部分B不加锁,A中的多个线程同步互斥,但是B和A,B内部的多个线程,都是并发并行)

synchronized原理

  1. monitor机制:编译后的字节码为1个monitorenter(加锁)和多个monitorexit(释放锁)指令在这里插入图片描述

  2. monitor是对对象的监视器(具体是对象头锁状态的监视),包含计数器,加锁成功,计数器++,释放锁,计数器–,(具有重入性)

  3. synchronized本质就是monitor监视器,在jdk优化后,提供了锁升级的机制

对象头的锁状态:
在这里插入图片描述
其他优化
锁粗化:锁粗化就是将多次连接在一起的加锁,解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁

public class Test{
	private static StringBuffer sb = new StringBuffer();
	public static void main(String[] args) {
		sb.append("a");
		sb.append("b");
		sb.append("c");
	}
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁消除:删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

public class Test{
	public static void main(String[] args) {
		StringBuffer sb = new StringBuffer();
		sb.append("a").append("b").append("c");
	}
}

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。

volatile

作用:
1)保证变量(分解为字节码以后的)可见性
2)建立一个内存屏障,禁止指令重排序
原理:
保证可见性:缓存一致性协议
在这里插入图片描述
总线嗅探技术:CPU核随时都探测是否缓存中volatile修饰的变量有没有改变

建立内存屏障,禁止指令重排序
1)Java层面上看
基于主存核工作内存(CPU高速缓存)之间进行读/写操作的原子性的8个字节码指令,存在happens-before原则
2)CPU层面上看(汇编语言)
Java中的volatile修饰的变量,生成一个lock前缀指令
变量值写回主存,指令前的所有操作都必须完成
写操作不能将后续的读操作重排序到最前面

**使用场景:**代码行本身保证原子性的前提下,变量使用volatile修饰,可以保证线程安全
这里有:
1)读 (从主存读取到线程的工作内存)
2)修改一个常量值 (把常量值写回主存)

以上是关于多线程学习 线程安全 synchronized volatile的主要内容,如果未能解决你的问题,请参考以下文章

万字狂淦多线程__(多线程学习笔记)

26 多线程——线程安全 synchronized

多线程学习(基础篇)

多线程学习(基础篇)

Java多线程学习

java多线程核心技术——synchronized同步方法与synchronized同步快