Oi! Hoje continuaremos a considerar os recursos da programação multithread e falaremos sobre sincronização de threads. Sincronização de threads.  O operador sincronizado - 1

O que é sincronização em Java?

Fora do domínio da programação, implica um arranjo que permite que dois dispositivos ou programas funcionem juntos. Por exemplo, um smartphone e um computador podem ser sincronizados com uma conta do Google, e uma conta de site pode ser sincronizada com contas de redes sociais para que você possa usá-las para fazer login. A sincronização de threads tem um significado semelhante: é um arranjo no qual os threads interagem com uns aos outros. Nas lições anteriores, nossos fios viviam e trabalhavam separados um do outro. Um realizou um cálculo, um segundo dormiu e um terceiro exibiu algo no console, mas eles não interagiram. Em programas reais, tais situações são raras. Múltiplos encadeamentos podem trabalhar ativamente e modificar o mesmo conjunto de dados. Isso cria problemas. Imagine vários threads escrevendo texto no mesmo local, por exemplo, em um arquivo de texto ou no console. Nesse caso, o arquivo ou console torna-se um recurso compartilhado. Os threads não sabem da existência um do outro, então eles simplesmente escrevem tudo o que podem no tempo alocado a eles pelo escalonador de threads. Em uma lição recente, vimos um exemplo de onde isso leva. Vamos relembrar agora: Sincronização de threads.  O operador sincronizado - 2A razão reside no fato de que as threads estão trabalhando com um recurso compartilhado (o console) sem coordenar suas ações umas com as outras. Se o agendador de thread alocar tempo para Thread-1, ele gravará instantaneamente tudo no console. O que outros threads conseguiram ou não conseguiram escrever não importa. O resultado, como você pode ver, é deprimente. É por isso que eles introduziram um conceito especial, o mutex (exclusão mútua) , na programação multithread. O objetivo de um mutexé fornecer um mecanismo para que apenas um thread tenha acesso a um objeto em um determinado momento. Se a Thread-1 adquirir o mutex do objeto A, as outras threads não poderão acessar e modificar o objeto. As outras threads devem esperar até que o mutex do objeto A seja liberado. Aqui está um exemplo da vida: imagine que você e 10 outros estranhos estão participando de um exercício. Revezando-se, você precisa expressar suas ideias e discutir algo. Mas como vocês estão se vendo pela primeira vez, para não se interromperem constantemente e ficarem furiosos, vocês usam uma 'bola falante': apenas a pessoa com a bola pode falar. Assim você acaba tendo uma boa e frutífera discussão. Essencialmente, a bola é um mutex. Se o mutex de um objeto estiver nas mãos de um thread, outros threads não poderão trabalhar com o objeto.Objectclasse, o que significa que cada objeto em Java tem um.

Como funciona o operador sincronizado

Vamos conhecer uma nova palavra-chave: sincronizado . É usado para marcar um determinado bloco de código. Se um bloco de código for marcado com a synchronizedpalavra-chave, esse bloco só poderá ser executado por um thread por vez. A sincronização pode ser implementada de diferentes maneiras. Por exemplo, declarando um método inteiro para ser sincronizado:

public synchronized void doSomething() {

   // ...Method logic
}
Ou escreva um bloco de código onde a sincronização é realizada usando algum objeto:

public class Main {

   private Object obj = new Object();

   public void doSomething() {

       // ...Some logic available simultaneously to all threads

       synchronized (obj) {

           // Logic available to just one thread at a time
       }
   }
}
O significado é simples. Se um thread entrar no bloco de código marcado com a synchronizedpalavra-chave, ele captura instantaneamente o mutex do objeto e todos os outros threads que tentam entrar no mesmo bloco ou método são forçados a esperar até que o thread anterior conclua seu trabalho e libere o monitor. Sincronização de threads.  O operador sincronizado - 3Por falar nisso! Durante o curso, você já viu exemplos de synchronized, mas eles pareciam diferentes:

public void swap()
{
   synchronized (this)
   {
       // ...Method logic
   }
}
O tema é novo para você. E, claro, haverá confusão com a sintaxe. Portanto, memorize-o logo para evitar ser confundido mais tarde pelas diferentes formas de escrevê-lo. Essas duas formas de escrever significam a mesma coisa:

public void swap() {

   synchronized (this)
   {
       // ...Method logic
   }
}


public synchronized void swap() {

   }
}
No primeiro caso, você cria um bloco de código sincronizado imediatamente após inserir o método. É sincronizado pelo thisobjeto, ou seja, o objeto atual. E no segundo exemplo, você aplica a synchronizedpalavra-chave ao método inteiro. Isso torna desnecessário indicar explicitamente o objeto que está sendo usado para sincronização. Como todo o método está marcado com a palavra-chave, o método será sincronizado automaticamente para todas as instâncias da classe. Não vamos mergulhar em uma discussão sobre qual caminho é melhor. Por enquanto, escolha a maneira que você mais gosta :) O principal é lembrar: você pode declarar um método sincronizado somente quando toda a sua lógica for executada por uma thread por vez. Por exemplo, seria um erro fazer o seguinte doSomething()método sincronizado:

public class Main {

   private Object obj = new Object();

   public void doSomething() {

       // ...Some logic available simultaneously to all threads

       synchronized (obj) {

           // Logic available to just one thread at a time
       }
   }
}
Como você pode ver, parte do método contém lógica que não requer sincronização. Esse código pode ser executado por vários threads ao mesmo tempo e todos os locais críticos são separados em um synchronizedbloco separado. E mais uma coisa. Vamos examinar de perto nosso exemplo da lição com troca de nomes:

public void swap()
{
   synchronized (this)
   {
       // ...Method logic
   }
}
Nota: a sincronização é realizada usando arquivosthis. Ou seja, usando umMyClassobjeto específico. Suponha que temos 2 threads (Thread-1eThread-2) e apenas umMyClass myClassobjeto. Nesse caso, seThread-1chamar omyClass.swap()método, o mutex do objeto estará ocupado e, ao tentar chamar, omyClass.swap()métodoThread-2travará enquanto aguarda a liberação do mutex. Se tivermos 2 threads e 2MyClassobjetos (myClass1emyClass2), nossos threads podem facilmente executar simultaneamente os métodos sincronizados em diferentes objetos. O primeiro thread executa isso:

myClass1.swap();
O segundo executa isso:

myClass2.swap();
Neste caso, a synchronizedpalavra-chave dentro do swap()método não afetará o funcionamento do programa, pois a sincronização é feita através de um objeto específico. E neste último caso, temos 2 objetos. Assim, os fios não criam problemas uns para os outros. Afinal, dois objetos têm 2 mutexes diferentes, e a aquisição de um é independente da aquisição do outro .

Recursos especiais de sincronização em métodos estáticos

Mas e se você precisar sincronizar um método estático ?

class MyClass {
   private static String name1 = "Ally";
   private static String name2 = "Lena";

   public static synchronized void swap() {
       String s = name1;
       name1 = name2;
       name2 = s;
   }

}
Não está claro qual papel o mutex desempenhará aqui. Afinal, já determinamos que cada objeto possui um mutex. Mas o problema é que não precisamos de objetos para chamar o MyClass.swap()método: o método é estático! Então o que vem depois? :/ Na verdade, não há problema aqui. Os criadores de Java cuidaram de tudo :) Se um método que contém lógica concorrente crítica for estático, a sincronização será realizada no nível de classe. Para maior clareza, podemos reescrever o código acima da seguinte forma:

class MyClass {
   private static String name1 = "Ally";
   private static String name2 = "Lena";

   public static void swap() {

       synchronized (MyClass.class) {
           String s = name1;
           name1 = name2;
           name2 = s;
       }
   }

}
Em princípio, você mesmo poderia ter pensado nisso: como não há objetos, o mecanismo de sincronização deve, de alguma forma, ser inserido na própria classe. E é assim mesmo: podemos usar classes para sincronizar.