多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发(线程安全)问题。解决并发问题可以用锁。
java的内置锁:
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。java内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果线程B不释放这个锁,那么线程A将永远等待下去。
java的对象锁和类锁:
java的内置锁基本上可以分为对象锁和类锁,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
synchronized只是一个内置锁的加锁机制,当某个方法加上synchronized关键字后,就表明要获得该内置锁才能执行,并不能阻止其他线程访问不需要获得该内置锁的方法。
一、synchronized的用法
1、对象锁:同步方法,是对该对象加锁,其他线程将无法访问需要获取该对象锁的方法和代码块。
public class Test{ public synchronized void print(){ System.out.println("hello world!"); } }
2、对象锁:同步代码块,这种写法也是锁住该对象,和1效果一样,其他线程将无法访问需要获取该对象锁的方法和代码块。
public class Test{ public void print(){ synchronized(this){ System.out.println("hello world!"); } } }
3、对象锁:同步代码块,这种写法锁住传入的对象,不影响需要获取当前对象锁的方法和代码块的访问。
public class Test{ private String a = "test"; public void print(){ synchronized(a){ System.out.println("hello world!"); } } public synchronized void print1(){ System.out.println("123456"); } }
执行print()里面的同步代码块,会给对象a加锁,注意不是给Test的对象加锁,也就是说Test对象的其它synchronized方法和代码块不会因为print()而被锁。同步代码块执行完,则释放对a的锁。
为了锁住一个对象的代码块而不影响该对象其它synchronized块的高性能写法:
public class Test{ private byte[] lock = new byte[0]; public void print(){ synchronized(lock){ System.out.println("hello world!"); } } public synchronized void print1(){ System.out.println("123456"); } }
4、类锁:用于静态方法。
public class Test{ public synchronized static void print(){ System.out.println("hello world!"); } }
效果等同于同步代码块传入该类的class对象。
public class Test{ public void print(){ synchronized(Test.class){ System.out.println("hello world!"); } } }
类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。类锁和对象锁是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
二、锁的释放
一般是执行完毕同步代码块(锁住的代码块)后就释放锁,也可以用wait()方式半路上释放锁。wait()方式就好比蹲厕所到一半,突然发现下水道堵住了,不得已必须出来站在一边,好让修下水道师傅(准备执行notify的一个线程)进去疏通马桶,疏通完毕,师傅大喊一声: "已经修好了"(notify),刚才出来的同志听到后就重新排队。注意啊,必须等师傅出来啊,师傅不出来,谁也进不去。也就是说notify后,不是其它线程马上可以进入封锁区域活动了,而是必须还要等notify代码所在的封锁区域执行完毕从而释放锁以后,其它线程才可进入。
由于wait()操作而半路出来的同志没收到notify信号前是不会再排队的,他会在旁边看着这些排队的人(其中修水管师傅也在其中)。注意,修水管的师傅不能插队,也得跟那些上厕所的人一样排队,不是说一个人蹲了一半出来后,修水管师傅就可以突然冒出来然后立刻进去抢修了,他要和原来排队的那帮人公平竞争,因为他也是个普通线程。如果修水管师傅排在后面,则前面的人进去后,发现堵了,就wait,然后出来站到一边,再进去一个,再wait,出来,站到一边,直到师傅进去修好后执行notify。这样,一会儿功夫,排队的旁边就站了一堆人,等着notify。
终于,师傅进去,然后修好了,接着notify了,接下来呢?
1. 有一个wait()的人(线程)被通知到。
2. 为什么被通知到的是他而不是另外一个wait()的人?取决于JVM。我们无法预先判断出哪一个会被通知到。也就是说,优先级高的不一定被优先唤醒,等待时间长的也不一定被优先唤醒,一切不可预知!(当然,如果你了解该JVM的实现,则可以预知)。
3. 他(被通知到的线程)要重新排队。
4. 他会排在队伍的第一个位置吗?回答是:不一定。他会排最后吗?也不一定。但如果该线程优先级设的比较高,那么他排在前面的概率就比较大。
5. 轮到他重新进入厕位时,他会从上次wait()的地方接着执行,不会重新执行。恶心点说就是,他会接着拉粑粑,不会重新拉。
6. 如果师傅notifyAll()。则那一堆半途而废出来的人全部重新排队,顺序不可知。
三、Lock的使用
用synchronized关键字可以对资源加锁。用Lock关键字也可以。它是JDK1.5中新增内容。
public class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
(注:这是JavaDoc里的例子,是一个阻塞队列的实现例子。所谓阻塞队列,就是一个队列如果满了或者空了,都会导致线程阻塞等待。Java里的 ArrayBlockingQueue提供了现成的阻塞队列,不需要自己专门再写一个了。)
一个对象的lock.lock()和lock.unlock()之间的代码将会被锁住。这种方式比起synchronize好在什么地方?
简而言之,就是对wait的线程进行了分类。用厕位理论来描述,则是那些蹲了一半而从厕位里出来等待的人原因可能不一样,有的是因为马桶堵了,有的是因为马桶没水了。通知(notify)的时候,就可以喊:因为马桶堵了而等待的过来重新排队(比如马桶堵塞问题被解决了),或者喊,因为马桶没水而等待的过来重新排队(比如马桶没水问题被解决了)。这样可以控制得更精细一些。不像synchronize里的wait和notify,不管是马桶堵塞还是马桶没水都只能喊:刚才等待的过来排队!假如排队的人进来一看,发现原来只是马桶堵塞问题解决了,而自己渴望解决的问题(马桶没水)还没解决,只好再回去等待(wait),白进来转一圈,浪费时间与资源。
Lock与synchronized对应关系:
synchronized | wait | notify | notifyAll |
Lock | await | signal | signalAll |
注意:不要在Lock方式锁住的块里调用wait、notify、notifyAll。
四、volatile的使用
volatile真正解决的问题是 JVM 在-server模式下(注意普通运行模式下没有此问题),线程优先取用自己的线程私有stack中的变量值,而不是公共堆中的值,造成变量值老旧的问题。换句话说,volatile强制要求了所有线程在使用volatile修饰的变量的时候要去公共内存堆中获取值,不可以偷懒使用自己的。volatile绝对不保证原子性,原子性只能用synchronized同步修饰符实现。
我们知道,在Java中设置变量值的操作,除了long和double类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。这在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,只需要把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。