该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。
前一篇Android并发编程开篇呢,主要是简单介绍一下线程以及JMM,虽然文章不长,但却是理解后续文章的基础。本篇文章介绍多线程与锁。
Thread的三种启动方式上篇文章已经说了,下面呢,我们继续看看Thread这个类。
Java中线程的状态分为6种。
在上一篇博文中,各位看官已经对JMM模型有了初步的了解,我们在谈论线程安全的时候也无外乎解决上篇博文中提到的3个问题, 原子性、可见性、时序性 。
当一个共享变量被volatile修饰之后, 其就具备了两个含义
义: 一个是当程序执行到volatile变量的操作时, 在其前面的操作已经全部执行完毕, 并且结果会对后面的
操作可见, 在其后面的操作还没有进行; 在进行指令优化时, 在volatile变量之前的语句不能在volatile变量后面执行; 同样, 在volatile变量之后的语句也不能在volatile变量前面执行。即该关键字保证了 时序性
如何正确使用volatile关键字呢
通常来说, 使用volatile必须具备以下两个条件:
去面试java或者Android相关职位的时候个东西貌似是必问的,关于synchronized这个关键字真是有太多太多东西了。尤其是JDK1.6之后为了优化synchronized的性能,引入了偏向锁,轻量级锁等各种听起来就头疼的概念,java还有Android面试世界流传着一个古老的名言,考察一个人对线程的了解成度的话,一个synchronized就足够了。不过本篇博文不讲那些,本篇博文本着让各位看官都能理解的初衷试着分析一下synchronized关键字把
synchronized 关键字自动提供了锁以及相关的条件。 大多数需要显式锁的情况使用synchronized非常方
便, 但是等我们了解了重入锁和条件对象时, 能更好地理解synchronized关键字。 重入锁ReentrantLock是
Java SE 5.0引入的, 就是支持重进入的锁, 它表示该锁能够支持一个线程对资源的重复加锁。
ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); try { ... } finally { reentrantLock.unlock(); }
如上代码所示,这一结构确保任何时刻只有一个线程进入临界区, 临界区就是在同一时刻只能有一个任务访问的代码区。 一旦一个线程封锁了锁对象, 其他任何线程都无法进入Lock语句。 把解锁的操作放在finally中是十分必要的。 如果在临界区发生了异常, 锁是必须要释放的, 否则其他线程将会永远被阻塞。
我们再来看看synchronized,synchronized关键字有以下几种使用方式
同步方法(即直接在方法声明处加上synchronized)
private synchronized void test() { }
等价于
ReentrantLock reentrantLock = new ReentrantLock(); private void test() { reentrantLock.lock(); try { ... } finally { reentrantLock.unlock(); } }
同步代码块
上面我们说过, 每一个Java对象都有一个锁, 线程可以调用同步方法来获得锁。 还有另一种机制可以获
得锁, 那就是使用一个同步代码块, 如下所示:
synchronized(obj){ }
其获得了obj的锁, obj指的是一个对象。 同步代码块是非常脆弱的,通常不推荐使用。 一般实现同步最h好用java.util.concurrent包下提供的类, 比如阻塞队列。 如果同步方法适合你的程序, 那么请尽量使用同步方法, 这样可以减少编写代码的数量, 减少出错的概率。
我们在代码中写的synchronized(this){} 其实是与上面一样的,this指代当前对象
静态方法加锁
static synchronized void test();
这种方式网上有人称它为“类锁”,其实这种说法有些迷惑人, 我们只需要记住一点,所有的锁都是锁住的对象,也就是Object本身,你可以简单理解为使用synchronized 是在堆内存中的某一个对象上加了一把锁,并且这个锁是可重入的,意思是说如果一个线程已经获得了某个对象的锁,那么该线程依然可以重新获得这把锁,但是其他线程如果想访问这个对象就必须等待上一个获得锁的线程释放锁。
我们在回过头来看静态方法加锁,为一个类的静态方法加锁,实际上等价于 synchronized(Class) ,即锁定的是该类的 Class对象。
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以
实现等待/通知模式
使用的前置条件
当我们想要使用Object的监视器方法时,需要或者该Object的锁,代码如下所示
synchronized(obj){ .... //1 obj.wait();//2 obj.wait(long millis);//2 ....//3 }
一个线程获得obj的锁,做了一些时候事情之后,发现需要等待某些条件的发生,调用obj.wait(),该线程会释放obj的锁,并阻塞在上述的代码2处
obj.wait()和obj.wait(long millis)的区别在于
synchronized(obj){ .... //1 obj.notify();//2 obj.notifyAll();//2 }
一个线程获得obj的锁,做了一些时候事情之后,某些条件已经满足,调用obj.notify()或者obj.notifyAll(),该线程会释放obj的锁,并叫醒在obj上等待的线程,
obj.notify()和obj.notifyAll()的区别在于
使用范式
synchronized(obj){ //判断条件,这里使用while,而不使用if while(obj满足/不满足 某个条件){ obj.wait() } }
放在while里面,是防止处于WAITING状态下线程监测的对象被别的原因调用了唤醒(notify或者notifyAll)方法,但是while里面的条件并没有满足(也可能当时满足了,但是由于别的线程操作后,又不满足了),就需要再次调用wait将其挂起
JDK1.5后提供了Condition接口,该接口定义了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的
public interface Condition { //等待 同object.wait() void await() throws InterruptedException; //无视中断等待 object没有此类方法 void awaitUninterruptibly(); //超时等待 同object.wait(long millis) long awaitNanos(long nanosTimeout) throws InterruptedException; //超时等待 boolean await(long time, TimeUnit unit) throws InterruptedException; //超时等待 到将来的某个时间 object没有此类方法 boolean awaitUntil(Date deadline) throws InterruptedException; //通知 同object.notify() void signal(); //通知 同object.notifyAll() void signalAll(); }
除了上述API之间的差别外,Condition与Object的监视器方法显著的差别在于前置条件
Condition接口对象需和Lock接口配合,通过lock.lock()获取锁,lock.newCondition()获取条件对象更为灵活
关于Condition接口的具体实现请往下看
上面说的Condition是一个接口,我们来看一下Condition接口的实现,Condition接口的实现主要是通过另外一套等待/通知机制完成的。
LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,
而LockSupport也成为构建同步组件的基础工具。
LockSupport定义了一组以 park开头 的方法用来阻塞当前线程,以及 unpark(Thread thread) 方法来唤醒一个被阻塞的线程。
既然JDK已经提供了Object的wait和notify/notifyAll方法等方法,那么LockSupport定义的一组方法有何不同呢,我们来看下面这段代码就明白了
Thread A = new Thread(new Runnable() { @Override public void run() { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } try { Thread.sleep(10000);//睡眠10s,保证LockSupport.unpark(A);先调用 } catch (InterruptedException e) { e.printStackTrace(); } //直接调用park方法阻塞当前线程,没在同步方法或者代码块内 LockSupport.park(this); System.out.println(sum); } }); A.start(); //调用unpark方法唤醒指定线程,即使unpark(Thread)方法先于park方法调用,依然能唤醒 LockSupport.unpark(A);
对比一下Object的wait和notify/notifyAll方法你就能明显看出区别
final Object obj = new Object(); Thread B = new Thread(new Runnable() { @Override public void run() { synchronized (obj) { int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } try { Thread.sleep(10000);//睡眠10s,保证obj.notify();先调用 } catch (InterruptedException e) { e.printStackTrace(); } try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(sum); } } }); B.start(); synchronized (obj) { //如果obj.notify();先于obj.wait()调用,那么调用调用obj.wait()的线程会一直阻塞住 obj.notify(); }
在LockSupport的类说明上其实已经说明了LockSupport类似于Semaphore,
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;
每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。
然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。
Semaphore经常用于限制获取某种资源的线程数量。
LockSupport通过许可证来联系使用它的线程。
如果许可证可用,调用park方法会立即返回并在这个过程中消费这个许可,不然线程会阻塞。
调用unpark会使许可证可用。(和Semaphores有些许区别,许可证不会累加,最多只有一张)
因为有了许可证,所以调用park和unpark的先后关系就不重要了,
讲解了上面那么多内容,现在出一个小小的笔试题,如何正确停止一个线程,别说是thread.stop()哈,那个已经被标记过时了。如果您想参与这个问题请在评论区评论。
本篇主要是说了关于多线程与锁的东西。这里总结一下
volatile 保证了共享变量的可见性和禁止重排序,
Synchronized的作用主要有三个:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见(这个可能会被许多人忽略了)
(3)有效解决重排序问题。
从JMM上来说
被volatile修饰的共享变量如果被一个线程更改,那么会通知各个线程你们的副本已经过期了,赶快去内存拉取最新值吧
被Synchronized修饰的方法或者代码块,我们都知道会线程互斥访问,其实其有像volatile一样的效果,如果被一个线程更改了共享变量,在Synchronized结束处那么会通知各个线程你们的副本已经过期了,赶快去内存拉取最新值吧
由于笔者能力有限,如有不到之处,还请不吝赐教。
Java中的原子类与并发容器
此致,敬礼