java synchronized关键字的用法
0.先导的问题代码
下面的代码演示了一个计数器,两个线程同时对i进行累加的操作,各执行1000000次.我们期望的结果肯定是i=2000000.但是我们多次执行以后,会发现i的值永远小于2000000.这是因为,两个线程同时对i进行写入的时候,其中一个线程的结果会覆盖另外一个.
public class accountingsync implements runnable { static int i = 0; public void increase() { i++; } @override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(string[] args) throws interruptedexception { accountingsync accountingsync = new accountingsync(); thread t1 = new thread(accountingsync); thread t2 = new thread(accountingsync); t1.start(); t2.start(); t1.join(); t2.join(); system.out.println(i); } }
要从根本上解决这个问题,我们必须保证多个线程在对i进行操作的时候,要完全的同步.也就是说到a线程对i进行写入的时候,b线程不仅不可以写入,连读取都不可以.
1.synchronized关键字的作用
关键字synchronized的作用其实就是实现线程间的同步.它的工作就是对同步的代码进行加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性.就像上面的代码中,i++的操作只能同时又一个线程在执行.
2.synchronized关键字的用法
指定对象加锁:对给定的对象进行加锁,进入同步代码块要获得给定对象的锁
直接作用于实例方法:相当于对当前实例加锁,进入同步代码块要获得当前实例的锁(这要求创建thread的时候,要用同一个runnable的实例才可以)
直接作用于静态方法:相当于给当前类加锁,进入同步代码块前要获得当前类的锁
2.1指定对象加锁
下面的代码,将synchronized作用于一个给定的对象.这里有一个注意的,给定对象一定要是static的,否则我们每次new一个线程出来,彼此并不共享该对象,加锁的意义也就不存在了.
public class accountingsync implements runnable {
final static object object = new object();
static int i = 0;
public void increase() {
i++;
}
@override
public void run() {
for (int j = 0; j < 1000000; j++) {
synchronized (object) {
increase();
}
}
}
public static void main(string[] args) throws interruptedexception {
thread t1 = new thread(new accountingsync());
thread t2 = new thread(new accountingsync());
t1.start();
t2.start();
t1.join();
t2.join();
system.out.println(i);
}
}
synchronized关键字作用于实例方法,就是说在进入increase()方法之前,线程必须获得当前实例的锁.这就要求我们,在创建thread实例的时候,要使用同一个runnable的对象实例.否则,线程的锁都不在同一个实例上面,无从去谈加锁/同步的问题了.
public class accountingsync implements runnable { static int i = 0; public synchronized void increase() { i++; } @override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(string[] args) throws interruptedexception { accountingsync accountingsync = new accountingsync(); thread t1 = new thread(accountingsync); thread t2 = new thread(accountingsync); t1.start(); t2.start(); t1.join(); t2.join(); system.out.println(i); } }
请注意main方法的前三行,说明关键字作用于实例方法上的正确用法.
2.3直接作用于静态方法
将synchronized关键字作用在static方法上,就不用像上面的例子中,两个线程要指向同一个runnable方法.因为方法块需要请求的是当前类的锁,而不是当前实例,线程间还是可以正确同步的.
public class accountingsync implements runnable { static int i = 0; public static synchronized void increase() { i++; } @override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(string[] args) throws interruptedexception { thread t1 = new thread(new accountingsync()); thread t2 = new thread(new accountingsync()); t1.start(); t2.start(); t1.join(); t2.join(); system.out.println(i); } }
3.错误的加锁
从上面的例子里,我们知道,如果我们需要一个计数器应用,为了保证数据的正确性,我们自然会需要对计数器加锁,因此,我们可能会写出下面的代码:
public class badlockoninteger implements runnable { static integer i = 0; @override public void run() { for (int j = 0; j < 1000000; j++) { synchronized (i) { i++; } } } public static void main(string[] args) throws interruptedexception { badlockoninteger badlockoninteger = new badlockoninteger(); thread t1 = new thread(badlockoninteger); thread t2 = new thread(badlockoninteger); t1.start(); t2.start(); t1.join(); t2.join(); system.out.println(i); } }
当我们运行上面代码的时候,会发现输出的i很小.这说明线程并没有安全.
要解释这个问题,要从integer说起:在java中,integer属于不变对象,和string一样,对象一旦被创建,就不能被修改了.如果你有一个integer=1,那么它就永远都是1.如果你想让这个对象=2呢?只能重新创建一个integer.每次i++之后,相当于调用了integer的valueof方法,我们看一下integer的valueof方法的源码:
public static integer valueof(int i) { if (i >= integercache.low && i <= integercache.high) return integercache.cache[i + (-integercache.low)]; return new integer(i); }
integer.valueof()实际上是一个工厂方法,他会倾向于返回一个新的integer对象,并把值重新复制给i;
所以,我们就知道问题所在的原因,由于在多个线程之间,由于i++之后,i都指向了一个新的对象,所以线程每次加锁可能都加载了不同的对象实例上面.解决方法很简单,使用上面的3种synchronize的方法之一就可以解决了.