目录
一、Callable接口
1、创建线程的操作
2、编写多线程代码
(1)实现Runnable接口(使用匿名内部类)
(2)实现Callable接口(使用匿名内部类)
二、ReentrantLock
1、ReentrantLock和synchronized的区别
2、如何选择使用哪个锁?
三、原子类
四、线程池
五、信号量 Semaphore
代码示例
六、CountDownLatch
代码示例
七、相关面试题
1、线程同步的方式有哪些?
2、为什么有了synchronized还需要juc下的lock?
3、AtomicInteger的实现原理是什么?
4、信号量听说过么?之前都用在过哪些场景下?
5、解释⼀下ThreadPoolExecutor构造方法的参数的含义
JUC即java.utill.concurrent,里面放了一些多线程编程时有用的类,下面是里面的一些类。
多线程编程时,创建线程有以下五种操作:
1、继承Thread类(包含了匿名内部类的方式)
2、实现Runnable接口(包含了匿名内部类的方式)
3、基于lambda表达式
4、基于Callable接口
5、基于线程池
为什么有那么多方式可以创建线程,前面三个创建线程很方便,也经常用,为啥还要学Callable接口创建线程的方式呢?答案是因为有它独特的优势和特性。
以下是Callable和Runnable的区别:
Runnable关注的是这个的过程,也就是重新run方法里面的内容,它的返回值是void。
Callable即关注过程,也关注结果,Callable提供call方法,返回值就是执行任务得到的结果。
创建一个线程,这个线程完成1+2+3+...+1000的任务,并打印出结果。
代码如下:
public class ThreadDemo1 {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
int result = 0;
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
result += i;
}
sum = result;
}
});
t.start();
t.join();
System.out.println("sum = " + sum);
}
}
执行结果:
可以看到,用实现Runnable接口的代码可以完成任务,但并不优雅,因为要创建一个全局的静态变量,如果其他场景下使用Runnable接口,需要创建很多这样的变量,就容易混淆、记错这些变量的代指。
代码如下:
public class ThreadDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println("result = " + result);
}
}
执行结果如下:
和预期值一样。
实现Callable接口,没有创建变量也可以完成任务,通过call方法的返回值,输出我们想要的值。
注意:
1、这里的FutureTask,因为Thread里面的构造方法的参数没有Callable接口,但有FutureTask类,所以成为了Thread和Callable的粘合剂。
2、FutureTask直接翻译的意思是:未来的任务,那么未来的任务肯定没有执行完,最终取结果的时候就需要一个凭据,而futureTask就是凭据;就像我们吃麻辣烫的时候,付完款拿到的小牌子,当前工作人员没做完麻辣烫,需要做完后叫到我们的号才能取餐。
3、FutureTask的get方法有阻塞功能,如果线程没有执行完,get就会阻塞,等线程执行完了,return了结果,才会执行get方法返回值。
Callable是一个“锦上添花”的东西,Callable能干的事,Runnable也能干,但对于这种带返回值的任务,使用Callable会更好,代码更直观、简单,但需要理解这里FutureTask起到的作用。
ReentrantLock是可重入互斥锁,跟synchronized定位类似,都是实现互斥的效果,保证线程安全。在java的远古时期,sychronized锁的功能没有那么强大,没有各种优化,ReentrantLock就是可以用来使用可重入锁的(历史遗留)。
传统的锁的风格,锁对象提供两个方法:lock和unlock,这个写法就容易忘记解锁unlock,或者在unlock之前,提前return了,可能引起unlock没执行到,所以正确使用ReentrantLock锁,unlock操作要放进finally。
那么有了synchronized,为啥还要有ReentrantLock呢?有以下三点(也是synchronized与ReentrantLock的区别):
1、ReentrantLock提供了tryLock操作。synchronized锁直接进行加锁,加锁不成功就会阻塞;ReentrantLock锁进行加锁时,进行加锁时,如果加锁不成功,不会阻塞,直接返回false,这里的tryLock的操作空间更大。
2、ReentrantLock锁是公平锁。synchronized锁是非公平锁。
3,、ReentrantLock和synchronized搭配的等待机制不同。synchronized锁搭配的是wait、notify,而ReentrantLock锁搭配的是Condition类,功能比wait、notify略强一点。
(1)当锁竞争不激烈时,使用synchronized锁,效率更高,自动释放更方便。
(2)锁竞争激烈时,使用ReentrantLock,搭配tryLock更加灵活控制加锁行为,而不是死等
(3)如果要使用公平锁,就使用ReentrantLock。
原⼦类内部⽤的是CAS实现,所以性能要比加锁实现i++高很多。原⼦类有以下几个
• AtomicBoolean
• AtomicInteger
• AtomicIntegerArray
• AtomicLong
• AtomicReference
• AtomicStampedReference
以AtomicInteger举例,常见方法有
addAndGet(int delta); i += delta
decrementAndGet(); --i
getAndDecrement(); i--
incrementAndGet(); ++i
getAndIncrement(); i++
CAS的详细介绍地址:多线程(进阶二:CAS)-CSDN博客
详细介绍地址:多线程(初阶九:线程池)-CSDN博客
信号量,用来表示“可用资源的个数”,本质还是一个计数器。而信号量可以理解成停车场的展示牌:当前有100个停车位,相当于有100个可用资源。
1、当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作)。
2、当有车开出去的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作)。
3、当停车场停满车的时候,也就是计数器值为0了,如果还尝试申请资源,就会阻塞等待,知道有其他线程释放资源。
注意:所谓的锁,本质也是信号量,这个信号量比较特殊,可以理解成计数器值为1的信号量。
锁处于释放状态,计数器值就是1;锁处于加锁状态,计数器值就是0。对于这种非0即1的信号量,称为 “二元信号量”。
用两个线程一起完成count自增10000次操作。
public class ThreadDemo1 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
执行结果:
注意:信号量也可以保证线程安全。保证线程安全有以下几种方式:
1、使用synchronized锁
2、使用ReentrantLock锁
3、CAS原子类操作
4、使用信号量Semaphore
CountDownLatch是针对特定场景的小工具。例如:多线程执行任务,把一个大的任务拆分成一个个的小任务,由每个线程执行这些小任务,等执行完全部的小任务,再进行一个汇总,从而完成这样的大任务。那么我们是怎么知道啥时候所有的小任务都完成了呢?如果使用join是无法感知到的,这时候就可以使用CountDownLatch这样的小工具了,它能感知到所有的小任务都完成。
有这样的多线程下载软件,像idm,基本可以下载任何网站的电影,如果使用浏览器默认的下载方式没有那么快,但idm不一样,因为是多线程下载,可以下载的很快,最终完成后把所有的内容都拼接在一起,就是使用CountDownLatch这样的小工具,可以感知到啥时候这些小任务都下载完了。
创建出5个线程下载一个任务,把这个任务分成5个小任务,5个线程进行下载,都下载往后,打印下载完成。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//1、此处的构造方法写10,意思是有10个线程/任务
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
int id = i;
Thread t = new Thread(() -> {
Random random = new Random();
//[0,5)
int time = (random.nextInt(5) + 1) * 1000;
System.out.println("线程" + id + "开始下载");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + id + "结束下载");
//2、告知CountDownLatch我执行完了
latch.countDown();
});
t.start();
}
//3、通过这个await操作来等待所有任务结束,也就是countDown被调用10次了
latch.await();
System.out.println("所有任务都下载完成!");
}
}
执行结果:
注意:
1、构造CountDownLatch实例,初始化5表⽰有5个任务需要完成.
2、每个任务执行完毕,都调用 latch.countDown() .在CountDownLatch内部的计数器同时自
减.
3、主线程中使用 latch.await(); 阻塞等待所有任务执行完毕.相当于计数器为0了.
答:synchronized、ReentrantLock、Semaphore、原子类的一些操作等都可以。
答:以JUC的ReentrantLock为例。
(1)ReentrantLock提供了tryLock操作,进行加锁时,如果失败则返回false,不会阻塞,搭配使用tryLock使用更灵活,而不是死等。而synchronized加锁失败会阻塞,可能会死等。
(2)ReentrantLock和synchronized搭配的wait、notify机制不同,ReentrantLock搭配的是Condition类,功能比wait、notify更强,可以精确的控制唤醒某个线程。
(3)synchronized是非公平锁,ReentrantLock是公平锁。
详细过程参考地址:多线程(进阶二:CAS)-CSDN博客
答:信号量,用来表示“可用资源的个数”,本质上是个计数器,停车场的展示牌原理就是使用了信号量。使用过的场景:两个线程完成count变量自增10000次;创建Semaphore实例的时候,构造方法的实参传1,表示计数器值为1,当一个线程自增钱就会申请1个资源——P操作,自增完后就会释放1个资源——V操作;两个线程不会同时自增,不会出现线程安全问题,自增前P操作的计数器-1,计数器值为0,另一个线程不会自增,等当前线程自增完后,V操作,计数器值+1,才能进行自增。可以解决线程安全问题。
参考下面地址内容:多线程(初阶九:线程池)-CSDN博客