Java的synchronized关键字可以帮助我们解决多线程并发的问题,比如我们有一个公共资源,多个线程都来操作这个公共的资源,就会出现并发的问题,比如不同的线程对同一个数据同时进行读和写,肯定会使得每个线程最后拿到的都不是自己所希望拿到的值,为了解决这个问题,我们可以使用synchronized关键字加锁。
以前synchronized由于性能消耗太大,在Java SE 1.6对它进行了优化,使得synchronized锁现在有4种状态:无锁、偏向锁、轻量级锁、重量级锁,他们性能消耗是由低向高的,在没有竞争出现的时候,它是偏向锁,性能消耗非常小,基本就没什么消耗,当竞争来了并且竞争变大,它就会逐渐升级成重量级锁,此时的锁的性能开销很大,当一个线程获得锁后,其它的线程只能阻塞等待。
这就是关键,当线程竞争时,synchronized会升级成重量级锁,当一个线程持有锁时,其他的线程只能阻塞等待,有些时候会给我们带来不必要的性能损耗。
我们先来看一段代码
public class Storage {
//被操作的公共数据
private int num;
//使用同步方法,锁对象是当前类对象
private synchronized void write(){
try {
//模拟一下耗时操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//给num增10
num+=10;
System.out.println("Writer:num="+num);
}
//使用同步方法,锁对象是当前类对象
private synchronized void read(){
try {
//模拟一下耗时操作
TimeUnit.SECONDS.sleep(1);
System.out.println("Reader:num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Storage类中有一个公共变量,write()和read()方法是来操作公共变量num的,我们使用synchronized关键在修饰方法,锁对象为当前类对象,那么当执行write()方法时,不能有线程执行read()方法,同理当执行read()方法时,也不能有线程执行write()方法。
public static class Writer implements Runnable{
private Storage storage;
public Writer(Storage storageByLock){
storage = storageByLock;
}
@Override
public void run() {
storage.write();
}
}
public static class Reader implements Runnable{
private Storage storage;
public Reader(Storage storageByLock){
storage = storageByLock;
}
@Override
public void run() {
storage.read();
}
}
这是写着和读者,他们实现了Runnable接口,在run方法里调用了Storage类的write()方法和read()方法。
public static void main(String[] args) {
Storage storage = new Storage();
List<Thread> threads = new ArrayList<Thread>();
for(int i=0 ; i<5 ; i++){
Thread t = new Thread(new Writer(storage));
threads.add(t);
}
for(int i=0 ; i<5; i++){
Thread t = new Thread(new Reader(storage));
threads.add(t);
}
long startTime = System.currentTimeMillis();
for(Thread t : threads){
t.start();
}
for(Thread t : threads){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("time="+(endTime-startTime));
}
这是我们的main方法,首先我们new了一个Storage对象,作为公共的资源,我们创建了5个写者和5个读者,他们分别都是一个线程,我们来运行一下,看看他们是如何执行的
我们看到他们是顺序执行的,当一个线程正在执行时,其他的线程是阻塞的,synchronized关键字为我们解决了并发引起的线程安全问题。
但是,我们想象这样一种情况,比如写着很少,读者很多,而读者只是想读出数据,并没有对数据进行修改,那么在读者很多的情况下,它还是得按照一个一个的顺序来执行,这样的效率会很慢,我们来模拟一下这种情况
for(int i=0 ; i<1; i++){
Thread t = new Thread(new Writer(storage));
threads.add(t);
}
for(int i=0 ; i<10; i++){
Thread t = new Thread(new Reader(storage));
threads.add(t);
}
我们现在只需要一个写着,但是我们有10个读者,我们来看一看运行的情况
我们看到读者们只是读出数据,并没有改变数据,他们读的值都是一样的,最后程序运行了11秒多才结束。
所以,上面那个情况是有问题的,我们应该需要这样一种机制,就是当有写者正在写的时候,他是独占资源的,其他无论读者还是写者只能阻塞等待;当没有写者正在写的时候,读者们是可以并行读到数据的,这样当写着很少,读者很多的时候,读者们几乎可以同时完成读的操作,这样就大大提升了程序的运行效率。
Java给我们提供了ReentrantReadWriteLock可以解决上面的问题,我们将Storage类中的write()方法和read()方法使用ReentrantReadWriteLock来进行加锁
public class Storage {
//被操作的公共数据
private int num;
//Lock锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
private void write(){
//获取到写者锁
//当线程获取到写着锁时,其他线程不可以再获得写者锁和读者锁
//它就相当于synchronized锁住的方法
lock.writeLock().lock();
try{
TimeUnit.SECONDS.sleep(1);
num+=10;
System.out.println("Writer:num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//一定不要忘了在finially中释放锁
lock.writeLock().unlock();
}
}
private void read(){
//获取读者锁
//读者锁可以同时由很多个线程获得,因此可以增加效率
lock.readLock().lock();
try{
TimeUnit.SECONDS.sleep(1);
System.out.println("Reader:num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//一定不要忘了释放锁
lock.readLock().unlock();
}
}
}
这里需要解释一下:为什么读者锁可以由多个线程同时获得?
如果当前没有写者存在,那么线程可以持有读者锁(只要有写者存在,就不能有读者存在,只有当写者释放了写者锁之后,读者才能够获得读者锁),每当一个线程持有一个读者锁后,系统就会将读者锁的数量加一,每当一个线程释放一个读者锁后,系统会将读者锁的数量减一,只有当读者锁的数量为0时,写者才能够获得写者锁,否则他会阻塞等待所有的读者都读取完毕后,才能进行写操作!
好了,其他地方的代码不变,我们还是模拟1个写者和10个读者,我们看看运行的结果如何
看见没,2秒就结束了!我们几乎提高了4倍的运行效率!如果在高并发的环境下,读者千千万万个,那么提高的性能就更加的明显了!