JUC简介
来源于 java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks 这三个包(简称JUC ),在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。23年之后CPU主频更新缓慢,采用多核模式,要想程序更快就要用到并行和并发编程,但并发编程会涉及的问题有:线程安全问题、线程锁、线程性能问题。
线程和进程
进程、线程的关系
- 进程:一个程序,QQ.exe Music.exe 程序的集合;
- 线程:一个进程往往可以包含多个线程,至少包含一个!
Java默认有两个线程:mian 线程、GC 线程
线程有几个状态
- 新生 NEW
- 运行 RUNNABLE
- 阻塞 BLOCKED
- 等待,死死地等 WAITING
- 超时等待 TIMED_WAITING
- 终止 TERMINATED
并发、并行的关系:
并发(多线程操作同一个资源)、并行:(多个人一起行走) CPU 多核 ,多个线程可以同时执行;
锁的理解
悲观锁、乐观锁
- 悲观锁:适合写操作多的场景,先加锁来保证数据正确性;
- 乐观锁:一版根据版本号机制、CAS算法(比较并替换);
公平锁、非公平锁
- 公平锁(FairSync)非常公平;不能插队的,必须先来后到。
- 非公平锁(NonfairSync)非常不公平,允许插队的,可以改变顺序。
可重入锁
- lock锁必须配对,相当于lock和 unlock 必须数量相同;
- 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
class Phone{
Lock lock = new ReentrantLock();
public void seedMsg(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ":发短信");
call(); // 这里也有锁
} finally {
lock.unlock();
}
}
public void call(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ":打电话");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{ phone.seedMsg(); },"A").start();
new Thread(()->{ phone.seedMsg(); },"B").start();
}
自旋锁
线程A先执行,进行加锁,其他线程在自旋中,只有A线程解锁后,其他线程进行操作解锁。
// 自己写的自旋锁
class SpinlockDemo {
// 默认值是 null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "===> 加锁");
// 如果当前对象是null,比较替换成对象,执行结果为false ,就一直循环 自旋锁
while (!atomicReference.compareAndSet(null, thread)) {
// System.out.println(Thread.currentThread().getName() + " ==> 自旋中~");
}
}
//解锁
public void myunlock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "===> 解锁");
atomicReference.compareAndSet(thread, null);
}
}
public static void main(String[] args) throws Exception{
//使用CAS实现自旋锁
SpinlockDemo lock = new SpinlockDemo();
new Thread(() -> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} finally {
lock.myunlock();
}
}, "线程A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} finally {
lock.myunlock();
}
}, "线程B").start();
}
生产者和消费者
生产者与消费者问题是多线程同步的一个经典问题。生产者和消费者同时使用一块缓冲区,生产者生产商品放入缓冲区,消费者从缓冲区中取出商品。我们需要保证的是,当缓冲区满时,生产者不可生产商品;当缓冲区为空时,消费者不可取出商品。
// Lock版的生产者和消费者。
public class Ticket {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// +1方法
public void increment(){
lock.lock(); //加锁
try {
while (number!=0){
try {
condition.await(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.out.println(Thread.currentThread().getName() + "执行:"+ number);
condition.signalAll(); // 通知其他线程
} finally {
lock.unlock(); //解锁
}
}
// -1方法
public synchronized void decrement(){
lock.lock(); //加锁
try {
while(number==0){
try {
condition.await(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName() + "执行:"+ number);
condition.signalAll(); // 通知其他线程
} finally {
lock.unlock(); //解锁
}
}
}
public static void main(String[] args) {
// 资源类
Ticket ticke = new Ticket();
// 线程A C 执行 +操作 B D执行 -操作
new Thread(()->{ for (int i=0;i<=10;i++){ ticke.increment(); } },"线程A").start();
new Thread(()->{ for (int i=0;i<=10;i++){ ticke.decrement(); } },"线程B").start();
new Thread(()->{ for (int i=0;i<=10;i++){ ticke.increment(); } },"线程C").start();
new Thread(()->{ for (int i=0;i<=10;i++){ ticke.decrement(); } },"线程D").start();
}
注意:多线程下判断使用while,代替 if 就可以防止虚假唤醒。
Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现 等待/通知模式,Condition可以精准的通知和唤醒的线程!
/**
* A 执行完 调用B
* B 执行完 调用C
* C 执行完 调用A
*/
public class Ticket {
private int number = 0;
// Lock版本的 生产者与消费
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
// 状态分为 1 condition1执行 2 condition2执行 3 condition3执行
private int state = 1;
// A方法
public void doA(){
lock.lock();
// 业务 - 判断 - 执行 - 通知
try {
while(state != 1){
condition1.await(); //等待
}
System.out.println(Thread.currentThread().getName() + "执行:"+ state);
state = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// B方法
public void doB(){
lock.lock();
try {
while(state != 2){
condition2.await(); //等待
}
System.out.println(Thread.currentThread().getName() + "执行:"+ state);
state = 3;
condition3.signal();
}catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// C方法
public void doC(){
lock.lock();
try {
while(state != 3){
condition3.await(); //等待
}
System.out.println(Thread.currentThread().getName() + "执行:"+ state);
state = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8锁现象
8锁现象,实际对应的就是8个问题。要清楚判断锁的是谁!永远的知道什么是锁,锁到底锁的是谁!注意:(能用对象锁,就不要用类锁)
问题一 : 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test1 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{phone.seedMsg();}, "A").start();
// 4秒延迟
TimeUnit.SECONDS.sleep(4);
// 线程B
new Thread(()->{phone.call();}, "B").start();
}
}
class Phone{
public synchronized void seedMsg(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
结果是:先打印发短信,然后再打电话!
问题二 : 在发短信方法中,延迟4秒,两个线程先打印 发短信 还是 打电话?
public class Test2 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
// 线程B
new Thread(()->{phone.call();}, "B").start();
}
}
class Phone{
public synchronized void seedMsg() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
结果:还是先打印发短信。
首先知道 锁的对象是谁?因为 synchronized 加在方法上,所以锁的对象是 方法的调用者,所以两个方法用的是同一个锁,谁先拿到谁先执行! 对象锁就是synchronized;
解释:
- phone对象,就是方法的调用者,也就是手机,它可以打电话和发短信。
- 现在有两个人线程A 和 线程B,他们一个想打电话,一个想发短信。
- 线程A,先拿到锁(也就是手机),抱着锁(手机)睡了4秒。
- 线程B肯定拿不到锁(手机),需要等待。
问题三 : Phone类增加一个普通方法,线程B调用,那么两个线程先打印 发短信 还是 打电话?
public class Test3 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{ phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
// 线程B
new Thread(()->{ phone.hello();}, "B").start();
}
}
class Phone{
//同步方法
public synchronized void seedMsg(){
// 1秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通方法
public void hello(){
System.out.println("hello");
}
}
结果:先打印hello,然后打印发短信。
解释:
- 锁的是方法的调用者
- 现在 线程B 调用普通方法,相当于可以远程操控,不需要接收消息
问题四 : 创建两个 phone对象,线程调用不同对象的方法,那么两个线程先打印 发短信 还是 打电话?
public class Test4 {
public static void main(String[] args) throws Exception {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{ phone1.seedMsg(); }, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call(); }, "B").start();
}
}
class Phone{
//同步方法
public synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//同步方法
public synchronized void call(){
System.out.println("打电话");
}
//普通方法
public void hello(){
System.out.println("hello");
}
}
结果:先打印打电话。
解释:synchronized用在方法上,那么锁的是方法的调用者。现在有两个调用者,所以互不影响。
问题五 : 将方法变为静态同步方法,那么两个线程先打印 发短信 还是 打电话?
问题六 : 现在有两个对象,调用不同对象的,那么两个线程先打印 发短信 还是 打电话?
public class Test5 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{ phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
// 线程B
new Thread(()->{ phone.call(); }, "B").start();
// 问题6
// Phone phone1 = new Phone();
// Phone phone2 = new Phone();
// new Thread(()->{ phone1.seedMsg();}, "A").start();
// new Thread(()->{ phone2.call();}, "B").start();
}
}
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 静态同步方法
public static synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印发短信。
解释:5、6的问题一样,对于static静态方法来说,对于整个类Class只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
问题七 : 资源类中,一个是静态同步方法,一个普通同步方法,那么两个线程先打印 发短信 还是 打电话?
public class Test7 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
new Thread(()->{ phone.seedMsg(); }, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone.call(); }, "B").start();
}
}
// 手机
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印打电话,然后打印发短信
解释:因为一个锁的是Class类,一个锁的是对象调用者。后面那个打电话不需要等待发短信,可以直接运行。
问题一八: 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test7 {
public static void main(String[] args) throws Exception{
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{ phone1.seedMsg(); }, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call(); }, "B").start();
}
}
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印打电话,然后打印发短信
解释:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。(类锁和对象锁互不干扰)
总结:
- new 和 this 是一个对象。
- static 和 Class 是唯一的一个模板。
文件反编译:javap - c xxx.class
死锁
有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁,从而产生死锁。
死锁检测:
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
命令:jps -l
jps -l # 得到进程编号
2、使用jstack
进程进程号 找到死锁信息
jstack是java虚拟机自带的一种堆栈跟踪工具,或者 使用jconsole(图形化)进行查看。
对象内存布局
在虚拟机中,对象在内存中存储的布局可以分为:对象头、实例数据、对齐填充。
对象头存储内容:Mark Word(对象标记)这里有【hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间戳,对象分代年龄】、类型指针、数组长度(只有数组对象才有);
实例数据:存放类的属性数据信息,包括父类的属性信息;
对齐填充:由于虚拟机要求,对象起始地址必须是8字节的整数倍。
压缩指针:默认是开启的,通过java -XX:+PrintCommandLineFlags -version,可查看是否开启,+代表开启,-代表不开启。
分析对象在JDK的大小和分布,引包该包:
# openjdk 分析对象在JDK的大小和分布
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
# 使用,可以查看value,第一个()后3位判断出是什么锁 # 如:001无锁 101偏向锁 000 轻量锁 010重级锁
System.out.println(ClassLayout.parseInstance(对象).toPrintable());
synchronized
JDK6之前是属于重量级锁,效率底下,通过监听器(monitor)来实现的,它需要从用户状态转向内核状态,所以耗时严重,JDK6之后有了偏向锁、轻量级锁。
- 偏向锁是通过Mark Word(对象标记)存储偏向的线程id;
- 轻量锁是通过Mark Word(对象标记)存储指向线程栈中的Lock Record指针;
- 重量锁是通过Mark Word(对象标记)存储指向线程堆中的监听器(monitor)指针;
偏向锁
当线程A第一次争到锁,通过操作修改Mark Word(对象标记)中偏向线程id,使用场景:多线程情况下,在争抢过程中,还可能由同一个线程争取到,就没必要进行从用户状态转向内核状态这部分耗时操作。但在JDK15废弃偏向锁。优点:加锁和解锁不需要额外消耗,缺点:适合一个线程干活,线程之间锁竞争会带来额外的消耗。
-XX:-UseBiasedLocking # 关闭偏向锁,默认是开启的
轻量锁
本质就是自旋锁CAS,不断的尝试获取锁,尽量不阻塞;默认是自旋10次,但在JDK6之后,自旋的次数是不固定的,根据同一个锁上一次自旋的时间,上次时间长,这次就时间长,上次时间短,会根据变短,避免空循环。优点:提高响应速度,缺点:消耗CPU。
-XX:-PreBlockSpin=10 # 默认自旋次数10次,可设置CPU核数一半
重量锁
synchronized 是重量级的锁,优点:不消耗CPU,追求吞吐量,缺点:线程阻塞,响应慢。
注意:
当一个对象已经计算获取了哈希值之后,就不能进行偏向锁,因为Mark Word(对象标记)空间就那么大,会存储哈希值,而偏向的线程id不再存储,直接进入重量锁。
集合类不安全
List不安全
ArrayList
在并发情况下是不安全的!
解决:
- Vector就是线程安全的
- 使用Collections.synchronizedList(new ArrayList<>());
- 使用 List arrayList = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制, 是计算机程序设计领域的一种优化策略
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;
Set不安全
和List、Set同级的还有一个BlockingQueue 阻塞队列;
Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;
解决:
- 使用Collections工具类的synchronized包装的Set类
- 使用CopyOnWriteArraySet 写入复制的JUC解决方案
Map不安全
HashMap
基础类也存在并发修改异常!
解决:
- 使用Collections.synchronizedMap(new HashMap<>());处理;
- 使用ConcurrentHashMap进行并发处理
阻塞队列
队列:是一种特殊的线性表,单向队列只能在一端插入数据(后),另一端删除数据(前);它和栈一样,队列是一种操作受限制的线性表;由于只能一端删除或者插入,所以只有最先进入队列的才能被删除,因此又被称为先进先出(FIFO—first in first out)线性表。
队列分为两种:
- 单向队列:只能在一端删除数据,另一端插入数据。
- 双向队列:两端都可以进行插入数据和删除数据操作。
阻塞队列:是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
Java里的阻塞队列:
- ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue: 一个不存储元素的阻塞队列。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
blockingQueue
阻塞队列,blockingQueue 是Collection的一个子类;
LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。
提供了四种处理方法:
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
获取并移除队列的头 | remove() | poll() | take() | poll(time,unit) |
检测队首元素 | element() | peek() | - | - |
// 抛出异常
@Test
void Test1(){
// 需要初始化队列的大小
ArrayBlockingQueue deque = new ArrayBlockingQueue<>(3);
// 添加
deque.add("a");
deque.add("c");
deque.add("b");
// 抛出异常:java.lang.IllegalStateException: Queue full
// deque.add("e");
// 移除
deque.remove();
// deque.remove();
// deque.remove();
System.out.println(deque);
//判断队列首
Object element = deque.element();
System.out.println(element);
}
// 不抛出异常
@Test
void Test2(){
ArrayBlockingQueue deque = new ArrayBlockingQueue<>(3);
// 添加
deque.offer("a");
deque.offer("c");
deque.offer("b"); //true
// 一个不能添加的元素 使用offer只会返回 false 不会抛出异常
System.out.println( deque.offer("e"));
// 移除
deque.poll();
// 移除 如果没有元素 只会返回null 不会抛出异常
System.out.println(deque);
// 判断队列首
Object element = deque.peek();
System.out.println(element);
}
// 等待 一直阻塞
@Test
void Test3() throws InterruptedException {
ArrayBlockingQueue deque = new ArrayBlockingQueue<>(3);
// 添加 一直阻塞 不会返回
deque.put("a");
// 如果队列已经满了, 再进去一个元素 这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
// deque.put("e");
// 移除
deque.take();
// deque.take();
// 如果我们再来一个 这种情况也会等待,程序会一直运行 阻塞
}
// 等待 超时阻塞
@Test
void Test4() throws InterruptedException {
ArrayBlockingQueue deque = new ArrayBlockingQueue<>(3);
// 添加 这种情况也会等待队列有位置 或者有产品 但是会超时结束
deque.offer("a",2, TimeUnit.SECONDS);
deque.offer("b",2, TimeUnit.SECONDS);
// 超时时间2s 等待如果超过2s就结束等待
// 移除
deque.poll(2, TimeUnit.SECONDS);
deque.poll(2, TimeUnit.SECONDS);
// 超过两秒 我们就不要等待了
System.out.println(deque);
}
SynchronousQueue
同步队列:没有容量,也可以视为容量为1的队列;
进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
put方法 和 take方法;
Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
put了一个元素,就必须从里面先take出来,否则不能再put进去值!
并且SynchronousQueue 的take是使用了lock锁保证线程安全的。
void test1(){
BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
// 使用两个进程
// 一个进程 放进去 一个进程 拿出来
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" Put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+" Put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+" Put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()
+" Take "+synchronousQueue.take());
// TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()
+" Take "+synchronousQueue.take());
// TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()
+" Take "+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
创建线程
我们之前创建线程:
new Thread(()->{ System.out.println("任务模块");},"线程名1").start();
Callable创建线程:
Java库具有FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。
使用Callable和Future的完整示例:
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 1; i < 10; i++) {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<>( Callable)).start();
//适配类:FutureTask
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
//放入Thread使用
new Thread(futureTask,String.valueOf(i)).start();
//获取返回值
String s = futureTask.get();
System.out.println("返回值:"+ s);
}
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Call-线程:"+Thread.currentThread().getName());
return "String"+Thread.currentThread().getName();
}
}
注意:使用Callable进行多线程操作,多个线程结果会被缓存,效率高。这个get 方法可能会产生阻塞!把他放到 最后,或者使用异步通信来处理!
Callable接口与Runnable接口的区别:
- 是否有返回值
- 是否抛出异常
- 一个是call(),一个是run()
线程池
优点:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
线程池的使用
Executors:
// 创建线程池
public static void main(String[] args) {
// 单个线程
// ExecutorService executorService = Executors.newSingleThreadExecutor();
// 创建一个固定的线程池的大小
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// 可伸缩的
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i=1; i<=80; i++){
//使用线程池之后创建线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} finally {
executorService.shutdown();
}
}
源码分析:线程池的真正实现类是ThreadPoolExecutor,有7大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大的线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler //拒绝策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
自定义创建线程池,参数类型就是7大参数,分别是 核心线程池大小、最大的线程池大小、超时了没有人调用就会释放、超时单位、阻塞队列、线程工厂 创建线程的 一般不用动、拒绝策略
public static void main(String[] args) {
// 注意:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
try {
for (int i=1; i<=80; i++){
//使用线程池之后创建线程
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} finally {
threadPoolExecutor.shutdown();
}
}
等待队列已经满了,塞不下新任务,同时,线程池中max线程也达到了,无法继续新任务。这个时候就需要拒绝策略机制
拒绝策略有4种:
- AbortPolicy:如果阻塞队列满了,直接抛出异常阻止系统正常运行,队列容量大小 + maxPoolSize
- CallerRunsPolicy:如果阻塞队列满了,该策略不会抛弃任务,也不抛出异常,而是将任务回退给调用者
- DiscardOldestPolicy:如果阻塞队列满了,抛弃队列中等待最久的任务,把当前任务加入队列中再次提交
- DiscardPolicy:如果阻塞队列满了,丢弃无法处理的任务,不抛出异常,如果允许任务丢失,是最好的策略
如何去设置线程池的最大大小如何去设置?
-
CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小
Runtime.getRuntime().availableProcessors() // 获取CPU核数
-
I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
单例模式
设计模式之一:单例模式。
最重要的思想,构造方法私有,private。保证内存中只有一个对象。
饿汉式:
public class Hungry {
// 可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
....
// 构造方法私有 ---
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry1();
public static Hungry getInstance(){
return HUNGRY;
}
}
一上来,把这里面的全部都加载出来,在没有使用这个对象,对象就已经存在,浪费空间。所以有了懒汉式,使用就创建对象,平时不加载。
懒汉式式:
public class LazyMan {
// 构造方法私有 ---
private LazyMan() {
}
private static LazyMan LazyMan;
// 如果对象存在就加载,平常不加载
public static LazyMan getInstance(){
if (LazyMan==null){
LazyMan = new LazyMan();
}
return LazyMan;
}
}
但在多线程下是有问题的。需要进行双重检测锁模式,再加上volatile关键字,DCL懒汉式。
但是通过反射,还是能破坏程序,所以我们需要在构造器上锁,通过枚举进行判断,枚举不能被反射。