Callable interfacce
也是一种创建线程的方式
Runnable 能表示一个任务(run方法),返回void
Callable 也能表示一个任务(call方法),返回一个具体的值,类型可以通过泛型参数来指定(object)
如果进行多线程操作,如果你只是关心多线程的执行过程,使用Runnable即可,如果是关心多线程的计算结果,使用Callable更合适
使用Callable不能直接作为Thread的构造方法参数,我们可以借用FutureTask来完成
,运算的结果什么时候能算出来,可以使用FutureTask来帮助我们解决这个问题
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum =0;
for (int i =0;i<1000;i++){
sum +=i;
}
return sum;
}
};
FutureTask<Integer> futureTask =new FutureTask<>(callable);
Thread t1 =new Thread(futureTask);
t1.start();
//获取call方法返回的结果,get类似于join,如果call方法没有执行完,就会阻塞等待
Integer result = futureTask.get();
System.out.println(result);
}
}
ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 的用法:
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
虽然synchronized也是一个可重入锁,但是这两者还是又明显的区别的
ReentrantLock的优点
1.提供了一个trylock的方法进行加锁
对于lock操作来说,如果加锁不成功,就会出现阻塞等待(死等).
对于trylock,如果加锁失败,直接返回false/也可以设定等待时间,该方法给锁操作提供了更多的可操作空间
2.有两种模式,可以工作在公平锁的状态下,也可以工作在非公平锁的状态下,构造方法中可以通过参数设定的公平/非公平模式
3.也有一个等待通知机制,搭配condition这样的类来完成,这里的等待要比wait notify功能更强
ReentrantLock的缺点
unlock()解锁方法容易被人忘记,常常需要搭配finally来使用
synchronized的锁对象可以是任意对象
ReentrantLock的锁对象就只能是自己本身,如果多个线程针对不同的ReentrantLock调用lock方法,此时是不会产生锁竞争的
原子类
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
以 AtomicInteger 举例,常见方法有
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i–;
incrementAndGet(); ++i;
getAndIncrement(); i++;
(具体可看CAS详解)
线程池
虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子”
中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.
(具体可看线程池详解)
信号量Semaphore
在操作系统中,也经常出现,是并发编程的一个重要的概念/组件
准确来说,Semaphore是一个计数器(变量),描述了"可用资源的个数",描述的是,当前这个线程,是否有"临界"资源可以用,所谓的临界资源,是指的多个线程/进程等并发执行的实体可以公共使用到的资源(多个线程修改同一个变量,这个变量就可以被认为是一个临界资源)
示例
停车场景:
停车场的入口,上面会挂这一个显示屏,上面会显示停车场内可用的车位
如果开车进入停车场,申请一个车位(申请了一个可用资源),此时计数器就会+1,称为p操作
如果开车离开停车场,释放了一个车位(释放了一个可用资源),此时计数器就会-1,称为v操作
如果发现车位计数器上面的计数器为0,这个时候,有两种选择:
1.等
2.放弃,寻找下个车位
这里我们选择第一种操作,继续进行p操作,就会阻塞等待,一直等到其他车辆进行了v操作,释放了一个空闲的车位为止,
锁,本质上是一个特殊的信号量(里面的数值,非0即1,二元信号量),信号量要比锁更广义,不仅仅可以描述一个资源,还可以描述N个资源,虽然概念上更广泛,实际开发中,还是锁更多一些(二元信号量的场景是更常见的)
package Thread;
import java.util.concurrent.Semaphore;
public class demo3 {
public static void main(String[] args) throws InterruptedException {
//构造方法中,就可以用来指定计数器的初始值
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();//计数器-1
System.out.println("执行p操作");
semaphore.acquire();//计数器-1
System.out.println("执行p操作");
semaphore.acquire();//计数器-1
System.out.println("执行p操作");
semaphore.acquire();//计数器-1
System.out.println("执行p操作");
semaphore.acquire();//计数器-1
System.out.println("执行p操作");
}
}
CountDownLatch
同时等待n个任务执行结束
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
package Thread;
import java.util.concurrent.CountDownLatch;
public class demo1 {
public static void main(String[] args) throws InterruptedException {
//相当于创建一个线程任务为10的计数器
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0;i<10;i++){
//防止因为变量捕获的机制使得无法正常访问i变量
int id =i;
Thread t = new Thread(()->{
System.out.println("线程"+id+"开始工作");
try {
//使用sleep来代指某些耗时操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+id+"结束工作");
//每次调用countDown就相当于使得计数器减一
countDownLatch.countDown();
});
t.start();
}
//如果计数器没有减到0,就会进入阻塞等待
countDownLatch.await();
System.out.println("多个线程的所有任务都执行完毕");
}
}