JUC
1、JUC(java.util.concurrent)
1.1 进程/线程
进程:进程叫做cpu分配的基本单位(进程就是多个线程组成的一个应用)
线程:线程是cpu执行的基本单位
1.2 线程的五种状态
package:Thread(Thread.State(枚举类))
NEW,(新建)
RUNNABLE,(准备就绪)
BLOCKED,(阻塞)
WAITING,(不见不散)
TIMED_WAITING,(过时不候)
TERMINATED;(终结)
1.3 并发/并行
并发:举个栗子:秒杀和春节抢票,多个服务去抢同一个资源
并行:多项工作一起执行,之后汇总,举个栗子:泡面,烧水,撕榨菜
2、三个page(java.util.concurrent在并发编程中使用的工具类)
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
2.1 java8函数式编程
概念:Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更
灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
栗子:lambda表达式,如果一个接口只有一个方法,我可以把方法名省略
Foo foo = () -> {System.out.println("****hello lambda");};
给接口加个注解(标记为函数式接口):@FunctionalInterface如有两个方法,立刻报错,如果方法是被修饰了static和default则不会
// 函数式编程,lambda表达式
// 口诀:拷贝小括号,写死右箭头,落地大括号
// 函数式编程只能有一个未被static\default修饰的方法,否则会报错
new Thread(() -> {
for(int i=0;i<= 40;i++)
ticket.sale();
},"D").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i <= 40; i++){ ticket.sale();
}
}},"A").start();
2.2 Collections.synchronizedList & VCopyOnWriteArrayList & Vector
基础理解:List的底层是数组Array,默认长度位10(hashMap为16),list第一次扩容的长度为5,第二次为22(扩容机制为 当前长度/2+当前长度(10/2+10))
2.2.1 ArrayList
ArrayList是**非线性安全**,即在一方在便利列表,而另一方在修改列表时,会报ConcurrentModificationException错误。而这不是唯一的并发时容易发生的错误,在多线程进行插入操作时,由于没有进行同步操作,容易丢失数据。
2.2.2 Vector
Vector是一个线程安全的列表,采用数组实现。其线程安全的实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了。(读写的时候都是加锁的)
2.2.3 CopyOnWriteArrayList
CopyOnWriteArrayList和Collections.synchronizedList(Collections是集合类Collection是接口)是实现线程安全的列表的两种方式。
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
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();
}
}
两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的***写操作性能较差***)(底层是数组,数组的数据读取快),而多线程的***读操作性能较好***。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。
2.3 HashSet & CopyOnWriteArraySet
2.3.1 HashSet
HashSet 的方法是不安全的,hashSet的底层是hashMap,第一个参数也就是泛型类型的,也就是传过来的val, 第二个参数是hashSet中自定义的常量map.put(e, PRESENT)
2.3.2 CopyOnWriteArraySet
CopyOnWriteArraySet的add方法调用的是CopyOnWriteArrayList的add,原理什么也就一致了
2.4 HashMap & HashTable & ConcurrentHashMap
2.4.1 HashMap
因为多线程环境下,使用Hashmap进行put操作可能会引起死循环,导致CPU利用率接近100%
2.4.2 Hashtable
线程安全但效率低下
2.4.3 ConcurrentHashMap
ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
应该根据具体的应用场景选择合适的HashMap。
3、lock(显示锁)
import java.util.concurrent.TimeUnit;
// (lock锁的八个问题)
class Phone
{
public synchronized void sendSMS() throws Exception
{
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception
{
System.out.println("------sendEmail");
}
public void getHello()
{
System.out.println("------getHello");
}
}
/**
*
* @Description: 8锁
*
1 标准访问,先打印短信还是邮件
2 停4秒在短信方法内,先打印短信还是邮件
3 新增普通的hello方法,是先打短信还是hello
4 现在有两部手机,先打印短信还是邮件
5 两个静态同步方法,1部手机,先打印短信还是邮件
6 两个静态同步方法,2部手机,先打印短信还是邮件
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
* ---------------------------------
*
*/
public class Lock_8
{
public static void main(String[] args) throws Exception
{
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
phone.sendEmail();
//phone.getHello();
//phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
3.1、问题分析
如果一个资源类对象中存在多个同步方法(synchronized修饰),然后另一个类去创建线程调用这个同步方法,它锁住的不是一个方法,而是整个资源类,所以你调度其他的同步方法的时候需要等待这个同步方法执行完毕
没有被synchronized修饰的方法则不会产生这个问题,因为它不需要等待线程结束,和资源竞争问题
当然如果你创建了两个对象,那当然也不会存在线程竞争的问题,互不干扰,因为此时是两个对象,已经不是同一个对象同一把锁了
对于普通同步方法,锁是当前实例对象。(局部锁)
对于静态同步方法,锁是当前类的Class对象。(全局锁)方法被static修饰之后属于整个类的同一层,也就是说它现在已经不是一个单独的个体了,当然,如果是一个静态同步方法和一个普通同步方法,它们锁的对象不一样,也是不冲突的,不会产生资源竞争问题
3.2、虚假唤醒
题:
现在存在++功能,和–功能,同一个实例对象,四个线程进行操作,两次执行–,两次执行++
分析:
此时他们就会进行线程竞争,没有竞争到的线程就等待,所以有可能在多次竞争中都被++抢到了线程资源,但是只执行一个++方法,直到–抢到了线程,执行了–功能,this.notifyAll()会唤醒所有正在在等待的线程,也就是多次竞争中抢到资源的++,还没有执行完毕的线程,这个时候就会出现问题,达不到预期的
// jdk1.8之前同步锁的方法
public synchronized void decrement() throws Exception{
// 当前线程被唤醒之后,不会再次验证,会接着往下执行
if(number == 0)
this.wait();
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
3.2.1、 解决方案
- 中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。if换成while
- 在线程中notify或者notifyAll会唤醒一个或多个线程,当线程被唤醒后,被唤醒的线程继续执行阻塞后的操作。
// jdk1.8之后推荐的方法
public void decrement() throws Exception{
lock.lock();
try {
while(number == 0)
condition.await();
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
3.3、 lock中condition与synchronized的区别
4、 Callable与Runnable
建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
今天我们就来讨论一下Callable、Future和FutureTask三个类的使用方法
// 有返回值的线程,通过get方法
public class FutureTask<V> implements RunnableFuture<V>
也就是说Future提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。( get()方法用来获取执行结果,这个方法会产生阻塞(闭锁实现),会一直等到任务执行完毕才返回; )
5、 synchronized和Lock的区别
1)Lock是个接口,而synchronized是java关键字,synchronized是内置语言实现
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象的发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很有可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
3)Lock可以让等待锁的线程相应**中断;**而synchronized不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
5)Lock可以提高多个线程读操作的效率
在性能上来说,如果资源竞争不激烈的话,两者的性能是差不多的;而当资源竞争非常激烈(即有大量线程同时竞争)时,Lock的性能要远远优于synchronized
6、 volatile关键字与内存可见性
volatile关键字的作用:
当多个线程进行操作共享数据的时候,可以保证缓存中的数据可见。
volatile相较于synchronized是一种较为轻量的同步策略
注意:
-
volatile不具备“互斥性”(与synchronized)
-
volatile不能保证变量的‘原子性’(AtomicInteger)
深入理解:volatile
7、Atomic(原子锁)
atomic作用:
多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁(CAS)锁住该属性。不允许其他的线程对其进行读取操作了。
但是它有一个很大的缺点:因为它要使用自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic慢20倍。
8、countDownLatch(闭锁)
CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch有一个正数计数器,countDown()方法对计数器做减操作,await()方法等待计数器达到0。所有await的线程都会阻塞直到计数器为0或者等待线程中断或者超时。
Semaphore(信号灯)
信号灯的主要目的有两个,一个用域多个共享资源的互斥使用,里一个用域并发线程数的控制
9、ReadWriteLock(读写锁)
…省略
10、ThreadPool(线程池)
线程池的主要特点:
线程复用;控制最大并发数;管理线程
// 一池5个工作线程,类似一个银行有五个受理窗口
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
// 一池1个工作线程,类似一个银行有1个受理窗口(效率极低)
Execut orService threadPool2 = Executors.newSingleThreadExecutor();
// 一池N个工作线程,类似一个银行有N个受理窗口(可扩展的)
ExecutorService threadPool3 = Executors.newCachedThreadPool();
10.1、 ThreaPoolExcutor
以上这几个方法的使用,底层都是调用了ThreaPoolExcutor
10.2、 ThreaPoolExcutor 七大参数
corePoolSize=> 线程池里的核心线程数量
maximumPoolSize=> 线程池里允许有的最大线程数量
keepAliveTime=> 空闲线程存活时间
unit=> keepAliveTime的时间单位,比如分钟,小时等
workQueue=> 缓冲队列
threadFactory=> 线程工厂用来创建新的线程放入线程池
handler=> 线程池拒绝任务的处理策略,比如抛出异常等策略
10.3 、自定义线程池
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不
会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中
尝试再次提交当前任务
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
如果允许任务丢失,这是最好的一种策略。
11、BlockingQueue(阻塞队列)
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
(建议看周🐏老师的教学视频和资料)
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
Queue也是controller的子类blockingQueue是通过继承Queue去实现的