Lock锁
锁实现提供了比使用
synchronized
方法和语句更广泛的锁操作。它们允许更灵活的结构,可能有不同的属性,可能支持多个关联的Condition对象。锁是用于控制多个线程对共享资源的访问的工具。
通常,锁提供对共享资源的独占访问:一次只有一个线程可以获得锁,并且所有访问共享资源的访问都需要先获得锁。但是,有些锁可能允许并发访问共享资源。例如readwritellock的读锁。
同步方法或语句的使用提供了对每个对象关联的隐式监视器锁的访问,但强制所有锁的获取和释放都以块结构的方式进行:当多个锁被获取时,它们必须以相反的顺序被释放,并且所有的锁都必须在获取锁的同一个词法作用域中被释放。虽然同步方法和语句的作用域机制使使用监视器锁进行编程变得更加容易,并有助于避免许多涉及锁的常见编程错误,但在某些情况下,您需要以更灵活的方式使用锁。
例如,一些用于遍历并发访问的数据结构的算法需要使用“手拉手”或“链锁”:您获取节点A的锁,然后是节点B,然后释放A并获取C,然后释放B并获取D,等等。
Lock接口的实现允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁,从而允许使用这种技术。
传统synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
lock.ReadWriteLock();
ReadlriteLock维护一对相关联的锁,一个用于只读操作,一个用于写操作。只要没有写线程,读锁可以被多个读线程同时持有。写锁是排他的。
所有的readwritellock实现必须保证writellock操作的内存同步效果(在Lock接口中指定)也与相关的readLock保持一致。也就是说,成功获取读锁的线程将看到之前发布的写锁的所有更新。
与互斥锁相比,读写锁在访问共享数据时允许更高级别的并发。它利用了一个事实,即虽然一次只有一个线程(写线程)可以修改共享数据,但在许多情况下,任意数量的线程都可以并发地读取数据(因此读取线程)。理论上,使用读写锁所允许的并发性的增加将导致性能的提高,而不是使用互斥锁。在实践中,只有在共享数据的访问模式合适的情况下,这种并发性的增加才能在多处理器上完全实现。
读写锁是否会提高性能的使用互斥锁的频率取决于读取数据被修改相比,读和写操作的持续时间,和争用数据——也就是说,线程的数量,将尝试读或写数据在同一时间。例如,最初用数据填充的集合,在频繁搜索的情况下(例如某种目录)很少修改的集合是使用读写锁的理想候选对象。然而,如果更新变得频繁,那么数据的大部分时间都被独占锁定,并且几乎不会增加并发性。此外,如果读操作太短,读写锁实现的开销(其本质上比互斥锁复杂)可能会主导执行成本,特别是当许多读写锁实现仍然通过一小段代码序列化所有线程时。最终,只有分析和测量才能确定读写锁的使用是否适合您的应用程序。
虽然读写锁的基本操作很简单,但实现必须做出许多策略决策,这些决策可能会影响给定应用程序中读写锁的有效性。
这些策略的例子包括:
当读锁和写锁都在等待时,当写锁释放时,决定是授予读锁还是写锁。作者的偏好是普遍的,因为写作被认为是短的和不频繁的。读者偏好不太常见,因为如果读者像预期的那样频繁且长期存在,就会导致写操作的长时间延迟。公平或“有序”的实现也是可能的。
当一个读取器处于活动状态,而一个写入器正在等待时,确定请求读锁的读取器是否被授予读锁。偏爱读器会无限期地延迟写器,而偏爱写器会降低并发的可能性。确定锁是否可重入:具有写锁的线程可以重新获得它吗?它能在持有写锁的同时获得读锁吗?读锁本身是可重入的吗?
写锁可以降级为读锁而不允许写入者介入?读锁是否可以优先升级为写锁,而不是其他等待的读锁或写锁?
在评估给定实现对应用程序的适用性时,应该考虑所有这些因素。
ReadWriteLockTest
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
//写入操作
for (int i = 0; i < 6; i++) {
int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
//读取操作
for (int i = 0; i < 6; i++) {
int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
class MyCacheLock {
private final Map<String, Object> map = new HashMap<>();
//读写锁
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 存,写入的时候只有一个人操作
public void get(String key) {
lock.readLock().lock();
Object o = null;
try {
System.out.println(Thread.currentThread().getName() + "读取");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok" + o);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完毕");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
}
执行结果, 【写入和读取是多发线程,结果打印顺序并不是唯一的,主要在于CPU时间切片选择】
1写入1
1写入完毕
2写入2
2写入完毕
0写入0
0写入完毕
4写入4
4写入完毕
5写入5
5写入完毕
3写入3
3写入完毕
1读取
2读取
0读取
4读取
5读取
3读取
0读取ok0
1读取ok1
4读取ok4
2读取ok2
5读取ok5
3读取ok3