线程安全性--锁

竞态条件与锁

当某个计算结果的正确性取决于多个线程执行的顺序时,就会发生竞态条件。常见的竞态类型是先检查后执行。

先检查后执行的一种常见情况是延迟初始化

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等转换为线程安全的类。

传统块结构的锁缺点有:

  1. 锁只有一种类型
  2. 只能在方法开始或同步块获取锁,结束释放
  3. 线程只能获取锁,或者阻塞。

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");
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值