解决资源竞争
- 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()
返回初始值。
参考
volatile 文章
https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
http://www.cnblogs.com/dolphin0520/p/3920373.html