如何使用Java同步化工作

137 阅读10分钟

与Java同步的工作

计算中的同步是指在许多地方保持一组数据或文件相同的做法。它使几个线程能够访问一个共同的资源,如外部文件、类变量和数据库信息。

同步化在多线程代码中很常见。它使你的代码能够不间断地在单线程上执行。

对Java中同步化的深入了解

几个线程查询同一个资源可能会导致意外的结果。为了防止几个线程同时访问一个只允许一个线程的资源,需要进行同步化。Java的同步块,用synchronized 关键字表示,允许你同时处理几个线程。在每一种情况下,一个线程必须获得并释放方法或块上的锁。

控制多线程系统中的互斥问题是同步化的目标。

请注意以下几点。

  • Java中不可变的对象不需要同步。
  • 在Java中,变量不能被同步化。这将导致编译错误。

不同类型的同步化

下面是同步的两种形式。

  1. 进程同步:它是协调进程执行的任务现象,其方式是没有两个进程可以访问相同的公共数据和资源。

  2. 线程同步:这种同步确保在同一时间内只有一个线程可以访问共享资源。

竞赛条件

在Java中,由于使用多个线程来并发实现应用程序,可能会出现竞赛条件。在某些方面,竞赛条件类似于死锁,因为它是由多线程引起的,并可能导致严重后果。

在没有充分同步的情况下对同一对象或数据进行工作的线程可能会导致重叠操作,这就是导致竞赛条件的原因。为了更好地理解,我们先来看看竞赛条件的类型。

有几种不同的竞速情况。Criticalnon-critical 是描述竞速条件对系统影响的两个类别。

  1. 一个critical race condition ,改变了设备、系统或软件的终端状态。例如,同时转动连接到一个共享电灯的两个电灯开关会炸毁电路。当一个情况导致一个不可预见的或未定义的问题时,一个灾难性的竞赛条件就会发生。
  2. 一个non-critical race condition ,对系统、设备或程序的结果没有影响。在灯的例子中,如果灯是关闭的,同时翻开两个开关就会打开,这就是一个非关键性的竞赛条件。非关键性的竞赛条件不会导致软件的错误。

电子学和编程并不是唯一的关键和非关键的竞赛条件情况。它们可以发生在许多竞赛条件系统中。在编程的情况下,竞赛条件情况发生在由几个线程或进程执行的代码中。当众多线程/进程试图读取同一个变量,然后对其采取行动时,有几种可能的结果。

现在让我们来看看涉及竞赛条件的可能错误情况。

  1. 读取-修改-写入条件:当两个线程/进程读取一个程序的值并将其写回时,就会发生这种情况。它经常导致软件的缺陷。像前面的例子一样,这两个线程/进程预计会依次发生。第一个进程产生一个值,第二个进程读取它并返回另一个。

例如,如果针对一个支票账户的支票是连续处理的,系统将首先检查是否有足够的资金来处理支票A,然后再次检查是否有足够的资金来处理支票B。如果两张支票同时处理,系统可能会对两笔交易解释为相同的账户余额,产生透支。

  1. 先检查再行动的情况:当两个线程/进程为一个外部操作验证相同的值时,就会出现这种竞赛情况。两个线程/进程都检查该值,但只有一个可以接受。后面的线程/进程会把它读成空值。结果是,程序的下一步行动是由一个过时的或不可用的观察值决定的。例如,需要相同位置数据的地图程序如果同时运行,就不能使用对方的数据。在随后的阶段中,这些数据会被当作空数据处理。

下面的例子程序说明了竞赛条件。

在这个基本的例子中,你可以看到一个整数变量的数值增加。变量的值一个接一个地被增加,并以十种不同的方式显示。每个线程将被编号为1至9。

public class Example {
    int check = 0;

    public void incrementCheck() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        check++;
    }

    public int getCheck() {
        return check;
    }

    public static void main(String[] args) {
        Example zy = new Example();
        for (int x = 1; x < 6; x++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    zy.incrementCheck();
                    System.out.println("The output of the thread is : " + Thread.currentThread().getName() + " - " + zy.getCheck());
                }
            }).start();
        }
    }
}
The output of the thread is : Thread-0 - 3
The output of the thread is : Thread-1 - 3
The output of the thread is : Thread-2 - 3
The output of the thread is : Thread-4 - 5
The output of the thread is : Thread-3 - 4

如上图所示,这些线程的选择顺序是不可预测的,而且数值也不正确。值应该上升1,但事实并非如此。通常情况下,输出值是3,线程0、1和2共享相同的值,因此显示了一个竞赛条件。在了解了什么是竞赛条件之后,我们现在来看看如何避免它。

很明显,关键因素(改变共享资源的代码)必须被限制。此外,通过Java的synchronized 关键字,我们可以同步访问共享资源。

这可以防止在原子操作中的线程干扰。原子操作这个词指的是一组总是统一执行的操作。所有的原子操作必须在同一时间完成,否则根本无法完成。

同步方法调用应该可以避免竞赛问题。

public class Example2 {
    int check = 1;

    public void incrementCheck() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        check++;
    }

    public int getCheck() {
        return check;
    }

    public static void main(String[] args) {
        Example2 zy = new Example2();
        int x;
        for (x = 1; x < 6; x++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (zy) {
                        zy.incrementCheck();
                        System.out.println("The output of the thread is : " + Thread.currentThread().getName() + " - " + zy.getCheck());
                    }
                }
            }).start();
        }
    }
}
The output of the thread is : Thread-0 - 2
The output of the thread is : Thread-4 - 3
The output of the thread is : Thread-3 - 4
The output of the thread is : Thread-2 - 5
The output of the thread is : Thread-1 - 6

从上面的线程的结果来看,没有线程共享一个值。我们避免竞赛条件的目的就达到了。

了解同步方法和同步块

为了理解这两者,首先让我们分别看一下这两者。

同步方法

这些方法包括以下属性。

  • 同步方法锁定了整个对象。在该方法执行期间,其他线程不得访问对象中的同步方法。对于静态方法,它们被其类锁定。
  • 同步方法需要对当前对象加锁,如果是静态方法,则需要对整个类加锁。这是因为锁是在线程进入时获得的,并在线程退出时释放(自然地或通过抛出异常)。
  • 同步方法保持方法范围内的锁。
  • 一个同步的静态函数可以防止实例被改变。

同步块

同步块可以用来对方法的任何指定资源进行同步。

假设我们的方法中有100行代码,但我们只想同步10行,在这种情况下,我们可以使用同步块。如果我们把方法中的所有代码都放在同步块中,那么它的工作原理与同步方法是一样的。

让我们来看看一个使用同步块的例子程序。

将其保存为TestSynchronizedBlock1.java

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
 class Table
 {
     void printTable(int n){
         synchronized(this){//synchronized block
             for(int i=1;i<=5;i++){
                 System.out.println(n*i);
                 try{
                     Thread.sleep(10);
                 }catch(Exception e){System.out.println(e.getMessage());}
             }
         }
     }//end of the method
 }
 class MyThread1 extends Thread{
     Table t;
     MyThread1(Table t){
         this.t=t;
     }
     public void run(){
         t.printTable(5);
     }
 }
 class MyThread2 extends Thread{
     Table t;
     MyThread2(Table t){
         this.t=t;
     }
     public void run(){
         t.printTable(100);
     }
 }
 public class TestSynchronizedBlock1{
     public static void main(String[] args) throws ExecutionException, InterruptedException {
         ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(2);
         Table obj = new Table();//only one object
         MyThread1 t1=new MyThread1(obj);
         MyThread2 t2=new MyThread2(obj);
         /*
         Forcing the execution order of the threads by setting a delay of when they may run.
         Even though we synchronized so that only one thread can access the critical section
         at a time (using the synchronized block/method), the scheduling order of the threads
         is still unpredictable
          */
         scheduler.schedule(t1, 0, TimeUnit.MILLISECONDS);
         scheduler.schedule(t2, 20, TimeUnit.MILLISECONDS);
         scheduler.shutdown();
     }
 }

上述代码将输出以下内容

5
10
15
20
25
100
200
300
400
500

以下就是它的全部内容。

  • synchronized关键字是用来标识Java中属于同步线程的块。在Java中,同步块是指与一个特定对象相联系的块。在同一对象上同步的所有同步块中,只能有一个线程操作。当同步块被退出时,所有试图进入它的后续线程都会被停滞,直到该线程退出。
  • 同步块利用对象作为一个锁。当一个方法被标记为同步时,该线程拥有监控器或锁对象。在这种情况下,你会被阻断,直到其他线程释放监视器。
  • 使用同步块使你能够通过相互排除重要部分的代码来微调锁的控制。
  • 当线程离开同步块时,锁会被解锁。
  • 如果一个参数表达式评估为null,同步块可能会产生NullPointerException ,而同步方法则不会。
  • 锁定只在同步块的整个块范围内保持。
  • 一个静态方法可以在同步块的括号内锁定一个对象。

在Java中实现同步化

为了提供内部同步,采用了Java的锁概念。在Java中,每个对象都有自己的锁。在这种情况下,只要我们使用synchronized关键字,锁的概念就会发挥作用。

一个拥有对象锁的线程必须执行它的任何同步方法。锁定后,线程可以调用该对象上的任何同步方法。在成功完成同步方法后,该线程负责释放锁。

当一个线程正在执行同步方法时,其他线程不得在同一对象上执行同步方法。然而,任何非同步程序都可以由其余线程并发执行。请注意,这个锁的概念可以应用在对象层面而不是方法层面。

让我们看一下一个例子程序。

import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
 class Synchronization {
    public synchronized void greet(String tag) {
       int x;
       for (x = 1; x <= 2; x++) {
          System.out.println("Hello : ");
          try {
             Thread.sleep(10);
          } catch (InterruptedException ignored) {
          }
          System.out.println(tag);
       }
    }
 }
 
 class OurThreadExample extends Thread {
    Synchronization b;
    String tag;
    public OurThreadExample(Synchronization b, String tag) {
       super();
       this.b = b;
       this.tag = tag;
    }
    public void run() {
       b.greet(tag);
    }
 }
 
 public class SynchImp {
    public static void main(String[] args) {
       ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(2);
       Synchronization b1 = new Synchronization();
       OurThreadExample mt1 = new OurThreadExample(b1, "SECTION");
       OurThreadExample mt2 = new OurThreadExample(b1, "ENGINEERING");
       /*
         Forcing the execution order of the threads by setting a delay of when they may run.
         Even though we synchronized so that only one thread can access the critical section
         at a time (using the synchronized block/method), the scheduling order of the threads
         is still unpredictable
          */
       scheduler.schedule(mt1, 0, TimeUnit.MILLISECONDS);
       scheduler.schedule(mt2, 20, TimeUnit.MILLISECONDS);
       scheduler.shutdown();
    }
 }

该代码将输出。

Hello : 
SECTION
Hello : 
SECTION
Hello : 
ENGINEERING
Hello : 
ENGINEERING

结论

为了确保每次只有一个线程可以访问资源,需要进行同步化。我们已经研究了如何使用同步和这个概念的各个方面。