竞态条件与锁
当某个计算结果的正确性取决于多个线程执行的顺序时,就会发生竞态条件。常见的竞态类型是先检查后执行。
先检查后执行的一种常见情况是延迟初始化。
public class LazyInitRace{
private People people=null;
public People getInstance(){
if(people==null){
people=new People();
}
return people;
}
}
这就包含一个竞态条件,在people未被实例化前,当多个线程都访问getInstance()时,就可能会得到不同的结果。如果不在同一瞬间调用这个方法,就不会出现问题。在people被实例化之后,多次同时访问这个方法,也不会出现问题。
要避免竞态问题,就必须在某个线程修改改变量时,通过某种方式防止其它线程使用这个变量,也就是保证修改操作的原子性。
加锁是java内置的确保原子性的机制。
synchronized关键字加锁:
public class TT{
private Integer a=4;
public static synchronized void do33(){
}
public synchronized void do34(){
}
public synchronized void do35(){
synchronized(a){
}
}
}
以上第一个synchronized是对TT的Class对象加锁,每一个类的字节码文件被加载到内存中以后,就会生成一个Class对象。
第二个synchronized是对创建的TT对象加锁,比如TT t=new TT();那么这个锁就对t生效。
第三个synchronized是对a对象加锁。
java 的内置锁是一种互斥锁。
java的锁是可以重入的,也就是说,当前线程持有了某个对象的锁,在线程中再次获取该对象的锁,是可以成功的。例子如下:
public class T2{
public synchronized void t1(){
System.out.println("t1执行了");
synchronized(this){
System.out.println("t1中的同步块执行了");
}
}
public static void main(String[] args){
T2 t=new T2();
t.t1();
}
}
例子2:
public class Father {
public synchronized void play(){
System.out.println("father play");
}
}
public class Son extends Father {
public synchronized void play(){
System.out.println("son play");
super.play();
}
public static void main(String[] args){
new Son().play();
}
}
一种常见的加锁约定是,把所有的可变状态封装在对象内部。通过内置锁对所有访问可变状态的路径(类似于get,set的方法)进行同步。Vector等集合类采用了这种模式。
不过上面这种方式也是有缺陷的,如果以后新增了一个方法,能够访问和修改哪些可变的变量,却又忘记增加synchronized关键字进行同步,那么就可能出现问题。
线程安全的类所组成的操作很多时候也需要额外的同步,如下:
if(!vector.contains(a))
vector.add(a)
这就存在竞态条件,需要把这2个操作组成一个原子性操作。
在没有全局检查的情况下,封装是保证线程安全性的一种有效手段,能够控制共享数据的访问。
线程限制的模式,是把非线程安全的类转换为线程安全的类的一种方式。
类库中的包装工厂Collections.synchronizedList方法就是将Arraylist、hashmap等转换为线程安全的类。
传统块结构的锁缺点有:
- 锁只有一种类型
- 只能在方法开始或同步块获取锁,结束释放
- 线程只能获取锁,或者阻塞。
Lock接口对上述做了改进。
ReentrantLock
ReentrantReadWriteLock
块结构的所有功能都能用Lock接口实现。
下面是使用reentrantLock实现的一种块结构的锁,可以解决死锁的问题。任务未完成时,会不断尝试获取锁,每次获取失败,都休眠一点时间(给予其它程序获取锁的机会),直到成功获取。
public void propagateUpdate(){
boolean acquired=false;
boolean done=false;
while (!done) {
try {
int wait = (int) (Math.random() * 10);
acquired = lock.tryLock(wait, TimeUnit.MILLISECONDS);
if (acquired) {
doSomething();
done=true;
}else {
Thread.sleep(wait);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
CountCownLatch
CountCownLatch这是一个锁存器。
方法 | 作用 |
---|---|
countDown | 对计数器减1 |
await | 在计数器到0之前一直等待 |
ConcurrentHashMap
ConcurrentHashMap是一种高效的锁,它只需要锁定修改的片,而不是整个集合。
CopyOnWriteArrayList
CopyOnWriteArrayList是arraylist的替代品,它在写时复制一份新复本。
BlockingQueue
阻塞队列,take和offer。
TransferQueue
放入时,如果没有消费者取走,就阻塞。
比如下面,程序就会一直阻塞
public static void main(String[] args) throws InterruptedException {
TransferQueue<Integer> transferQueue=new LinkedTransferQueue<>();
transferQueue.transfer(3);
System.out.println("wanle");
}
下面就会立刻执行最后一句。当前如果有消费者等着取消息,那么就直接把消息交给消费者,否则就不放入队列。
public static void main(String[] args) throws InterruptedException {
TransferQueue<Integer> transferQueue=new LinkedTransferQueue<>();
transferQueue.tryTransfer(3);
System.out.println("wanle");
}
会等待3秒,如果有消费者在指定时间内来取走,就交给它,否则就不入队列。
public static void main(String[] args) throws InterruptedException {
TransferQueue<Integer> transferQueue=new LinkedTransferQueue<>();
transferQueue.tryTransfer(400,3000,TimeUnit.MILLISECONDS);
System.out.println("wanle");
}