java多线程2:synchronized

synchronized

先看一段代码:

public class Account {

	private int money;

	public Account(int money) {
		this.money = money;
	}

	public void deposit(int my) {
		int tmp = this.money;
		tmp += my;
		try {
			Thread.sleep((int)(Math.random() * 1000));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.money = tmp;
	}
    public void withdraw(int my) {
		int tmp = this.money;
		tmp -= my;
		try {
			Thread.sleep((int)(Math.random() * 1000));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.money = tmp;
	}

	public int getMoney() {
		return money;
	}

	private static final int THREAD_AMOUNT = 100;
	private static Thread[] threads = new Thread[THREAD_AMOUNT];
	public static void main(String[] args) {
		final Account account = new Account(1000);
		for (int i = 0; i < THREAD_AMOUNT; i++) {
			threads[i] = new Thread(new Runnable() {
				@Override
				public void run() {
					account.deposit(100);
					account.withdraw(100);
				}
			});
			threads[i].start();
		}
		for (int i = 0; i < THREAD_AMOUNT; i++) {
			try {
				threads[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		System.out.println("money is " + account.getMoney());
	}
}

我们用Account模拟一个账户。然后启动1000个线程,对账户中的1000元进行存取。我们先存100元,再取100元,结果应该是1000元不变。但是事与愿违,我们的结果是1000、1100等随机结果。原因是当一个线程存了100元,还没有被取走时,另一个线程的结果把当前的值覆盖掉了。这就需要我们对方法进行同步。

public synchronized void deposit(int my) {

}

public synchronized void withdraw(int my) {

}

这样就能得到正确结果了,但是运行时间貌似增加了~~囧。

下面我们来介绍一下synchronized:

synchronized可以作为方法的修饰符,也可以出现在代码中,如:

synchronized (o) {
    //your code
}

o是一个Object类型

1.当synchronized修饰方法的时候,就相当于类实例的该方法加了一把锁(是每个类实例都拥有各自的锁,而不是所有类实例共享一把锁)。

public class ThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printText("thread-1");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printText("thread-2");
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public synchronized void printText(String text) {
		while (true) {
			System.out.println(text);
		}
	}
}

上面的demo,这两个线程都会打印,因为每个线程中都持有一个实例,这两个实例的synchronized方法锁并不互斥。但如果改为下面这样:

public class ThreadTest {

	public static void main(String[] args) {
		final PrintClass pc = new PrintClass();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printText("thread-1");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printText("thread-2");
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public synchronized void printText(String text) {
		while (true) {
			System.out.println(text);
		}
	}
}

只有一个线程会打印出来,因为他们持有的同一个实例。要是synchronized修饰的是静态方法又会怎样呢:

public class ThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass.printText("thread-1");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass.printText("thread-2");
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public synchronized static void printText(String text) {
		while (true) {
			System.out.println(text);
		}
	}
}

还是只有一个线程会打印。这是因为在静态方法上使用synchronized,是在这个类的静态方法上加了把锁,同一时刻只能有一个线程访问该静态方法。

2.当synchronized修饰代码块的时候,在谁的身上加锁取决于synchronized(o) {}中的对象o。在这里你可以把o看成一个门卫,进入synchronized标注的代码块必须取得门卫的同意,门卫每次只放一个线程进入。一个门卫可以看管多个代码块,同一个门卫只允许同一个线程进入。

我们最常见到的门卫是this:

public class ThreadTest {

	public static void main(String[] args) {
		final PrintClass pc = new PrintClass();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printText("Thread-1:text1", "Thread-1:text2", "Thread-1:text3");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printText("Thread-2:text1", "Thread-2:text2", "Thread-2:text3");
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public void printText(String text1, String text2, String text3) {
		System.out.println(text1);
		synchronized (this) {
			for (int i=0; i < 1000; i++) {
				System.out.println(text2);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		System.out.println(text3);
	}
}

运行结果是:

Thread-1:text1
Thread-1:text2
Thread-2:text1
Thread-1:text2
Thread-1:text2
Thread-1:text2

           .

           .

           .

我们看Thread-1和Thread-2都进入了该方法,但是只有Thread-1进入了同步块,Thread-2被阻塞了,所以text3也未打印出,只能等到Thread-1完成Thread-2才可以进入。我们再看一个同一个门卫看守两块代码的例子:

public class ThreadTest {

	public static void main(String[] args) {
		final PrintClass pc = new PrintClass();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printText("text");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				pc.printVal(1111);
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public void printText(String text) {
		synchronized (this) {
			while (true) {
				System.out.println(text);
			}
		}
	}
	public void printVal(int val) {
		synchronized (this) {
			while (true) {
				System.out.println(val);
			}
		}
	}
}

这段代码只能打印出一种结果,原因是这个两个方法都是门卫this来看管的,他只让一个线程通过,即使是不同的代码块。其实由this看管的代码块相当于在类的方法上加synchronized(不包括静态方法哦!),是在类实例上加锁的。如果是新的实例,那么this就不是同一个了。不同的门卫this就可以让不同的线程进入了,这里就不在举例了,那我们在看看其他的门卫:

public class ThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printText("text");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printVal(1111);
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	public void printText(String text) {
		synchronized (PrintClass.class) {
			while (true) {
				System.out.println(text);
			}
		}
	}
	public void printVal(int val) {
		synchronized (PrintClass.class) {
			while (true) {
				System.out.println(val);
			}
		}
	}
}

这次是这个类来作门卫了,虽然我们使用了2个类实例(在run方法里new了两个实例),但这次还是只打印出一种结果,这就是类同步。就相当于在静态方法上加synchronized,这个代码块同一时间只允许一个线程访问。你可以试试将PrintClass.class换成this,这样两个方法都可以打印出结果的。其实添加一个静态成员变量,用他做门卫,也可以达到上面demo的效果:

public class ThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printText("text");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printVal(1111);
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	private static Object lock = new Object();
	public void printText(String text) {
		synchronized (lock) {
			while (true) {
				System.out.println(text);
			}
		}
	}
	public void printVal(int val) {
		synchronized (lock) {
			while (true) {
				System.out.println(val);
			}
		}
	}
}

因为静态成员变量是类持有的,每个实例共享的,所以这样加锁与给类本身加锁效果相同。但要是只是普通的成员变量呢,在这里我们用String这个对象来举例:

public class ThreadTest {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printText("text");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				PrintClass pc = new PrintClass();
				pc.printVal(1111);
			}
		});
		t1.start();
		t2.start();
	}

}

class PrintClass {

	private String str = new String();

	public void printText(String text) {
		synchronized (str) {
			while (true) {
				System.out.println(text);
			}
		}
	}
	public void printVal(int val) {
		synchronized (str) {
			while (true) {
				System.out.println(val);
			}
		}
	}
}

打印出了2种结果,证明这String对象是被类实例持有的,所以只针对类实例加锁。但是,试一试将定义改为private String str = “”。呵呵,只打印出一种结果吧。原因可能是JVM虚拟机优化了String,使之在赋值之后不可变了,所以这个门卫始终就是他一人了。

最后,还要注意一点,方法上的synchronized是不能继承的哦,子类想要同步,要在方法上自己添加。

1 Reply to “java多线程2:synchronized”

发表评论