其实这一节要说的重点又是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();
}
}
关于第二点,主要是要理解概念,重入,即reentrant。When 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的两个重要特点已经讲完,至于synchronized和Lock的性能,以及synchronized底层实现原理,将在后面的章节中讲述。其实讲到这里基本是能完成JDK1.5之前的并发任务了,虽然还有很多可以由算法实现的原子操作或者锁,但是synchronized还是用起来最方便的。只是,大家被它的重量级给吓坏。好在1.6之后就有所改观了。
当然后面还有更好的类和方法会介绍。关于并发有太多太多的内容可以写。我也一直有一个疑问,底层到底是怎么做事务回滚的?有相关的代码实现,还是在硬件层做的。很好奇。
谢谢观赏。
来源:oschina
链接:https://my.oschina.net/u/589742/blog/86150