本章探讨的是关于多线程安全的获取和修改临界资源的方式。
- 临界资源:
需要被有序访问的共享资源,指给定时刻只允许一个任务可以访问到资源。
synchronized 关键字
在执行 synchronized
关键字所保护的代码块时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
将要控制的资源包装为一个对象,并把所有访问该对象的方法标记为 synchronized
。这时一个线程在调用被标记的同步方法时,其他想要调用synchronized
方法的任务线程都会被阻塞。
public class Resource {
private static int count = 0;
synchronized static public int getCount() {
count++;
TimeUnit.MILLISECONDS.sleep(100);// 忽略了 try
return count;
}
}
- 所有的对象实例都自动含有单一的锁(也称为监视器),同步块本质上就是在这个锁上加锁来实现同步;
- 临界资源声明为 private 才能防止直接被外部的类访问数据域;
- 一个任务可以多次获取对象的锁(所以是递归锁);
- 针对每个类,也有一个锁(作为 Class 对象的一部分),所以
synconzied
关键字可以用于同步静态方法和静态成员资源;
默认情况下,同步块获取的是当前对象的的锁(也就是this
)。当然,也可以通过下面的方法,去获取其他对象的内置锁。
synchronized (syncObject){
}
Lock
使用锁也是一种常见的安全访问临界资源的方法。
public class Resource {
private static Lock lock = new ReentrantLock();
private static int count = 0;
public static int getCount() {
try {
lock.lock();// 加锁
// 访问临界资源
count++;
TimeUnit.MILLISECONDS.sleep(100);
return count;
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
} finally {
lock.unlock(); // 解锁
}
}
}
Lock 接口
-
lock()
- 无其他任务持有该锁时,获取该锁并返回,计数置 1
- 本任务已持有该锁,返回并将计数置 1
- 其他任务持有该锁,在获取锁前阻塞线程。
-
lockInterruptibly()
,抛出InterruptedException
异常- 获得锁前阻塞线程,但接受中断信号。
-
tryLock()
,返回boolean
- 如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回
true
- 其他任务持有该锁,返回
false
(非阻塞)
- 如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回
ReentrantReadWriteLock
ReentrantReadWriteLock
允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。
- 读取操作:没有线程正在做写操作,且没有线程在请求写操作,就可以读取。
- 写入操作:没有线程正在做读写操作。
ReentrantReadWriteLock
的使用:
public class Resource {
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static int count = 0;
public static void setCount() {
lock.writeLock().lock(); // 写锁
++count;
lock.writeLock().unlock();
}
public static int getCount() {
try {
lock.readLock().lock(); // 读锁,可以多个读锁同时读取
TimeUnit.MILLISECONDS.sleep(100);
return count;
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
} finally {
lock.readLock().unlock();
}
}
}
原子性
原子操作 指不能被线程调度机制中断的操作,一旦开始操作,那么一定会在“上下文切换”前完成。
原子性可以应用于所有非 long
和 double
的基本类型上的赋值操作和返回操作,而 long
与 double
使用 volatile
关键字修饰后,也能获取原子性。
-
volatile
关键字
volatile
关键词保证了一个域的可视性, 如果volatile
域进行写操作,这个数据会立即写入主存中。
非
volatile
域的写入操作不必刷新入主存中,所有不能保证读取该域的值是最新的,所以在多个任务同时访问一个域时,这个域需要声明为volatile
,否则,需要同步来防护值。
ThreadLocal
线程本地储存(ThreadLocal)实现了同一个变量,每个线程都拥有一个独立的数据域。
// 实现一个简单的 ThreadLocal 封装
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static Integer getValue() {
return value.get();
}
public static void setValue() {
value.set(getValue() + 1);
}
}
// 在 Main 中执行后能保证每个线程都得到自己的数据。
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
private int count = 3;
@Override
public void run() {
while (count > 0){
count --;
ThreadLocalVariableHolder.setValue();
System.out.println(Thread.currentThread() + ": " + ThreadLocalVariableHolder.getValue());
Thread.yield();
}
}
});
}
executorService.shutdown();
}
}
// 结果
Thread[pool-1-thread-1,5,main]: 1
Thread[pool-1-thread-3,5,main]: 1
Thread[pool-1-thread-2,5,main]: 1
Thread[pool-1-thread-3,5,main]: 2
Thread[pool-1-thread-2,5,main]: 2
Thread[pool-1-thread-1,5,main]: 2
Thread[pool-1-thread-3,5,main]: 3
Thread[pool-1-thread-1,5,main]: 3
Thread[pool-1-thread-2,5,main]: 3
小结
本章讲了多线程访问临界资源的安全方式,顺便提了一个从根本上解决同步问题的 ThreadLocal
的理念和用法。