Java并发:解决资源竞争

解决资源竞争

  1. Java 提供关键字 synchronized 的内置支持。
  2. 使用 Lock 对象。

synchronized 关键字

synchronized 关键字获取的锁分为 对象锁 和 类锁。

public class Test {

    public synchronized void f(){}
    
    public synchronized static void g(){}
}

调用 f() 是获取的 Test 对象的锁,而调用 g() 获取的是类锁。

synchronized 关键字也可以用来同步代码块

public class Test {

    public void f(){
        synchronized(this){
            // ...
        }
    }

    public static void g(){
        synchronized(Test.class){
            // ...
        }
    }
    
}

同样,f() 方法获取的是对象锁,而 g() 获取的是类锁。

Lock

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock() 尝试获取锁,如果获取不到,会阻塞线程,直到获取到锁为止。

lockInterruptibly() 会尝试获取锁,如果获取不到,也会阻塞线程,但是它可以响应中断并且释放锁,也就避免了死锁问题。

tryLock() 会尝试获取锁,如果获取到了就返回 true,否则返回 false

tryLock(long time, TimeUnit unit) 在限定时间内获取锁,如果获取到了返回 true,如果获取不到就会阻塞线程,但是它可以响应中断并且释放锁。当超过了规定时间还没有获取到锁,自动释放锁。

unLock() 当然就是解锁。

newCondition() 会产生一个与 Lock 对象绑定的 Condition 对象 ,在操作 Condition 对象前,必须要获取 Lock 对象的锁。这个可以用于线程之间的协作。

ReentrantLock

Lock 直接实现类只有 ReentrantLock类。

public class Test {
    private int i = 0;
    private Lock mLock = new ReentrantLock();

    public void f() {
        mLock.lock();
        try {
            i++;
            i++;
        } finally {
            mLock.unlock();
        }
    }
}

在同步的地方,需要行调用 Lock 对象的 lock() 方法获取锁,为了保证不产生死锁情况,用 try-finally 释放锁。

ReadWriteLock

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReadWriteLock 接口把锁分为了读锁和写锁两种,因为有时候,多线程只会读取共享资源,而不会写,因为并发地读其实并不会产生并发问题。而写操作是不能与读操作并发进行的,需要等待锁的释放。

ReentrantReadWriteLock

ReentrantReadWriteLockReadWriteLock 唯一实现类。

public class Test {
    private ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
    private Lock mReadLock = mReadWriteLock.readLock();

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    new Test().read();
                }
            });
        }
        service.shutdown();
    }

    public void read() {
        mReadLock.lock();
        try {
            long start = System.currentTimeMillis();

            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(Thread.currentThread().getName() + " is Reading ...");
            }
            System.out.println(Thread.currentThread().getName() + " read complete!");
        } finally {
            mReadLock.unlock();
        }
    }
}

结果为

pool-1-thread-1 is Reading ...
pool-1-thread-1 read complete!
pool-1-thread-1 is Reading ...
pool-1-thread-2 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-2 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-2 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-2 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-2 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-2 read complete!
pool-1-thread-1 read complete!
pool-1-thread-1 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-1 is Reading ...
pool-1-thread-3 is Reading ...
pool-1-thread-3 is Reading ...
pool-1-thread-3 read complete!
pool-1-thread-1 is Reading ...
pool-1-thread-1 read complete!

从 Log 中可以发现,线程池只创建了三个线程,就完成了多任务的执行。 并且更重要一点就是读锁造成了多线程并发的读操作。

比较

  1. synchronized 关键字更简洁
  2. Lock 控制粒度更细

volatile

原子性

原子操作: 是不能被线程调试机制中断的操作,一旦开始,它就一定可以在可能发生的“上下文切换”之前执行完毕。

对于读取和写入除 long 和 double 之外的所有基本类型变量的操作都是原子操作。 对于 long 和 double 类型,如果使用 volatile 关键字,可以保证原子性。 如果不用 volatile 关键字,那么对 long 和 double 类型的读写必须加锁控制,也就是使用 synchronized 关键字, 或者 Lock 对象 。

可见性

在多任务处理器系统上,如果一个任务做出修改,例如修改一个 int 类型变量的值,即使这个操作是原子操作,但是对其他任务是不可视的,因为修改只是暂时性的存储在本地的处理器中。而处于其它处理器中的任务,如果要看到这个修改,必须把修改后的 int 类型值,刷新到主存中,而读取操作就发生在主存中。

如果把这个 int 类型变量加上 volatile 关键字,那么修改它的值后会被立即刷新到主存中,这样其它处理器中的任务也就可以看到这个修改了。 如果不用 volatile 关键字,那么必须加锁(synchronized 关键字或者 Lock 对象)访问来达到多线程可见性。

volatile 总结

volatile关键字可以保证基本类型的读写的原子性和可见性,但是对于其它的操作,例如递增,并不能保证原子性和可见性。 因此,最安全的做法就加锁控制。

原子类

Java 提供诸如 AtomicInteger, AtomicLong 这样的原子类,可以保证获取值以及操作值是原子操作以及多线程的可见性。

public class Test implements Runnable {
    private AtomicInteger i = new AtomicInteger(0);

    public int getValue() {
        return i.get();
    }

    public void evenIncrement() {
        i.getAndAdd(2);
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.err.println("Aborting!");
                System.exit(0);
            }
        },5000);
        ExecutorService service = Executors.newCachedThreadPool();
        Test test = new Test();
        service.execute(test);
        while (true) {
            int value = test.getValue();
            if (value % 2 != 0) {
                System.out.println(value);
                System.exit(0);
            }
        }
    }
}

ThreadLocal

如果一个资源被多个线程共享,然而每个修改只关心自己的修改,而不关心其它线程的对这个资源的修改,那么可以用 ThreadLocal 来对共享资源进行线程本地存储,它使得资源与线程进行关联进来。

public class Test {
    static ThreadLocal tl = new ThreadLocal() {
        @Override
        protected Long initialValue() {
            return Thread.currentThread().getId();
        }
    };

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Test.tl.get());
                }
            });
        }
        service.shutdown();
    }
}

通常把 ThreadLocal 对象设置为静态域,最好先复写 initalValue() 方法, 如果这个方法中产生了资源竞争,也要加上锁(synchronized 或 Lock)控制。 如果不复写这个方法,那么在每个线程中调用 get() 之前,必须先 set().

代码中开启了三个线程,虽然只调用 了 get() 方法,但是在 get() 之前,会先获取与线程相关的值,如果没有,就调用 initialValue() 返回初始值。

参考

volatile 文章
https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
http://www.cnblogs.com/dolphin0520/p/3920373.html

你可能感兴趣的:(Java并发:解决资源竞争)