只是个大概学习框架,都需要具体学的…
这个来自狂神的JUC视频
视频资料pdf链接:
链接:https://pan.baidu.com/s/1szA37eDsReakkIJKpfQ8Mg
提取码:vytr
文章目录
MyProblemList
- 为什么一个类一定要有构造方法
- CopyOnWriteArrayList是不是只是把Vector中的synchronized锁换成了Lock而已
1.什么是JUC
就是java.util.concurrent在并发编程中使用的工具类
2.线程和进程
进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程? 2 个 mian、GC线程
开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言,线程的实现主要通过三种方式:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
获得多线程的方法有几种?
传统的是继承thread类和实现runnable接口,
java5以后又有实现callable接口 和java的线程池 获得
并发、并行
并发编程:并发、并行
并发(多线程操作同一个资源)
- CPU 一核,模拟出来多条线程,天下武功,唯快不破,快速交替
并行(多个人一起行走)
- CPU 多核,多个线程可以同时执行;线程池
public class Test1 {
public static void main(String[] args) {
// 获取cpu的核数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:充分利用CPU资源
线程有几个状态
- 6个状态, 可以通过Thread.State点进去查看
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待,死死地等
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
wait和sleep的区别
-
来自不同的类
wait=>object
sleep=>Thread为什么wait是Object的?
因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object -
关于锁的释放
wait会释放锁,sleep抱着锁睡觉,不会释放锁 -
使用的范围是不同的
wait必须在同步代码块中
sleep可以在任何地方睡觉 -
是否需要捕获异常(现在wait和sleep都要捕获异常InterruptedException)
3. Lock锁
- Lock是个接口,有三个实现类
- RetreenLock默认非公平的
Synchronized 和 Lock 区别
- Synchronized 是一个内置的Java关键字,Lock是一个接口
- Synchronized 无法判断锁的状态,Lock可以判断是否获取到了锁
- Synchronized 可以自动释放锁,lock必须手动释放锁,如果不释放,会死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;(tryLock()尝试获得锁)
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,非公平(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
锁的是谁,如何判断锁对象
生产者消费者和虚假唤醒问题
虚假唤醒
什么是虚假唤醒? 当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
- 比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁
生产者消费者Lock写法
public class Goods {
private int goods = 5;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void producer() {
lock.lock();
try {
while (goods > 0) {
condition.await();
}
++goods;
System.out.println("生产者,还剩下"+goods+"个");
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumer() {
lock.lock();
try {
while (goods <= 0) {
condition.await();
}
--goods;
System.out.println("消费者,还剩下"+goods+"个");
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Goods goods = new Goods();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
goods.consumer();
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
goods.producer();
}
},"B").start();
}
}
syschronized原理
TimeUnit
8锁现象
一个类中存在一个synchronized修饰的方法和一个普通的方法,不同线程同时访问这两个方法,会出现什么情况?如果这两个方法都是同步方法又会出现什么现象?
总结:一个线程持有对象锁,另一个线程可以以异步的方式调用对象里面的非synchronized方法,输出结果是不按照顺序的
一个线程持有对象锁,另一个线程可以以同步的方式调用对象里面的synchronized方法,需要等待上一个线程释放资源,也就是同步。
一个类中存在一个synchronized修饰的静态方法和一个synchronized修饰的普通方法,不同线程同时访问这两个方法,会出现什么情况?
总结:修饰static的那个锁,锁住的是一个类模板,没有static的那个锁,锁住的是调用这个方法的那个对象。这两个不是同一把锁。所以互不干扰,没有static的那个方法不需要等有static的那个方法执行完,就可以执行
集合类不安全
ArrayList:
- ArrayList底层一般可以说是初始容量为10的Object数组
- ArrayList扩容,一般扩容到原来的1.5倍。然后采用Arrays.copyOf()把旧的数组搬到新的里面去
- ArrayLIst线程不安全
List不安全:
- 故障现象:
- java.util.ConcurrentModificationException 并发修改异常!(只要是并发,经常遇到这个错误)
- 导致原因
- 解决方法
3.1 使用Vector,他的基本上方法都加了Synchronized
3.2 使用工具类,Collections.synchronizedList(new ArrayList<>());
3.3 使用CopyOnWriteArrayList()
CopyOnWrite
写的蛮好的链接:https://www.jianshu.com/p/4f594a84f2dd
- 顾名思义就是写时复制,JDK中一共有两个类CopyOnWriteArrayList和CopyOnWriteArraySet。
- 下面是CopyOnWriteArrayList的add方法的源码,可以看到add方法里面使用的是RetreenLock锁。
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();
}
}
适用场景:
copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作。
HashSet的底层是什么
- 说白了,HashSet就是限制了功能的HashMap,所以了解HashMap的实现原理,这个HashSet自然就通
- 对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性
- 虽说是Set是对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了(这个Set的put方法的返回值真有意思)
- HashSet没有提供get()方法,愿意是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得
- HashMap的put方法返回值问题:调用put方法时,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value;如果是新的一个key,则返回的是null;
ConcurrentHashMap & HashTable详解
原博客链接:https://blog.youkuaiyun.com/qq_35190492/article/details/103589011
Callable
Callable和Runnable对比
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable没有返回值,Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call方法允许抛出异常,Runnable的run()方法异常只能在内部消化,不能继续往上抛
注意:
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续向下执行,如果不调用不会阻塞
Thread的构造方法如下图:
- 由Thread的构造方法可知,Callable不能直接调用Thread的start方法。But,Callable就是通过Thread().start启动线程的。那说明Callable是通过Runnable的子类实现调用的。Runnable拥有的实现类如下
- 顺着实现类往下找,点开FutureTask,你会发现他的构造方法正是我们要找的Callable。这样我们就可以把Callable对象放到Future里面,然后把Future放到Thread里面实现Thread.start()方法了
代码示例如下:
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer>futureTask=new FutureTask<Integer>(new MyThread());
new Thread(futureTask,"A").start();
System.out.println(futureTask.get()); //用get获取call()的返回值
}
}
class MyThread implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("HelloHello");
return 1024;
}
}
细节: futureTask.get()尽可能的往后放,因为如果futureTask没有计算完,但是你又要获得值,就会一直阻塞到futureTask计算完了,你能get到为止
关于FutureTask详解
相关类的关系:
Future只是一个接口,FutureTask是实现了RunnableFuture。RunnableFuture实现了Future和Runnable接口。
认识Runnable、Future、FutureTask
Runnable:Runnable仅仅表示这个对象是可执行的。是一个函数式接口,仅有一个Run方法
ExecutorService
常用的辅助类
1. CountDownLatch
参考链接:https://www.cnblogs.com/Lee_xy_z/p/10470181.html
原理:
CountDownLatch主要有两个方法await和countDown,当一个或多个线程调用await 方法时,这些线程会阻塞。
其他线程调用countDown 方法会将计数器减1(调用countDown方法的线程不会阻塞)
当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
CountDownLatch(倒计时计算器)使用说明:
1. public void countDown()
递减锁存器的计数,如果计数到达0,则释放所有因await等待的线程。如果当前计数大于0,则将计数减少。
2. public boolean await(long timeout,TimeUnit unit) throws InterruptedException
使当前线程在锁存器倒计数为0之前一直等待,除非线程被中断或者超出了指定的等待时间。如果当前计数为0,则此方法立刻返回true值
如果当前计数大于0,则处于线程调度的目的,禁用当前线程,且在发生以下三种情况之一前,该线程将一直处于休眠状态:
- 计数器到达0,则该方法返回true值
- 如果当前线程,在进入此方法时已经设置了该线程的中断状态;或者在等待时被中断,则抛出InterruptedException,并且清除当前线程的已中断状态。
- 如果超出了指定的等待时间,则返回值为false。如果该时间小于等于零,则该方法根本不会等待。
CountDownLatch的两个典例:
- 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
- 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。
代码示例1:
public class Test01 {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch=new CountDownLatch(6);
for(int i=1;i<=6;i++)
{
new Thread(()->{
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"离开了");
},String.valueOf(i)).start();
}
countDownLatch.await(); //countDown计数器没有减到0,main线程会一直阻塞
System.out.println("main"+"离开了");
}
}
运行结果:
代码示例2:
public class Test01 {
public static void main(String[] args) throws Exception {
CountDownLatch cdl1=new CountDownLatch(4); //四个运动员
CountDownLatch cdl2=new CountDownLatch(1); //相当于那把发令枪
for(int i=1;i<=4;i++)
{
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"准备就绪");
try {
cdl2.await();
System.out.println(Thread.currentThread().getName()+"听到指令");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"到达终点");
cdl1.countDown();
},String.valueOf(i)).start();
}
System.out.println("各运动员准备,裁判即将发布口令");
cdl2.countDown(); //相当于发令枪发出命令
cdl1.await();
System.out.println("运动员全部到达终点,结束");
}
}
运行结果
2. CyclicBarrier
参考链接:https://www.cnblogs.com/williamjie/p/9456746.html
原理:
CyclicBarrier大致是可循环利用的的屏障。顾名思义,这个名字也将这个类的特点给明确的表示出来了。首先,便是可重复利用,说明该类创建的对象可以复用。其次,屏障则体现了该类的原理:每个线程执行时都会遇到一个屏障,直到所有线程执行结束,然后屏障会打开,使所有线程往下执行.
这里介绍CyclicBarrier的两个构造函数: CyclicBarrier(int parties)和CyclicBarrier(int parties,Runnable barrierAction):前者只需要申明需要拦截的线程数即可,后者还需要定义一个等待所有线程到达屏障优先执行的Runnable对象
实现原理: 在CyclicBarrier 的内部定义了一个Lock对象,每当一个线程调用await方法时,将拦截的线程数减1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中所有线程放入锁等待队列中,这些线程会一次的获取锁、释放锁
代码示例:
public class Test01 {
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("集齐七颗龙珠,召唤神龙");
});
for(int i=1;i<=7;i++)
{
final int temp=i;
new Thread(()-> {
try {
System.out.println("收集到第"+temp+"颗龙珠");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
3. Semaphore
原理:
信号灯,类似于抢车位。多个线程争夺多个资源
信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
常用方法说明:
acquire()
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。(将信号量减1)
acquire(int permits)
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
acquireUninterruptibly()
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。(将信号量加1)
hasQueuedThreads()
等待队列里是否还存在等待线程。
getQueueLength()
获取等待队列里阻塞的线程数。
drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。
availablePermits()
返回可用的令牌数量。
代码示例:
public class Test01 {
public static void main(String[] args) throws Exception {
/*
1. 两个目的之一的并发线程数量的控制,通过这句话实现
2. 在这里模拟三个停车位
*/
Semaphore semaphore=new Semaphore(3);
for(int i=1;i<=6;i++) //模拟6辆汽车
{
final int temp=i;
new Thread(()->{
try {
semaphore.acquire();
System.out.println("第"+temp+"抢到了停车位");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("第"+temp+"已离开");
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
运行结果:
读写锁
- 读写互斥,写的时候是不能读的。(所以读也得加锁,为了读和写互斥)
- 写写互斥
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
//暂停一会儿线程
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
System.out.println();
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public Object get(String key) {
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.put(num + "", num + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num + "");
}, String.valueOf(i)).start();
}
}
}
运行结果:
阻塞队列
阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列,在数据结构中起的作用如下图:
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
阻塞队列的用处:
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
种类分析:
BlockingQueue的核心方法:
代码示例:
/**
* 阻塞队列
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// List list = new ArrayList();
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//第一组
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.element());
//System.out.println(blockingQueue.add("x"));
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// 第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("x"));
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 第三组
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// //blockingQueue.put("x");
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// 第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
}
}
线程池
讲的不错的链接:https://www.jianshu.com/p/210eab345423
三大方法,七大参数,四种拒绝策略
Executor相当于Collection
Executor相当于Collections,是线程池的工具类
ExecutorService相当于List,是一个接口,所以不让new、在线程池里面一般使用工具类Collections来用它
线程池主要使用的实现类是ThreadPoolExecutor
代码示例如下:
四大函数式接口
- lambda表达式就是为了解决内部类冗余的问题,能使用lambda的前提是这是一个函数式接口
- lambda表达式就是为了简化编程模型的
- 函数式编程,因为一个函数里面只有一个抽象方法,所以甚至可以省略方法名,直接()->{},就知道实现的是那个唯一的方法;
注意: Java7之前要求接口里面只能有抽象方法,Java8及之后允许在接口中实现部分static和default方法。需要注意的是,此处的静态方法只能被public修饰(或者省略不写),不能是private或者protected。
关键字: default 在接口中方法前面加上修饰符default 编译器就会认为该方法
并非抽象方法,可以在接口中写实现。
补充:java泛型中T、E、K、V、?等含义
E -Element 在集合中使用,因为集合中存放的是元素。E是对各方法中的泛型类型进行限制,以保证同一个对象调用不同的方法时,操作的类型必定是相同的。E可以用其它任意字母代替
T - Type(Java类 ),T代表在调用指定类型时,会进行类型推断
K,V 键值对中的key,value
N - Number 数值类型
? - 表示不确定的java类型,是类型通配符,代表所有类型。?不会进行类型推断
S、U、V - 2nd、3rd、4th types
这几个本质上没有什么区别,只是一种约定。随便用哪个字符都是可以的
实现接口,方法名冲突问题
我们知道Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法(default)在Java8中的引入,有可能出现一个类继承了多个签名一样的方法。这种情况下,类会选择使用哪一个函数呢?
为解决这种多继承关系,Java8提供了下面三条规则:
-
类中的方法优先级最高,类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
-
如果第一条无法判断,那么子接口的优先级更高:方法签名相同时,优先选择拥有最具体实现的默认方法的接口, 即如果B继承了A,那么B就比A更加具体。
-
最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法, 显式地选择使用哪一个默认方法的实现。
Stream流式计算
- Java8之后,list和Stream可以互换,list.Stream
ForkJoin
异步回调
JMM
Volatile
内存屏障避免指令重排
- 内存屏障在单例模式使用的最多
单例模式
- 利用反射可以破坏单例
- 反射不能破坏枚举的单例模式