并发编程基础

线程会带来额外的开销,如cpu调度

继承thread类,重写run()方法,调用start()开启线程

run() 只有主线程一条执行路径;start()多条执行路径,主线程和子线程并行交替执行。

实现runnable接口

实现callable接口(FutureTask,可以通过get阻塞获取线程结果)

java默认2个线程:main和gc

java无法开启线程,只能调用底层c++,无法操作硬件

并发:cpu一核(充分利用cpu资源) 并行:cpu多核

线程有几个状态

1
2
3
4
5
6
NEW,  //新生
RUNNABLE,
BLOCKED, //阻塞
WAITING,
TIMED_WAITING, //超时等待
TERMINATED; //终止

wait/sleep区别

1、wait()方法属于Object类,sleep()属于Thread类;

2、wait()方法释放cpu给其他线程,自己让出资源进入等待池等待;sleep占用cpu,不让出资源;

3、sleep()必须指定时间,wait()可以指定时间也可以不指定;sleep()时间到,线程处于临时阻塞或运行状态;

4、wait()方法会释放持有的锁,不然其他线程不能进入同步方法或同步块,从而不能调用notify(),notifyAll()方法来唤醒线程,产生死锁,所以释放锁,可以执行其他线程,也可以唤醒自己,只是设置停止自己的时间时不确定的;sleep方法不会释放持有的锁,设置sleep的时间是确定的会按时执行的;

5、wait()方法只能在同步方法或同步代码块中调用,否则会报illegalMonitorStateException异常,如果没有设定时间,使用notify()来唤醒;而sleep()能在任何地方调用,必须捕获异常;

线程礼让 yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功,看CPU心情

线程强制执行 join()

  • Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞
  • 可以想象成插队

集合类

Vector:jdk1.0就有,加锁

ArrayList :jdk1.2才有,不加锁

1
2
3
4
5
6
Collections.synchronizedList(new ArrayList<>());


CopyOnWriteArrayList //修改副本,替换引用

HashSet 底层HashMap add时只添加key value是一个PRESENT的定值

image-20211123093419762

线程池

image-20211125132127773

image-20211125132035181

三大方法,七大参数,四种拒绝策略

程序的运行,本质:占用系统的资源 优化资源的使用 =》 池化技术

线程池、连接池、内存池、对象池。。。 池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还我。

三大方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//固定线程数池 core = max,都不可回收
ExecutorService executorService = Executors.newFixedThreadPool(5);
//单例线程池  单线程线程池,后台从队列里获取任务,挨个执行
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
//缓存线程数池 core是0,所有都可回收
ExecutorService executorService2 = Executors.newCachedThreadPool();

try {
    for (int i = 0; i < 10; i++) {
        executorService.execute(()->{
            System.out.println(Thread.currentThread().getName()+" ok");
        });
    }
} catch (Exception e) {
    e.printStackTrace();
}finally{
    //线程池用完,程序结束,关闭线程池
    executorService1.shutdown();
}

七大参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
corePoolSize 核心线程数

//获取cpu核心
System.out.println(Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        2,// 核心线程数
        Runtime.getRuntime().availableProcessors(),// 最大核心线程数
        3,// 超时时间
        TimeUnit.SECONDS,// 超时单位
        new LinkedBlockingDeque<>(3),// 阻塞队列
        Executors.defaultThreadFactory(),// 默认线程工厂,不用动它
        new ThreadPoolExecutor.AbortPolicy() //线程池处理不过来就抛出异常(拒绝策略)
);
try {
    //最大承载:deque+max
    for (int i = 0; i < 20; i++) {
        threadPoolExecutor.execute(()->{
            System.out.println(Thread.currentThread().getName()+" ok");
        });
    }
} catch (Exception e) {
    e.printStackTrace();
}finally{
    //线程池用完,程序结束,关闭线程池
    threadPoolExecutor.shutdown();
}

image-20211126164009345

四种拒绝策略

1
2
3
4
5
6
7
new ThreadPoolExecutor.AbortPolicy() 线程池处理不过来就抛出异常

new ThreadPoolExecutor.CallerRunsPolicy() 哪来的去哪里它直接在 execute 方法的调用线程中运行被拒绝的任务如果执行程序已关闭则会丢弃该任务

new ThreadPoolExecutor.DiscardPolicy() 队列满了丢掉任务不会抛异常

new ThreadPoolExecutor.DiscardOldestPolicy() 抛弃最老的
  1. 降低资源的消耗
  2. 提高响应的速度
  3. 方便管理

线程复用、可以控制最大并发数、管理线程

image-20210917193323477

1
System.out.println(Runtime.getRuntime().availableProcessors()); //h

最大线程数怎么定义:

  1. cpu 密集型 :几核,就是几,可以保持cpu的效率最高
  2. io 密集型 :判断你程序中十分耗费io的线程

CompletableFuture异步编排

https://www.jianshu.com/p/934057982c25

Lock锁

公平锁和非公平锁

image-20211123104622052

image-20211123111657212

可重入锁(递归锁)

image-20211123112215735

image-20211123112244297

image-20211123112127097

自旋锁

image-20211123123652678

image-20211123125208633

image-20211123125245656

读写锁

image-20211123155234261

synchronized和Lock锁区别

image-20211124154515017

  1、首先synchronized是java内置关键字,Lock是个java类;

  2、synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

  3、synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

  4、用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

  5、synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);

  6、synchronized锁适合代码少量的同步问题,Lock锁适合大量同步的代码的同步问题。

生产者消费者问题

所谓的生产者消费者问题,是通过一个容器来解决生产者和消费者的强耦合问题。通俗的讲,就是生产者在不断的生产,消费者也在不断的消费,可是消费者消费的产品是生产者生产的,这就必然存在一个中间容器,我们可以把这个容器想象成是一个货架,当货架空的时候,生产者要生产产品,此时消费者在等待生产者往货架上生产产品,而当货架满的时候,消费者可以从货架上拿走商品,生产者此时等待货架的空位,这样不断的循环。那么在这个过程中,生产者和消费者是不直接接触的,所谓的‘货架’其实就是一个阻塞队列,生产者生产的产品不直接给消费者消费,而是仍给阻塞队列,这个阻塞队列就是来解决生产者消费者的强耦合的。就是生产者消费者问题。

回忆 synchronized 关键字,它配合 Object 的 wait()、notify() 系列方法可以实现等待/通知模式。对于 Lock,通过 Condition 也可以实现等待/通知模式。Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。

if只判断一次,虚假唤醒,一般使用while

经典8锁问题

1、new this 调用的是这个对象,是一个具体的对象! 2、static class 唯一的一个模板! 在我们编写多线程程序得时候,只需要搞明白这个到底锁的是什么就不会出错了!

安全类

并发下ArrayList是不安全的,Vector是安全的

1
2
3
4
5
6
//解决办法
List<String> list2 = new Vector<>(); 使用synchronized
List<String> list1 = Collections.synchronizedList(new ArrayList<>());  
List<String> list3 = new CopyOnWriteArrayList<>();
//CopyOnWrite 写入时复制 COW计算机程序设计的一种优化策略,再写入时避免覆盖
//我们都知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

Set不安全

1
2
3
Set<String> set = new HashSet<>();
Set<String> set1 = Collections.synchronizedSet(new HashSet<>());
Set<String> set2 = new CopyOnWriteArraySet<>();

HashSet的底层就是HashMap

HashMap不安全

1
2
3
Map<String,String> map= new HashMap<>(16,0.75);
Map<String,String> map= new HashMap<>();
Map<String,String> map1= new ConcurrentHashMap<>();

callable

Callable 和 Runnable 的使用方法大同小异, 区别在于: 1.Callable 使用 call() 方法, Runnable 使用 run() 方法 2.call() 可以返回值, 而 run()方法不能返回。 3.call() 可以抛出受检查的异常,比如ClassNotFoundException, 而run()不能抛出受检查的异常。

通过FutureTask和Runnable 挂上关系

三大常用辅助类

CountDownLatch(减法计数器)

说明: 一个线程等待其他线程执行完之后再执行,相当于加强版的join,在初始化CountDownLatch是需要设定计数器的数值(计数器数据不一定跟线程数相同,但是一定计数器的值一定是要大于等于线程数,一个线程中可以进行多次扣减。当计数器扣减至0时才可继续向下执行)

举例说明: 比如LOL在游戏开始时需要玩家全部准备完毕之后才开始,开始游戏可以理解为“主线程”,玩家准备理解为“其他线程”,在“其他线程”没有准备完毕之前,“主线程时等待状态”,当“其他线程”准备完毕之后“主线程”就会执行下一步开始游戏的动作

1
2
3
private static CountDownLatch countDownLatsh = new CountDownLatch(5);
countDownLatsh.countDown();
countDownLatsh.await();

CyclicBarrier(加法计数器)

说明: 让一组线程到达某个屏障,然后被阻塞,一直到最后一个线程到达屏障,然后屏障开放,所有被阻塞的线程继续执行,计数器与线程数相等。 CyclicBarrier(int parties, Runnable barrierAction) 当时使用这个构造函数时,barrierAction定义的任务会执行

集齐7科龙珠召唤神龙

1
2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
cyclicBarrier.await();

Semaphore(限流的时候用)

1
2
3
private static final Semaphore semaphore=new Semaphore(3);
semaphore.acquire();
semaphore.release();

相当于os中的资源量,得到/释放

ReadWriteLock

假设有个数据对象拥有写方法与读方法,多线程环境中要想保证数据的安全,需对该对象的读写方法都要加入 synchronized同步块。这样任何线程在写入时,其它线程无法读取与改变数据;如果有线程在读取时,其他线程也无法读取或写入。这种方式在写入操作远大于读操作时,问题不大,而当读取远远大于写入时,会造成性能瓶颈,因为此种情况下读取操作是可以同时进行的,而加锁操作限制了数据的并发读取。

​ ReadWriteLock解决了这个问题,当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,但却可以读取数据 。

1
2
3
4
5
6
7
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock read = lock.readLock();  
Lock write = lock.writeLock();
write.lock(); 
write.unlock(); 
read.lock(); 
read.unlock(); 

独占锁(写锁):一次只能被一个线程占有

共享锁(读锁):多个线程可以同时占有

BlockingQueue(阻塞队列)

多线程并发处理,线程池

方式 抛出异常 有返回值,不会抛出异常 阻塞等待 超时等待
添加 add() offer() put() offer(object , long , TimeUnit)
移除 remove() poll() take() poll(long , TimeUnit)
检测队首元素 element() peek() - -
1
BlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);

SynchronousQueue(同步队列)

没有容量

进去一个元素必须等它出来才能再加一个元素

1
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();

四大函数式接口

新时代的程序员:lambda表达式、函数式接口、Stream流式计算、链式编程、异步调用

lambda表达式

基本语法: (parameters) -> expression 或 (parameters) ->{ statements; }

Lambda表达式由三部分组成:

paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。 ->:可理解为“被用于”的意思 statements:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不反回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) {
//        Function<String, String> function = new Function<String, String>() {
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };
    Function<String, String> function = (str)->{return str;};
    System.out.println(function.apply("啦啦啦啦啦"));
}

函数式接口

如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口 如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的。所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的,保证安全。Function函数式接口

Function 函数型接口

Function 函数型接口, 有一个输入参数,有一个输出

1
2
3
4
5
6
 	public static void main(String[] args) {
        //输出大写后的原字符串
        Function<String, String> function = str -> str.toUpperCase();

        System.out.println(function.apply("abc"));
    }

Predicate 断定型接口

有一个输入参数,返回值只能是 布尔值!

1
2
3
4
5
6
 	public static void main(String[] args) {
        //判断一个字符串是否为a开头
        Predicate<String> predicate = s -> !s.isEmpty() && s.charAt(0) == 'a';

        System.out.println(predicate.test("abc"));
    }

Consumer 消费型接口

只有输入没有输出

1
2
3
4
5
6
    public static void main(String[] args) {
        //输出一个大写字符串
        Consumer<String> consumer = s -> System.out.println(s.toUpperCase());

        consumer.accept("abc");
    }

Supplier 供给型接口

没有参数,只有输出值

1
2
3
4
5
6
   public static void main(String[] args) {
        //返回一个Demo对象
        Supplier<Demo> supplier = () -> {return new Demo();};

        System.out.println(supplier.get().hashCode());
    }

Stream流式计算

存储交给集合,计算交给流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 题目要求:只用一行代码实现
 * 现在有五个用户!筛选:
 * 1、ID必须是偶数
 * 2、年龄必须大于23岁
 * 3、用户名转化为大写字母
 * 4、用户名字母倒着排序
 * 5、只输出一个用户
 */
public class Test {
    public static void main(String[] args) {
        User u1 = new User(1,"a",21);
        User u2 = new User(2,"b",22);
        User u3 = new User(3,"c",23);
        User u4 = new User(4,"d",24);
        User u5 = new User(5,"e",25);
        User u6 = new User(6,"f",26);
        //集合用来存储
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5,u6);
        //使用stream计算
        //lambda表达式、链式编程、函数式接口、Stream流式计算
        list.stream()
                .filter(u->{return u.getId()%2 == 0;})
                .filter(u->{return u.getAge() > 23;})
                .map(u->{return u.getName().toUpperCase();})
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

Forkjoin

Forkjoin类似于一个递归算法,可以将一系列大问题拆分成小问题,然后逐个解决。 其中有个工作窃取的概念,即一个线程处理完成任务以后会从另一个线程中获取其他线程的任务进行处理。 由于线程是一种双端队列,可以从底部进行窃取。

JMM

JMM:Java内存模型,不存在的东西,概念,约定!

JMM同步约定

线程加锁前,必须把共享变量刷回主存

线程解锁前,必须读取主存的最新值到工作内存中

加锁和解锁是同一把锁

image-20210615135024324

store和write换一下位置

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

  • read 读取,作用于主内存把变量从主内存中读取到本本地内存。
  • load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
  • use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
  • assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
  • lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

所以看似简单的通信其实是这八种状态来实现的。

同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:

  • 不允许read和load、store和write的操作单独出现。
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

volatile

https://www.cnblogs.com/dolphin0520/p/3920373.html

单例模式

CAS

image-20211122092348319

image-20211122091229750

image-20211122092046556

image-20211122094254849

image-20211122094400752

ABA问题

image-20211122094747144

image-20211122100014265

image-20211122100918366

image-20211122104954690