JUC多线程及高并发包 (尚硅谷互联网大厂高频重点面试题(第2季))
百度
尚硅谷-互联网大厂高频重点面试题 (第2季)JUC多线程及高并发
JUC多线程及高并发
1. 谈谈你对volatile的理解
1.1. volatile是什么
volatile实现原理
有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。
将当前处理器缓存行数据刷写到系统主内存。
这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。
这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里
1.2.JMM(java内存模型)
JMM的特性:
- 线程间可见性
- 原子性(包含的代码一个整体,不可分割(不可以只执行一半,被其他线程打断))
- 有序性 (禁止指令重排)
volatile基本满足JMM的三个特性,除了原子性之外
1.2.1 JMM(java内存模型)之可见性
JMM内存模型的可见性,只要有一个线程改变数据后要写回到主内存中,其它的线程马上就会知道主内存中的数据已经改变了。
及一个线程改变一个共享对象的内容,改变及时通知其他线程
JMM:java内存模型
各个线程对主内存中的数据进行改变,不是直接修改,而是会把age=25拷贝到自己的工作内存中再进行改变
t1改为37后要把新数据写回到主内存中,t2,t3不知道主内存中的值已经改变了
所以我们需要有一个机制:JMM内存模型的可见性,只要有一个线程改变数据后要写回到主内存中,其它的线程马上就会知道主内存中的数据已经改变了
2.2.1volatile可见性的代码验证说明
通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.
这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.
运行结果
3秒之后,a线程已经把number改了,但是main线程不知道,对main线程不可见,还在傻傻的等着,没有人通知我
现在修改程序,加了volatile
运行结果
AAA线程 修改number后main线程立马知道
2.2.2. volatile不保证原子性
原子性:不可分割、完整性,也就是某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。
原子性对于线程来讲就是,A线程执行期间不能被其他线程打断,及A线程操作修改的的数据不能被B覆盖
20个线程去调用addplus方法,每个线程调用1000次,则结果应该是2000。
加了synchronuzed可以解决volatile不能保证原子性的问题,但针对number++这一简单操作不建议使用synchronized。
可以把volatile看成低配版的synchronized
volatile不保证原子性理论解释
三个线程都拿到共享数据(number),都在各自的工作内存中加1,写回到的时候,没有拿到最新的值就又写了,写覆盖
拷贝回自己的内存空间,每个人都拿到0,写回到主内存时,线程1写回到的时候被挂起了,线程2歘的写回了。然后线程1恢复后又写回了一遍,把原来的1给覆盖了。
n++在底层汇编是三条语句
volatile不保证原子性问题解决:atomic
解决++ – 运算符使用volatile不保证原子性:使用atomic类
2.2.3.volatile指令重排
- 指令重排案例1
public void mySort(){
int x=11;//语句1
int y=12;//语句2
x=x+5;//语句3
y=x*x;//语句4
}
重排的可能:
1234
2134
1324
问题:
请问语句4 可以重排后变成第一条码?
存在数据的依赖性 没办法排到第一个
- 案例2
- 案例3
答案是6
但是指令重排后,可能先执行flag=true 此时其他线程执行,if(flag)就满足条件,答案可能是5
所以在这些变量前面加上volitaile就可以禁止指令重排
- 指令重排小结
1.3 哪些地方用到过volatile
单例模式在多线程环境下可能存在安全问题
单例模式有六种
掌握一种即可
第一步 定义私有的实例变量
第二步 构造方法
第三步 新建,返回同一个变量
多线程就发生了变化
有可能每个线程都创建一个实例
- 双端检索机制
双端检索机制有可能应为指令重排,存在安全隐患不一定线程安全
多线程的下的单例模式写法
2.CAS
++ --操作的时候为为什么使用AtomicInteger, 不适应synchronized
AtomicInteger为什么使用CAS不使用synchronized
synchronized保证原子一致性,降低了并发性
AtomicInteger(CAS思想)保证原子一致性,没有降低并发性
CAS compare and set 比较并交换
多个线程去操作主内存中的数据。
一个叫做期望值、一个叫做更新值
期望值与真实的值,则跟新新值否则不跟新
2.2CAS的底层原理
期望值满足
运行结果
主内存中数据是5
一个线程拷贝回去自己的工作内存,对它进行修改,然后写回到主内存的时候,会进行比较和交换,如果主内存中的值和期望值(拷贝的数据)一样的话,就将改变后的数据写回去;否则的话,就不进行写回。重新读取主物理内存中的再进行修改。
CAS底层原理上
- atomicInteger.getAndIncrement()
-
Unsafe类
-
unsafe.getAndIncrement的主要执行步骤如下:
底层反汇编
CAS简单总结
2.3CAS的确点
- 如果do while的情款,cpu开销大
- 只能保证一个共享变量的原子性
- CAS引出的ABA的问题(CAS的主要缺点)
t2号线程比t1号线程运行时间短,在他运行期间把原来的A改为B写会主内存,然后再从主内存取回B又改为A后又写回主内存。
t1号线程回来后,期望的和原来的一样,以为没有改变过,于是写回主内存。
但是中间有猫腻,2号线程已经把它改过了又改回去了。
解决CAS带来的ABA问题
原子引用
通过时间戳的原子引用解决CAS导致的ABA问题
/**
* Description: ABA问题的解决
*
* @author veliger@163.com
* @date 2019-04-12 21:30
**/
public class ABADemo {
private static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("===以下是ABA问题的产生===");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
//先暂停1秒 保证完成ABA
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
},"t2").start();
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("===以下是ABA问题的解决===");
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
//暂停1秒钟t3线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
//保证线程3完成1次ABA
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp());
System.out.println("最新的值\t"+stampedReference.getReference());
},"t4").start();
}
4.集合类(List/Set/Map)不安全之并发修改异常
set、map和list在对多线程下的问题一样,以list为例讲解
我们知道ArrayList是线程不安全的,请编码写一个不安全案例:
导致原因: 并发争抢修改导致
解决方案1:
使用vector 不建议
解决方法2:
Vector类可以解决这个问题,加锁一致性可以保证,但是并发性急剧下降。
不许用Vector
解决方法3:
写时复制
list:
HashSet:
HashMap: 使用ConcurrentHashMap解决多线程问题
CopyOnWriteArrayList<>() 添加list内容
**
* Description: 集合类不安全的问题
*
* @author veliger@163.com
* @date 2019-04-12 22:15
**/
public class ContainerNotSafeDemo {
/**
* 笔记
* 写时复制 copyOnWrite 容器即写时复制的容器 往容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行
* copy 复制出一个新的object[] newElements 然后向新容器object[] newElements 里面添加元素 添加元素后,
* 再将原容器的引用指向新的容器 setArray(newElements);
* 这样的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁 因为当前容器不会添加任何容器.所以copyOnwrite容器也是一种
* 读写分离的思想,读和写不同的容器.
* public boolean add(E e) {
* final ReentrantLock lock = this.lock;
* lock.lock();
* try {
* Object[] elements = getArray();
* int len = elements.length;
* Object[] newElements = Arrays.copyOf(elements, len + 1);
* newElements[len] = e;
* setArray(newElements);
* return true;
* } finally {
* lock.unlock();
* }
* }
* @param args
*/
public static void main(String[] args) {
List<String> list= new CopyOnWriteArrayList<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(1,8));
System.out.println(list);
},String.valueOf(i)).start();
}
/**
* 1.故障现象
* java.util.ConcurrentModificationException
* 2.导致原因
* 并发争抢修改导致
* 3.解决方案
* 3.1 new Vector<>()
* 3.2 Collections.synchronizedList(new ArrayList<>());
* 3.3 new CopyOnWriteArrayList<>();
*
*
* 4.优化建议
*/
}
5.公平锁/非公平锁/可重入锁/递归锁/自旋锁
synchronzized和ReenTrantLock的区别
锁绑定多个条件Condition
公平锁/非公平锁/可重入锁/递归锁/自旋锁 谈谈你的理解?请手写一个自旋锁。
5.1公平锁/非公平锁:
5.2可重入锁(递归锁)
可重入锁(递归锁)概念
Synchronized是非公平可重入锁
package cn.atguigu.interview.study.thread;
class Phone{
public synchronized void sendSms() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendSms");
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendEmail");
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
*
* 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
*
* @author veliger@163.com
* @date 2019-04-12 23:36
**/
public class ReenterLockDemo {
/**
* t1 sendSms
* t1 sendEmail
* t2 sendSms
* t2 sendEmail
* @param args
*/
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
}
}
ReentrantLock是非公平可重入锁
package cn.atguigu.interview.study.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Phone implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tget");
set();
} finally {
lock.unlock();
}
}
private void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tset");
} finally {
lock.unlock();
}
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一先生外层函数获得锁后,内层敌对函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* <p>
* 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
*
* @author veliger@163.com
* @date 2019-04-12 23:36
**/
public class ReenterLockDemo {
/**
* Thread-0 get
* Thread-0 set
* Thread-1 get
* Thread-1 set
*
* @param args
*/
public static void main(String[] args) {
Phone phone = new Phone();
Thread t3 = new Thread(phone);
Thread t4 = new Thread(phone);
t3.start();
t4.start();
}
}
5.3自旋锁
手写自旋锁
A线程进来,发现期望的是空的,那么while的条件就是false,于是不进入循环,直接拿到了锁。
B线程进来,发现期望的值不是空,那么while的条件就是true,于是它进入锁中,一直会循环的判断,直到期望的值是空了,才能推出循环,获得锁。
BB等5秒钟,等A解锁了,B才能解锁
5.4读写锁
以前使用ReentrantLock和synchronized读和写通通不能并发执行,数据一致量可以保证,但并发性急剧下降。
不加读写锁
加读写解锁
ReentrantLock可以避免写被打断,但同时读时也会独占,及ReentrantLock不能进行细粒度的划分,只能把全部进行封杀
使用ReentrantReadWritelock
/**
* 资源类
*/
class MyCaChe {
/**
* 保证可见性
*/
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
/**
* 写
*
* @param key
* @param value
*/
public void put(String key, Object value) {
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
//模拟网络延时
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t正在完成");
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
/**
* 读
*
* @param key
*/
public void get(String key) {
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取");
//模拟网络延时
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t正在完成" + result);
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
public void clearCaChe() {
map.clear();
}
}
/**
* Description:
* 多个线程同时操作 一个资源类没有任何问题 所以为了满足并发量
* 读取共享资源应该可以同时进行
* 但是
* 如果有一个线程想去写共享资源来 就不应该有其他线程可以对资源进行读或写
* <p>
* 小总结:
* 读 读能共存
* 读 写不能共存
* 写 写不能共存
* 写操作 原子+独占 整个过程必须是一个完成的统一整体 中间不允许被分割 被打断
*
* @author veliger@163.com
* @date 2019-04-13 0:45
**/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCaChe myCaChe = new MyCaChe();
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCaChe.put(temp + "", temp);
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
myCaChe.get(finalI + "");
}, String.valueOf(i)).start();
}
}
}
6.JUC类:CountDownLatch/CyclicBarrier/Semaphore使用过吗?
6.1 CountDownLatch
运行完子线程的倒计数
关门案例:
所有同学都走了班长才能关门
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception {
closeDoor();
}
/**
* 关门案例
* @throws InterruptedException
*/
private static void closeDoor() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "上完自习");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t班长锁门离开教室");
}
}
枚举
/**
* Description
* 枚举的使用
*
* @author veliger@163.com
* @version 1.0
* @date 2019-04-13 10:14
**/
public enum CountryEnum {
/**
*
*/
ONE(1, "齐"),
/**
*
*/
TWO(2, "楚"),
/**
*
*/
THREE(3, "燕"),
/**
*
*/
FOUR(4, "赵"),
/**
*
*/
FIVE(5, "魏"),
/**
*
*/
SIX(6, "韩");
CountryEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
@Getter
private Integer code;
@Getter
private String name;
public static CountryEnum forEach(int index) {
CountryEnum[] countryEnums = CountryEnum.values();
for (CountryEnum countryEnum : countryEnums) {
if (index == countryEnum.getCode()) {
return countryEnum;
}
}
return null;
}
}
秦灭六国案例:
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception {
sixCountry();
}
/**
* 秦灭六国 一统华夏
* @throws InterruptedException
*/
private static void sixCountry() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "国,灭亡");
countDownLatch.countDown();
}, CountryEnum.forEach(i).getName()).start();
}
countDownLatch.await();
System.out.println("秦统一");
}
}
6.2 CyclicBarrier
CyclicBarrier 与CountDownLatch相反,CountDownLatch是减法,CyclicBarrier是加法
循环屏障
人都到齐了才能开会
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 1; i <=7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 收集到第"+ temp +"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
6.3 Semaphore
Semaphore 是有加也有减
占用共享资源减一,释放共享资源加一
多对对多,多个线程抢多个共享资源
6辆汽车抢3个车位
/**
* Description
*
* @author veliger@163.com
* @version 1.0
* @date 2019-04-13 11:08
**/
public class SemaphoreDemo {
public static void main(String[] args) {
//模拟3个停车位
Semaphore semaphore = new Semaphore(3);
//模拟6部汽车
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//抢到资源
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "\t抢到车位");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t 停3秒离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放资源
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
7 阻塞队列
- 阻塞队列使用的地方:
阻塞队列的优点
BlockingQueue
BlockingQueue常用api
- 抛异常
- 特殊值
-
阻塞
阻塞的意思是:我现在满了,就等着,直到有元素出去。因为我不能丢消息呀,就等着
取不出来就堵着
-
超时
过时不候
只阻塞2秒钟。2秒钟之后就打印出false
阻塞队列之同步SynchronousQueue队列
生产一个消费一个,不消费不生产,0库存
你不消费,你想到里面插第三个你插不进去
/**
* Description
* 阻塞队列SynchronousQueue演示
*
* @author veliger@163.com
* @version 1.0
* @date 2019-04-13 13:49
**/
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + "\t put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + "\t put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AAA").start();
new Thread(() -> {
try {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BBB").start();
}
}
阻塞队列版:线程通信之生产者消费者
class MyResource {
/**
* 默认开启 进行生产消费的交互
*/
private volatile boolean flag = true;
/**
* 默认值是0
*/
private AtomicInteger atomicInteger = new AtomicInteger();
private BlockingQueue<String> blockingQueue = null;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd() throws Exception {
String data = null;
boolean returnValue;
while (flag) {
data = atomicInteger.incrementAndGet() + "";
returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (returnValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "\t 停止 表示 flag" + flag);
}
public void myConsumer() throws Exception {
String result = null;
while (flag) {
result = blockingQueue.poll(2L, TimeUnit.SECONDS);
if(null==result||"".equalsIgnoreCase(result)){
flag=false;
System.out.println(Thread.currentThread().getName()+"\t"+"超过2m没有取到 消费退出");
System.out.println();
System.out.println();
return;
}
System.out.println(Thread.currentThread().getName() + "消费队列" + result + "成功");
}
}
public void stop() throws Exception{
flag=false;
}
}
/**
* Description
* volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
*
* @author veliger@163.com
* @version 1.0
* @date 2019-04-13 14:02
**/
public class ProdConsumerBlockQueueDemo {
public static void main(String[] args) throws Exception {
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
try {
myResource.myProd();
} catch (Exception e) {
e.printStackTrace();
}
},"Prod").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
try {
myResource.myConsumer();
} catch (Exception e) {
e.printStackTrace();
}
},"consumer").start();
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println();
System.out.println();
System.out.println();
System.out.println("时间到,停止活动");
myResource.stop();
}
}
8.线程池
Callable接口实现线程
因为我们不会等 Callable接口实现线程,给它充足的时间去计算
如果把get放到前面,mian线程就被堵住了
两个线程都开始做同一个任务,只会执行一次!即复用
多个Thread公用一个futureTask只会执行一次run
线程池的优势
实现线程池方法
一池固定线程
newFixedThreadPool
一池一线程
newSingleFixedThreadPool
一池多线程
newCachedThreadPool
线程池的底层
线程池的七个参数
int corePoolSize
正在工作的占用的线程数
int maximumPoolSize
把线程池比作一个银行网点
线程中能容下的最大线程数
long keepAliveTime,
TimeUnit unit,
默认情况下:
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze,
BlockingQueue workQueue,
阻塞队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler
线程池的底层原理
9.线程池参数合理设置
线程池的拒绝策略
手写线程池+拒绝策略
- AbortPolicy策略
- CallerRunsPolicy
-
DiscardOldestPolicy
-
DiscardPolicy
自定义线程池
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
//默认抛出异常
//new ThreadPoolExecutor.AbortPolicy()
//回退调用者
//new ThreadPoolExecutor.CallerRunsPolicy()
//处理不来的不处理
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardPolicy()
);
//模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
//threadPoolInit();
}
private static void threadPoolInit() {
/**
* 一池5个处理线程
*/
//ExecutorService threadPool= Executors.newFixedThreadPool(5);
/**
* 一池一线程
*/
//ExecutorService threadPool= Executors.newSingleThreadExecutor();
/**
* 一池N线程
*/
ExecutorService threadPool = Executors.newCachedThreadPool();
//模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
合理配置线程池参数
cpu密集型
- 获取cpu的核数
System.out.println(Runtime.getRuntime().availableProcessors());查看CPU核数
IO密集型
情况一
情况二
对于一台服务器而言,程序的线程数并不是越大越好。线程数过大(上千上万)显然是不合适的,创建线程会消耗很多的系统资源,CPU切换上下文所耗费的成本也会更多,大量的线程也会长时间处于等待状态而无法及时处理。合适的数量大致如下:
线程数 = CPU核心数 / (1 - 阻塞系数)
阻塞系数 = 阻塞时间 / (阻塞时间 + 计算时间)
假设每个线程其阻塞时间为8个时间单位,计算时间为2个时间单位(对于大多数I/O系统而言,其阻塞时间通常大于计算时间,也就是说线程访问I/O多数时间处于等待状态),那么其阻塞系数就为0.8;那对于一个双核处理器而言,最好为其开启十个线程,才能不浪费系统资源,同时也能够兼顾到所有的线程。(由于不了解操作系统,我也一直在思考阻塞时难道文件系统不使用CPU吗?暂时先这么理解吧
扩展:协程(Coroutine)又名纤程,可以理解为线程中的线程。一个进程可有多个线程,同时一个线程也可拥有多个协程。在一些高并发的原生语言中(比如golang)自然支持,同时Java中也有第三方的库(quasar)支持。线程的切换需要依靠CPU执行上下文切换会产生开销,且支持的最大线程数有限,但协程是在程序内部实现的切换,不存在切换开销,而且协程存在于一个线程当中,不需要锁,无需担心写变量冲突的问题,所以其执行效率远高于线程。单台服务器可能最多支持几百个线程,但支持上百万的协程,如此可以极大地提升系统地并发能力。
10.死锁
- 什么是死锁
持有自己的锁还想得到别人的锁
线程 1 持有了 lockA,现在尝试获取 lockB,而线程 2 持有了 lockB,尝试获取 lockA。
关于避免死锁,我在这里给你几点建议:
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测 - 再次强调一下,避免滥用锁,程序里用的锁少,写出死锁 Bug的几率自然就低。
- 对于同一把锁,加锁和解锁必须要放在同一个方法中,这样一次加锁对应一次解锁,代码清晰简单,便于分析问题。
- 尽量避免在持有一把锁的情况下,去获取另外一把锁,就是要尽量避免同时持有多把锁。
- 如果需要持有多把锁,一定要注意加解锁的顺序,解锁的顺序要和加锁顺序相反。比如,获取三把锁的顺序是 A、B、C,释放锁的顺序必须是 C、B、A。
- 给你程序中所有的锁排一个顺序,在所有需要加锁的地方,按照同样的顺序加解锁。比如我刚刚举的那个例子,如果两个线程都按照先获取lockA 再获取 lockB 的顺序加锁,就不会产生死锁。
产生死锁的代码
class HoldThread implements Runnable {
private String lockA;
private String lockB;
public HoldThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockA + "尝试获得" + lockB);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockB + "尝试获得" + lockA);
}
}
}
}
/**
* Description:
* 死锁是指两个或者以上的进程在执行过程中,
* 因争夺资源而造成的一种相互等待的现象,
* 若无外力干涉那他们都将无法推进下去
*
* @author veliger@163.com
* @date 2019-04-14 0:05
**/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldThread(lockA, lockB), "threadAAA").start();
new Thread(new HoldThread(lockB, lockA), "threadBBB").start();
}
}
- 排查死锁