多线程/JUC (1
1. 线程有几个状态
有2种说法
第一种:
5个
- 新生
- 就绪
- 运行
- 阻塞
- 死亡
第二种:
看源码6个
- NEW 新生
- RUNNABLE 运行
- BLOCKED 阻塞
- WAITING 等待,死死的等
- TIMED_WAITING 超时等待
- TERMINATED 终止
2. wait和sleep区别
类不同
wait是Object类
sleep是Thread类
企业中一般用TimeUnit.xxx方法来休眠
释放锁
wait会释放锁
sleep不会释放锁,抱着锁睡觉了
使用范围不同
wait只能在同步代码块中用
sleep可以在任意地方睡
捕获异常
wait不需要捕获异常
sleep需要捕获异常
3. Lock锁(重点)
用synchronized和用lock锁的区别
synchronized | Lock |
---|---|
synchronized是内置java关键字 | lock是个java类 |
synchronized无法判断获取锁的状态 | lock可以判断是否获取了锁 |
synchronized会自动释放锁 | lock要手动释放锁,如果不释放锁,会出现死锁 |
synchronized 线程1(获得锁,阻塞),线程2(等待,傻傻的等) | Lock有方法tryLock()不一定会一直等待下去 |
synchronized 可重入锁,非公平,不可中断 | lock 可重入锁,非公平(可以自己设置),可以判断锁 |
synchronized适合少量的代码同步问题 | lock适合锁大量的同步代码 |
synchronized
锁的对象是方法的调用者
生产者消费者问题
//用lock锁写的
Lock lock = new ReentrantLock();
public void sale(){ //不用synchronized 用lock锁
lock.lock(); //加锁
try {
//核心业务代码 放try catch final中
if (number>0) {
number--;
System.out.println("线程:"+Thread.currentThread().getName()+"卖出第"+n++
+"张票,剩余票数:"+number);
}
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
//用synchronized写的
public synchronized void sale(){
if (number>0) {
number--;
System.out.println("线程:"+Thread.currentThread().getName()+"卖出第"+n++
+"张票,剩余票数:"+number);
}
}
用if循环带来的虚假唤醒问题
如果是if解除阻塞状态时,会继续执行接下来的代码。而使用while,在解除阻塞时,需要重新判断,直到满足条件才会执行接下来的代码
public synchronized void decrement() throws InterruptedException {
if (number == 0) { //!!!这里应该用while!!!
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
//唤醒其他人
this.notifyAll();
}
lock代替synchronize
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition.await(); //代替wait()
}
number--;
System.out.println(Thread.currentThread().getName() + " => " + number);
//唤醒其他人
condition.signalAll(); //代替notifyAll()
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Condition 可以精准通知哪一个进程唤醒
创建多个condition分别配给每一个方法
要求 A -> B -> C -> A
public void printA(){
lock.lock();
try {
while (num!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+":AAAA");
//唤醒指定人: B
num = 2;
condition2.signal();
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num!=2){
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+":BBBB");
num = 3;
condition3.signal();
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num!=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+":CCCC");
num = 1;
condition1.signal();
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
4. 8锁问题
如何判断锁的是谁? 永远地知道是什么锁,锁到底锁的是谁?
- synchronized锁的对象是方法的调用者
- Phone phone = new Phone3(); 两个方法都加了static
两个方法都加了static后,锁的是Class,这个Class全局唯一,所以并不是因为同一个对象调用,而是因为静态static是类对象,唯一, ==> 所以先发短信
- 上面的基础上,Phone phone = new Phone3() 变成了下面, 变成了两个对象调用
就算是两个对象分别调用,还是发短信先,因为静态,只有唯一一个Phone的Class对象,锁的是Class对象,两个对象只有一个Class类模板
- Phone phone = new Phone4() ,只有一个对象;但是打电话不再静态
虽然先静后普,先父后子
但是 发短信要等4秒,所以打电话先。他们一个锁的是类对象,一个锁的是调用者,不是同一个,所以同时执行
- 上面的基础上,Phone phone = new Phone4() 变成了下面, 变成了两个对象调用
结果:还是打电话
结论
同步静态方法的锁对象是Class,故不受创建对象的数量影响!!!
普通方法 不受锁的影响
new this 是具体的一个手机
static Class 唯一的一个模板
5. 不安全集合: CopyOnWriteArrayList
ArrayList 在并发下是不安全的
报 java.util.ConcurrentModificationException 并发修改异常!
public static void main(String[] args) {
List<String> list = new ArrayList<>(); //ArrayList不安全 改!
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
解决方案
- List list = new Vector<>(); 用Vector就不会报错了
- List list = Collections.synchronizedList(new ArrayList<>());
- List list = new CopyOnWriteArrayList<>(); 正确方案
CopyOnWriteArrayList比Vector好在哪?:
Vector用了synchronized锁;效率变慢,CopyOnWriteArrayList用的lock锁
6. 不安全集合: CopyOnWriteArraySet
基本同上
HashSet 在并发下也是不安全的
报 java.util.ConcurrentModificationException 并发修改异常!
解决方案
- Set set = Collections.synchronizedSet(new HashSet<>());
- CopyOnWriteArraySet set = new CopyOnWriteArraySet<>(); 正确方案
HashSet的底层是什么
HashMap
Map不安全
Map<String, String> map = new HashMap<>();
// 初始化容量 、 加载因子
map是这样用的吗? :不是,工作中不这样用
默认等价于什么? : Map<String, String> map = new HashMap<>(16 , 0.75);
解决方案
- 同上可以用Collections.synchronizedMap()…
- 同上用Map<String, String> map = new ConcurrentHashMap<>();
ps: 类似于CopyOnWriteArraySet
7. Callable(简单)
- 可以有返回值
- 可以抛出异常
为什么Thread.start() 可以启动Callable?
- Thread中只跟Runable扯上关系, 但是有一个类:FutureTask
- FutureTask 有一个带Callable类型参数的构造器,所以future task可以作为runnable接口的实现类传到thread的构造器中, 所以Callable --> FutureTask --> Runable --> Thread
- 适配类 FutureTask 即时Runable的实现类, 又有个构造器可以处理Callable
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask,"A").start();
Integer integer = futureTask.get(); //get可能会产生阻塞
//所以一般把他放在最后 或者 使用异步通信
System.out.println(integer);
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("调用了call()");
//耗时的操作 --> 导致阻塞
return 1024;
}
}
输出
有个问题: 果然启动两个线程,那么打印结果会输出两边吗?
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start();
不会 :( 因为缓存?)
网友解答:
这个并不是缓存,是由于JVM第二次再调用FutrueTask对象所持有的线程
此时FutrueTask的state此时已非NEW状态(各个状态,这边不做详细解释)
则此时会直接结束对应线程,就会导致任务也不执行
只是在第一次调用时返回结果保存了,可能这就是老师所说的缓存
8. 常用辅助类
8.1 CountDownLatch (减法 计数器)
总数是(6) 必须要执行任务的时候再使用
等待计数器归零,然后才向下执行
public static void main(String[] args) throws InterruptedException {
//总数是6 必须要执行任务的时候再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go Out!");
countDownLatch.countDown(); //数量-1
},String.valueOf(i+1)).start();
}
countDownLatch.await(); //等待计数器归零,然后才向下执行
System.out.println("走完啦 关门辣");
}
8.2 CyclicBarrier (加法 计数器)
等待 会直接计数 如果没有到达7 就会阻塞,
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙!");
});
for (int i = 0; i < 7; i++) {
final int temp = i+1;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"召唤龙珠"+temp);
try {
//等待 会直接计数 如果没有到达7 就会阻塞,
// 等到了7之后才会唤醒7个线程 所以召唤神龙后会打印7次"h "
cyclicBarrier.await();
System.out.print("h ");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
关于打印次数的问题: 两个都是只能开启多线程(8次),不能开启少(5次)
多了还是可以关门/召唤神龙,但是少了就不行,程序会停着未结束
8.3 Semaphore (信号量)
抢车位 限流 限制指定的线程数量
只有有车离开了,其他车就会赶紧进来
public static void main(String[] args) {
//抢车位 限流 限制指定的线程数量
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire(); //获取
System.out.println(Thread.currentThread().getName()+"获得了车位");
System.out.println("--------"+Thread.currentThread().getName()+"停了一会---------");
TimeUnit.SECONDS.sleep(4); //模拟停了一会
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"离开了车位");
semaphore.release(); //释放
}
},String.valueOf(i+1)).start();
}
}
semaphore.acquire(); //获取
如果已经满了,就会等待,等待被释放位置
semaphore.release(); //释放
会将当前的信号量释放 +1
作用: 多个共享资源互斥的使用,并发限流,控制最大的线程数!
9. 读写锁
ReentrantReadWriteLock()
一次只能被一个线程占有(写锁 / 独占锁)
多个线程可以同时占有(读锁 / 共享锁)
public class testRW {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 0; i < 5; i++) {
final int temp = i+1;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(temp)).start();
}
//读取
for (int i = 0; i < 5; i++) {
final int temp = i+1;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(temp)).start();
}
}
}
//模拟缓存
class MyCache{
private volatile Map<String , Object> map = new HashMap<>();
//读写锁 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存 写
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入了:"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"完成写入操作");
} catch (Exception exception) {
exception.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//放 读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取了:"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"完成读取操作");
} catch (Exception exception) {
exception.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
加读写锁前: 不符合预期 ! 1写入了之后,还没完成写入操作就被其他人插队写入。
加了读写锁后
10. 阻塞队列
什么情况下会使用阻塞? : 多线程并发处理、线程池
队列满的时候写
队列空的时候读
都会阻塞
学会使用队列:
添加、移除
ArrayBlockingQueue<>() 四组API
1、 抛出异常
2、 不会抛出异常
3、 阻塞 等待
4、 超时等待
方式 \ 结果 | 抛出异常 | 有返回值,不抛异常 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer( Obj , 秒数x , TimeUnit.Second ) |
移除 | remove | poll | take | poll( , ) |
多添加结果 | 抛异常 | 返false | 一直等待 | 等待 —> x秒后返false然后终止 |
多移除结果 | 抛异常 | 返null | 最后一个数不出现,一直等待 | 等待 —> x秒后返null然后终止 |
检测队首元素 | element | peek | - | - |
public static void test() throws InterruptedException {
//队列大小
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
//1 加:add
// System.out.println(queue.add("a"));
// System.out.println(queue.add("b"));
// System.out.println(queue.add("c"));
//2 加:offer()
// System.out.println(queue.offer("a"));
// System.out.println(queue.offer("b"));
// System.out.println(queue.offer("c"));
// System.out.println(queue.offer("d"));
//3 加:put 没有返回值
// queue.put("a");
// queue.put("b");
// queue.put("c");
// queue.put("d");
//4 加:offer()
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
// System.out.println(queue.offer("d",3,TimeUnit.SECONDS));
//取
for (int i = 0; i < 3; i++) {
//1 remove
// System.out.println(queue.remove());
//2 poll
// System.out.println(queue.poll());
//3 take
// System.out.println(queue.take());
//4 poll
System.out.println(queue.poll());
}
// System.out.println(queue.remove());
// System.out.println(queue.poll());
// System.out.println(queue.take());
System.out.println(queue.poll(3,TimeUnit.SECONDS));
}
同步队列 SynchronousQueue
BlockingQueue queue = new SynchronousQueue();
同步队列
和其他BlockingQueue不一样,SynchronousQueue不存储元素
put了一个元素后,不能再put进去,必须先take取出来