java synchronized加锁和释放流程详解

 更新时间:2025年05月16日 08:40:56   作者:程序黑板报  
这篇文章主要介绍了java synchronized加锁和释放流程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

为什么需要加锁

在多线程环境中,多个线程同时运行同一个方法时,如果其中有对某一个资源就行修改处理时,可能会存在先后操作的问题,使得逻辑不一致,程序运行的结果不时我们想要的。

线程如何加锁

这里只讲synchronized进行加锁,并且只进行使用原理的阐述,其他加锁方式使用另外的篇幅。

加锁是为了避免多个线程同时进行逻辑处理时,可能会有数据不一致等情况从而影响程序的逻辑的准确性。 所以我们可以使用一个对象,给该对象设置一个锁状态标记,其他线程要进行逻辑处理时需要把该状态设置成功才能正常进行,不然就阻塞挂起。 这里问题来了,如果是我们直接在代码中添加一个状态标志,那么多线程的情况下设置这个状态下可能还是会有同时处理的情况。

这里我们可以依赖java提供的synchronized关键字。

java内存布局和监视器锁

刚刚我们提到,可以给对象设置一个锁状态标记,其实vjm已经帮我们实现了,我们平常写的java对象经过编译字节码后,是会在内存中添加一个额外的信息的,这里就涉及到另一个概念,java对象的内存布局或者说java对象的数据结构。

当我们通过new关键字来新建一个对象时,jvm会在堆内存中开辟一块内存存储该对象实例,对象实例除了拥有我们自己定义的一些属性方法等,还会拥有额外的其他的信息。

分为三块:

  • 对象头

对象头中会存储有hashcode,GC信息,锁标记等等。

  • 实例数据

实例数据就是我们自定义的各个字段和方法信息。

  • 填充对齐

简单理解为虚拟机中存储一个对象约定对象大小为8字节的整数倍,所以如果不够的话会额外占用一点空间凑数。

好了,简单说到这里就ok了,这里可以看到对象在实际运行过程中拥有锁标记的,这里称为监视器锁,实际上对象头的锁信息会更多,这里只是简单概括一下。在程序中通过synchronize关键字进行加锁的话,jvm会帮助我们标记该对象是由那个线程占有了,并且保证其他线程不会再拥有,只有当线程释放了改对象的锁后才可以重新进行锁竞争。

同时synchorize关键词能保证操作的对象是直接从内存中获取的(内存可见性)。

使用方式如下:

public class ThreadTest {

    public static void main(String[] args) {
        Task task = new Task();

        for (int i = 0; i< 50; i++) {
            new Thread(task).run();
        }

        System.out.println(task.getCount());
    }
}

class Task implements Runnable{
    private int count;

    private Object lock = new Object();

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        int total = 0;
        while (++total <= 10000) {
            synchronized (lock) {
                count++;
            }
        }
    }
}

synchronized究竟锁了谁

synchronized关键字的语法规则是定义在代码块中或者在定义方法时。

刚刚我们提到,java对象头中有锁标记,所以下面的逻辑就是对lock这个对象进行锁竞争

while (++total <= 10000) {
    synchronized (lock) {
        count++;
    }
}

而如果我们synchronized是在方法中定义的话,则是对当前类的实例进行锁竞争,这里就是C1的实例对象,也即是C1 c1 = new C1()中的c1;而如果程序中还有C1 c11 = new C1()的定义,那么是分开竞争的。也即是同一个对象才进行锁竞争。

class C1{
    private int count;

    public synchronized void run() {
        int total = 0;
        while (++total <= 10000) {
           count++;
        }
    }
}

如果对象的方法是static的,那么进行锁竞争的是类对象,这个是jvm进行class字节码加载时生成的。

class C1{
    private int count;

    public static synchronized void run() {
        int total = 0;
        while (++total <= 10000) {
           count++;
        }
    }
}

至此,我们可以把监视器锁和synchronized关键字梳理了一遍。以上的重点信息是:java对象内存布局和监视器锁以及synchronized关键字的处理逻辑。如果需要深入可以对各个点进行往下研究。

线程的等待和唤醒

wait()方法

  • 首先我们需要了解wait()方法的继承体系,他是在Object对象的基类方法,也就是说所有的对象都拥有wait()方法。一个线程调用了java的wait()方法后,当前线程会被阻塞挂起,这里的调用指的是线程里面调用了加锁对象的wait()方法。
  • 线程被阻塞挂起后是需要唤醒的,下面会讲到唤醒方法,但是也可以调用重载方法wait(long timeout),让线程被阻塞后超过一定时间还没被唤醒而自动唤醒。

notify()方法

  • notify()方法也是继承于Object对象。当某个线程调用了加锁对象的notify方法后,会唤醒之前在该对象进行获取监视器锁时失败而被阻塞的线程,如果有多个线程同时被阻塞,notify()方法只会有一个线程被唤醒,如果需要唤醒全部,则可以调用notifyAll()方法。

所以面试中会被问到wait和notify的作用,可以侧重的知识点是:

  • 1.调用wait之前一定是获取到锁的,所以要保证在synchronized块中。
  • 2.调用wait后会释放该对象的锁。
  • 3.调用notify()方法也要是获取锁, 也要保证在synchronized块中。
  • 4.调用notify()方法唤醒一个线程,调用notifyAll()方法唤醒全部被阻塞线程。
  • 5.调用notify()或者notifyAll()方法只是唤醒了其他被阻塞的线程,他们有了重新竞争锁的条件,但是当前线程还没有释放锁的,只有调用了wait()方法才会释放锁。

用一个生产者消费者模型来看看wait和notify的用法。生产者消费者模型可以简单理解为有一个容器,当里面没有数据时生产者会往里面添加数据,满了则暂停当前的工作等待消费者消费数据后通知他继续添加。

消费者会往里面拿数据,没有了数据则暂停工作等待生产者生产了数据并通知他继续消费。

public static void main(String[] args) {
        Object lock = new Object();
        AtomicInteger counter = new AtomicInteger(0);
        Queue<Integer> queue = new LinkedList<>();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    while (true) {
                        //如果队列没有数据,调用wait()方法,阻塞自己
                        if (queue.isEmpty()) {
                            try {
                                System.out.println("消费者线程阻塞");
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        //如果队列不为空,消费数据;如果线程被生产者通过notifyAll()方法唤醒后,线程重新获取到锁时是从这里执行的
                        System.out.println("消费者线程消费数据: " + queue.poll());
                        //消费者消费后,唤醒可能由于之前队列满了而主动阻塞自己的生产者
                        lock.notifyAll();

                    }

                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    while (true) {
                        //如果队列数据满了,调用wait()方法,阻塞自己
                        if (queue.size() > 10) {
                            System.out.println("生产者线程阻塞");
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        //如果队列没有满,生产数据; 如果被其他线程唤醒,在下次获取到锁的时候生产数据
                        System.out.println("生产者线程生产数据");
                        queue.add(counter.incrementAndGet());

                        //队列有数据了,唤醒之前可能没有数据而主动祖寺啊自己的消费者
                        lock.notifyAll();
                    }

                }
            }
        }).start();

    }

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java中负数的绝对值竟然不一定是正数

    Java中负数的绝对值竟然不一定是正数

    这篇文章主要介绍了Java中负数的绝对值竟然不一定是正数,文中给大家提到Java 中怎么把负数转换为正数,需要的朋友可以参考下
    2021-07-07
  • Spring Cloud Alibaba Nacos Config配置中心实现

    Spring Cloud Alibaba Nacos Config配置中心实现

    这篇文章主要介绍了Spring Cloud Alibaba Nacos Config配置中心实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • 使用Java编写一个输出九九口诀乘法表的程序

    使用Java编写一个输出九九口诀乘法表的程序

    在学习编程的过程中,编写简单的程序来实现基本的数学运算是一个很好的练习,本文将介绍如何使用Java语言编写一个程序,用于输出9*9的乘法口诀表,需要的朋友可以参考下
    2026-01-01
  • Java在运行时识别类型信息的方法详解

    Java在运行时识别类型信息的方法详解

    这篇文章主要给大家介绍了关于Java在运行时识别类型信息的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考借鉴,下面来一起看看吧
    2019-01-01
  • IDEA左侧项目结构被隐藏如何实现显示

    IDEA左侧项目结构被隐藏如何实现显示

    这篇文章主要介绍了IDEA左侧项目结构被隐藏如何实现显示问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-08-08
  • spring-session自定义序列化方式

    spring-session自定义序列化方式

    这篇文章主要介绍了spring-session自定义序列化方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 基于Zookeeper实现服务注册和服务发现功能

    基于Zookeeper实现服务注册和服务发现功能

    无论是采用SOA还是微服务架构,都需要使用服务注册和服务发现组件,本文将基于 Zookeeper 实现服务注册和服务发现功能,如果跟我一样有同样的困惑,希望可以通过本文了解其他组件如何使用 Zookeeper 作为注册中心的工作原理
    2023-09-09
  • SpringBoot整合RabbitMQ的5种模式实战

    SpringBoot整合RabbitMQ的5种模式实战

    本文主要介绍了SpringBoot整合RabbitMQ的5种模式实战,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • java实现异步线程,回调接口方式

    java实现异步线程,回调接口方式

    这篇文章主要介绍了java实现异步线程,回调接口方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • springboot如何通过session实现单点登入详解

    springboot如何通过session实现单点登入详解

    单点登录(SSO)的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,下面这篇文章主要给大家介绍了关于springboot如何通过session实现单点登入的相关资料,需要的朋友可以参考下
    2021-12-12

最新评论