该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。
前一篇Android并发编程开篇呢,主要是简单介绍一下线程以及JMM,虽然文章不长,但却是理解后续文章的基础。本篇文章介绍多线程与锁。
Thread的三种启动方式上篇文章已经说了,下面呢,我们继续看看Thread这个类。
Java中线程的状态分为6种。
线程的几个常见方法的比较
在上一篇博文中,各位看官已经对JMM模型有了初步的了解,我们在谈论线程安全的时候也无外乎解决上篇博文中提到的3个问题,原子性、可见性、时序性。
当一个共享变量被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关键字有以下几种使用方式
这种方式网上有人称它为“类锁”,其实这种说法有些迷惑人,我们只需要记住一点,所有的锁都是锁住的对象,也就是Object本身,你可以简单理解为使用synchronized 是在堆内存中的某一个对象上加了一把锁,并且这个锁是可重入的,意思是说如果一个线程已经获得了某个对象的锁,那么该线程依然可以重新获得这把锁,但是其他线程如果想访问这个对象就必须等待上一个获得锁的线程释放锁。
我们在回过头来看静态方法加锁,为一个类的静态方法加锁,实际上等价于synchronized(Class),即锁定的是该类的Class对象。
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式
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的监视器方法显著的差别在于前置条件
wait和notify/notifyAll方法只能在同步代码块里用(这个有的面试官也会考察)
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中的原子类与并发容器
此致,敬礼