Java中的关键字Synchronized

news/2024/5/17 15:21:44 标签: java, synchronized, 同步块, 同步方法

介绍

Java并发系列的文章中,这个是第二篇文章。在前面的一篇文章中,我们学习了Java中的Executor池和Excutors的各种类别。

在这篇文章中,我们会学习synchronized关键字以及我们在多线程的环境中如何使用。

synchronized_6">synchronized关键字是什么?

在一个多线程的环境中,多个线程同时访问相同的资源的情况是存在的。例如,两个线程试图写入同一个文本文件。它们之间没有任何的同步,当两个或多个线程对同一文件具有写访问权时,写入该文件的数据可能会损坏。
同理,在JVM中,每个线程在自己的栈上面都存储了自己变量的一份复制。某些其他线程可能会更改这些变量的实际值。但是被更改后的值是不会被刷新到另外一个线程的本地复制中。
这可能导致程序执行错误和非确定性行为。

为了避免这种问题,Java给我们提供了synchronized关键字,其作用类似于对特定资源的锁定。这个有助于实现线程之间的通信,只有一个线程可以访问同步资源,而其它的线程都必须
等待直到可以访问资源。

synchronized关键字可以被用在下面一些不同的方式中,比如一个同步块:

synchronized(someobject){
	//thread-safe code here
}

对方法进行同步:

public synchronized void someMethod(){
	//thread-safe code here
}

synchronized_31">在JVM中synchronized是如何实现的

当一个线程试图进入一个同步块或者同步方法中的时候,它必须先获得一个同步对象上的锁。一次只可以有一个线程获取锁,并且执行块中的代码。

如果有一个线程要去访问同步块,那么它必须等待,一直等待到当前线程执行完同步的代码即可。当当前的线程执行完并且退出了同步块,同步锁也会自动的释放掉。同时任何其他
正在等待的线程是可以获取锁,并且进入同步块中。

  • 对于一个synchronized块来说,在synchronized关键字后的括号中指定的对象上获取锁;
  • 对于一个同步的静态方法,锁定是在.class对象上获取的;
  • 对于同步实例方法来说,锁定是在该类的当前实例上获得的,即该实例;

同步方法

定义同步方法就像在返回类型之前简单地包含关键字一样简单。让我们定义一种方法,以顺序方式打印1到5之间的数字。会有两个线程来访问这个方法,所以让我们来看看在没有
使用synchronized关键字它们的运行情况和我们使用关键字来锁住共享对象会发生什么:

public class NonSynchronizedMethod {

    public void printNumbers() {
        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }

        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

现在,让我们实现两个访问该对象并希望运行printNumbers()方法的自定义线程:

class ThreadOne extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

class ThreadTwo extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

这些线程共享一个相同的对象NonSynchronizedMethod,它们会在这个对象上同时去调用非同步的方法printNumbers()

为了测试这个,写一个main方法来做测试:

public class TestSynchronization {  
    public static void main(String[] args) {

        NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod();

        ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod);
        threadOne.setName("ThreadOne");

        ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod);
        threadTwo.setName("ThreadTwo");

        threadOne.start();
        threadTwo.start();

    }
}

运行上面的代码,我们会得到下面的结果:

Starting to print Numbers for ThreadOne  
Starting to print Numbers for ThreadTwo  
ThreadTwo 0  
ThreadTwo 1  
ThreadTwo 2  
ThreadTwo 3  
ThreadTwo 4  
Completed printing Numbers for ThreadTwo  
ThreadOne 0  
ThreadOne 1  
ThreadOne 2  
ThreadOne 3  
ThreadOne 4  
Completed printing Numbers for ThreadOne

虽然ThreadOne先开始执行的,但是ThreadTwo先结束的。

当我们再次运行上面的程序的时候,我们会得到一个不同的结果:

Starting to print Numbers for ThreadOne  
Starting to print Numbers for ThreadTwo  
ThreadOne 0  
ThreadTwo 0  
ThreadOne 1  
ThreadTwo 1  
ThreadOne 2  
ThreadTwo 2  
ThreadOne 3  
ThreadOne 4  
ThreadTwo 3  
Completed printing Numbers for ThreadOne  
ThreadTwo 4  
Completed printing Numbers for ThreadTwo  

这些输出完全是偶然的,完全不可预测。每次运行都会给我们一个不同的输出。因为可以有更多的线程,我们可能会遇到问题。在实际场景中,在访问某种类型的共享资源(如文件或其他类型的IO)时,这一点尤为重要,而不是仅仅打印到控制台。

下面我们采用同步的方法,使用synchronized关键字:

public synchronized void printNumbers() {  
    System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + " " + i);
    }

    System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
}

代码中只是给方法添加了一个synchronized关键字,没有其它的改动。现在我们运行上面的代码,得到如下所示的结果:

Starting to print Numbers for ThreadOne  
ThreadOne 0  
ThreadOne 1  
ThreadOne 2  
ThreadOne 3  
ThreadOne 4  
Completed printing Numbers for ThreadOne  
Starting to print Numbers for ThreadTwo  
ThreadTwo 0  
ThreadTwo 1  
ThreadTwo 2  
ThreadTwo 3  
ThreadTwo 4  
Completed printing Numbers for ThreadTwo  

在这里,我们看到即使两个线程同时运行,只有一个线程一次进入synchronized方法,在这种情况下是ThreadOne。一旦完成执行,ThreadTwo就可以执行printNumbers()方法

同步块

多线程的主要目的是尽可能并行地执行任意数量的任务。但是,同步限制了必须执行同步方法或块的线程的并行性。

多线程的主要目的是尽可能并行地执行任意数量的任务。但是,同步限制了必须执行同步方法或块的线程的并行性。

但是,我们可以尝试通过在同步范围内保留尽可能少的代码来减少以同步方式执行的代码量。可能有许多场景,而不是在整个方法上同步,而是可以在方法中同步几行代码。

我们可以使用synchronized块来包含代码的那部分而不是整个方法。也就是说对于需要同步的代码块进行同步,而不是对整个方法进行同步。

由于在同步块内部执行的代码量较少,因此每个线程都会更快地释放锁定。结果,其他线程花费更少的时间等待锁定并且代码吞吐量大大增加。

让我们修改前面的例子,只同步for循环打印数字序列,实际上,它是我们示例中应该同步的唯一代码部分:

public class SynchronizedBlockExample {
    public void printNumbers() {
        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

运行结果:

Starting to print Numbers for ThreadOne  
Starting to print Numbers for ThreadTwo  
ThreadOne 0  
ThreadOne 1  
ThreadOne 2  
ThreadOne 3  
ThreadOne 4  
Completed printing Numbers for ThreadOne  
ThreadTwo 0  
ThreadTwo 1  
ThreadTwo 2  
ThreadTwo 3  
ThreadTwo 4  
Completed printing Numbers for ThreadTwo  

尽管ThreadTwoThreadOne完成其任务之前“开始”打印数字似乎令人担忧,但这只是因为我们允许线程通过System.out.println(开始为ThreadTwo打印Numbers)语句,然后停止ThreadTwo锁。

这很好,因为我们只想同步每个线程中的数字序列。我们可以清楚地看到两个线程只是通过同步for循环以正确的顺序打印数字。

结论

在这个例子中,我们看到了如何在Java中使用synchronized关键字来实现多个线程之间的同步。我们还了解了何时可以使用synchronized方法和块来举例说明。

与往常一样,您可以找到此示例中使用的代码

说明

本译文均采用java语言编写代码。

原文:https://stackabuse.com/synchronized-keyword-in-java/
作者:Chandan Singh
译者:lee

在这里插入图片描述


http://www.niftyadmin.cn/n/749028.html

相关文章

Jenkins-k8s-helm-harbor-githab-mysql-nfs微服务发布平台实战

基于 K8S 构建 Jenkins 微服务发布平台 搜小说 198200.com 实现汇总&#xff1a; 发布流程设计讲解准备基础环境 K8s环境&#xff08;部署Ingress Controller&#xff0c;CoreDNS&#xff0c;Calico/Flannel&#xff09;部署代码版本仓库Gitlab配置本地Git上传测试代码&#…

Java中的并发Executor框架

介绍 随着处理器核数的增加&#xff0c;随着对实现更高吞吐量的不断增长的需求&#xff0c;多线程API变得非常流行,Java提供了它自己的多线程框架Executor Framework。 Executor 框架是什么&#xff1f; Executor Framework包含一组用于有效管理工作线程的组件。Executor AP…

关于Java的坑问题来源于占小狼公众号今天发的几个问题

今天看公众号的时候&#xff0c;看到占小狼公众号有篇文章叫Java中的坑&#xff0c;然后就点进去看了&#xff0c;我先罗列一下问题&#xff1a; 第一个坑&#xff1a; Set<Integer> set new HashSet<>(); for (int i 0; i < 100; i) {set.add(i);set.remov…

动画 | 什么是红黑树?(与2-3-4树等价)

二分搜索树是为了快速查找而生&#xff0c;它是一颗二叉树&#xff0c;每一个节点只有一个元素&#xff08;值或键值对&#xff09;&#xff0c;左子树所有节点的值均小于父节点的值&#xff0c;右子树所有的值均大于父节点的值&#xff0c;左右子树也是一颗二分搜索树&#xf…

如何在匿名thread子类中保证线程安全

在做性能测试的过程中&#xff0c;我写了两个虚拟类ThreadLimitTimeCount和ThreadLimitTimesCount做框架&#xff0c;通过对线程的标记来完成超时请求的记录。旧方法如下&#xff1a; Overrideprotected void after() {requestMark.addAll(marks);marks new ArrayList<>…

Qt Installer Framework翻译(5-2)

创建在线安装程序 传奇小说 https://www.xs86.com 联机安装程序获取二进制安装文件中的内容以及存储库描述&#xff08;Updates.xml&#xff09;。请创建一个存储库&#xff0c;并将其上传到Web服务器。然后在用于创建安装程序的config.xml文件中指定存储库的位置。 创建存储…

leetcode17 Sum of Two Integers

Calculate the sum of two integers a and b, but you are not allowed to use the operator and -.Example 1:Input: a 1, b 2 Output: 3 Example 2:Input: a -2, b 3 Output: 1指定两个整数&#xff0c;对齐求和&#xff0c;但是不可以使用加号或者减号。 这个题16年校…

修改现有消息类让.net core项目支持Protobuf - 【无需使用 [ProtoBuf.ProtoContract] 的方法】

前言 小说搜索引擎 198200.com 第二次发博客&#xff0c;希望大家多多鼓励&#xff01;&#xff01;&#xff01; 又接无上老板的一个需求&#xff0c;需要让.net core消息发送端跟消息接收端通信的消息是protobuf格式的&#xff08;基于protobuf比json小一倍数据量&#xf…