当一个数据被多个线程所共同使用,且线程并发执行时,我们需要保证保证该数据的准确性,既一个线程对数据的操作不会对另一个线程产生不合理的影响。
实现的手段基本上是对数据加锁,当线程要对数据进行操作时必须获得锁后再进行操作。锁可分为乐观锁和悲观锁。
乐观锁,总是乐观地假设最好的情况,每次去拿数据的时候都认为别人不会修改这个数据,所以不会上锁,只会要对数据进行更新时判断一下在此期间(拿到数据到更新的期间)别人有没有去更改这个数据,可以使用版本号机制和CAS算法实现。
以AtomicInteger为例看看底层是怎么进行操作的
AtomicInteger integer=new AtomicInteger(123);
int a=integer.addAndGet(321);//+321
System.out.println(a);//结果为444
上面编写了一个示例,创建一个AtomicInteger对象,调用它的addAndGet方法,此方法是加上一个数并返回相加后的结果。然后我们来看看这个方法的源码。
public final int addAndGet(int delta) {
return U.getAndAddInt(this, VALUE, delta) + delta;
}
可以看到这个方法的返回值调用了U(Unsafe对象)的getAndInt方法来获取当前对象在内存中的值
下面是getAndInt方法的源码
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);//获取对象中offset偏移地址对应的整型field的值
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
可以看到逻辑就是若weakCompareAndSetInt的返回值为false则不断的获取整形值field
下面是weakCompareAndSetInt的源码
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);//比较当前内存中的值和期望值x是否相等
}
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
Java中的关键字,是由JVM来维护的。是JVM层面的锁。
是非公平锁。
如果获取锁的线程由于要等待一些原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待。
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。采用synchronized则会导致一个线程在进行读操作,其他线程会等待此线程读完。
综上所述,下synchronized十分的影响效率,上述的这些问题通过使用Lock可以解决。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
是JDK5以后才出现的接口。使用Lock是调用对应的API。是API层面的锁
在创建对象时从构造方法传入true可创建公平锁,不传入默认是不公平锁。
相较synchronized的自动获得和释放锁,Lock需要手动获得和释放锁。
Lock是一个接口,一般使用它的实现类ReentrantLock创建对象来获取和释放锁。
ReadWriteLock是一个接口,用来获取只读的锁和写锁。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
实现类有ReentrantReadWriteLock。
通过ReentrantReadWriteLock的readLock()和WriteLock()方法获取读锁。
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
public static class ReadLock implements Lock, java.io.Serializable {......}
public static class WriteLock implements Lock, java.io.Serializable {......}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
可通过以下的方式获得锁实例
Lock lock=reentrantReadWriteLock.readLock();
Lock lock=reentrantReadWriteLock.writeLock();
获得锁的方法和释放锁的方法与Lock接口中使用方式相同。
写锁获取锁:
如果读取锁定和写入锁定都不被另一个线程保持并立即返回,则将获取写入锁定。
如果当前线程已经保持此锁定,则该方法立即返回。
如果锁由另一个线程持有,那么当前线程将被禁用以进行线程调度,并且在发生以下两种情况之一之前处于休眠状态:
读锁获取锁:
如果写锁定未被另一个线程保持并立即返回,则获取读锁定。
如果写锁定由另一个线程保持,则当前线程将被禁用以进行线程调度,并且在获取读取锁定之前处于休眠状态。
锁降级:从写锁变成读锁;锁升级:从读锁变成写锁。
ReentrantReadWriteLock支持锁升级,不支持锁降级
下面是一段测试锁升级,代码在还没释放写锁的情况下去申请读锁。
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
System.out.println(3);
reentrantReadWriteLock.writeLock().lock();
System.out.println(2);
reentrantReadWriteLock.readLock().lock();
System.out.println(1);
}
结果如下:
上面代码不会产生死锁。会发生锁降级,从写锁降级成读锁。但没有正确的释放写锁,不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
下面是测试锁升级的代码,在读锁还没释放的情况下去申请写锁
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
System.out.println(3);
reentrantReadWriteLock.readLock().lock();
System.out.println(2);
reentrantReadWriteLock.writeLock().lock();
System.out.println(1);
}