JUC即 java.util.concurrent,其中存放了一些进行多线程编程时有用的类
Callable
Callable是一个接口,在我们实现Runnable创建线程时,Runnable关注的是其过程,而不关注其执行结果(其返回值为void),而Callable会关注执行结果,Callable提供了call方法,其返回值就是线程执行任务得到的结果
因此,当我们在编写多线程代码时,希望得到线程种代码的返回值时,就可以使用Callable
例如:一个线程需要实现 1 + 2 + 3 + ... + 100,并返回其结果
若我们使用实现Runnable的方式创建线程时:
public class Demo1 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { @Override public void run() { int ret = 0; for (int i = 1; i <= 100; i++) { ret += i; } } }); t.start(); t.join(); } }
若此时主线程需要获取到其计算结果,则需要使用一个成员变量来保存结果
public class Demo1 { private static int sum = 0; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { @Override public void run() { int ret = 0; for (int i = 1; i <= 100; i++) { ret += i; } sum = ret;//保存结果 } }); t.start(); t.join(); System.out.println(sum); } }
而若我们使用Callable时:
Callable<V> 其中泛型参数表示返回值的类型
Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int ret = 0; for (int i = 1; i <= 100; i++) { ret += i; } return ret; } };
此时就不必引入额外的成员变量了,直接使用返回值即可
然而,Thread未提供构造函数来传入callable
此时,我们需要使用 FutureTask 来作为Thread和callable的“粘合剂”
将callable实例使用FutureTask包装一下,再在构造方法传入FutureTask:
此时线程就会执行FutureTask内部的Callable中的call方法,计算完成后,就将结果放入FutureTask对象中
FutureTask<V> 泛型参数表示结果的类型
此时在主线程中调用FutureTask中的get()方法(带有阻塞功能),等待计算完毕,从FutureTask中获取到结果
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int ret = 0; for (int i = 1; i <= 100; i++) { ret += i; } return ret; } }; FutureTask<Integer> futureTask = new FutureTask<>(callable); Thread t = new Thread(futureTask); t.start(); //此时无需使用join,使用futureTask中的get()方法获取到结果 System.out.println(futureTask.get()); } }
观察两种实现方式,我们可以发现:
使用Callable和FutureTask,使代码简化了很多,且不用使用成员变量来保存结果
Callable和Runnable相对,都是描述一个“任务”,Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务
Callable通常需要搭配FutureTask使用,FutureTask用来保存Callable的返回结果
ReentrantLock
ReentrantLock是一种可重入互斥锁,与synchronized类似,都是用来实现互斥效果,保证线程安全的
ReentrantLock的用法:
lock():加锁,若获取不到锁就死等
unlock():解锁
tryLock(超时时间):加锁,若获取不到锁,等待时间一定后就放弃加锁
当我们在使用ReentrantLock时,需要加锁和解锁两个操作,因此当我们在加锁后,很容易忘记解锁,例如在unlock之前,触发了异常 或 return语句,此时就可能不会执行unlock
因此,为了保证unlock的执行,我们可以将其放在finally中
例如:
ReentrantLock lock = new ReentrantLock(); lock.lock(); try{ //执行代码 }finally { lock.unlock(); }
synchronzied使用时无需手动释放锁,ReentrantLock使用时需要手动释放,使用起来更加灵活,但也容易漏掉unlock
为什么有了synchronized后,还有ReentrantLock?
1. ReentrantLock提供了tryLock操作。lock直接进行加锁,当加锁失败时,就阻塞,而tryLock尝试进行加锁,若加锁失败,等待一定时间后,不阻塞,直接放弃加锁,返回 false,表示加锁失败
2. ReentrantLock提供了公平锁的实现(遵循“先来后到”规则,即等待时间长的线程先获取到锁)。通过队列来记录加锁线程的先后顺序,ReentrantLock默认是非公平锁,在构造方法中传入true 则可设置为公平锁
3. 更强大的唤醒机制。synchronized,搭配wait和notify来实现等待唤醒,每次随机唤醒一个等待的线程;而ReentrantLock,搭配Condition类来实现通知等待机制,可以更精确控制唤醒某个指定的线程
Semaphore
信号量Semaphore,用来表示“可用资源的个数”,其本质就是一个计数器
当申请资源时,可用资源数 -1(信号的P操作)
当释放资源时,可用资源数 +1(信号的V操作)
Semaphore中的加减计算操作都是原子的,可以在多线程环境下直接使用
信号量也是操作系统提供的机制,JVM对其对应的API进行封装,因此我们可用直接通过Java代码来调用这里的相关操作
Semaphore类中acquire()方法表示申请资源(P操作),release()方法表示释放资源(V操作)
例如:
public class Demo2 { public static void main(String[] args) { Semaphore semaphore = new Semaphore(5);//有5个可用资源 for (int i = 0; i < 10; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { try{ System.out.println("申请资源"); semaphore.acquire(); System.out.println("获取到资源"); Thread.sleep(1000); System.out.println("释放资源"); semaphore.release(); }catch (InterruptedException e){ e.printStackTrace(); } } }); t.start(); } } }
CountDownLatch
同时等待多个任务执行结束
例如,构造CountDownLatch实例latch,初始化n表示有n个任务需要完成。
在任务执行完毕时,调用countDown()方法,告知latch当前任务执行完毕,则CountDownLatch内部的计数器 -1
在主线程中调用latch.await(),阻塞等待所有任务都执行完毕(计数器为0)
import java.util.Random; import java.util.concurrent.CountDownLatch; public class Demo3 { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(10);//此时有10个任务需要完成 for (int i = 0; i < 10; i++) { int id = i; Thread t = new Thread(()->{ Random random = new Random(); int time = (random.nextInt(5) + 1) * 1000;//执行任务的时间 try { Thread.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(id + "任务执行结束"); //告知latch执行结束 latch.countDown(); }); t.start(); } //通过await来等待所有任务执行结束 latch.await(); System.out.println("所有任务都已执行完成"); } }