WhatsApp网页版登录WhatsApp网页版登录

WhatsApp中文版

同步代码块的锁是什么

多线程并发synchronized详解

在 java 语言中,保证线程安全性的主要手段是加锁,而 Java 中的锁主要有两种:synchronized 和 Lock,我们今天重点来看一下 synchronized 的几种用法:

类锁synchronized的使用

在开始前,让我们先记住使用synchronized是需要注意的几点:

对象锁方法锁形式:synchronized修饰普通方法,锁对象默认为this

public class ObjectSynchronizedUsage implements Runnable {
   @Override
   public void run() {
     method();
   }
  
   public synchronized void method() {
       System.out.println(Thread.currentThread().getName() + "开始");
     try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
   }
  
   public static void main(String[] args) {
    ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
    }
}

输出结果:

Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束

代码块形式:自己指定锁对象(this/自定义锁对象)

public class ObjectSynchronizedUsage implements Runnable {
   @Override
    public void run() {
      //锁为this的同步代码快形式(本质与方法锁一样),两个线程使用同一把锁(this),Thread-1必须等到Thread-0释放掉该锁后,才能执行
       synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "开始");
     try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
       }
    }
  
    public static void main(String[] args) {
    ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
    }
}

输出结果:

Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束

public class ObjectSynchronizedUsage implements Runnable {
   // 创建2把锁
    Object block1 = new Object();
    Object block2 = new Object();
   @Override
    public void run() {
       synchronized (block1) {
            System.out.println("block1锁:" + Thread.currentThread().getName() + "开始");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1锁:"+Thread.currentThread().getName() + "结束");
        }
        synchronized (block2) {
            System.out.println("block2锁:" + Thread.currentThread().getName() + "开始");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block2锁:"+Thread.currentThread().getName() + "结束");
        }
    }
  
  public static void main(String[] args) {
    ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
    }
}

输出结果:

block1锁:Thread-0开始
block1锁:Thread-0结束
block2锁:Thread-0开始  // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把
block1锁:Thread-1开始
block2锁:Thread-0结束
block1锁:Thread-1结束
block2锁:Thread-1开始
block2锁:Thread-1结束

类锁synchronized修饰静态方法

示例1:

public class ClassSynchronizedUsage implements Runnable {
   @Override
    public void run() {
        method();
    }
  //synchronized用在普通方法上,默认的锁就是this,当前实例
     public synchronized void method() {
       System.out.println(Thread.currentThread().getName() + "开始");
     try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
   }
    public static void main(String[] args) {
    ClassSynchronizedUsage instance1 = new ClassSynchronizedUsage();
    ClassSynchronizedUsage instance2 = new ClassSynchronizedUsage();
        // t1和t2对应的this是两个不同的实例,所以代码不会串行
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
    }
}

输出结果:

Thread-0开始
Thread-1开始
Thread-1结束
Thread-0结束

public class ClassSynchronizedUsage implements Runnable {
  ...
    // 将示例1中的method()方法改为静态方法,synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
    public static synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }
  ...
}

输出结果:

Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束

synchronized原理加锁和释放锁的原理

我们从字节码的角度来看下synchronized实现细节。

从上一节我知道synchronized既可以作用于方法,也可以作用于代码块。但在实现上是有区别的。 比如如下代码whatsapp网页版,使用 synchronized 作用于代码块:

public class SynchronizedTest{
   private int number;
   public void test1(){
        int i = 1;
     synchronized(this){
           number = i + 1;
     }
  }
}

使用javap命令反编译查看字节码:

> javap -verbose SynchronizedTest.class

得到如下信息:

Cgq2xl6X-COAC4hMAAEjW40t64s500.png

可以看到,编译而成的字节码中会包含monitorenter和monitorexit这两个字节码指令。

你可能已经发现了,上面的字节码中有1个monitorenter和2个monitorexit,这是因为虚拟机需要保证当异常发生时也能释放锁。因此2个monitorexit一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。

在看下synchronized 修饰方法有哪些区别:

public class SynchronizedTest{
   public synchronized void test1(){
        int i = 0;
        i = i + 1;
  }
}

使用javap查看上面方法编译后的字节码如下:

image-20230923213943899.png

从图中可以看出,被synchronized修饰的方法在被编译为字节码后,在方法的flags属性中会被标记为ACC_SYNCHRONIZED标志。当虚拟机访问一个被标记为ACC_SYNCHRONIZED的方法时,会自动在方法的开始和结束(或异常)位置添加monitorenter和monitorexit指令。

关于monitorenter和monitorexit,可以理解为一把具体的锁。在这个锁中保存着两个比较重要的属性:计数器和指针。

用一张图表示:

Cgq2xl6X-COAEskYAABd1Qkprak432.png

我们来看下monitorenter和monitorexit具体是如何工作的:

下图表现了Object,Monitor,SynchronizedQueue以及Thread状态之间的关系:

WechatIMG197.jpg

从上图可以看出任意线程对Object的访问,首先要获得Object的monitor,如果获取失败,该线程就会进入同步队列中,该线程状态变为BLOCKED。当monitor持有者释放后,在同步队列中的线程才会有机会重新获取monitor,才能继续执行。synchronized是可重入锁吗,及其实现原理?

其实在上一节中已经无意透漏出了答案——synchronized是可重入锁。

什么是可重入,可重入锁?

可重入:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另一端代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调单个线程执行时重新进入同一个子程序仍然是安全的。(来源于维基百科)

可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

示例:

public class ChildProgram extends SuperProgram {
    public static void main(String[] args) {
        ChildProgram childProgram = new ChildProgram();
        childProgram.childSomeSthing();
    }
    public synchronized void childSomeSthing (){
        superDoSomeSthing();
        System.out.println("child do Something");
    }
    @Override
    public synchronized void superDoSomeSthing() {
        System.out.println("child do Something");
        super.superDoSomeSthing();
    }
}
public class SuperProgram {
    public synchronized void superDoSomeSthing (){
        System.out.println("super,doing something");
    }
}

执行结果:

child do Something
super,doing something
child do Something

可以看到调用的三个方法均得到了执行。我们知道synchronized修饰普通方法时,使用的是对象锁,也就是ChildProgram对象。三个方法的锁都是ChildProgram对象。我们在子类中执行childSomeSthing方法时,获取了ChildProgram对象锁,然后在childSomeString时调用了重写父类的superDoSomeSthing方法whatsapp网页版,该方法的锁也是ChildProgram对象锁,然后在其中调用父类的superDoSomeSthing方法,该方法的锁也是ChildProgram对象锁。一个锁多次请求,而且都成功了,所以synchronized是可重入锁。

synchronized可重入锁的实现原理:

结合上一节中加锁和释放锁原理,不难理解:

执行monitorexit命令:

这就是synchronized的重入性,及在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会+1,释放锁后就会将monitor计数器-1,线程不需要再次获取同一把锁。

保证可见性的原理:synchronized的happens-before关系

synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。为进一步了解synchronized的并发语义,通过示例代码分析这条happens-before规则,示例代码如下:

public class MonitorTest{
  private int a = 0;
  public synchronized void writer(){//1
    a++;//2
  }//3
  public synchronized void reader(){//4
    int i = a;//5
  }//6
}

在并发时,第5步操作中读取到的变量a是多少?这就需要通过happens-before规则来进行分析telegram中文版,示例代码的happens-before关系如下图所示:

WechatIMG198.jpg

上图中每一个箭头连接的两个节点就代表之间的happens-before关系:

黑色的是通过程序顺序规则推到出来。红色的为监视器规则推导而出:线程A释放锁happens-before线程B加锁。蓝色的则是通过传递性规则进一步推导的happens-before关系。

最终得到结论就是操作2 happens-before 5,通过这个关系我们可以得出以下:

根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见。那么在上面的代码中,线程A先对共享变量a进行+1,由2 happens-before 5关系可知线程A的执行结果对线程B可见及线程B所读取到的 a的值为1。

©著作权归作者所有,转载或内容合作请联系作者

相关文章

«    2025年8月    »
123
45678910
11121314151617
18192021222324
25262728293031

控制面板

您好,欢迎到访网站!
  查看权限

网站分类

最近发表

最新留言

    文章归档

    标签列表

    友情链接