synchronized(Java升级计划2)

synchronized原理

为什么会出现脏读呢?

当两个线程同时对一个共享数据进行操作的时候,如下图:
image.png
会把内存中的x=1读到工作空间,然后进行+1,这个时候就有一个CPU的时间片轮询的情况。当线程1进行执行的时候,读取x=1到工作空间,然后+1,刚好CPU轮询的时间到了,此时线程1中x=2的结果还没有刷新到内存中,导致线程2读取的数据是线程1更改数值之前的,出现脏读。
这里还有一种情况是,如果内存中的x=1是static修饰的,那么会出现跳号的情况,比如:线程1拿到x+1,此时x已经改为2了,但是没有输出,然后时间片轮询线程2执行,线程2拿到的值就是2了

当一段代码块被synchronized修饰的时候,那段代码被一个线程执行时,要么全部执行,要么都不执行,当执行到一半的时候,另一个线程进行访问,就会等待这个线程执行完成后再切换。此时synchronized具有的特性:独占性排他性可见性原子性

为什么具有可见性呢?因为对于线程1中修改的值,线程2要能够感知到线程1把synchronized所包含的变量给修改了,此时对于线程2来说就具有可见性

synchronized (this){

           

}

在Java中,每一个对象都会有一个monitor对象,也就是一个监视器,

  • 当某一个线程要占有这个资源对象的时候,先看monitor的计数器是不是等于0,如果是0,那就是还没有线程占有它,这个时候线程占有这个对象,并且对这个对象的monitor进行+1,如果不为0,表示这个对象已经被其他线程占有,那么这个线程进行等待。当线程释放占有权的时候,monitor-1

为什么这里monitor是-1而不是=0呢?
同一线程可以对同一对象进行多次加锁,所以monitor可以变成2,也就是锁中锁

synchronized (this){

           synchronized (this){

		}

}

synchronized原理分析:

    1. 线程堆栈分析

如何使用jconsole进行分析?

示例代码:

/**  
 ** *修饰代码块,锁**Class**对象***/  
*public void test3(){ synchronized (Handler3.class){ //由Handler3.class对应的所有Handler3对象都共同使用这一个锁 try {  
 TimeUnit.*MINUTES*.sleep(2); System.*out*.println(Thread.*currentThread*().getName()+" is running."); } catch (InterruptedException e) {  
 e.printStackTrace(); }  
 }  
}  
  
public static void main(String[] args) {  
 Handler3 handler3 = new Handler3();  
 for (int i = 0; i < 5; i++) { new Thread(handler3::test3).start(); }  
}

image.png
先运行Java程序,然后打开软件

找到正在运行的Java进程
image.png
进行连接,可以看到程序创建的线程
image.png
点击某个线程,可以看到线程当前的状态,如图中所示,这个线程正处于等待状态
image.png

通过这个工具,可以分析程序运行时,线程的状态

Jstack分析,jstack也是Java提供的一种工具

如果配置了环境变量,那么在cmd窗口可以直接使用,如果没有,那么进入Java的安装目录/bin打开cmd
image.png
通过jconsole可以拿到这个程序的pid,或者直接去windows中的任务管理器找
image.png
使用命令jstack pid
image.png
在图中同样可以看到这个进程所创建的线程,以及线程的状态

从图中也就验证了synchronized具有排他性

Java反编译命令javap

Javap -v 类名.class文件
image.png
可以看到当中的main方法
image.png
对其中的方法一些说明
image.png
而在monitor和monitorexit当中的就是锁🔒
image.png
Synchronized传入的参数的作用也就是给了这个对象一个锁标记

有一个monitorenter,就有对应的一个monitorexit,但是会有两个monitorexit,原因是因为有一个是正常出口,一个是异常出口
image.png

上述就是代码块的加锁

如果是对方法加锁,那么反编译之后是没有monitorenter或者monitorexit这样的,为什么呢,因为这个时候Java是通过flags中的ACC_SYNCHRONIZED区分的,如果有这个flags就表示整个方法已经被加锁了,而它加锁的对象就是整个方法
image.png

image.png

在jdk1.6之前,synchronized是重量级的,为了提高synchronized的效率,加入了偏向锁和轻量级锁

细说重量级锁和偏向锁,轻量级锁

首先对于任何一个在堆中存储的对象,都包含对象头,实例变量和填充数据三方面的信息
image.png

对象头:对象头包含两部分,第一部分用于存储对象自身运行时的数据,包括GC分代年龄、哈希码、锁状态、线程持有的锁等数据,这部分的数据长度在32位和64位虚拟机中分别为32位和64位,被称为“Mark Word”。
对象头的另一部分用于存储对象的类元数据的指针,虚拟机通过这个指针可以知道对象是哪个类的实例。

实例变量:实例数据用于存储在程序代码中定义的各种类型的字段内容,也包含从父类继承来的。这部分数据的存储顺序会受到虚拟机分配策略参数和字段在代码中定义的顺序的影响。

填充数据:在HotSpot虚拟机中,对象的大小要求是8字节的整数倍,因为对象头的大小正好是8字节的一倍或两倍,而实例数据部分可能不是8字节的整数倍,所以需要凑齐8字节的整数倍,就用到了对齐填充部分,它仅仅是用于占位的作用。
image.png

偏向锁:在对象第一次被某线程占有的时候,把是否偏向锁标志置为1,锁标记置为01,写入线程号,当其他线程访问的时候进行竞争,如果多次被第一次占有的线程获取的次数比较多,那么比较线程ID,是第一次占有的就直接进行运行,如果竞争失败的话,那就会升级到轻量级锁。和无锁状态执行的时间非常接近。
CAS算法:compare and set,竞争不激烈的时候使用
锁状态升级是通过CAS算法进行升级的。
轻量级锁:线程有交替使用,互斥性不是很强的时候可以进行使用。CAS竞争失败,那么把锁标志位置为00.
重量级锁:强互斥,等待时间长。

启动锁与锁之间的转换是非常耗时的,涉及到用户线程和核心线程的转换,我们应该尽量减少这种转换,所以就诞生了自旋锁

自旋锁:线程竞争失败的时候,不是马上转换级别,而是执行空循环,比如5次或者10次,任然失败的话就会进行锁转换,由偏向锁转到轻量级锁,再由轻量级锁转到重量级锁,这里应该注意的是,锁与锁之间的转换是不会降级的,也就是说,不可能由重量级锁转换到轻量级锁。

锁消除:JIT进行编译的时候,把不必要地而锁取消。

synchronized应该注意的

参考文章深入理解Java并发之synchronized实现原理

synchronized关键字最主要有以下3种应用方式

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了

错误演示,以为加锁,其实没有加锁

public class AccountingSyncBad implements Runnable{

    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<2000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

把这段代码直接进行编译执行,输出的结果并不是4000000,为什么呢?因为同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。

如果synchronized修饰的是静态方法,那么其锁对象就是当前类的类对象

synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×