在多线程应用中,两个或两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。而解决这种问题的办法通常是当线程A调用修改对象方法时,我们就交给它一把锁,等他处理完后在把锁给另一个要调用这个方法的线程。
重入锁和条件对象
synchronized 关键字提供了锁以及相关的条件。大多数需要显示锁的情况使用 synchronized 非常方便,但是等我们了解重入锁和条件对象时,能更好的理解 synchronized 关键字。重入锁 ReentrantLock 是Java se5.0引入的,就是支持重进入的锁,他表示锁能够支持一个线程对资源的重复加锁。
Lock lock = new ReentrantLock(); try { ... }catch (Exception e){ lock.unlock(); }
Demo如下
class Test implements Runnable { private Boolean on; public Test(Boolean on) { this.on = on; } @Override public void run() { new MyClass().getData(on); } } public class MyClass { private static Lock lock; private static Condition condition; public static void main(String[] args) { lock = new ReentrantLock(); //我们这个线程已经获取了锁,具有排他性,别的线程无法获取锁,此时我们需要引入条件对象 //一个锁对象拥有多个相关的条件对象。 //得到条件对象 condition = lock.newCondition(); Thread a = new Thread(new Test(true)); Thread b = new Thread(new Test(false)); a.start(); b.start(); } void getData(Boolean on) { lock.lock(); System.out.println("我被锁住了"); try { if (on) { System.out.println("我阻塞了,放弃锁"); condition.await(); } condition.signalAll(); System.out.println("解除阻塞"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
一旦一个线程调用await 方法,他就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的 signalAll 方法时为止。
当调用 singalAll 方法时并不是立即激活一个等待线程,他仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。还有一个方法时 sinal ,它则是随机解除某个线程的阻塞,如果该线程任然不能运行,则再次被阻塞。 如果没有其他线程再次调用 singal ,那么系统就死锁了
同步方法
Java 的每一个对象都有一个内部锁,如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
public synchronized void method(){ } 等价于下面这个Demo Lock lock=new ReentrantLock(); public void method(){ try { ... }finally { lock.unlock(); } }
对于上面的例子,我们可以用 synchronized 修饰getData ,而不是使用一个显示锁。内部对象锁只有一个相关条件, wait 方法将一个线程添加到等待集中, notifyAll 或者 notify 方法解除等待线程的阻塞状态。也就是说 wait相当于调用 await(), notifyAll 等价于 condition.signalAll(), 上面的Demo改为下面这样
class Test implements Runnable { private Boolean on; public Test(Boolean on) { this.on = on; } @Override public void run() { new MyClass().getData(on); } } public class MyClass { public static void main(String[] args) { Thread a = new Thread(new Test(true)); Thread b = new Thread(new Test(false)); a.start(); b.start(); } synchronized void getData(Boolean on) { System.out.println("我被锁住了"); try { while (on) { System.out.println("我阻塞了,放弃锁"); wait(); } notifyAll(); System.out.println("解除阻塞"); } catch (InterruptedException e) { e.printStackTrace(); } } }
同步代码块
每一个java对象都有一个锁,线程可以调用同步方法来获得锁。还有一种机制可以获得锁,那就是使用一个同步代码块。
synchronized (this){ }
同步代码块是非常脆弱的,通常不推荐使用。一般实现同步最好使用 java.util.concurrent包下提供的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量使用 同步方法,这样可以减少编写代码的数量,减少出错的概率。如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition.
volatile
有时仅仅为了读写一个或两个实例域就使用同步的话,显得开销过大;而volatile 关键字为实例域的同步访问提供了免锁的机制。如果声明一个域 为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
学习volatile之前,我们需要了解一下内存模型的相关概念以及并发编程中的3个特性:原子性,可见性,有序性
Java的内存模型
Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,他存在内存可见性的问题。而局部变量,方法定义的参数则不会再线程之间共享,他们不会有内存可见性的问题,也不受内存模型的影响。
Java内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是Java 内存模型的一个抽象概念,其实并不真实存在,它涵盖了缓存,写缓冲区,寄存器等区域。Java内存模型控制线程之间的通信,他决定一个线程对主存共享变量的写入核实对另一个线程可见。
线程A 和 线程B 之间若要通信的话,必须要经历下面两个步骤:
- 线程A把线程A本地内存中更新过的共享内存刷新到主存中去。
- 线程 B到主存中去读取线程A之前已更新过的共享变量。由此可见,如果我们执行下面的语句: i=3;
执行线程必须先在自己的工作线程中对变量 i 所在的缓存进行赋值操作,然后再写入主存中,而不是直接将数值3写入到主存中