java 资源竞争_Java并发:解决资源竞争

解决资源竞争

Java 提供关键字 synchronized 的内置支持。

使用 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

ReentrantReadWriteLock 是 ReadWriteLock 唯一实现类。

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 中可以发现,线程池只创建了三个线程,就完成了多任务的执行。 并且更重要一点就是读锁造成了多线程并发的读操作。

比较

synchronized 关键字更简洁

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() 返回初始值。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值