Synchronized
synchronized可以用来修饰以下3个层面:
synchronized修饰实例方法:
synchronized修饰实例方法的时候,锁对象是当前的实例对象,同一个实例调用此方法的时候才会产生互斥效果,不同的实例对象之间不会有互斥效果。
上面的代码中,在不同的线程中调用不同对象的printLog方法,两者相互不排斥,两个线程会随机竞争CPU资源:
上面的打印效果可以看出,两个线程的执行互不影响,打印信息是随机的。
如果将代码改成两个线程公用一个对象进行printLog:
代码执行的效果如下:
可以看出只有在一个线程执行完毕之后,另一个线程的调用才会执行,不同线程中的调用是相互排斥的。
修饰静态类方法
如果synchronized修饰的静态类的方法,那么锁对象就是当前Class。在不同的线程中调用不同的实例对象,也会有互斥的效果。
下面将printLog修改为静态方法:
执行代码进行打印:
上面的打印结果可以看出,两个线程是依次执行的。
synchronized修饰代码块
synchronized作用于代码块的时候,锁对象是跟在synchronized后面的对象。任何的对象都可以当作synchronize的锁对象。
实现细节
synchronized既可以作用于方法,也可以作用于代码块。但不同的锁对象的实现是有区别的。
下面的代码中,synchronized作用于代码块上:
使用javap查看对应的字节码:
从上面编译好的字节码文件可以看出,synchronized修饰代码块的时候,会把被修饰的代码用monitorenter和monitorexit进行包裹。另外字节码中有一个monitorenter指令,和两个monitorexit指令。这是虚拟机为了保证在异常发生的情况下,锁也能够被释放,所以两个monitorexit,一个是正常流程释放锁,另一个是异常流程释放锁。
当synchronized修饰方法的时候
从上面的代码和字节码中可以看到,synchronized修饰方法的时候,代码编译时会在方法的flags中标示ACC_SYNCHRONIZED标志。当虚拟机访问一个ACC_SYNCHRONIZED方法的时候,会自动在方法的开始和结束的位置添加monitorenter和monitorexit指令。
monitorenter和monitorexit指令可以理解为一把锁,这个锁有两个重要的属性:计数器和指针。
锁计数器默认为0,当执行monitorenter指令的时候,如果这把锁的计数器为0,说明这把锁没有被任何线程锁持有,此时线程会将锁计数器加1,并将锁的指针指向自己。当执行monitorexit的时候,会将计数器减1。
ReentrantLock
ReentrantLock和synchronized不同,ReentrantLoock的加锁解锁都是需要手动完成的:
上面的代码中lock和unlock分别是加锁和解锁的过程。
上面的打印可以看出ReentrantLock可以实现和synchronized一样的效果。
【注意】ReentrantLock的加锁和解锁需要手动完成,为了保证在异常流程中也能够成功的解锁,我们需要在try-catch的finally中解锁,从而保证任何时候锁都可以被正常释放。
公平锁
ReentrantLock有一个带参数的构造函数:
默认情况下,synchronized和ReentrantLock都是非公平锁。但是ReentrantLock可以通过传入fair = true来创建一个公平锁。公平锁是通过一个同步队列来实现多线程按申请锁的顺序获得锁。
运行代码:
【分析】
读写锁(ReentrantReadWriteLock)
concurrent包中提供了ReentrantReadWriteLock,在读操作的时候获取读锁,在写操作的时候获得写锁。
1. 创建一个读写锁:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
2. 通过rwLock对象分别获得读锁(ReadLock)和写锁(WriteLock):
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
3. 使用读锁和写锁:
当写入操作在执行的时候,读取数据的操作会被阻塞,当写入操作执行完毕之后,肚脐数据的操作继续执行,并且读取的数据是最新写入的数据;多个读操作之间是互不排斥的。