问题定义: 定义一个类Number,其中包括变量num。多个线程对num进行读取和减1的操作。
一、不安全的实现
将num定义成静态变量:
public class Number {
public static int num = 30;
public static AtomicInteger cnt = new AtomicInteger(0);
}
为了更好地看出线程多num的操作是否正确,用cnt来记录对num修改的次数,每有一个线程对num减去了1,就将cnt+1。cnt是AtomicInteger类型,使用了CAS来保证线程安全,因此cnt一定是正确的数值。
线程安全: 在多线程环境下能保证执行结果永远是正确的。
CAS: 假设要修改数据A,首先记录数据A的内存地址V,之后读出数据记作A,将A改为B。写回内存前检查V处的数据是否仍为A,若是则将B写回。若不是重新执行读出数据、修改、判断的操作。这是一种乐观并发控制。
乐观并发控制: 假设不会有多个线程同时操作同一数据(所以称为“乐观”),在操作数据前不加锁,操作结束提交之前检查这个数在这期间有没有修改过。相对的是悲观并发控制,不论读取数据都先加锁。
对num进行操作的线程:
public class UnsafeImpl implements Runnable {
@Override
public void run() {
while (Number.num != 0) {
Number.num--;
System.out.println("这是第"+ Number.cnt.incrementAndGet()+"次修改,"+Thread.currentThread().getName() + ",剩余" + Number.num);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
sleep(100)是为了更好看出线程的调度效果。在运行结果中,可以看到对num修改的过程不正确,比如会有多个线程修改后值不变,导致最终修改的次数>30
二、使用synchronized
将对num的操作封装到Number类中,使用synchronized修饰两个方法,保证了同一时间只有一个线程会执行这一方法。一个线程在调用dec(), get()方法之前,会先尝试获取NumberSync对象拥有的锁,获取了锁才能执行方法,获取失败则将自己挂起,等待占有锁的线程发出通知。执行方法后,线程会释放自己占有的锁并通知其他想要拥有锁的线程。
public class NumberSync {
private int num = 30;
public static AtomicInteger cnt = new AtomicInteger(0);
synchronized public void dec() {
num--;
System.out.println("这是第" + NumberSync.cnt.incrementAndGet() + "次修改," + Thread.currentThread().getName() + ",剩余" + num);
}
synchronized public int get() {
return num;
}
}
synchronized不需要手动释放锁,因此方法中的代码不变。
用于操作Number的类,要将同一个Number对象作为自己的成员变量:
public class SyncImpl implements Runnable {
private NumberSync numberSync;
public SyncImpl(NumberSync numberSync) {
this.numberSync = numberSync;
}
@Override
public void run() {
while (numberSync.get() != 0) {
numberSync.dec();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、使用读写锁
在上面一种实现中,dec(), get()方法都只允许同一时刻只有一个线程在执行。事实上,可以有多个线程同时执行get()方法,但只允许有一个线程执行dec()方法,且这个时期不能有线程在执行get()方法。这种要求可以使用读写锁来实现。将Number类修改如下:
public class NumberRWLock {
private int num = 30;
public static AtomicInteger cnt = new AtomicInteger(0);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void dec() {
writeLock.lock();
--num;
writeLock.unlock();
}
public int get() {
readLock.lock();
try {
return num;
} finally {
readLock.unlock();
}
}
}
首先创建ReentrantReadWriteLock对象,之后用这个对象创建readLock和writeLock对象,分别对应读锁和写锁。在写数据的方法中加写锁,在只有读取操作的方法中加读锁。要注意,return语句不会自动释放锁,因此需要用try catch包围return语句,在方法结束后手动释放读锁。
用这种方法允许多线程同时读取num,提高了效率。下面将这种方法与使用synchronized的方法进行一个对比,创建10个线程,每个线程调用100000次get()方法。将Number类中的cnt改为基本类型int,在get()方法中每次将cnt+1。
public class NumberRWLock {
private int num = 30;
public int cnt = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void dec() {
writeLock.lock();
--num;
writeLock.unlock();
}
public int get() {
readLock.lock();
try{
++cnt;
return num;
} finally {
readLock.unlock();
}
}
}
由于synchronized修饰的方法是线程安全的,因此所有线程结束后cnt的值为10*100000 = 1000000。而使用读写锁的方法中,所有线程结束后cnt值为829751。说明确实存在对get()方法的并发调用。
四、使用线程池
在一些应用中,要处理大量的小任务,比如web服务器,面对1000个请求,按照前面的方法需要创建1000个线程,分别处理每个请求。然而线程的创建、销毁、调度开销很大,创建太多线程是不可行的。解决这种问题可以使用线程池。
线程池维护一定数量的线程,将多个任务提交给线程池,线程池利用自己拥有的线程陆续处理各个任务。比如固定3个线程的线程池处理10个任务,会先处理前3个,有一个任务完成后再处理第4个任务,……
使用线程池代码如下:
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
NumberSync numberSync = new NumberSync();
for (int i = 0; i < 6; ++i) {
executorService.submit(new ThreadPool(numberSync, Integer.toString(i)));
}
executorService.shutdown();
}
}
首先定义线程池对象executorService,可以定义固定大小的线程池,也可定义根据任务数动态调整的(Executors.CachedThreadPool方法)。最后线程池对象要调用shundown()方法,否则线程池会一直存在,程序会一直运行。
要执行的任务定义在ThreadPool类中,这个类同样要实现Runnable接口:
public class ThreadPool implements Runnable {
private NumberSync numberSync;
private String name;
public ThreadPool(NumberSync numberSync, String name) {
this.numberSync = numberSync;
this.name = name;
}
@Override
public void run() {
while (numberSync.get() != 0) {
numberSync.dec();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上代码,不论执行多少次,都只有任务0,任务1,任务2在执行,因为线程池中固定只有三个线程,线程池会先执行完前三个任务,之后执行任务3~5,但此时num已经减到0了,因此不会后三个任务不会执行。
五、使用Callable和Future
Runnable接口的run()方法没有返回值,多线程任务要想返回一个值需要在实现Runnable接口的类中设置一个成员变量,之后读取这个变量,比较麻烦。因此可以实现Callable<>接口,实现其中的call()方法作为多线程的任务。Callable<>是一个泛型接口,可以指定任意对象类型作为call()方法的返回值。
比如要计算每个线程对num的修改次数并返回,实现Callable<>的代码为:
public class CallableImpl implements Callable<Integer> {
private NumberSync numberSync;
public CallableImpl(NumberSync numberSync) {
this.numberSync = numberSync;
}
@Override
public Integer call() throws Exception {
AtomicInteger count = new AtomicInteger(0);
while (numberSync.get() != 0) {
numberSync.dec();
count.incrementAndGet();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return count.get();
}
}
实现了Callable<>接口的类不能用来创建线程,只能作为任务提交给线程池。当线程池对象调用submit()方法时,会返回一个Future<>对象,用来接收任务的返回值。
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
NumberSync numberSync = new NumberSync();
// 以下语句报错
// Thread t = new Thread(new CallableImpl(numberSync));
// 正确写法
ExecutorService executorService = Executors.newFixedThreadPool(3);
List<Future<Integer>> futureList = new ArrayList<>();
for (int i = 0; i < 3; ++i) {
futureList.add(executorService.submit(new CallableImpl(numberSync)));
}
executorService.shutdown();
for (int i = 0; i < 3; ++i) {
System.out.println(futureList.get(i).get());
}
}
}
对Future<>对象调用get()方法会得到线程执行的返回值,若线程还没有执行完,则这个调用get()方法的线程会阻塞。比如将上面的代码改为:
ExecutorService executorService = Executors.newFixedThreadPool(3);
List<Future<Integer>> futureList = new ArrayList<>();
for (int i = 0; i < 3; ++i) {
futureList.add(executorService.submit(new CallableImpl(numberSync)));
System.out.println(futureList.get(i).get());
}
executorService.shutdown();
得到的结果总是任务1执行30次,任务2和3执行0次。因为主线程在for循环中调用get()方法,这时子线程1还没有执行完任务1,主线程阻塞,直到子线程1(任务1)执行完,之后任务2和3提交到线程池中,但此时num已经减为0,因此任务2,3不会执行。