Oi! Hoje continuaremos a considerar os recursos da programação multithread e falaremos sobre sincronização de threads.
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: A 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.Object
classe, 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 asynchronized
palavra-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 synchronized
palavra-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. Por 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 this
objeto, ou seja, o objeto atual. E no segundo exemplo, você aplica a synchronized
palavra-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 synchronized
bloco 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 umMyClass
objeto específico. Suponha que temos 2 threads (Thread-1
eThread-2
) e apenas umMyClass myClass
objeto. Nesse caso, seThread-1
chamar omyClass.swap()
método, o mutex do objeto estará ocupado e, ao tentar chamar, omyClass.swap()
métodoThread-2
travará enquanto aguarda a liberação do mutex. Se tivermos 2 threads e 2MyClass
objetos (myClass1
emyClass2
), 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 synchronized
palavra-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.
GO TO FULL VERSION