并发问题,总是一个比较值得探究和有趣的话题,
有时候并发问题看起来会很简单,有时又让人迷惑,不出问题则已,一出问题则难揪诱因,
只能靠个人小心为慎,来尽量降低这种威胁,并发中,
锁则起到了至关重要的作用,
用得好谢天谢地,用得不好只能等待问题出现,所以觉得,还是有必要对锁进行一番整理记忆,
可能其中,我也会头脑发晕,感谢邮件指出。
-
并发问题
-
什么是并发问题
并发问题总结即为,在多线程环境下,
程序运行出现预料之外的错误,如
状态不一致,
死锁,
甚至程序崩溃等等。
-
要引起并发问题,事必也是由于几个条件下引起的,个人总结为
1. 多个线程: 当然得在多线程环境下。
2. 共享变量: 若不存在共享变量(即存在本地变量),多个线程只能访问本地线程栈的数据。
3. 可变变量: 即使存在共享变量,但是该变量不可变(即初始化后,就不会更新状态)。
4. 写操作: 即使共享变量可变,但是多个线程不存在更新操作(仅读操作),此时依然是线程安全的。
-
为什么有了上述条件成立后,就有可能出现并发问题
-
归根结底,还是由于Java内存模型和硬件架构引起的,
摘得一张好图,共赏之:
类似这种情形,跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。
只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。
这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。
-
而Java中volatile的作用就是,
让线程从主内存中读取变量,当这个变量被修改时,总会被写会到主内存中,
这样就能保证多个线程能看到共享变量最新的值了(Java同步块(synchronized)也能达到同样的效果)。
-
Java锁
-
锁在并发中,无疑是基础而直观重要的组件,是避免并发问题的好帮手。
-
Java内置锁(synchronized)
-
Java内置的同步块(synchronized)就是最简单的锁工具,并且并发大师
Doug Lea也建议,
在synchronized能满足并发程序需求时,尽量就使用它,
因为它足够简单明了,大部分开发人员都知道该关键字的用意,并且其性能问题,有可能随JVM优化而得到提升,其基本用法
-
普通锁
-
当然,我们也可以通过synchronized关键字实现一个简单的锁:
-
在lock()方法中,我们通过synchronized(this)对当前锁对象加锁,
若locked=true,当前线程将进行wait(),
等待被notify(),这里之所以white(locked),
是为了防止操作系统的假唤醒,
避免locked=FALSE时,等待线程也穿过wait()继续往下执行,其基本的使用模式就是
-
可重入锁
-
当同一线程对同一对象锁进行多次(重入)加锁时,
如上述的锁,将发生永久阻塞
-
将上述锁改版为可重入锁(synchronized原生支持重入):
-
上面通过Thread.currentThread() != locking判断当前线程是否是已经加锁的线程,
若是,就不必要再等待,而是通过locks计数被同一线程加锁几次,
在unlock时,也需判断locks为0时再解锁,也就是同一线程
lock()次数与unlock()次数一致。
-
公平锁
-
有时,我们需要严格要求锁的公平性,即先发起请求锁的等待线程,优先被执行。
由于我们无法控制锁上的哪个线程被唤醒,只能在某个线程被唤醒时,来判断该线程是否是应该被执行,
是则不需wait(),否则继续wait(),
如下面的公平锁:
-
在lock()中,通过
(locked && Thread.currentThread() != locking) || waitings.get(0) != placeholder
来判断当前线程是否须要等待,waitings.get(0) != placeholder则判断是否是优先等待的线程,
一旦线程加锁成功,就将占位符从等待队列移除。一旦unlock()后,所有等待线程将得到通知。但是,
此处this.notify()是在锁对象上通知,其实最终只有那个优先等待的线程才会能继续执行,
其他线程都得到一次无用的通知。可以让每个线程在其占位符上进行等待,线程解锁时,仅是在该占位符对象上通知,
这样就能准确通知那个优先进入等待队列的线程,如下的实现
-
读写锁
-
在读写锁中,
其基本需求就是读-读能共存,读-写不能共存,写-写不能共存,
并且写请求比读请求优先级更高。如下面简易的读写锁
-
上面的读写锁并不能保证可重入
(读锁重入和写锁重入),
可以先满足读锁重入,
在进行lockRead()时,若线程已获取过读锁,则不需等待,如下的读锁重入
-
对于写重入,只要当前线程已经获得写锁,就不需要再等待,只需记录其重入次数即可:
-
乐观锁 & 悲观锁
-
乐观锁允许在多线程并发时,不使用同步阻塞的方式来保证线程安全,
比如CAS(Compare and Set),
在如AtomicInteger等数据结构都大量使用CAS:
该方法虽然没有进行加锁操作,但通过compareAndSet()方法(该方法比较current与变量内存中的值,若相等,将变量值设置为next,虽然是先比较后设置"两步"操作,
但该方法利用CPU的compare-and-swap指令达到了原子操作,所以不会有线程安全问题),这样就能在不加锁的情况下实现线程安全,也叫非阻塞算法。
还有一种叫复制后再更新的非阻塞线程安全的实现方式,如CopyOnWriteArrayList,
利用volatile关键字和更新前拷贝副本的方式实现线程安全。
而悲观锁则相反,每次进行访问都会有加锁请求,直到获取到对象锁才进行后续操作,如上面实现的各种锁。
-
基本的锁原理也就在此,JDK1.5之后java.util.concurrent包中已经提供足够多的并发工具,已经不需要我们造了,但了解原理还是有必要的,
有兴趣可以翻一翻源码,也提供了一些常用的并发数据结构,如ConcurrentHashMap等。
本文也并非空穴来风,得益于这里。