本文主要内容:
package org.example.demo;
public class Test01 {
public static int count=0;
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main( String[] args ) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()-> Test01.incr()).start();
}
Thread.sleep(3000); //保证线程执行结束
System.out.println("运行结果:"+count);
}
}
上面代码中,理论上最后应该输出1000,实际上每次运行结果都是一个不确定的小于等于1000的数
产生这种结果的原因有两点: 线程的可见性和 原子性 , 先撇开可见性,研究下原子性。事实上,count++ 虽然在java中是一条指令,但是在CPU层面上它是三条指令,对java来说,++操作不是原子操作(线程中的原子性体系在一系列的操作/指令一旦开始就不能被打断,这个指令是不可再分割的最小的指令单元),为什么java中的count++不是原子操作? 找到target目录下的Test01.class,open in terminal:
然后输入以下命令:javap -v Test01.class ,就会得到这个类的字节码文件,找到incr()方法:
关键在12~17行: getstatic是访问一个静态变量, putstatic是设置一个静态变量, iconst 1 是把常量1压入操作数栈,然后通过iadd指令进行递增操作,java中的++操作在CPU层面并不是一个指令来完成的,因此java中的++操作不是原子操作
12: getstatic #5
15: iconst_1
16: iadd
17: putstatic #5
在一开始的代码中,1000个线程都去执行i++操作,10000个线程是无法同时去执行的,线程之间一定会去抢占CPU的时间片,存在线程的上下文切换,实际的代码执行过程中的状态可能就变成了下面这样一种状态,导致输出结果不正确。
线程本来的目的是为了并发处理任务,提升任务处理效率,如果是不同的线程去处理不同的对象,那么不存在问题,如果不同的线程访问到了同一个对象,就可能会产生问题了。在使用线程时,需要考虑到这样的场景所带来的影响。
线程安全本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。共享,是指这个数据变量可以被多个线程访问;可变,指这个变量的值在它的生命周期内是可以改变的。
一个对象是否是线程安全的,取决于它是否会被多个线程访问,以及程序中是如何去使用这个对象的。如果多个线程访问同一个共享对象,在不需额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态依然是正确的(正确性意味着这个对象的结果与我们预期规定的结果保持一致),那说明这个对象是线程安全的。
线程并行导致的数据安全问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使得线程的并行变成串行,那是不是就不存在这个问题呢,锁就是这样一种实现。
synchronized void dmeo(){
}
Object obj = new Object();
void demo2(){
synchronized (obj){
// 线程不安全操作
}
}
如果按照锁能够锁住的范围来分,锁可以分为 实例锁(实例锁就是对象实例)和类锁;
public class SynchronizedTest {
void demo(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
new Thread(()->{
synchronizedTest1.demo();
},"t1").start();
new Thread(()->{
synchronizedTest1.demo();
},"t2").start();
}
}
上面代码中两个线程都调用同一个实例 synchronizedTest1 的demo方法,这种场景能够保证锁的互斥性,t1获得锁并且未释放前,t2只能去等待,如果代码改成下面这样,两个线程调用不同的对象的实例方法,实际上是两把锁,两个线程之间不存在互斥性:
public class SynchronizedTest {
void demo(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
new Thread(()->{
synchronizedTest1.demo();
},"t1").start();
new Thread(()->{
synchronizedTest2.demo();
},"t2").start();
}
}
类锁:
下面代码中,SynchronizedTest 提供了静态方法demo2,并进行了加锁,当两个线程去访问demo2方法,它们之间也存在互斥性:
public class SynchronizedTest {
static int i = 0;
synchronized static void demo2(){
System.out.println(Thread.currentThread().getName() + "-" +i++);
}
public static void main(String[] args) {
new Thread(()->{
SynchronizedTest.demo2();
},"t1").start();
new Thread(()->{
SynchronizedTest.demo2();
},"t2").start();
}
}
demo2与下面的写法等效,都属于类锁,类锁,对于该类的所有实例,都是互斥的
static void demo3(){
synchronized (SynchronizedTest.class){
System.out.println(Thread.currentThread().getName() + "-" +i++);
}
}
类锁和对象锁体现的是在对资源进行加锁时,锁能够互斥的范围。 如果希望保护不同对象实例的同一个方法的话,那么就需要这几个对象持有同一把锁。
要实现多线程的互斥特性,那这把锁需要些因素?
Java 代码中,使用 new 创建一个对象实例时(hotspot 虚拟机)JVM 层面实际上会创建一个instanceOopDesc 对象。
Hotspot 虚拟机采用 OOP-Klass 模型来描述 Java 对象实例,OOP(Ordinary Object Point)指的是普通对象指针,Klass 用来描述对象实例的具体类型。
Hotspot 采用instanceOopDesc 和 arrayOopDesc 来 描述对象 头,arrayOopDesc 对象用来描述数组类型,instanceOopDesc 的定义在 Hotspot 源 码的instanceOop.hpp 文件中
代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义在 Hotspot 源码oop.hpp 文件中,在普通实例对象中,oopDesc 的定义包含两个成员,分别是 _mark 和 _metadata
_mark : 表示对象标记、属于 markOop 类型,也就是 Mark World,它记录了对象和锁有关的信息
_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针
在 Hotspot 中,markOop 的定义在 markOop.hpp 文件中,代码为:
Mark word 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。Mark Word 里面存储的数据会随着锁标志位的变化而变化,Mark Word 可能变化为存储以下 5 种情况:
static void demo3(){
synchronized (SynchronizedTest.class){
System.out.println(Thread.currentThread().getName() + "-" +i++);
}
}
线程去访问上面这段加锁的代码是,会先去查找锁住的对象SynchronizedTest 的对象头中锁的状态和信息,决定线程是否有资格能够访问这个加锁保护的资源。
在java中,可以通过openjdk提供的工具打印出类的布局:
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10version>
dependency>
编写这么一个类:
public class ClassLayoutDemo {
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
接下来运行代码就可以打印这个类的布局信息(无锁状态下):
16进制存储为: 00 00 00 00 01 00 00 01 把红框里面按照这样的顺序排列(大端存储和小端存储)
64位二进制:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
最后两位是01, 代表无锁,倒数第三位是0,代表不是偏向锁
注: 上面OFFSET 为偏移量,SIZE 为占用内存大小,单位字节,那么对象头一共是12个字节96位,是启用了压缩的结果,也可以把压缩关闭:-XX:-UseCompressedOops
现在我们对程序进行加锁然后再打印:
public class ClassLayoutDemo {
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
synchronized (classLayoutDemo){
System.out.println("locking");
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
}
然后锁的状态其实只需要看红色框里面(00011000)的最后三位就可以了,它的值是000,最后2位 00代表轻量级锁,
也就是上面的代码中默认加的是轻量级锁,并不是重量级锁。
(1)Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象,oop/oopDesc 进行对应
(2)线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的
Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码
多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系。
使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。有没有办法能够实现不加锁的情况下也能满足线程安全的要求呢?
大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此在 synchronized 中,锁存在四种状态,分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态
根据竞争激烈的程度从低到高不断升级.
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步
锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等
表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了
jvm中偏向锁默认情况下是关闭的,可以通过下面的命令去打开:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
再次运行代码,查看打印出来的ClassLayoutDemo 类的布局信息:
上图中红框中的101, 1代表获得的锁是偏向锁,01 是偏向锁 的状态。在只有一个线程访问同步代码块并且偏向锁是打开的情况下,获得的锁是偏向锁;如果偏向锁关闭,只有一个线程访问同步代码块,默认获得的是轻量级锁
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:
看如下代码,主线程和子线程共同抢占同一把锁,此时获取到的必然是重量级锁:
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo=new LockDemo();
Thread t1=new Thread(()->{
synchronized (lockDemo){
System.out.println("t1 抢占到锁");
// 输出抢占到锁的状态
System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
}
});
t1.start();
synchronized (lockDemo){
System.out.println("Main 抢占锁");
System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
}
}
}
运行结果如下:
如果让主线程睡眠一会,结果又是怎样?
运行,发现主子线程都获取的是轻量级锁,锁状态00 (偏向锁默认是关闭,未启用),这是因为两个线程之间隔了5秒,线程获取锁不存在竞争关系了,因此默认都获取到了轻量级锁
补充知识:https://www.jianshu.com/p/e54415c529f0
package org.example;
import org.openjdk.jol.info.ClassLayout;
public class ClassLayoutDemo {
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
synchronized (classLayoutDemo){
System.out.println("locking");
classLayoutDemo.hashCode();
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
}