synchronized关键字与锁
在单线程环境下,所有的代码都是串行执行的,共享资源在这种情况的读写操作不会有安全问题的,但是单线程环境已经不使用于当前所处的时代了,现如今即便是一两千元的主机也是多核多线程的,如果在这种大环境下依然使用单线程处理程序,势必带来资源上的浪费和性能上的降低。
多线程可以更快的运行代码,带来了在性能上质的提升,但是操作多线程也为编写代码引入了跟多的问题,因为不同的线程能被不同的CPU内核并行执行,导致在读写一些共享资源时会产生一些副作用。
下面我们先来看一个简单的例子
class Counter {
private int sum = 0;
public void count(){
while (sum < 1000000) sum++;
}
public static void main(String[] args) {
Counter counter=new Counter();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) { //启动100个线程
Thread thread=new Thread(() ->counter.count(), "thread-" + i);
threads.add(thread);
thread.start();
}
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(counter.sum); //输出
}
}
我们预期应输出值为 1000000,但是实际上在多线程环境下,会有个别情况出现输出值大于 1000000 的情况。
怎么解决这个问题呢?其实很简单,Java已经内置好了一个关键字可以很好的解决这个问题
关键字 synchronized
我们将上面的代码修改一下:
class Counter {
...
public synchronized void count(){
while (sum < 1000000) sum++;
}
...
}
在Counter类的count()方法上修饰synchronized关键字,即可将这个方法修改为一个同步方法,加上synchronized后,无论这段代码使用多少个线程跑多少次,输出结果总是能得到我们的期望值。
那么 synchronized 是怎么做到的呢?
首先,我们先来回忆一下Java基础中的内容,在Java中每一个实例对象都一个monitor(监视器),monitor可以与对象一起创建、销毁,也可以在线程试图获取对象锁的时候自动生成。
monitor 是由C++实现的:
ObjectMonitor.hpp
ObjectMonitor() {
_count = 0 ;
_owner = NULL;
_WaitSet = NULL ;
_WaitSetLock = 0 ;
_EntryList = NULL ;
...
}
这里定义两个队列:_WaitSet和 _EntryList。
其中 _WaitSet 用来存放每个等待锁的线程对象。
_owner指向持有ObjectMonitor对象的线程。
当多线程同时访问同一段同步代码时,首先会存放到 _EntryList 队列中,当其中一个线程获取到对象monitor时,将 _owner 设置为当前线程,并且将 _count 数值加一,当 _owner 线程释放monitor时,将 _count 数值减一,这意味着当 _owner线程每获取一次对象monitor,_count都要加一,每释放一次对象,_count都要减一,最终当方法执行完毕或者调用wait()方法时,_owner变量就会被置为null,同时_count减1,并且该线程进入 WaitSet集合中,等待下一次被唤醒。
当我们在方法上修饰synchronized时,编译器编译代码生成的字节码指令如下:
字节码中有一个ACC_SYNCHRONIZED标识,Java通过这个标识即可知道这是一个需要进行同步的方法,线程在进入方法时,首先回去尝试获取对象monitor(ObjectMonitor.hpp),如果获取成功,monitor会将_owner指向当前线程,并将_count加一,当方法执行完成后,会将_count减一,并将_owner置为null。
当我们使用synchronized代码块时,编译器编译代码生成的字节码指令如下:
字节码中 第 3 行有一个 monitorenter ,第 27 、33 行各有一个monitorexit,但是27、33行之间是一个分支代码,所有 进入monitorenter之后,仅会执行一个monitorexit,一进一出之间就代表了并将_count加一减一的过程。
获取同步锁的几种方式:
- 执行一个同步方法获取对象锁 public synchronized void method();
- 执行一个同步代码块取对象锁 synchronized(obj){}
- 执行一个静态同步方法获取类锁 public static synchronized void method();
对象锁锁的当前类的实例对象,不同实例对象的锁可以被不同线程获取。
类锁则锁的试当前类,只影响类方法的调用,而不影响多线程获取不同实例对象的锁。
说了这么多,synchronized的作用其实可以用一个句话描述:被修饰的代码,同一时刻同一个对象只能被一个线程执行。
synchroized 性能问题
在 jdk1.5之前,synchroized的性能确实相对捉急,在jdk1.5之后,经过对其经过了大刀阔斧的优化之后,synchronized的性能已经不弱于Lock类了。
除了使用synchronized,还有什么其他方法能够进行同步呢?
有,在jdk1.5之后,Oracle团队在java.util.concurrent.locks开发了很多更灵活的Lock类。
这部分内容,等待后期在进行补充。