Java中的关键字synchronized

1. 介绍

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

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

2. 什么是同步?

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

为了不这种问题,Java给咱们提供了synchronized这有助于实现线程之间的通讯,使得只有一个线程访问同步资源,而其余线程等待资源变为空闲。多线程

synchronized关键字能够被用在下面一些不一样的方式中,好比一个同步块:并发

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

对方法进行同步:ide

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

3.在JVM中synchronized是如何实现的

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

若是其余线程尝试访问该同步块,则必须等待,直到当前线程执行完同步块的代码。当前线程退出后,锁将被自动释放,其它线程能够获取锁并进入同步代码块。测试

  • 对于一个synchronized块来讲,在synchronized关键字后的括号中指定的对象上获取锁;
  • 对于一个synchronized static方法,锁是在.class对象上获取的;
  • 对于synchronized实例方法来讲,锁定是在该类的当前实例上得到的,即该实例(this);

4.同步方法

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

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() 方法的自定义线程:spa

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()方法

5.同步块

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

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

咱们可使用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完成其任务以前“开始”打印数字彷佛使人担心,这只是由于咱们在中止ThreadTwo锁以前,容许线程经过System.out.println("Completed printing Numbers for " + Thread.currentThread().getName())语句。

这很好,由于咱们只想同步每一个线程中的数字序列。咱们能够清楚地看到两个线程只是经过同步for循环以正确的顺序打印数字。

6.结论

在这个例子中,咱们看到了如何在Java中使用synchronized关键字来实现多个线程之间的同步。咱们还经过例子了解了什么时候可使用synchronized方法和块。

与往常同样,您能够找到此示例中使用的代码

原文:https://stackabuse.com/synchronized-keyword-in-java/

 

做者:Chandan Singh

 

译者:lee

相关文章
相关标签/搜索