测试环境
OS:windows7_X64
JDK:jdk1.8.0_20
IDE: eclipse_neon
一、可重入函数
相信很多人都听说过可重入函数,可重入函数最重要的两条法则就是:
- 只使用非静态局部变量;
- 不调用不可重入的函数。
public class Reentrant1 {
private int count = 0;
/** * 不可重入 * 当有多个线程调用同一个Reentrant对象的increament(),输出数值是不可预测的。count可能是1,也可能是任何正整数。 */
public void increament(){
count ++;
System.out.println(count);
}
/** * 可重入 * 无论多少线程调用同一个Reentrant对的象decreament方法,输出数值都是传入参数length的值减1。 */
public void decreament(int length){
length--;
System.out.println(length);
}
}
如上代码所述,increament是不可重入的函数,decreament是可重入的函数。区别在于一个引用了实例变量,一个未引用实例变量。
不可重入函数被视为不安全的函数,无状态的函数则是线程安全的。在spring 的IOC容器中,默认为单例模式,所以在编程时要尽量使用无状态的类和方法。如像incerement()这样类似的方法,为了得到期望的值,则应该每次访问都新建一个实例对象,也就是要在类上标记@Scope(prototype)。
二、可重入锁
说完了可重入函数,那么再来看看可重入锁。
可重入锁的意思是当同一个线程获取锁后,在未释放锁的情况下,多次获取同一个锁对象并不会发生死锁现象。
public class Reentrant2 {
public static void main(String[] args){
Reentrant2 rt2 = new Reentrant2();
rt2.getCount("a", 1);
}
public void getCount(String str, int count){
// 第一次获取锁
synchronized (str.intern()) {
System.out.println(count);
count++;
// 第二次获取锁
synchronized (str.intern()) {
System.out.println(count);
count++;
}
}
}
}
运行Reentrant2的输出结果为:
1
2
getCount方法中第一次获取锁之后在没有释放锁的情况下,第二次获取同一个字符串对象的锁,并没有发生死锁现象,因此使用synchronized修饰的是可重入锁。
递归调用进行同步必须使用可重入的锁,如下代码所示:
public class Reentrant3 {
public static void main(String[] args){
Reentrant3 rt3 = new Reentrant3();
new Thread(rt3.new ReentrantInner()).start();
new Thread(rt3.new ReentrantInner()).start();
}
class ReentrantInner implements Runnable{
public int getCount(String str, int count) throws InterruptedException{
synchronized (str.intern()) {
if(count>5){
return count;
}
System.out.println(Thread.currentThread().getId() + "==" + count);
count++;
Thread.sleep(100);
//递归调用
return getCount(str, count);
}
}
@Override
public void run() {
try {
this.getCount(new String("a"), 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行Reentrant3的输出结果为:
10==1
10==2
10==3
10==4
10==5
9==1
9==2
9==3
9==4
9==5
可以看到与预期结果一致,并没有发生死锁。另外,我想有些人一定注意到了我使用的是new String("a"),同步时又调用了str.intern()方法。因为在实际生产环境中每一个String字符串对象都可能是由不同的线程产生,即使字符串的值完全相同,也可能不是同一个对象。因此需要调用intern()方法,保证相同值的字符串均引用的是同一对象。
synchronized是根据对象进行加锁,如果不是同一个对象,那么锁就会失效。各位可自行去掉intern()方法试试,输出顺序肯定会发生变化。
至于为什么调用intern()返回的会是同一个对象?请参阅String类的api。
三、其它可重入锁
除了使用synchronized以外,ReentrantLock和ReentrantReadWriteLock也是可重入锁,具体的api请自行查阅,这里不再详谈。ReentrantLock的递归实现如下代码所示:
import java.util.concurrent.locks.ReentrantLock;
public class Reentrant4 {
private final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Reentrant4 rt4 = new Reentrant4();
new Thread(rt4.new ReentrantInner()).start();
new Thread(rt4.new ReentrantInner()).start();
}
class ReentrantInner implements Runnable{
public int getCount(String str, int count){
lock.lock();
try{
if(count>5){
return count;
}
System.out.println(Thread.currentThread().getId() + "==" + count);
count++;
Thread.sleep(100);
return getCount(str, count);
}catch(Exception e){
e.printStackTrace();
return count;
}finally{
lock.unlock();
}
}
@Override
public void run() {
this.getCount("a", 1);
}
}
}
运行Reentrant4的输出结果为:
9==1
9==2
9==3
9==4
9==5
10==1
10==2
10==3
10==4
10==5
可以看到与预期结果一致,并没有发生死锁。
四、实现自己的可重入锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyReentrantLock implements Lock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
private int count = 0;
@Override
public void lock() {
Thread current = Thread.currentThread(); //获取当前线程
if (current == owner.get()) { //如果当前线程是持有锁的线程,count计数+1
count++;
System.out.println("lock重入次数:"+count);
return;
}
/* * 1.如果持有锁的线程为空,将当前线程设定为持有锁 * 2.如果持有锁的线程不为空,且并非当前线程,全部进入休眠状态100毫秒 */
while (!owner.compareAndSet(null, current)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
return false;
}
@Override
public void unlock() {
Thread current = Thread.currentThread();
/* * 1.如果当前线程为持有锁的线程: * 如果count不为0,说明当前线程至少获得过一次锁,count--; * 如果count为0,说明当前线程为最后一次释放锁,将持有锁的线程设置为空 */
if (current == owner.get()) {
if(count > 0){
count--;
}else{
owner.compareAndSet(current, null);
}
}
System.out.println("unlock重入次数:"+count+ "ThreadId:"+Thread.currentThread().getId());
}
}
实际开发时还要考虑锁超时,锁的效率,中断异常等等因素,此代码仅供参考。
五、后记
本文源于最近在玩的一个项目,发现对可重入函数和可重入锁的概念并不是特别清晰,于是看api看书看博客,有些心得,所以发出来分享。
如果您有更好的理解或者文中有不对的地方,欢迎留言探讨补充指正。谢谢。
参考资料: http://ifeve.com/java_lock_see4/ Java锁的种类以及辨析(四):可重入锁 作者:山鸡