Threads - Exercício de Faculdade

Já vou logo avisando que não quero que resolvam para mim o exercício, eheeh, gostaria apenas de algumas opniões, talvez um “norte”. Já pesquisei e pesquisei muuuito e não consigo resolver a bagaça, resolvi postar aqui também pq em minha pesquisa acabei encontrando informações que divergem frontalmente com as quais o professor nos passa. Passo a desecrever abaixo a descrição do problema, algumas observações em relação às restrições e, em seguida, o código, e, após este último, a descrição de minhas tentativas.

Modifique a implementação da classe AgenteConta utilizando somente travas explicitas de modo que, ao final da execução, suas quatro instâncias tenham o mesmo saldo (1.000.000,00). É proibido modifcar o método main() e é proibido também usar trava ou monitor global único para todas as threads. Também é probibido mudar a lógica da aplicação (i.e., cada instancia de AgenteConta deve continuar debitando das outras contas, creditando para si e, em seguida, transferindo uma unidade para cada uma das outras contas) e a sequência de operações realizadas no método run().

[color=darkblue] Considerações sobre as restrições:[/color]
Como o professor viu que ninguém estava conseguindo resultados resolveu afrouxar as restrições e disse que poderiamos fazer uma primeira versão quebrando uma ou outra restrição, como por exemplo usar monitores globais.

Sobre a divergencia com o que professor considera como travas explicitas
Segundo ele, travas explicitas são travas em que se usa synchronized/wait/sleep em blocos de codigos ou em métodos, em minha pesquisa encontrei exatamente o contrário, várias fontes dizem que estes são locks implícitos, que travas explícitas seriam quando instanciamos uma implementação da interface Lock. Normalmente, instancia-se ReentrantLock. O que me dizem?

[color=darkred]O Código original:[/color]

public class AgenteConta implements Runnable
{
	private double saldo = 1000000.0;

	private AgenteConta[] contas = new AgenteConta[ 3 ];

	public AgenteConta( )
	{
	}

	public AgenteConta( AgenteConta c1, AgenteConta c2, AgenteConta c3 )
	{
		contas[ 0 ] = c1;
		contas[ 1 ] = c2;
		contas[ 2 ] = c3;
	}

	public void debitar( double valor )
	{
		if ( valor <= saldo )
		{
			saldo -= valor;
		}
		else
		{
			System.out.println( "Saldo Insuficiente" );
		}
	}

	public void creditar( double valor )
	{
		saldo += valor;
	}

	public void transferir( AgenteConta c, double quantia )
	{
		c.saldo = c.saldo - quantia;
		this.saldo += quantia;
	}

	public void run( )
	{
		for ( long i = 0; i < 1000000; i++ )
		{
			contas[ 0 ].debitar( 1 );
			contas[ 1 ].debitar( 1 );
			contas[ 2 ].debitar( 1 );
			this.creditar( 3 );
			this.transferir( contas[ 0 ], 1 );
			this.transferir( contas[ 1 ], 1 );
			this.transferir( contas[ 2 ], 1 );
		}
	}

	public static void main( String[] args )
	{
		long t = System.currentTimeMillis( );
		AgenteConta c1 = new AgenteConta( );
		AgenteConta c2 = new AgenteConta( );
		AgenteConta c3 = new AgenteConta( );
		AgenteConta c4 = new AgenteConta( c1, c2, c3 );
		c1.contas = new AgenteConta[] { c2, c3, c4 };
		c2.contas = new AgenteConta[] { c1, c3, c4 };
		c3.contas = new AgenteConta[] { c1, c2, c4 };
		Thread t1 = new Thread( c1 );
		Thread t2 = new Thread( c2 );
		Thread t3 = new Thread( c3 );
		Thread t4 = new Thread( c4 );
		t1.start( );
		t2.start( );
		t3.start( );
		t4.start( );
		try
		{
			t1.join( );
			t2.join( );
			t3.join( );
			t4.join( );
		}
		catch ( InterruptedException e )
		{
			System.out.println( "Erro" + e.getMessage( ) );
		}
		System.out.println( " C1: " + c1.saldo + " C2: " + c2.saldo + " C3: "
		        + c3.saldo + " C4: " + c4.saldo );
		System.out.println( " Tempo: " + (System.currentTimeMillis( ) - t) );
	}
}

O que já tentei:

  1. Sincronizar o bloco principal dentro de run(), da seguinte maneira:
    Também tentei sem o sinchronized interno

[code] public void run() {
for (long i = 0; i < 1000000; i++) {
synchronized (contas) {
contas[0].debitar(1);
contas[1].debitar(1);
contas[2].debitar(1);

			synchronized (this) {
				this.creditar(3);
				this.transferir(contas[0], 1);
				this.transferir(contas[1], 1);
				this.transferir(contas[2], 1);
			}
		}
	}
}[/code]
  1. Sincronizando junto os metodos debitar, creditar e transferir.

  2. Também tentei criar uma variável (membro da classe AgenteConta) para monitorar um lock e 2 metodos sincronizados, um para setar esta varivel como locked e outra para liberar, alterando também o métod run() conforme segue:

[code]
public synchronized void lock(){

	isLocked = true;
}

	  
public synchronized void unlock(){
	isLocked = false;
    notify();
}

public void run() {
	lock();
	System.out.println(Thread.currentThread().getName());

	if (! isLocked) {
		for (long i = 0; i < 5; i++) {
			
			System.out.println(i);
			contas[0].debitar(1);
			contas[1].debitar(1);
			contas[2].debitar(1);
			this.creditar(3);
			this.transferir(contas[0], 1);
			this.transferir(contas[1], 1);
			this.transferir(contas[2], 1);
		}
		unlock();
	}
	else {
		try {
			wait();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}[/code]

Tudo sem sucesso. :-/

Bom, no código que você postou existem dois problemas a serem resolvidos. O primeiro está relacionado com o que é denominado como região crítica, ou seja, pensando em várias threads executando paralelamente, qual informação que pode ser alterada por mais de uma thread ao mesmo tempo? No caso que você mostrou é a variável saldo que será acessada pelas threads. Para resolver este tipo de problema, você deve sincronizar os locais do código que modificam essa variável e, desta forma, mesmo que acessada de forma concorrente, a variável será atualizada apenas por uma thread por vez. Para resolver este primeito problema, eu sugiro que você comente as chamadas ao método transferir que estão no método run e faça os testes apenas com as chamadas aos métodos debitar e creditar. Após corrigir este primeiro problema através das sincronizações de acesso necessárias, será necessário resolver o segundo problema, que ocorre com o método transferir. Para este método específico será necessário adquirir o lock nos dois objetos (o “this” e o passado como parâmetro) antes de executar as operações. Isto é necessário pois o método vai atualziar duas regiões críticas (os dois saldos), porém ocorrerá um deadlock e o programa vai ficar travado quando você rodar. Então para resolver isso primeiro você terá que entender o que é o deadlock e porque ele ocorre. Para evitar o deadlock, você terá que “simular” uma ordem nos locks dos dois objetos, já que essa ordem não pode ser garantida pelos parâmetros passados.
O capítulo 10 deste livro:

explica o problema que é encontrado relacionado aos deadlocks e a como devemos proceder para obter os locks na ordem correta. Nos capítulos anteriores (não me lembro exatamente qual agora) há a explicação sobre sincronização de variáveis em regiões críticas.
Eu arrumei o código aqui pra funcionar da forma que seu professor pediu. Se depois de tentar resolver ainda estiver muito complicado me manda uma mp que eu envio pra você. Espero ter ajudado. Até mais!

Rogerio,
Em primeiro lugar, obrigado pela ajuda, antes de te mandar uma mp e ver seu codigo, gostaria de continuar tentando entender onde estou errando, ou o que não estou enxergando, seguindo suas sugestões fiz alterações no código, sincronizando os métodos que fazem referencia ao saldo (aliás, eu ja tinha tentado isto), e também comentei as chamadas ao metodo transferir, conforme o codigo abaixo, mas contiuo sem sucesso.

[code] public synchronized void debitar( double valor )
{
if ( valor <= saldo )
{
saldo -= valor;
}
else
{
System.out.println( “Saldo Insuficiente” );
}
}

public synchronized void creditar( double valor )
{
	saldo += valor;
}

public synchronized void transferir( AgenteConta c, double quantia )
{
	c.saldo = c.saldo - quantia;
	this.saldo += quantia;
}

public void run() {
	for (long i = 0; i < 5; i++) {
		contas[0].debitar(1);
		contas[1].debitar(1);
		contas[2].debitar(1);

		this.creditar(3);

	//	this.transferir(contas[0], 1);
	//	this.transferir(contas[1], 1);
	//	this.transferir(contas[2], 1);

	}
}[/code]

[color=darkblue]Sobre o conceito de travas explicitas? o que vc me diz? neste link http://www.cin.ufpe.br/~if686/aulas/14_Synchronized_Travas_PC.pdf por exemplo, na pagina 17 tem um conceito bem interessante mas bem diferente do que nos foi passado.[/color]


O que você fez está correto. Posta o código completo aqui, deveria estar funcionando sincronizando os métodos debitar e creditar e comentando as chamadas ao método transferir.
Com relação à nomenclatura eu não sei dizer qual é a correta. Quando tiver tempo eu vou ler o que você passou.

Mais uma vez obrigado pela disposição.

Aqui vai meu codigo completo (apenas mudei o saldo inicial para ficar mais fácil os testes):

[code]public class AgenteConta implements Runnable
{
private double saldo = 5.0;

private AgenteConta[] contas = new AgenteConta[ 3 ];

public AgenteConta( )
{
}

public AgenteConta( AgenteConta c1, AgenteConta c2, AgenteConta c3 )
{
	contas[ 0 ] = c1;
	contas[ 1 ] = c2;
	contas[ 2 ] = c3;
}

public synchronized void debitar( double valor )
{
	if ( valor <= saldo )
	{
		saldo -= valor;
	}
	else
	{
		System.out.println( "Saldo Insuficiente" );
	}
}

public synchronized void creditar( double valor )
{
	saldo += valor;
}

public synchronized void transferir( AgenteConta c, double quantia )
{
		c.saldo = c.saldo - quantia;
		this.saldo += quantia;

}

public void run() {
	for (long i = 0; i < 5; i++) {
		contas[0].debitar(1);
		contas[1].debitar(1);
		contas[2].debitar(1);

		this.creditar(3);

		// this.transferir(contas[0], 1);
		// this.transferir(contas[1], 1);
		// this.transferir(contas[2], 1);

	}
}

public static void main( String[] args )
{
	long t = System.currentTimeMillis( );
	AgenteConta c1 = new AgenteConta( );
	AgenteConta c2 = new AgenteConta( );
	AgenteConta c3 = new AgenteConta( );
	AgenteConta c4 = new AgenteConta( c1, c2, c3 );
	c1.contas = new AgenteConta[] { c2, c3, c4 };
	c2.contas = new AgenteConta[] { c1, c3, c4 };
	c3.contas = new AgenteConta[] { c1, c2, c4 };
	Thread t1 = new Thread( c1 );
	Thread t2 = new Thread( c2 );
	Thread t3 = new Thread( c3 );
	Thread t4 = new Thread( c4 );
	t1.start( );
	t2.start( );
	t3.start( );
	t4.start( );
	try
	{
		t1.join( );
		t2.join( );
		t3.join( );
		t4.join( );
	}
	catch ( InterruptedException e )
	{
		System.out.println( "Erro" + e.getMessage( ) );
	}
	System.out.println( " C1: " + c1.saldo + " C2: " + c2.saldo + " C3: "
	        + c3.saldo + " C4: " + c4.saldo );
	System.out.println( " Tempo: " + (System.currentTimeMillis( ) - t) );
}

}
[/code]

[color=darkblue]Resultado de uma execução:[/color]
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
Saldo Insuficiente
C1: 5.0 C2: 7.0 C3: 8.0 C4: 9.0
Tempo: 1

Volta o saldo para o valor do código original (1000000) e o looping para 1000000 também ao invés de 5 e testa de novo com os métodos sincronizados. Vai funcionar. Depois tira a sincronização e testa pra ver que dá errado (só pra vc ver como foi a sincronização que resolveu o problema).
Em outras palavras, não altere nenhum valor do código original, apenas sincronize os métodos como você já fez e comente as chamadas ao método transferir.

Ok,
Realmente “funciona”, mas alterei para um numero baixo por sugestão do próprio professor, fiz varios testes com varios números, a impressão que dá é que quanto maior o numero menor a probabilidade de erro, experiemente retirar apenas um zero e rode varias vezes o programa e vc vai ver que uma hora vai surgir inconsistencias, muuuito provavelmente se tivessemos paciencia de monitorar por um enooorrme tempo, apareceriam inconsistencias para qualquer número, não?

Para ser considerado sucesso o algoritmo teria que funcionar para qualquer número acima de 1.

Dependendo do valor do saldo que você colocar pode ser que o valor final mude sim, mas isso é difícil de testar, como você mesmo disse, pois estamos trabalhando com várias contas. No exemplo abaixo eu fiz algo um pouco mais simples pra entender. Existe apenas uma conta, e 10 threads executando as oprações incrementar e decrementar na mesma instância da conta. Você pode ver que no método run eu coloquei um sleep. Eu fiz isso de propósito, pois senão o loop ia ficar tão rápido que, dependendo da velocidade do processador, mesmo executando em várias threads, o efeito do paralelismo não seria notado porque cada trhead conseguiria terminar todas as operações no seu tempo de execução. No método run eu estou incrementando 1 cinco vezes e decrementando 5, ou seja, independente do número de threads executando, no final o saldo deve ser o mesmo que tínhamos no início. Teste o primeiro código, sem sincronização. Depois teste o segundo e você verá que o saldo será sempre igual ao inicial, devido à sincronização. Acho que somente com um objeto compartilhado fica mais fácil de entender do que com três:

Primeiro teste (vai dar errado):


public class Conta {

	private double saldo = 100;
	
	public  void incrementar(double valor) {
		this.saldo = this.saldo + valor;
	}
	
	public  void decrementar(double valor){
		this.saldo = this.saldo - valor;
	}

	public static void main(String[] args) {
		System.out.println("Começo do teste...");
		for(int i = 0; i < 10; i++, teste());
		System.out.println("Final do teste...");
	}	
	public static void teste() {
		final Conta c = new Conta();
		
		Runnable r = new Runnable() {
			
			@Override
			public void run() {
				for(long i = 0; i < 100; i++ ) {
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					try{Thread.sleep(20);}catch(Exception e){e.printStackTrace();}
					c.decrementar(5);
				}
			}
		};

			Thread t1 = new Thread(r);
			Thread t2 = new Thread(r);
			Thread t3 = new Thread(r);
			Thread t4 = new Thread(r);
			Thread t5 = new Thread(r);
			Thread t6 = new Thread(r);
			Thread t7 = new Thread(r);
			Thread t8 = new Thread(r);
			Thread t9 = new Thread(r);
			Thread t10 = new Thread(r);
			
			t1.start();
			t2.start();
			t3.start();
			t4.start();
			t5.start();
			t6.start();
			t7.start();
			t8.start();
			t9.start();
			t10.start();

			try{
				t1.join();
				t2.join();
				t3.join();
				t4.join();
				t5.join();
				t6.join();
				t7.join();
				t8.join();
				t9.join();
				t10.join();
			}catch(Exception e){e.printStackTrace();}
			
			System.out.println("Saldo esperado: 100     Saldo obtido: " + c.saldo);
		}
		
	}

Segundo teste (vai dar certo devido ao synchronized nos métodos):


public class Conta {

	private double saldo = 100;
	
	public  synchronized void incrementar(double valor) {
		this.saldo = this.saldo + valor;
	}
	
	public  synchronized void decrementar(double valor){
		this.saldo = this.saldo - valor;
	}

	public static void main(String[] args) {
		System.out.println("Começo do teste...");
		for(int i = 0; i < 10; i++, teste());
		System.out.println("Final do teste...");
	}	
	public static void teste() {
		final Conta c = new Conta();
		
		Runnable r = new Runnable() {
			
			@Override
			public void run() {
				for(long i = 0; i < 100; i++ ) {
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					c.incrementar(1);
					try{Thread.sleep(20);}catch(Exception e){e.printStackTrace();}
					c.decrementar(5);
				}
			}
		};

			Thread t1 = new Thread(r);
			Thread t2 = new Thread(r);
			Thread t3 = new Thread(r);
			Thread t4 = new Thread(r);
			Thread t5 = new Thread(r);
			Thread t6 = new Thread(r);
			Thread t7 = new Thread(r);
			Thread t8 = new Thread(r);
			Thread t9 = new Thread(r);
			Thread t10 = new Thread(r);
			
			t1.start();
			t2.start();
			t3.start();
			t4.start();
			t5.start();
			t6.start();
			t7.start();
			t8.start();
			t9.start();
			t10.start();

			try{
				t1.join();
				t2.join();
				t3.join();
				t4.join();
				t5.join();
				t6.join();
				t7.join();
				t8.join();
				t9.join();
				t10.join();
			}catch(Exception e){e.printStackTrace();}
			
			System.out.println("Saldo esperado: 100     Saldo obtido: " + c.saldo);
		}
		
	}


Rogerio,
Ai é que está, somente com 1 objeto compartilhado fizemos varios exercicios e pararece não restar mais dúvidas para ninguem da turma, mas o desafio é para este problema específico, depois que postei a ultima msg, fiz varios testes e, mesmo com o numero original (10000000) acontece a inconsistencia varias vezes.

Como eu disse, o desafio é resolver o exercicio como apresentado, o professor diz que tem solução mas não a apresenta.

Há quem aposte que é insolúvel, sei lá, tá estranho.

Bom, eu acho que o desafio que ele quer que vcs resolvam é na verdade o que vocês tem que fazer no método transferir, que é diferente da sincronização normal devido ao problema do deadlock que vai acontecer, e não nos outros dois métodos. No caso dos métodos acrescentar e decrementar o que precisa ser feito é só sincronizar mesmo. Eu testei aqui com os valores originais e não deu problema nenhuma vez (o valor original era um milhão e não dez milhões). Eu não sei que valores você está colocando para o saldo e para o contador do looping para dar errado. Me fala um caso pra eu testar aqui.

o problema é o debitar.
é o único método q sofre o efeito de um saldo baixo.

mude ele pra ficar assim:

[code] public synchronized void debitar( double valor )
{
// if ( valor <= saldo )
// {
saldo -= valor;
// }
// else
// {
// System.out.println( “Saldo Insuficiente” );
// }
}

[/code]
coloque qq valor agora no saldo.

pode ser até 0.00

@Rogério,

O desafio é que o algoritmo rode com qualquer valor, inclusive a sugestão é que iniciemos com o valor 5, segui sua sugestão e comentei as chamadas ao metodo transferir, e entao testei com varios valores, valores baixos com o 5 ja dão erro “de cara”, valores muito altos podem demorar mas uma hora ocorre o erro.

De qualquer forma, o principal desafio é que funcione com qualquer valor acima de 1, com suas sugestoes, mesmo desconsiderando o metodo transferir (que seria o “gargalo”) nao obitve sucesso com valores baixos.

@Gilson,

Vou testar sua sugestão

Se vc comentar como o Gilson disse vai funcionar com qualquer saldo, porém você tem que sincronizar os métodos. Se não sincronizar não adianta comentar ou não aquele ponto. Então a solução final é deixar ele fazer o saldo ficar negativo e sincronizar os métodos. E não esqueça de deixar as chamadas ao método transferir comentadas também como eu falei antes pra você testar essa primeira parte, senão o valor vai sair errado.
Depois que descomentar as chamadas ao método transferir tem que arrumar ele també, só que é um pouco mais complicado que esta primeira parte.

[quote=rogeriopaguilar]Se vc comentar como o Gilson disse vai funcionar com qualquer saldo, porém você tem que sincronizar os métodos, comentando ali ou não.
Então a solução final é deixar ele fazer o saldo ficar negativo e sincronizar os métodos.
Falta agora o método transferir, que é um pouco mais complicado.[/quote]

creio q o transferir funciona normal pq ele não se condiciona

Bom, nos casos que testei aqui não funcionou. Eu acredito que o professor quer mostrar como resolver um deadlock com este método.

Rogerio,
Neste momento não importa a resolução do deadlock, o fato é que mesmo comentando o metodo transferir não esta funcionando, entao só vou pensar em resolver o DL depois que conseguir fazer funcionar sem o metodo transferir, se não nem vale o esforço, aliás, cheguei nesta conculsão a partir de uma sugestão sua.

Não adiantaria resolver o DL e o resultado continuar saindo inconsistente.

Gilson,
Verdade assim funciona mas deste jeito estou quebrando uma regra que nao poderia, pois posso até acrescentar coisas mas retirar coisas dos metodos.

A sincronização para os métodos adicionar e decrementar está correta. Se quiser coloca o enunciado aqui pra eu entender quando você diz que não está correto, queria entender o caso que não está dando certo.

tente assim:

fiz aki mts testes e tds ok.

[code] public class AgenteConta implements Runnable
{
private double saldo = 1000000.0;

private AgenteConta[] contas = new AgenteConta[ 3 ];  

public AgenteConta() {
}

public AgenteConta(AgenteConta c1, AgenteConta c2, AgenteConta c3)  
{  
    contas[ 0 ] = c1;  
    contas[ 1 ] = c2;  
    contas[ 2 ] = c3;  
}  

private void incSaldo(double v){
    synchronized (this) {			
        saldo = saldo + v;
    }
}

public void debitar( double valor )  
{  
    if ( valor <= saldo )  {  
        incSaldo(- valor);  
    } else {  
        System.out.println( "Saldo Insuficiente" );  
    }  
}  

public void creditar( double valor )  
{  
    incSaldo(valor);  
}  

public void transferir( AgenteConta c, double quantia )  
{  
    c.incSaldo(- quantia);  
    this.incSaldo(quantia);
}  

public void run() { 
    for (long i = 0; i < 1000000; i++) {  
        contas[0].debitar(1);  
        contas[1].debitar(1);  
        contas[2].debitar(1);  

        this.creditar(3);  

         this.transferir(contas[0], 1);  
         this.transferir(contas[1], 1);  
         this.transferir(contas[2], 1);
    }
}  

public static void main( String[] args )  
{  
    long t = System.currentTimeMillis( );  
    AgenteConta c1 = new AgenteConta();  
    AgenteConta c2 = new AgenteConta();  
    AgenteConta c3 = new AgenteConta();  
    AgenteConta c4 = new AgenteConta( c1, c2, c3);  
    c1.contas = new AgenteConta[] { c2, c3, c4 };  
    c2.contas = new AgenteConta[] { c1, c3, c4 };  
    c3.contas = new AgenteConta[] { c1, c2, c4 };  
    Thread t1 = new Thread( c1 );  
    Thread t2 = new Thread( c2 );  
    Thread t3 = new Thread( c3 );  
    Thread t4 = new Thread( c4 );  

    t1.start( );
    t2.start( );  
    t3.start( );  
    t4.start( );  

    try  
    {  
        t1.join( );  
        t2.join( );  
        t3.join( );  
        t4.join( );  
    }  
    catch ( InterruptedException e )  
    {  
        System.out.println( "Erro" + e.getMessage( ) );  
    }  
    System.out.println( " C1: " + c1.saldo + " C2: " + c2.saldo + " C3: "  
            + c3.saldo + " C4: " + c4.saldo );  

    System.out.println( " Tempo: " + (System.currentTimeMillis( ) - t) );  
}  

}
[/code]

poderia ser assim tb.

[code]public class AgenteConta implements Runnable
{
private double saldo = 1000000.0;

private AgenteConta[] contas = new AgenteConta[ 3 ];    

public AgenteConta() {  
}  

public AgenteConta(AgenteConta c1, AgenteConta c2, AgenteConta c3)    
{    
    contas[ 0 ] = c1;    
    contas[ 1 ] = c2;    
    contas[ 2 ] = c3;    
}    

public void debitar( double valor )    
{    
    synchronized (this) {             
	    if ( valor <= saldo )  {    
	        saldo -= valor;    
	    } else {    
	        System.out.println( "Saldo Insuficiente" );    
	    }   
    }
}    

public void creditar( double valor )    
{    
    synchronized (this) {             
    	saldo += valor;
    }
}    

public void transferir( AgenteConta c, double quantia )    
{    
    synchronized (c) {             
    	c.saldo -= quantia;
    }
    
    synchronized (this) {             
    	this.saldo += quantia;
    }	
}    

public void run() {   
    for (long i = 0; i < 1000000; i++) {    
        contas[0].debitar(1);    
        contas[1].debitar(1);    
        contas[2].debitar(1);    

        this.creditar(3);    

         this.transferir(contas[0], 1);    
         this.transferir(contas[1], 1);    
         this.transferir(contas[2], 1);  
    }  
}    

public static void main( String[] args )    
{    
    long t = System.currentTimeMillis( );    
    AgenteConta c1 = new AgenteConta();    
    AgenteConta c2 = new AgenteConta();    
    AgenteConta c3 = new AgenteConta();    
    AgenteConta c4 = new AgenteConta( c1, c2, c3);    
    c1.contas = new AgenteConta[] { c2, c3, c4 };    
    c2.contas = new AgenteConta[] { c1, c3, c4 };    
    c3.contas = new AgenteConta[] { c1, c2, c4 };    
    Thread t1 = new Thread( c1 );    
    Thread t2 = new Thread( c2 );    
    Thread t3 = new Thread( c3 );    
    Thread t4 = new Thread( c4 );    

    t1.start( );  
    t2.start( );    
    t3.start( );    
    t4.start( );    

    try    
    {    
        t1.join( );    
        t2.join( );    
        t3.join( );    
        t4.join( );    
    }    
    catch ( InterruptedException e )    
    {    
        System.out.println( "Erro" + e.getMessage( ) );    
    }    
    System.out.println( " C1: " + c1.saldo + " C2: " + c2.saldo + " C3: "    
            + c3.saldo + " C4: " + c4.saldo );    

    System.out.println( " Tempo: " + (System.currentTimeMillis( ) - t) );    
}    

} [/code]

A solução que eu cheguei foi a seguinte:

package teste;

public class AgenteContaFinal implements Runnable
{
	private double saldo = 2000000.0;

	private AgenteContaFinal[] contas = new AgenteContaFinal[ 3 ];
	
	public AgenteContaFinal( )
	{
	}

	public AgenteContaFinal( AgenteContaFinal c1, AgenteContaFinal c2, AgenteContaFinal c3 )
	{
		contas[ 0 ] = c1;
		contas[ 1 ] = c2;
		contas[ 2 ] = c3;
	}

	public synchronized void debitar( double valor )
	{
		if ( valor <= saldo )
		{
			saldo -= valor;
		}
		else
		{
			System.out.println( "Saldo Insuficiente" );
		}
	}

	public synchronized void creditar( double valor )
	{
		saldo += valor;
	}

	public void transferir( AgenteContaFinal c, double quantia )
	{
		int hash = System.identityHashCode(this);
		int hashOutroObjeto = System.identityHashCode(c);
		Object lockA = null;
		Object lockB = null;
		if(hash < hashOutroObjeto) {
			lockA = this;
			lockB = c;
		} else {
			lockA = c;
			lockB = this;
		}
		
		synchronized(lockA) {
			synchronized (lockB) {
				if ( quantia <= c.saldo ) {
					c.saldo = c.saldo - quantia;
					this.saldo += quantia;
				} else {
					System.out.println("Saldo insuficiente");
				}
			}
		}
		
	}

	public void run( )
	{
		for ( long i = 0; i < 2000000; i++ )
		{
			contas[ 0 ].debitar( 1 );
			contas[ 1 ].debitar( 1 );
			contas[ 2 ].debitar( 1 );
			this.creditar( 3 );
			this.transferir( contas[ 0 ], 1 );
			this.transferir( contas[ 1 ], 1 );
			this.transferir( contas[ 2 ], 1 );
		}
	}

	public static void main( String[] args )
	{
		
	   long t = System.currentTimeMillis( );  
        AgenteContaFinal c1 = new AgenteContaFinal( );  
        AgenteContaFinal c2 = new AgenteContaFinal( );  
        AgenteContaFinal c3 = new AgenteContaFinal( );  
        AgenteContaFinal c4 = new AgenteContaFinal( c1, c2, c3 );  
        c1.contas = new AgenteContaFinal[] { c2, c3, c4 };  
        c2.contas = new AgenteContaFinal[] { c1, c3, c4 };  
        c3.contas = new AgenteContaFinal[] { c1, c2, c4 };  
        Thread t1 = new Thread( c1 );  
        Thread t2 = new Thread( c2 );  
        Thread t3 = new Thread( c3 );  
        Thread t4 = new Thread( c4 );  
        t1.start( );  
        t2.start( );  
        t3.start( );  
        t4.start( );  
		try
		{
			t1.join( );
			t2.join( );
			t3.join( );
			t4.join( );
		}
		catch ( InterruptedException e )
		{
			System.out.println( "Erro" + e.getMessage( ) );
		}
        
		System.out.println( " C1: " + c1.saldo + " C2: " + c2.saldo + " C3: "
		        + c3.saldo + " C4: " + c4.saldo );
		System.out.println( " Tempo: " + (System.currentTimeMillis( ) - t) );
	}
}

A diferença está na sincronização do método transferir, pois eu fiz o lock nas duas regiões críticas (a da conta corrente atual e da outra) antes de atualizar elas, só que tem que garantir a ordem do lock independente dos objetos passados como parâmetro, para evitar problemas de deadlock. Você tem que adquirir o lock das duas referências, e não de uma delas por vez. Pra entender o porque eu é necessário ler o item 10.1.2 do livro que eu falei lá no começo. Pra entender porque os outros métodos precisam de sincronização e sobre sincronização em geral, é bom ler os capítulos 2 e 3 do mesmo livro.