与Lock或者ReadWriteLock接口提供的锁机制都不同,StampedLock类提供了一种特殊的锁。事实上,StampedLock类不实现这些接口,但其提供的功能相似。
首先要注意的是这种锁的主要目的是作为帮助类实现线程安全组件,所以在标准的应用中并不多见。
StampedLock锁最重要的特性如下所示:
- 三种不同的模式获得锁控制:
- **写入:**这种模式下,独占锁的使用权,其它线程均无法控制此锁。
- **读取:**这种模式下,非独占锁的使用权,其它线程在此模式或者乐观读取模式下都可以使用此锁。
- **乐观读取:**线程无法控制程序块,其它线程在写入模式下能够控制锁。当在乐观读取模式下得到锁,并且想要访问此锁保护的共享数据时,检查是否具有访问权,或者不使用validate()方法。
- StampedLock类提供的方法用来:
- 使用这三种模式得到锁控制权。如果方法(readLock(),writeLock(),readLockInterruptibly())无法控制锁,当前线程将被暂停直到方法得到锁。
- 使用这三种模式得到锁控制权。如果方法(tryOptimisticRead(),tryReadLock(),tryWriteLock())无法控制锁,则返回代表这种情形的特殊值。
- 进行模式转换,如果可行的话。如果不能进行模式转换,方法(asReadLock(),asWriteLock(),asReadWriteLock())会返回特殊值。
- 释放锁。
- 所有方法返回称之为标记的长整型值,用来对锁起作用。如果返回值为零,意味着此方法无法得到锁。
- StampedLock锁不是像Lock和ReadWriteLock接口实现的重入锁。如果调用这些方法尝试再次得到锁,线程可能会被阻断并且形成死锁。
- StampedLock锁没有所有权的概念,一个线程能够控制它们,同时被其它线程释放。
- 最后,对后面将要控制锁的线程不设约束。
在本章中,学习如何使用StampedLock类的不同模式保护访问共享数据对象。在三个并发任务中使用共享对象,来测试StampedLock的三种访问模式(写入、读取、乐观读取)。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
首先,实现共享数据对象。创建名为Position的类,包括两个名为x和y的整型属性,以及属性值的读写方法。代码很简单不在这里显示。
-
实现Writer任务,实现Runnable接口,包含两个属性:名为position的Position对象,名为lock的StampedLock对象,并在构造函数中进行初始化:
public class Writer implements Runnable{ private final Position position; private final StampedLock lock; public Writer (Position position, StampedLock lock) { this.position = position; this.lock = lock; }
-
实现run()方法,在写入模式中得到锁,改变位置对象的两个属性值,中断线程执行1秒钟,释放锁(在任何情形下使用try…catch…finally结构,将代码置于finally部分来释放锁),然后再中断线程一秒钟。在循环中重复上述过程10次:
@Override public void run() { for(int i = 0 ; i < 10; i ++){ long stamp = lock.writeLock(); try { System.out.printf("Writer : Lock acquired %d\n", stamp); position.setX(position.getX() + 1); position.setY(position.getY() + 1); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlockWrite(stamp); System.out.printf("Writer : Lock released %d\n", stamp); } try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
然后,实现Reader任务,读取共享对象的值。创建名为Reader的类并实现Runnable接口,包含两个属性:名为position的Position对象,名为lock的StampedLock对象,并在构造函数中进行初始化:
public class Reader implements Runnable{ private final Position position; private final StampedLock lock; public Reader(Position position, StampedLock lock){ this.position = position; this.lock = lock; }
-
实现run()方法,在读取模式中得到锁,在控制台中输出位置对象的值,并且中断线程200毫秒,最后使用try…catch…finally结构中的finally里置入代码来释放锁。在循环中重复上述过程50次:
@Override public void run() { for(int i = 0 ; i < 50 ; i ++){ long stamp = lock.readLock(); try { System.out.printf("Reader : %d - (%d, %d)\n", stamp, position.getX(), position.getY()); TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlockRead(stamp); System.out.printf("Reader : %d - Lock released\n" , stamp); } } }
-
然后,实现OptimisticReader任务,实现Runnable接口,包含两个属性:名为position的Position对象,名为lock的StampedLock对象,并在构造函数中进行初始化:
public class OptimisticReader implements Runnable { private final Position position; private final StampedLock lock; public OptimisticReader(Position position, StampedLock lock){ this.position = position; this.lock = lock; }
-
现在实现run()方法,首先使用tryOptimisticRead()方法在乐观读取模式中得到锁的标记。然后,重复循环操作100次。每次循环中,使用validate()方法验证是否能够访问数据,如果方法返回true,在控制台中输出位置对象值,否则,在控制台中输出一条信息并且再次使用tryOptimisticRead()方法得到另一个标记。然后,中断线程200毫秒:
@Override public void run() { long stamp; for(int i =0 ; i < 100 ; i++){ try { stamp = lock.tryOptimisticRead(); int x = position.getX(); int y = position.getY(); if(lock.validate(stamp)){ System.out.printf("OptimisticReader : %d - (%d, %d)\n", stamp, x, y); }else{ System.out.printf("OptimisticReader : %d - Not Free\n", stamp); } TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
最后,实现包含main()方法的Main类,创建Position和StampedLock对象,创建三个执行不同模式任务的线程,开始执行,等待结束:
public class Main { public static void main(String[] args){ Position position = new Position(); StampedLock lock = new StampedLock(); Thread threadWriter = new Thread(new Writer(position, lock)); Thread threadReader = new Thread(new Reader(position, lock)); Thread threadOptReader = new Thread(new OptimisticReader(position, lock)); threadWriter.start(); threadReader.start(); threadOptReader.start(); try { threadWriter.join(); threadReader.join(); threadOptReader.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
工作原理
本范例中,通过使用标记锁测试三种模式。在Writer任务中,使用writeLock()方法得到锁(需要写入模式的锁)。在Reader任务中,使用readLock()方法得到锁(需要读取模式的锁)。最后在OptimisticReader任务中,先使用tryOptimisticRead()方法,然后使用validate()方法判断是否能够访问数据。
如果前两个方法能够控制锁的话,保持等待直到获得锁。tryOptimisticRead()方法始终返回值,如果无法使用锁的话则返回0,否则是非零整数。切记在这种情况下,需要使用validate()方法判断是否确定能够访问数据。
下图显示运行范例输出的部分结果:
当Writer任务控制锁时,Reader和OptimisticReader均无法访问值。readLock()方法中断Reader任务,而在OptimisticReader任务中,调用validate()方法返回false并且调用tryOptimisticRead()方法返回0表明其它线程在写入模式下控制锁。当Writer任务释放锁时,Reader和OptimisticReader均能访问共享对象的值。
扩展学习
还需要了解StampedLock类的其它方法:
- tryReadLock()和tryReadLock(long time , TimeUnit unit):尝试在读取模式中获得锁。如果无法得到,第一个方法立即返回,第二个方法等待参数中的传入的时间值再返回。这些方法同样返回必须被检查的标记值(stamp != 0)。
- tryWriteLock()和tryWriteLock(long time , TimeUnit unit):尝试在写入模式中获得锁。如果无法得到,第一个方法立即返回,第二个方法等待参数中的传入的时间值再返回。这些方法同样返回必须被检查的标记值(stamp != 0)。
- isReadLocked()和isWriteLocked():如果锁分别在读取模式和写入模式中正被占用,返回这些方法。
- tryConvertToReadLock(long stamp)、tryConvertToWriteLock(long stamp)、和tryConvertToOptimisticRead(long stamp):将标记值作为参数传入,尝试转换成方法名中指定的模式。如果能够转换,返回新的标记值,否则返回0。
- unlock(long stamp:释放锁的对应模式。
更多关注
- 本章中”锁同步代码块“小节。