JUC(java并发编程)

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

CopyOnWriteArrayListCollections.synchronizedListCollections是集合类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是一种较为轻量的同步策略

注意:

  1. volatile不具备“互斥性”(与synchronized)

  2. 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=> 线程池拒绝任务的处理策略,比如抛出异常等策略

ThreadPoolExecutor参数图解

10.3 、自定义线程池

AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不
会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中
尝试再次提交当前任务

DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
如果允许任务丢失,这是最好的一种策略。

在这里插入图片描述

11、BlockingQueue(阻塞队列)

在这里插入图片描述
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞

(建议看周🐏老师的教学视频和资料)

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

Queue也是controller的子类blockingQueue是通过继承Queue去实现的

在这里插入图片描述
BlockingQueue详情

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值