Java并发编程--加锁机制初步,内置锁以及内置锁的重入

三世轮回 提交于 2019-11-28 22:55:51

其实这一节要说的重点又是synchronized,因为和内置锁最相关的应该就是synchronized了。当然我们还是会从一些例子开始讲起。样例程序还是来自那本书:

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
     private final AtomicReference<BigInteger> lastNumber
         = new AtomicReference<BigInteger>();
     private final AtomicReference<BigInteger[]>  lastFactors
         = new AtomicReference<BigInteger[]>();

     public void service(ServletRequest req, ServletResponse resp) {
         BigInteger i = extractFromRequest(req);
         if (i.equals(lastNumber.get()))
             encodeIntoResponse(resp,  lastFactors.get() );
         else {
             BigInteger[] factors = factor(i);
             lastNumber.set(i);
             lastFactors.set(factors);
             encodeIntoResponse(resp, factors);
         }
     }
}

这个程序很明显是一个线程不安全的类,而程序想要告诉我们的是这样一件事:尽管lastNumber和lastFactors都是线程安全的类,而且它们的方法是原子性的,是线程安全的但是,如果将它们组合在一起,就破坏了原子性,就是不安全的。这也告诫我们,不要随意的去拼凑两个原子性操作。

 解决的方法很简单,在方法前加修饰符synchronized。当然这不是一个好办法,虽然能解决问题,但是它却带来了性能上的问题。每一个要执行service方法的线程,必须要等到前一个线程执行完了service,才能执行。当然,稍微好点儿的办法是写成synchronized块。

@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;

    public synchronized long getHits() { return hits; }
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this)  {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

值得注意的是,如果使用了synchronized块包围了的对象或者操作,尽量使用最简单纯粹的变量(避免使用原子对象)。这里涉及到一个锁的嵌套的问题,这个内容会在后续的章节中讲述,现在只要记住一点:避免锁的嵌套。

而今天的主题是为什么这样做了,程序就变得Thread Safe了,synchronized的原理到底是什么,底层到底是怎么实现的。Every Java object can implicitly act as a lock for purposes of synchronization; these built-in locks are called intrinsic locks or monitor locks。内置锁或者监视器锁。内置锁是一种互斥锁,同一时刻只允许一个线程执行内置锁保护的代码。所以一旦程序执行到synchronized锁包括的代码时,就会去拿内置锁,如果内置锁没有被别的线程占用,则拿到锁,待执行完相关代码后,释放锁;如果内置锁已经被别的线程占用,则等待。

上述的概念都容易理解,只不过还要注意两点,第一,内置锁对那些没有加synchronized修饰符的方法是不起作用的,线程还是能照样访问到;第二,内置锁是可重入的。

针对第一点,我们看下面的程序就能理解:

package com.a2.concurrency.chapter1;

/**
 * 主要验证内置锁的作用域和作用时间
 * 
 * @author ChenHui
 * 
 */
public class TestSynchronized {

	private int i = 0;

	public synchronized void add() {
		System.out.println("excute add()");
		i++;
		try {
			Thread.sleep(2000);
			System.out.println("after excute add()...");
		} catch (Exception e) {
			System.err.print("error");
		}
	}

	public synchronized void sub() {
		System.out.println("excute sub()...");
		i--;
	}

	public void print() {
		try {
			Thread.sleep(1000);
		} catch (Exception e) {
		}
		System.out.println("value of i:" + i);
	}

	public static void main(String[] args) throws Exception {
		final TestSynchronized ts = new TestSynchronized();
		Thread th1 = new Thread("th1") {
			public void run() {
				System.out.println("Thread:" + super.getName());
				ts.add();
			}
		};

		final Thread th2 = new Thread("th2") {
			public void run() {
				System.out.println("THread:" + super.getName());
		/**改变ts.sub()和ts.print()执行顺序,就能体会出内置锁对print方法是不上锁的*/
				ts.sub();
				ts.print();
				// ts.sub();
			}
		};
		th1.start();
		Thread.sleep(1000);
		th2.start();
	}
}

关于第二点,主要是要理解概念,重入,即reentrantWhen a thread requests a lock that is already held by another thread, the requesting thread blocks. But because intrinsic locks are reentrant, if a thread tries to acquire a lock that it already holds, the request succeeds.注意这个it already holds,这个指的是,如果当前线程已经由它自己持有的锁。看代码:

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

上述代码,如果子类调用doSomething方法时,获得自身类锁的同时会获得父类的锁,如果内置锁不可重入,那调用super.doSomething时就会发生死锁。所以重入就是,你拿了父类的锁,再调用该锁包含的代码可以不用再次拿锁。注意,/**重入始终是发生在一个线程中的-->这句话有问题改为-->重入是针对同一个引用的*/重入是针对同一个引用的

下面的代码主要是为了证明:获得自身类锁的同时会获得父类的锁。


package com.a2.concurrency.chapter1;

/**
 * 主要为了测试,子类覆盖父类方法后,调用子类方法时,也获取父类的锁
 * 
 * @author ChenHui
 * 
 */
public class TestWidget {

	public static void main(String[] args) throws InterruptedException {

		final LoggingWidget widget = new LoggingWidget();
		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				System.out.println(super.getName() + ":start\r\n");
				widget.doSometing();
			}
		};

		Thread th2 = new Thread("th2") {
			@Override
			public void run() {
				System.out.println(super.getName() + ":start\r\n");
				/** 为了说明子类复写父类方法后,调用时也持有父类锁*/
				widget.doAnother();
				/**证明了内置锁对那些没有加synchronized修饰符的方法是不起作用的*/
				// widget.doNother();
				/**为了说明子类复写父类方法后,调用时也持有父类锁,也持有自己本类的锁*/
				// widget.doMyLike();
				/**这是两个线程,这是需要等待的,并不是继承的关系,不是重入,重入是发生在一个线程中的*/
				// widget.doSometing();
			}
		};

		th1.start();
		Thread.sleep(1000);
		th2.start();
	}
}

class Widget {
	public synchronized void doSometing() {
		System.out.println("widget ... do something...");
	}

	public synchronized void doAnother() {
		System.out.println("widget... do another thing...");
	}

	public void doNother() {
		System.out.println("widget... do Nothing...");
	}
}

class LoggingWidget extends Widget {

	@Override
	public synchronized void doSometing() {
		try {
			System.out.println("loggingwidget do something...");
			Thread.sleep(3000);
			System.out.println("end loggingwidget do something...");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		super.doSometing();
	}

	public synchronized void doMyLike() {
		System.out.println("loggingwidget do my like...");
	}
}


至此,synchronized的两个重要特点已经讲完,至于synchronizedLock的性能,以及synchronized底层实现原理,将在后面的章节中讲述。其实讲到这里基本是能完成JDK1.5之前的并发任务了,虽然还有很多可以由算法实现的原子操作或者锁,但是synchronized还是用起来最方便的。只是,大家被它的重量级给吓坏。好在1.6之后就有所改观了。

 当然后面还有更好的类和方法会介绍。关于并发有太多太多的内容可以写。我也一直有一个疑问,底层到底是怎么做事务回滚的?有相关的代码实现,还是在硬件层做的。很好奇。

谢谢观赏。




易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!