No artigo anterior, introduzi o método de usar Java8 para implementar o padrão de observador (parte 1). Este artigo continua a introduzir o conhecimento relevante do padrão de observador Java8. O conteúdo específico é o seguinte:
Implementação segura para threads
O capítulo anterior apresenta a implementação do padrão de observador em um ambiente java moderno. Embora seja simples, mas completo, essa implementação ignora uma questão -chave: segurança do thread. A maioria dos aplicativos Java abertos é multithread e o modo Observer é usado principalmente em sistemas multithread ou assíncronos. Por exemplo, se um serviço externo atualizar seu banco de dados, o aplicativo também receberá uma mensagem de forma assíncrona e notificará o componente interno a ser atualizado no modo Observador, em vez de registrar e ouvir diretamente o serviço externo.
A segurança do encadeamento no modo de observador está focada principalmente no corpo do modo, porque é provável que ocorram conflitos de encadeamento ao modificar a coleção de ouvintes registrados. Por exemplo, um thread tenta adicionar um novo ouvinte, enquanto o outro thread tenta adicionar um novo objeto animal, que desencadeia notificações a todos os ouvintes registrados. Dada a ordem da sequência, o primeiro thread pode ou não ter concluído o registro do novo ouvinte antes que o ouvinte registrado receba notificação do animal adicionado. Este é um caso clássico de concorrência de recursos de threads, e é esse fenômeno que diz aos desenvolvedores que eles precisam de um mecanismo para garantir a segurança do thread.
A solução mais fácil para esse problema é: todas as operações que acessam ou modificam a lista de ouvintes de registro devem seguir o mecanismo de sincronização Java, como:
public sincronizado AnimalAddedListener RegisteranimalAddedListener (ouvinte AnimalAddedListener) {/*...*/} public sincronizado void un -registerAnimaladdedListener (lister animalDlistener {/*...*/} aninhanizado ivid / NotifedListener.Dessa maneira, ao mesmo tempo, apenas um thread pode modificar ou acessar a lista de ouvintes registrados, que pode evitar com sucesso problemas de concorrência de recursos, mas surgem novos problemas, e essas restrições são muito rigorosas (para obter mais informações sobre palavras -chave sincronizadas e modelos de concorrência de Java, consulte a página oficial). Através da sincronização do método, o acesso simultâneo à lista de ouvintes pode ser observado o tempo todo. Registrar e revogar o ouvinte é uma operação de gravação para a lista de ouvintes, enquanto notifica o ouvinte para acessar a lista de ouvintes é uma operação somente leitura. Como o acesso através da notificação é uma operação de leitura, várias operações de notificação podem ser executadas simultaneamente.
Portanto, desde que não haja registro ou revogação do ouvinte, desde que o registro não seja registrado, desde que qualquer número de notificações simultâneas possa ser executado simultaneamente sem acionar a competição de recursos para a lista de ouvintes registrados. Obviamente, a concorrência de recursos em outras situações existe há muito tempo. Para resolver esse problema, o bloqueio de recursos do ReadWritelock foi projetado para gerenciar operações de leitura e gravação separadamente. O código de implementação do ThreadSafeZoo, seguro para threadsafezoo, é o seguinte:
classe pública threadSafeZoo {private final readWritElock readWritelock = new ReentrantreadWritelock (); Readlock de bloqueio final protegido = readWritelock.readlock (); Lock final protegido WriteLock = readWritelock.Writelock (); Lista privada <Armal> Animals = New ArrayList <> (); Lista privada <ArmingAddedListener> ouvintes = novo ArrayList <> (); Os ouvintes de ouvividos (Animal);} public AnimalAddedListener RegisteranImaladdedListener (Listener AnimalAddedListener) {// bloqueia a lista de ouvintes para escrever this RegisterSthis.Listnis.Listn. lockthis.writelock.unlock ();} retornar ouvinte;} public void un -registerAnimaladdedListener (listener animaldlistener). LockThis.Writelock.unlock ();}} public void notifyanimaladdedListeners (animal animal) {// bloqueia a lista de ouvintes para leituraThis.readlock.lock (); tente {// notificar cada um dos ouvintes na lista de ouvintes registrados. {// Desbloquear o leitor Lockthis.readlock.unlock ();}}}Por meio dessa implantação, a implementação do assunto pode garantir que a segurança dos threads e vários threads possam emitir notificações ao mesmo tempo. Mas, apesar disso, ainda existem dois problemas de concorrência de recursos que não podem ser ignorados:
Acesso simultâneo a cada ouvinte. Vários tópicos podem notificar o ouvinte de que novos animais são necessários, o que significa que um ouvinte pode ser chamado por vários threads ao mesmo tempo.
Acesso simultâneo à lista de animais. Vários tópicos podem adicionar objetos à lista de animais ao mesmo tempo. Se a ordem das notificações tiver um impacto, pode levar à concorrência de recursos, que requer um mecanismo de processamento de operação simultâneo para evitar esse problema. Se a lista de ouvintes registrados receber uma notificação para adicionar Animal2 e depois receber uma notificação para adicionar Animal1, a concorrência de recursos ocorrerá. No entanto, se a adição de Animal1 e Animal2 for realizada por diferentes fios, também é possível concluir a adição de Animal1 antes do Animal2. Especificamente, o Thread 1 adiciona Animal1 antes de notificar o ouvinte e trava o módulo, o Thread 2 adiciona Animal2 e notifica o ouvinte e, em seguida, Thread 1 notifica o ouvinte que o Animal1 foi adicionado. Embora a concorrência de recursos possa ser ignorada quando a ordem da sequência não for considerada, o problema é real.
Acesso simultâneo aos ouvintes
Os ouvintes de acesso simultâneo podem ser implementados, garantindo a segurança do tópico dos ouvintes. Aderindo ao espírito de "responsabilidade auto-responsabilidade" da classe, o ouvinte tem a "obrigação" de garantir sua própria segurança de threads. Por exemplo, para o ouvinte contado acima, aumentar ou diminuir o número de animais em vários threads pode levar a problemas de segurança de roscas. Para evitar esse problema, o cálculo dos números de animais deve ser operações atômicas (variáveis atômicas ou sincronização do método). O código de solução específico é o seguinte:
Classe pública threadsAfeccinginganimaladdedListener implementa AnimalDlistener {private estático atômico de animais atômicos = novo atômico (0);@substituir o público void updateanimaladded (animal animal) {// incremento o número de animais.O código da solução de sincronização do método é o seguinte:
classe pública CountingAnimaladdedListener implementa AnimalAddedListener {private estático Int AnimalDedCount = 0; @OverridePublic Syncronized void updateanimaladded (animal animal) {// incremento o número de animais de animais).Deve -se enfatizar que o ouvinte deve garantir sua própria segurança de threads. O sujeito precisa entender a lógica interna do ouvinte, em vez de simplesmente garantir a segurança do thread para acessar e modificar o ouvinte. Caso contrário, se vários sujeitos compartilharem o mesmo ouvinte, cada classe de assunto precisará reescrever o código seguro para threads. Obviamente, esse código não é conciso o suficiente, portanto, a segura de threads precisa ser implementada na classe do ouvinte.
Notificações ordenadas de ouvintes
Quando o ouvinte é obrigado a executar de maneira ordenada, o bloqueio de leitura e gravação não pode atender às necessidades, e um novo mecanismo precisa ser introduzido para garantir que a ordem de chamada da função de notificação seja consistente com a ordem em que o animal é adicionado ao zoológico. Algumas pessoas tentaram implementá -lo usando a sincronização do método, mas de acordo com a introdução da sincronização do método na documentação do Oracle, pode -se ver que a sincronização do método não fornece o gerenciamento de pedidos da execução da operação. Ele apenas garante que as operações atômicas não sejam interrompidas e não garante a ordem do encadeamento da execução do primeiro a chegar, primeiro a chegar, primeiro (FIFO). ReentrantreadWriteLock pode implementar uma ordem de execução, o código é o seguinte:
classe pública OrderEdThreadSafeZoo {private final ReadWritelock ReadWritelock = new ReentrantreadWritelock (true); Readlock de bloqueio final protegido = readWritelock.readlock (); Lock final protegido WriteLock = readWritelock.Writelock (); Lista privada <Armal> Animals = New ArrayList <> (); Lista privada <ArmingAddedListener> ouvintes = novo ArrayList <> (); Os ouvintes de ouvividos (Animal);} public AnimalAddedListener RegisteranImaladdedListener (Listener AnimalAddedListener) {// bloqueia a lista de ouvintes para escrever this RegisterSthis.Listnis.Listn. lockthis.writelock.unlock ();} retornar ouvinte;} public void un -registerAnimaladdedListener (listener animaldlistener). LockThis.Writelock.unlock ();}} public void notifyanimaladdedListeners (animal animal) {// bloqueia a lista de ouvintes para leituraThis.readlock.lock (); tente {// notificar cada um dos ouvintes na lista de ouvintes registrados. {// Desbloquear o leitor Lockthis.readlock.unlock ();}}}Dessa forma, registre-se, o registro e notifique as funções obterão as permissões de leitura e gravarão na ordem da primeira partida (FIFO). Por exemplo, o thread 1 registra um ouvinte, o Thread 2 tenta notificar o ouvinte registrado após iniciar a operação de registro, o Thread 3 tenta notificar o ouvinte registrado quando o Thread 2 estiver aguardando o bloqueio somente leitura, adotando o método de ordem justa, o Thread 1 completa a operação de registro primeiro, depois o Thread 2 pode notificar o ouvinte e, finalmente, o Thread 3 não. Isso garante que a ordem de execução e a ordem de partida da ação sejam consistentes.
Se a sincronização do método for adotada, embora o thread 2 faça filas primeiro para ocupar recursos, o thread 3 ainda pode obter o bloqueio de recursos antes do thread 2 e não pode ser garantido que o thread 2 notifique o ouvinte primeiro que o thread 3. A chave para o problema é: o método de ordem justa pode garantir que os threads sejam executados na ordem em que os recursos são aplicados. O mecanismo de ordem dos bloqueios de leitura e gravação é muito complicado. Você deve consultar a documentação oficial do ReentrantreadWritelock para garantir que a lógica da fechadura seja suficiente para resolver o problema.
A segurança do thread foi implementada até agora, e as vantagens e desvantagens de extrair a lógica do tópico e encapsular sua classe de mixin em unidades de código repetíveis serão introduzidas nos capítulos a seguir.
Lógica do tema encapsulada à classe Mixin
É muito atraente encapsular a implementação do design de padrões de observador acima mencionado na classe de mixin de destino. De um modo geral, os observadores no modo observador contêm uma coleção de ouvintes registrados; registrar funções responsáveis por registrar novos ouvintes; Funções não registradoras responsáveis por revisar funções registradas e registradas e notificar as funções responsáveis pela notificação dos ouvintes. Para o exemplo acima do zoológico, todas as outras operações da classe do zoológico, exceto que a lista de animais é necessária para o problema é implementar a lógica do assunto.
O caso da classe Mixin é mostrado abaixo. Deve -se notar que, para tornar o código mais conciso, o código sobre segurança do thread é removido aqui:
public abstrate classe observablesbjectmixin <listerType> {Lista privada <IlvenerType> ouvintes = new ArrayList <> (); public ListerType RegisterListener (listenertype listener) {// Adicione o ouvinte Lista da lista de ouvintes registrados. Lista) {// Remova o ouvinte da lista dos ouvintes registrados.Como as informações da interface do tipo de ouvinte registradas não são fornecidas, um ouvinte específico não pode ser notificado diretamente, por isso é necessário garantir a universalidade da função de notificação e permitir que o cliente adicione algumas funções, como aceitar a correspondência de parâmetros de tipos de parâmetros genéricos a serem aplicáveis a cada ouvinte. O código de implementação específico é o seguinte:
classe pública zoousingmixin estende obserabablesubjectmixin <animaladdedlistener> {Lista privada <animal> animais = new ArrayList <> (); public void addanimal (animal animal) {// Adicione o animal à lista de animais. ouvinte.UpDateanImaladded (Animal));}}A maior vantagem da tecnologia da classe Mixin é encapsular o assunto com padrão de observador em uma classe repetível, em vez de repetir a lógica em cada classe de assunto. Além disso, esse método simplifica a implementação da classe do zoológico, armazenando apenas informações de animais sem considerar como armazenar e notificar os ouvintes.
No entanto, o uso de classes de Mixin não é apenas uma vantagem. Por exemplo, e se você quiser armazenar vários tipos de ouvintes? Por exemplo, também é necessário armazenar o tipo de ouvinte AnimalRemovedListener. A classe Mixin é uma classe abstrata. Múltiplas classes abstratas não podem ser herdadas ao mesmo tempo em Java, e a classe Mixin não pode ser implementada usando uma interface. Isso ocorre porque a interface não contém estado e o estado no modo Observer precisa ser usado para salvar a lista de ouvintes registrados.
Uma solução é criar um zoolistener do tipo ouvinte que será notificado quando os animais aumentarem e diminuirem. O código se parece com o seguinte:
interface pública Zoolistener {public void onanimaladded (animal animal); public void onanimalRemoved (animal animal);}Dessa forma, você pode usar esta interface para implementar o monitoramento de várias mudanças no estado do zoológico usando um tipo de ouvinte:
public classe zoousingmixin estende obserablesbjectmixin <Zoolistener> {Lista privada <animal> animais = novo ArrayList <> (); public void addanimal (animal animal) {// Adicione o animal à lista de animais registrados. ouvinte.onanimaladded (animal));} public void removeanimal (animal animal) {// remove o animal da lista de animais This.animals.Remove (animal); // notifique a lista de ouvintes registrados.A combinação de vários tipos de ouvintes em uma interface do ouvinte resolve o problema mencionado acima, mas ainda existem deficiências, que serão discutidas em detalhes nos capítulos seguintes.
Ouvinte e adaptador multimetodes
No método acima, se a interface do ouvinte implementar muitas funções, a interface será muito detalhada. Por exemplo, o swing mouselistener contém 5 funções necessárias. Embora você possa usar apenas um deles, você deve adicionar essas 5 funções, desde que use o evento de clique do mouse. É mais provável que use órgãos de função vazios para implementar as funções restantes, que, sem dúvida, trarão confusão desnecessária ao código.
Uma solução é criar um adaptador (o conceito vem do padrão adaptador proposto pelo GOF). A operação da interface do ouvinte é implementada na forma de funções abstratas para herança da classe de ouvinte específica. Dessa forma, a classe de ouvinte específica pode selecionar as funções necessárias e usar as operações padrão para funções não necessárias pelo adaptador. Por exemplo, na classe Zoolistener no exemplo acima, crie o Zooadapter (as regras de nomeação do adaptador são consistentes com o ouvinte, você só precisa alterar o ouvinte no nome da classe para adaptador), o código é o seguinte:
classe pública Zooadapter implementa Zoolistener {@OverridePublic void onanimaladded (animal animal) {} @OverridePublic void onanimalremoved (animal animal) {}}À primeira vista, essa classe adaptadora é insignificante, mas a conveniência que ela traz não pode ser subestimada. Por exemplo, para as seguintes classes específicas, basta selecionar as funções úteis para eles:
classe pública NamePrinterZooAdapter estende o zooadapter {@OverridePublic Void onanimaladded (animal animal) {// Imprima o nome do animal que foi adicionado.Existem duas alternativas que também podem implementar as funções da classe adaptadora: uma é usar a função padrão; O outro é mesclar a interface do ouvinte e a classe adaptadora em uma classe específica. A função padrão é proposta recentemente pelo Java 8, permitindo que os desenvolvedores forneçam métodos de implementação padrão (de defesa) na interface.
Esta atualização da biblioteca Java é principalmente para facilitar os desenvolvedores para implementar extensões de programa sem alterar a versão antiga do código; portanto, esse método deve ser usado com cautela. Depois de usá -lo muitas vezes, alguns desenvolvedores acham que o código escrito dessa maneira não é profissional o suficiente, e alguns desenvolvedores acham que esse é o recurso do Java 8. Não importa o quê, eles precisam entender qual é a intenção original dessa tecnologia e depois decidir se o usará com base em perguntas específicas. O código da interface do Zoolistener implementado usando a função padrão é o seguinte:
interface pública Zoolistener {Padrão public void onanimaladded (animal animal) {} Padrão public void onanimalremoved (animal animal) {}}Ao usar funções padrão, a implementação das classes específicas da interface não precisa implementar todas as funções na interface, mas implementar seletivamente as funções necessárias. Embora essa seja uma solução relativamente simples para o problema de expansão da interface, os desenvolvedores devem prestar mais atenção ao usá -la.
A segunda solução é simplificar o modo de observador, omitir a interface do ouvinte e usar classes específicas para implementar as funções do ouvinte. Por exemplo, a interface Zoolistener se torna a seguinte:
classe pública Zoolistener {public void onanimaladded (animal animal) {} public void onanimalremoved (animal animal) {}}Esta solução simplifica a hierarquia do padrão de observador, mas não é aplicável a todos os casos, porque se a interface do ouvinte for mesclada em uma classe específica, o ouvinte específico não poderá implementar várias interfaces de escuta. Por exemplo, se as interfaces AnimalAddedListener e AnimalRemovedListener estiverem escritas na mesma classe de concreto, um único ouvinte específico não poderá implementar ambas as interfaces ao mesmo tempo. Além disso, a intenção da interface do ouvinte é mais óbvia do que a da classe específica. É óbvio que o primeiro deve fornecer interfaces para outras classes, mas o último não é tão óbvio.
Sem documentação apropriada, o desenvolvedor não saberá que já existe uma classe que desempenha o papel de uma interface e implementa todas as suas funções correspondentes. Além disso, o nome da classe não contém adaptadores porque a classe não se encaixa em uma determinada interface; portanto, o nome da classe não implica especificamente essa intenção. Para resumir, um problema específico requer a escolha de um método específico e nenhum método é onipotente.
Antes de iniciarmos o próximo capítulo, é importante mencionar que os adaptadores são comuns no modo de observação, especialmente nas versões mais antigas do código Java. A API de swing é implementada com base em adaptadores, pois muitos aplicativos antigos usam no padrão de observador em Java 5 e Java 6. O ouvinte no caso do zoológico pode não exigir um adaptador, mas precisa entender o objetivo do adaptador e de sua aplicação, porque podemos usá -lo no código existente. O capítulo seguinte introduzirá ouvintes intensivos em tempo. Esse tipo de ouvinte pode executar operações demoradas ou fazer chamadas assíncronas e não pode fornecer imediatamente o valor de retorno.
Ouvinte complexo e bloqueador
Uma suposição sobre o padrão do observador é que, quando uma função é executada, uma série de ouvintes é chamada, mas supõe -se que esse processo seja completamente transparente para o chamador. Por exemplo, quando o código do cliente adiciona Animal in Zoo, não se sabe que uma série de ouvintes será chamada antes que o retorno seja bem -sucedido. Se a execução de um ouvinte levar muito tempo (seu tempo for afetado pelo número de ouvintes, o tempo de execução de cada ouvinte), o código do cliente estará ciente dos efeitos colaterais do tempo desse simples aumento nas operações animais.
Este artigo não pode discutir este tópico de maneira abrangente. A seguir, são apresentadas as coisas que os desenvolvedores devem prestar atenção ao ligar para os ouvintes complexos:
O ouvinte inicia um novo tópico. Após o início do novo thread, ao executar a lógica do ouvinte no novo thread, os resultados do processamento da função do ouvinte são retornados e outros ouvintes são executados.
O sujeito inicia um novo thread. Ao contrário das iterações lineares tradicionais das listas de ouvintes registrados, a função Notify do sujeito reinicia um novo thread e depois itera a lista de ouvintes no novo thread. Isso permite que a função Notify produz seu valor de retorno ao executar outras operações dos ouvintes. Deve -se notar que é necessário um mecanismo de segurança de threads para garantir que a lista de ouvintes não sofra modificações simultâneas.
O ouvinte da fila chama e executa funções de escuta com um conjunto de threads. Encapsula operações do ouvinte em algumas funções e as fila em vez de uma simples chamada iterativa para a lista de ouvintes. Uma vez que esses ouvintes estiverem armazenados na fila, o thread pode aparecer um único elemento da fila e executar sua lógica de escuta. Isso é semelhante ao problema do consumidor do produtor. O processo Notify produz uma fila de funções executáveis, que então os threads retiram a fila por sua vez e executam essas funções. A função precisa armazenar o tempo em que foi criado e não o tempo em que foi executado para a função do ouvinte chamar. Por exemplo, uma função criada quando o ouvinte é chamado, a função precisa armazenar o ponto no tempo. Esta função é semelhante às seguintes operações em Java:
public class AnimalAddedFunctor {private Final AnimalDedListener ouvinte; parâmetro final de animal final; CreationThis.Listener.UpDateanImaladded (this.parameter);}}As funções são criadas e salvas em uma fila e podem ser chamadas a qualquer momento, para que não haja necessidade de executar suas operações correspondentes imediatamente ao atravessar a lista da lista da lista. Uma vez que cada função que ativa o ouvinte seja empurrada para a fila, o "thread do consumidor" retornará os direitos operacionais ao código do cliente. O "thread do consumidor" executará essas funções em algum momento mais tarde, como se o ouvinte fosse ativado pela função Notify. Essa tecnologia é chamada de ligação de parâmetros em outros idiomas, que apenas se encaixa no exemplo acima. A essência da tecnologia é salvar os parâmetros do ouvinte e depois chamar a função Execute () diretamente. Se o ouvinte receber vários parâmetros, o método de processamento será semelhante.
Deve -se notar que, se você deseja salvar a ordem de execução do ouvinte, precisará introduzir um mecanismo de classificação abrangente. No Esquema 1, o ouvinte ativa novos threads em ordem normal, o que garante que o ouvinte seja executado na ordem de registro. No esquema 2, as filas suportam a classificação e as funções nelas serão executadas na ordem em que entram na fila. Simplificando, os desenvolvedores precisam prestar atenção à complexidade da execução multithread dos ouvintes e lidar com cuidadosamente para garantir que eles implementem as funções necessárias.
Conclusão
Antes de o modelo Observer ser escrito no livro em 1994, ele já era um modelo de design de software convencional, fornecendo muitas soluções satisfatórias para problemas que geralmente surgem no design de software. A Java sempre foi líder no uso desse padrão e encapsula esse padrão em sua biblioteca padrão, mas, como o Java foi atualizado para a versão 8, é muito necessário reexaminar o uso de padrões clássicos. Com o surgimento de expressões lambda e outras novas estruturas, esse padrão "antigo" assumiu uma nova vitalidade. Seja lidando com programas antigos ou usando esse método de longa data para resolver novos problemas, especialmente para desenvolvedores de Java experientes, o padrão de observador é a principal ferramenta para os desenvolvedores.
O OneApm fornece soluções de desempenho de aplicativos Java de ponta a ponta. Suportamos todas as estruturas Java e servidores de aplicativos comuns para ajudá -lo a descobrir rapidamente gargalos do sistema e localizar as causas raiz das anormalidades. A implantação em níveis de minuto e a experiência instantaneamente, o monitoramento Java nunca foi tão fácil. Para ler mais artigos técnicos, visite o blog oficial de tecnologia da OneApm.
O conteúdo acima apresenta a você como usar o Java8 para implementar o modo Observer (parte 2), espero que seja útil para todos!