一、解决共享资源竞争
1、原因:
我们永远不知道一个线程何时在运行。比如你坐在桌子边,正要去叉盘子中的最后一片实物,都够着它时,突然这片食物消失了,因为你的线程被挂起,另一个用餐者坐下并吃掉了它。
2、解决方式:
当一个资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,是其他任务在被 解锁之前无法访问它,其被解锁之时,另一个任务就可以锁定这项资源并使用它。
方案:
基本上所有并发模式在解决线程冲突问题时,都是采用序列化访问共享资源的方案。这意味着在给定的时刻只允许一个任务访问共享资源。通常通过在代码前面加上一条锁语句来实现,锁语句产生了一种互相排斥的效果,所以这种机制常常被称为互斥量(mutex)。
3、互斥同步:
关键字:synchronized 和使用显式的Lock对象
3.1 synchronized
java以提供关键字synchronized的形式,为防止资源冲突提供内置的支持。当任务要执行被synchronized 关键字保护的代码片段时,它将检查锁是否可用,然后获得锁,执行代码,释放锁。
使用方法:
共享资源一般以对象的形式存在内存片段,但也可以是文件、输入/输出端口、或者打印机。要控制对共享资源的访问,需要把它包装进对象,然后所有要访问资源的方法都标记为synchronized 。
声明synchronized 方法的方式:
synchronized void f(){}
synchronized void g(){}
所有对象都含有单一的锁(也称为监视器)。当在对象上调用任意synchronized方法时,此对象都被加锁,这时该对象上的其他synchronized 方法只有等到前一个方法调用完毕 并释放锁之后才能被调用。所以对于特定对象来说,其所有synchronized 方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存
注意在使用并发时将域设置为private是非常重要的,否则,synchronized 关键字就不能防止其他任务直接访问域,这样就会产生冲突。
锁具有重如机制:一个对象可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者有调用了另一个方法,就会出现多次获得锁的情况。
针对每个类也有一个锁,所以synchronized static 方法可以在类的范围内防止对static数据的并发访问。
如果在你的类中有一个方法正在处理临界数据,那么你必须同步所有相关的方法,否则他就不能正常工作。
Brian的同步规则:
如果你在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程都必须用相同的监视器锁同步。
3.2显式的Lock对象
在java SE5的java.util.concurrent类库中包含定义在java.util.concurrent.locks中的显式互斥机制。Lock对象必须被显式的创建、锁定和释放。
Lock lock = new ReentrantLock();
public int locktest(){
lock.lock();
try{
dosomething();
return value;
}finally {
lock.unlock();
}
}
对lock()的调用你必须放在finally子句中带有unlock()的try-finally语句中,注意,return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将任务暴露给第二个任务。
synchronized 和lock的使用场景:
一般使用synchronized,如果使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常,但是你没有机会做任何清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,你就可以使用finally子句将系统维护在正确的状态。
相比synchronized,ReentrantLock增加了一些高级功能
- 尝试获取锁且最终会失败,或者尝试获取锁一段时间,然后放弃它.可以通过
tryLock() tryLock(long timeout, TimeUnit unit)
```实现。
- 等待可中断指当持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断性对于处理执行时间非常长的同步块很有帮助。可以通过
lockInterruptibly()
- 公平锁指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁。可通过
ReentrantLock(true)
- 绑定多个条件指ReentrantLock对象可以同时绑定多个Condition对象,而synchroinzed中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个条件关联的时,就需要额外的田间一个锁,ReentrantLock只需多次调用newCondition()方法即可。
ReentrantLock trylock的使用Demo:
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed(){
boolean captured = lock.tryLock();
try{
System.out.println("tryLock() " + captured);
}finally {
if(captured){
lock.unlock();
}
}
}
public void timed(){
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
}finally {
if(captured){
lock.unlock();
}
}
}
public static void main(String[] args){
final AttemptLocking al = new AttemptLocking();
al.untimed();
al.timed();
new Thread(){
{
setDaemon(true);
}
public void run(){
al.lock.lock();
System.out.println("acquired ");
}
}.start();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.yield();
al.untimed();
al.timed();
}
}
4、原子性、可见性和有序性
原子性:
原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生 "上下文切换"之前(切换到其他线程执行)执行完毕。
原子性可以应用与除long和double之外的所有基本类型的读写操作。对于除long和double之外的基本类型变量的操作,可以保证他们被当作不可分(原子)的操作来操作内存。当定义long或double变量时,如果使用volatile关键字就会获得原子性。
可见性:
可见性指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量的值这种依赖主内存作为传递媒介的方式实现可见性的。在非volatile变量的原子操作不必刷新到主内存中去,因此其他读取该域的任务不必看到这个新值。synchronized和final也实现了可见性
有序性:
java程序有序性总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程所有的操作都是无序的。前半句指“线程内表现为串行的语义”,后半句指“指令重排序”现象和“工作内存和主内存同步延迟”现象。
volatile关键字和synchronized关键字保证了线程之间操作的有序性。volatile关键字本身包含禁止指令重排序,synchronized则由“一个变量在同一时刻只允许一个线程对其进行lock操作”这条规则获得的,持有同一个锁的两个同步块只能串行地进入。
5、线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
创建和管理本地线程由java.lang.ThreadLocal类来实现。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
Random random = new Random(47);
return random.nextInt(1000);
}
};
public static void increment(){
value.set(value.get() + 1);
}
public static int get(){
return value.get();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++){
exec.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
exec.shutdownNow();
}
static class Accessor implements Runnable{
private final int id;
public Accessor(int idn){
id = idn;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
return "# " + id + ": " + ThreadLocalVariableHolder.get();
}
}
}