Java >> Java tutorial >  >> Tag >> synchronized

Vejledning til det synkroniserede søgeord i Java

1. Oversigt

Denne hurtige tutorial vil være en introduktion til brugen af ​​synkroniseret blok i Java.

Kort sagt, i et multi-threaded miljø opstår en race-tilstand, når to eller flere tråde forsøger at opdatere mutable delte data på samme tid. Java tilbyder en mekanisme til at undgå løbsforhold ved at synkronisere trådadgang til delte data.

Et stykke logik markeret med synkroniseret bliver en synkroniseret blok, tillader kun én tråd at køre på et givet tidspunkt .

2. Hvorfor synkronisering?

Lad os overveje en typisk racetilstand, hvor vi beregner summen, og flere tråde udfører calculate() metode:

public class BaeldungSynchronizedMethods {

    private int sum = 0;

    public void calculate() {
        setSum(getSum() + 1);
    }

    // standard setters and getters
}

Så lad os skrive en simpel test:

@Test
public void givenMultiThread_whenNonSyncMethod() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(summation::calculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

Vi bruger en ExecutorService med en 3-tråds pulje til at udføre calculate() 1000 gange.

Hvis vi udførte dette serielt, ville det forventede output være 1000, men vores multi-threaded udførelse mislykkes næsten hver gang med et inkonsistent faktisk output:

java.lang.AssertionError: expected:<1000> but was:<965>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
...

Selvfølgelig finder vi ikke dette resultat uventet.

En simpel måde at undgå løbstilstanden på er at gøre operationen trådsikker ved at bruge den synkroniserede søgeord.

3. Den Synkroniserede Søgeord

Vi kan bruge den synkroniserede søgeord på forskellige niveauer:

  • Forekomstmetoder
  • Statiske metoder
  • Kodeblokke

Når vi bruger en synkroniseret blok, bruger Java internt en skærm, også kendt som skærmlås eller indre lås, til at levere synkronisering. Disse monitorer er bundet til et objekt; derfor kan alle synkroniserede blokke af det samme objekt kun have én tråd, der udfører dem på samme tid.

3.1. Synkroniseret Forekomstmetoder

Vi kan tilføje den synkroniserede nøgleord i metodeerklæringen for at gøre metoden synkroniseret:

public synchronized void synchronisedCalculate() {
    setSum(getSum() + 1);
}

Bemærk, at når vi synkroniserer metoden, passerer testcasen med det faktiske output som 1000:

@Test
public void givenMultiThread_whenMethodSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods method = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(method::synchronisedCalculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, method.getSum());
}

Forekomstmetoder er synkroniserede over forekomsten af ​​klassen, der ejer metoden, hvilket betyder, at kun én tråd pr. forekomst af klassen kan udføre denne metode.

3.2. Synkroniseret Static Metoder

Statiske metoder er synkroniserede ligesom instansmetoder:

 public static synchronized void syncStaticCalculate() {
     staticSum = staticSum + 1;
 }

Disse metoder er synkroniserede klassen objekt tilknyttet klassen. Da kun én klasse objekt eksisterer pr. JVM pr. klasse, kun én tråd kan udføres inde i en statisk synkroniseret metode pr. klasse, uanset antallet af forekomster, den har.

Lad os teste det:

@Test
public void givenMultiThread_whenStaticSyncMethod() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(BaeldungSynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, BaeldungSynchronizedMethods.staticSum);
}

3.3. Synkroniseret Blokke inden for metoder

Nogle gange ønsker vi ikke at synkronisere hele metoden, kun nogle instruktioner i den. Vi kan opnå dette ved at ansøge synkroniseret til en blok:

public void performSynchronisedTask() {
    synchronized (this) {
        setCount(getCount()+1);
    }
}

Så kan vi teste ændringen:

@Test
public void givenMultiThread_whenBlockSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(synchronizedBlocks::performSynchronisedTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, synchronizedBlocks.getCount());
}

Bemærk, at vi har sendt en parameter denne til den synkroniserede blok. Dette er monitorobjektet. Koden inde i blokken bliver synkroniseret på monitorobjektet. Kort sagt kan kun én tråd pr. skærmobjekt udføres inde i den kodeblok.

Hvis metoden var statisk , ville vi videregive klassenavnet i stedet for objektreferencen, og klassen ville være en monitor for synkronisering af blokken:

public static void performStaticSyncTask(){
    synchronized (SynchronisedBlocks.class) {
        setStaticCount(getStaticCount() + 1);
    }
}

Lad os teste blokken inde i den statiske metode:

@Test
public void givenMultiThread_whenStaticSyncBlock() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount());
}

3.4. Genindtræden

Låsen bag den synkroniserede metoder og blokke er genindtræden. Dette betyder, at den aktuelle tråd kan erhverve den samme synkroniserede lås igen og igen, mens du holder den:

Object lock = new Object();
synchronized (lock) {
    System.out.println("First time acquiring it");

    synchronized (lock) {
        System.out.println("Entering again");

         synchronized (lock) {
             System.out.println("And again");
         }
    }
}

Som vist ovenfor, mens vi er i en synkroniseret  blok, kan vi anskaffe den samme skærmlås gentagne gange.

4. Konklusion

I denne korte artikel undersøgte vi forskellige måder at bruge synkroniseret på nøgleord for at opnå trådsynkronisering.

Vi lærte også, hvordan en race-tilstand kan påvirke vores applikation, og hvordan synkronisering hjælper os med at undgå det. For mere om trådsikkerhed ved brug af låse i Java, se vores java.util.concurrent.Locks artikel.

Den komplette kode til denne artikel er tilgængelig på GitHub.


Java tag