这几天不断添加新内容,给个大概的提纲吧,方面朋友们阅读,各部分是用分割线隔开了的:
- synchronized与wait()/notify()
- JMM与synchronized
- ThreadLocal与synchronized
- ReentrantLock与synchronized
最重要一条:
synchronized是针对对象的隐式锁使用的,注意是对象! 举个小例子,该例子没有任何业务含义,只是为了说明synchronized的基本用法:Class MyClass(){ synchronized void myFunction(){ //do something } } public static void main(){ MyClass myClass = new MyClass(); myClass.myFunction(); }
好了,就这么简单。
myFunction()方法是个同步方法,隐式锁是谁的?答:是该方法所在类的对象。 看看怎么使用的:myClass.myFunction();很清楚了吧,隐式锁是myClass的。 说的在明白一点,线程想要执行myClass.myFunction();就要先获得myClass的锁。 下面总结一下: 1、synchronized关键字的作用域有二种: 1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法; 2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。(注:这个可以认为是对Class对象起作用) 2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是this,即是当前对象。当然这个括号里可以是任何对象,synchronized对方法和块的含义和用法并不本质不同; 3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法; synchronized可能造成死锁,比如:class DeadLockSample{ public final Object lock1 = new Object(); public final Object lock2 = new Object(); public void methodOne(){ synchronized(lock1){ ... synchronized(lock2){...} } } public void methodTwo(){ synchronized(lock2){ ... synchronized(lock1){...} } } }
假设场景:线程A调用methodOne(),获得lock1的隐式锁后,在获得lock2的隐式锁之前线程B进入运行,调用methodTwo(),抢先获得了lock2的隐式锁,此时线程A等着线程B交出lock2,线程B等着lock1进入方法块,死锁就这样被创造出来了。
下面举一个有业务含义的例子帮助理解,并展示一下synchronized与wait()、notifyAll()的使用。 这里先介绍一下这两个方法: wait()/notify():调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择一个解除阻塞(但要等到获得锁后才真正可执行)。 好了,再来看看synchronized与这两个方法之间的关系: 1.有synchronized的地方不一定有wait,notify 2.有wait,notify的地方必有synchronized.这是因为wait和notify不是属于线程类,而是每一个对象都具有的方法(事实上,这两个方法是Object类里的),而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。 慢着,让我们思考一下Java这个设计是否合理?前面说了,锁是针对对象的,wait()/notify()的操作是与对象锁相关的,那么把wait()/notify()设计在Object中也就是合情合理的了。 恩,再想一下,为什么有wait,notify的地方必有synchronized? synchronized方法中由当前线程占有锁。另一方面,调用wait()notify()方法的对象上的锁必须为当前线程所拥有。因此,wait()notify()方法调用必须放置在synchronized方法中,synchronized方法的上锁对象就是调用wait()notify()方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。 好了,以上准备知识充足了,现在说例子:银行转账,同一时刻只有一个人可以转账。 那么我们自然想到在Bank类中有一个同步的转账方法:public Class Bank(){ float account[ACCOUNT_NUM]; ... public synchronized void transfer(from, to, amount){ //转账 } }
现在有一个问题,如果一个人获得了使用银行的锁,但是余额不足怎么办?
好,那我们进行改进:public Class Bank(){ float account[ACCOUNT_NUM]; ... public synchronized void transfer(int from, int to, float amount){ while(account[from]){ wait(); } account[from] -= amount; account[to] += amount; notifyAll(); } }
这样就满足需求了。 可见,用对象锁来管理试图进入synchronized方法的线程, 另外,由条件判断来管理已经进入同步方法中的线程即当前线程
这里还补充两点: 1. 调用wait()方法前的判断最好用while,而不用if;因为while可以实现被唤醒后线程再次作条件判断;而if则只能判断一次 2. 用notifyAll()优先于notify()。 另外注意一点: 能调用wait()/notify()的只有当前线程,前提是必须获得了对象锁,就是说必须要进入到synchronized方法中。 -------------------------------------我是分割线---------------------------------------- 补充一点JMM的相关知识,对理解线程同步很有好处。 (说明一下:以下内容参考了一些网上零零碎碎的帖子,非照搬且无商业目的,请勿跨省。) JVM中(留神:马上讲到的这两个存储区只在JVM内部与物理存储区无关)存在一个主内存(Main Memory),Java中所有的变量存储在主内存中,所有实例和实例的字段都在此区域,对于所有的线程是共享的(相当于黑板,其他人都可以看到的)。每个线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中变量的拷贝,(相当于自己笔记本,只能自己看到),工作内存由缓存和堆栈组成,其中缓存保存的是主存中的变量的copy,栈保存的是线程局部变量。线程对所有变量的操作都是在工作内存中进行的,线程之间无法直接互相访问工作内存,变量的值得变化的传递需要主存来完成。在JMM中通过并发线程修改的变量值,必须通过线程变量同步到主存后,其他线程才能访问到。 看看这个图是不是更形象![](http://photo2.bababian.com/usr832855/upload1/20090614/sBtYKP1GU1okcV3oHvBCcWyO9WOwCsqaK9YtCbHK70RnhSeacQjf2Mg==.jpg)
public class ThreadLocalDemo implements Runnable { private final static ThreadLocal studentLocal = new ThreadLocal(); //ThreadLocal对象在这 public static void main(String[] agrs) { TreadLocalDemo td = new TreadLocalDemo(); Thread t1 = new Thread(td,"a"); Thread t2 = new Thread(td,"b"); t1.start(); t2.start(); } public void run() { accessStudent(); } public void accessStudent() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName+" is running!"); Random random = new Random(); int age = random.nextInt(100); System.out.println("thread "+currentThreadName +" set age to:"+age); Student student = getStudent(); //每个线程都独立维护一个Student变量 student.setAge(age); System.out.println("thread "+currentThreadName+" first read age is:"+student.getAge()); try { Thread.sleep(5000); } catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge()); } protected Student getStudent() { Student student = (Student)studentLocal.get(); //从ThreadLocal对象中取 if(student == null) { student = new Student(); studentLocal.set(student); //如果没有就创建一个 } return student; } protected void setStudent(Student student) { studentLocal.set(student); //放入ThreadLocal对象中 } }
ThreadLocal通过一个Map来为每个线程都持有一个变量副本,用ThreadLocal对象以键值对的方式来维护这些线程独立变量 。
-------------------------------呦这么巧,我也是分割线---------------------------------- 既然说到了synchronized,顺便说说ReentrantLock吧。 ReentrantLock不熟悉?没事,concurrent包里的ArrayBlockingQueue知道吧,去看看源码,发现了吧,里面全是ReentrantLock。 好,言归正传,ReentrantLock是何方神圣?先这么说吧,你可以认为ReentrantLock是具有和synchronized类似功能的性能功能加强版同步锁。 让我们先来看看synchronized有什么缺点: 1. 只有一个condition与锁相关联,这个condition是什么?就是synchronized对针对的对象锁。 2. 多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况对于大量的竞争线程会造成性能的下降等后果。 针对synchronized的一系列缺点,JDK5提供了ReentrantLock,目的是为同步机制进行改善。下面来看看它是怎么改善上面这两个缺点的: 1. 一个ReentrantLock可以有多个Condition实例。 举个例子,还是刚才说的ArrayBlockingQueue类,看看源码(节选):- public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
- ...
- private final ReentrantLock lock;
- private final Condition notEmpty;
- private final Condition notFull;
- ...
- public ArrayBlockingQueue(int capacity, boolean fair) {
- ...
- lock = new ReentrantLock(fair);
- notEmpty = lock.newCondition();
- notFull = lock.newCondition(); //为该ReentrantLock设置了两个Condition
- }
- public E take() throws InterruptedException {
- final ReentrantLock lock = this.lock;
- lock.lockInterruptibly();
- try {
- try {
- while (count == 0)
- notEmpty.await(); //这里针对notEmpty这个condition,如果队列为空则线程等待这个条件
- } catch (InterruptedException ie) {
- notEmpty.signal();
- throw ie;
- }
- E x = extract();
- return x;
- } finally {
- lock.unlock();
- }
- }
- private E extract() {
- final E[] items = this.items;
- E x = items[takeIndex];
- items[takeIndex] = null;
- takeIndex = inc(takeIndex);
- --count;
- notFull.signal(); //这里针对notFull这个condition,唤醒因该条件而等待的线程
- return x;
- }
- ...
- }
这里notEmpty和notFull作为lock的两个条件是可以分别负责管理想要加入元素的线程和想要取出元素的线程。例如put()方法在元素个数达到最大限制时会使用notFull条件把试图继续插入元素的线程都扔到等待集中,而执行了take()方法时如果顺利进入extract()则会空出空间,这时notFull负责随机的通知被其扔到等待集中的线程执行插入元素的操作。(这里没给出put方法,有兴趣的童鞋可以去查查源码,其实和take方法很类似) 2. ReentrantLock提供了lockInterruptibly()方法可以优先考虑响应中断,而不是像synchronized那样不响应interrupt()操作。 解释一下响应中断是什么意思:比如A、B两线程去竞争锁,A得到了锁,B等待,但是A有很多事情要处理,所以一直不返回。B可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。在这种情况下,synchronized的做法是,B线程中断自己(或者别的线程中断它),我不去响应,继续让B线程等待,你再怎么中断,我全当耳边风。而lockInterruptibly()的做法是,B线程中断自己(或者别的线程中断它),ReentrantLock响应这个中断,不再让B等待这个锁的到来。 有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。 由于ReentrantLock在提供了多样的同步功能(除了可响应中断,还能设置时间限制),因此在同步比较激烈的情况下,性能比synchronized大大提高。 不过,在同步竞争不激烈的情况下,synchronized还是非常合适的(因为JVM会进行优化,具体不清楚怎么优化的)。因此不能说ReentrantLock一定更好,只是两者适合情况不同而已,在同步竞争不激烈时用synchronized,激烈时用ReentrantLock。换句话说,ReentrantLock的可伸缩性可并发性要更好一些。除非您对 ReentrantLock的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。(这里推荐一个帖子) 再补充一点,使用ReentrantLock时,切记要在finally中释放锁,这是与synchronized使用方式很大的一个不同。对于synchronized,JVM会自动释放锁,而ReentrantLock需要你自己来处理。给个代码片段吧:
- //synchronized
- public synchronized void increment() {
- count++;
- }
- //ReentrantLock
- public void increment() {
- lock.lockInterruptibly();//上锁
- try {
- count++;
- } finally {
- lock.unlock();//手动释放锁
- }
- }
推荐一个帖子,讲ReentrantLock还不错我参考了的
http://hellosure.iteye.com/blog/1121157