在上一节中,我们通过以下代码完成多个线程对共享资源的竞争问题。synchronized (ThreadCompetitionDemo.class){
count++;
}
这段代码保证了count++操作可以得到正确的累加结果。
在java并发编程中,这段代码称之为一个同步代码块。同步代码块的标准语法如下:synchronized(锁对象){
...
}
synchronized关键字的作用就是用于声明这是一段同步代码块。JVM在遇到synchronized关键字时,会把花括号"{...}"中间的代码当成一个原子操作,也就是说,只有等到同步代码块中的代码在执行完成的时候,CPU才会进行线程的上下文切换,而不会再同步代码块中的内容只执行了一部分的时候,就切换到其他线程运行。
每个synchronized关键字都必须要配合锁进行使用,在java中,任何对象实例都可以当做一个锁来使用,而上面代码中,ThreadCompetitionDemo.class表示的一个Class对象的实例,因此其也是一个锁。我们经常会看到一些概念,例如隐式锁(intrinsic lock)、监视器锁(monitor lock),其实指的都是把一个普通java对象当成一个锁来使用。在java官方的并发编程教程中,就提到每一个java对象都会关联一个隐式锁,因此当我们在使用synchronized关键字编写同步代码块时,实际上利用的就是小括号中的java对象的关联的隐式锁。
需要注意的是,在使用同步代码块解决多线程竞争共享资源的问题时,我们使用的必须是同一把锁。所谓同一把锁,其实指得就是同一个对象实例。多个线程竞争同一把锁,先得到锁的先执行,在同步代码块执行完成之后,就把自动把锁释放掉。然后其他线程再来抢夺这把锁,进行代码的执行。
读者从这里应该看到,如果多个线程竞争的是同一把锁,JVM除了保证同步代码块中的内容可以以原子方式执行,其实还隐含了另外一个功能,就是先后执行的顺序,先得到锁的先执行,这实际上是happens-before原则,我们在后面会详细详解。
考虑下,在第一节的案例中,如果我们使用的是不同的锁(其实就是不同的java对象实例),会产生什么情况?我们将代码修改为如下:public class MultiLock {
static int count=0;
//定义两个锁,前面我们已经提到,任何的Java对象实例都可以当成锁,这里用两个不同的字符串实例表示两个不同的锁
static String lock1="lock1";
static String lock2="lock2";
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
new Thread(){
@Override
public void run() {
for (int i = 0; i <5000000 ; i++) {
synchronized (lock1){//使用lock1
count++;
}
}
System.out.println("自定义线程:计算完成...,耗时"+(System.currentTimeMillis()-start));
}
}.start();
for (int i = 0; i <5000000 ; i++) {
synchronized (lock2){//使用lock2
count++;
}
}
ArrayList arrayList = new ArrayList<>();
System.out.println("主线程:计算完成....,耗时"+(System.currentTimeMillis()-start));
Thread.sleep(10000);
System.out.println("count:"+count);
}
}
这段代码中,我们用了两个不同的字符串实例代表两个不同的锁,主线程和自定义线程分别使用不同的锁。代码运行结果如下:主线程:计算完成....,耗时390
自定义线程:计算完成...,耗时450
count:8699531
可以看到结果依然是不正确的。为什么会出现这个问题呢?
虽然同步代码块中的内容可以保证以原子的方式运行,但是因为使用了不同的锁,所以两个线程依然可以并发执行。也就是说,两个线程依然可以同时对变量count进行修改。只有在使用同一个锁的情况下,才能保证任意时刻,只能有一个线程对变量进行修改。