多线程(二) synchronized分析

一 .synchronied 的使用

为了充分利用cpu资源,我们经常会使用多线程来合理的利用cpu空闲资源。但是在使用多线程的时候,由于多个线程存在对内存中对象的拷贝,当多个线程对一个资源进行访问的时候,会出现线程不安全的情况。为了避免这种线程不安全的情况 ,jdk提供了synchronied的方式来保证同步代码块的安全问题。

synchronid的加锁有2种类型,分别为对象锁和类锁。

第一种对象锁:

    public synchronized   void  method1(){
        while (true){
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
 public  void  method2(){
        synchronized (this){
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

    }

上面演示了2种对象锁,一种是在方法上面直接加synchronied修饰,一种是在代码块种增加synchronized(this)修饰,这2种的效果是一样的,锁对象都是当前类的实例对象。一般采用第二种,避免锁住不需要同步的代码。

第二种:类锁

public static  synchronized  void method3(){
        while (true){
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
 public  void method4(){
        synchronized(SyncDemo.class){
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("当前线程:"+Thread.currentThread().getName()+" 开始工作。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

    }

上面也演示了两种类锁的形式,一种是在静态方法上面增加synchronied,一种是在代码块种增加synchronied(类名.class)。2种类锁的实现效果一致,一般采用第二种,避免锁住不需要同步的代码。

那么对象锁和类锁的区别在那呢,我们通过实践来验证。

demo1:

    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();

        new Thread(() -> syncDemo.method1(),"t1").start();
        new Thread(() -> syncDemo.method1(),"t2").start();

    }

执行结果:

当我们使用对象锁的时候,2个线程同时调用一个对象的方法的时候,后面执行的线程会处于阻塞状态。

 

demo2:

    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();
        SyncDemo syncDemo2 = new SyncDemo();

        new Thread(() -> syncDemo.method1(),"t1").start();
        new Thread(() -> syncDemo2.method1(),"t2").start();

    }

我们创建2个对象实例,通过2个线程去去执行不同实例的同步方法,运行结果如下:

可以看出,2个线程互不干扰,没有阻塞操作。

通过demo1和demo2可以证明

当我们是使用对象锁的时候,锁住的是当前的对象,如果有多个对象实例,是不会有任何同步关系的。

 

demo3:

    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();
        new Thread(() -> SyncDemo.method3(),"t1").start();
        new Thread(() -> syncDemo.method4(),"t2").start();

    }

执行结果:

建立2个线程分别执行加了同一个类锁的方法,第一个线程会阻塞第2个线程。

从demo3可以得出结论

加了类锁的方法,如果存在多个线程同时访问的情况,无论是通过类调用,还是对象调用,都会阻塞其他线程

 

二.synchronied底层实现

我们既然知道保证线程安全的方法是通过加锁的来实现,那么锁加在哪里呢,就保存在锁对象信息里面。

在java虚拟机中,每个对象中都存在3个部分的数据,分别为对象头,实例数据,对齐填充。其中对象头中就保存了对象锁的基本信息。

 

 

对象头的信息,也就是MarkWord。

MarkWord根据操作系统的不同,存储的信息也不同,以下图32位操作系统为例:

MarkWord存储的信息随着锁标记位变化而变化。那这些锁标记是如何变化的,我们继续往下分析

 

三.synchronied锁的升级

我们知道加了synchronied之后, 代码就能实现线程安全 ,但是synchronied的最初的实现是当一个线程占用了对象锁之后,其他线程直接进行阻塞,而线程从运行状态到阻塞状态再到运行状态,是会浪费很多资源的,所以在jdk6之后, jvm对synchronied做了改良操作,也就是锁的升级操作。

1.无锁:还没有被线程调用到同步代码块的时候

2.偏向锁:只有1个线程ThreadA访问,虚拟机通过CAS操作,从对象锁中获取锁标示,如果直接能够获取到,那么就处于当前状态。这个时候对象的头的信息修改成上图偏向锁的内容,保存了当前线程id

3.轻量级锁:当ThreadA还没有执行结束,这个时候ThreadB执行到了同步代码块,发现对象锁中已经偏向,并且保存的线程信息不是当前线程。这个时候,会暂停线程ThreaA,并将对象有的信息修改成 轻量级锁的内容。而ThreadB在这个时候,会进行自旋操作 ,不断的循环调用CAS操作。如果在这个自旋期间ThreadA执行结束,那么ThreadB会立马直接执行,如果自旋多次之后还是无法获取到锁标示,那么就锁升级生重量级锁。(自旋操作可以理解成多次重试补救操作)

4.重量级锁:重量级锁就是我们平常说的锁了,直接阻塞其他线程。

 

 

从锁的膨胀过程来看,我们可以得出其实偏向锁和轻量级锁的实现都不是真正加锁阻塞实现的。偏向锁是通过CAS实现的,轻量级锁是通过自旋操作实现的。

那么重量级锁是怎么实现的呢,重量级锁是通过对象监视器monitor来实现的。每个对象都会于一个监视器monitor关联名,我们可以理解成一把锁。

我们可以通过javap -v 类的路径 指令来查看同步代码块的编译指令,对于加了同步代码块的代码中会增加monitorenter和monitorexit指令

 

monitorenter表示去获取一个对象监视器,monitorexit表示去释放monitor监视器的所有权。

monitor监视器的底层结构如下:

其中count为重入次数,owner为持有的线程对象,waitSet为阻塞队列,entrySet为等待队列。

重量级锁的获取和释放流程如下图所示

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
synchronized 是 Java 中用于实现线程同步的关键字。它主要用于控制对共享资源的访问,确保在同一时间只有一个线程可以执行被 synchronized 修饰的代码块或方法。 synchronized 采用的是互斥锁(也称为监视器锁)的机制来实现线程的同步。每个对象都有一个关联的互斥锁,当一个线程访问 synchronized 代码块或方法时,它会尝试获取对象的互斥锁。如果锁已经被其他线程获取,那么当前线程将被阻塞,直到获得锁为止。 具体来说,synchronized 可以分为两种使用方式:synchronized 代码块和 synchronized 方法。 1. synchronized 代码块:可以用来对指定的对象或类进行加锁。当一个线程进入 synchronized 代码块时,它会尝试获取指定对象的锁。如果获取成功,则执行代码块中的代码,执行完毕后释放锁。如果获取失败,则进入阻塞状态,等待锁释放。 2. synchronized 方法:可以用来对整个方法进行加锁。当一个线程调用 synchronized 方法时,它会尝试获取该方法所属对象的锁。如果获取成功,则执行方法体中的代码,执行完毕后释放锁。如果获取失败,则进入阻塞状态,等待锁释放。 需要注意的是,synchronized 锁的是对象而非代码,也就是说,如果多个线程访问的是同一个对象的 synchronized 代码块或方法,那么它们将会相互排斥,只能有一个线程执行。但如果是多个线程分别访问不同对象的 synchronized 代码块或方法,则它们之间不会相互影响。 总结来说,synchronized 通过互斥锁机制确保了多线程对共享资源的安全访问。它的使用可以有效避免竞态条件和数据不一致的问题。然而,需要注意合理使用 synchronized,避免过多的同步操作导致性能下降。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值