多线程进阶——JUC并发

一、什么是JUC

JUC是Java中的三个包,java.util.concurrent,
在这里插入图片描述
业务:普通的线程代码 Thread 调用Runnable没有返回值,效率较Callable低,所以企业中Runnable使用少,大多情况下使用Callable。

二、线程和进程

  1. 进程:一个程序
    一个进程包含多个线程,最少包含一个
    Java默认有2个线程:Main、GC

  2. 线程:
    对于Java而言,开启线程:Thread、Runnable、Callable
    Java真的可以开启线程吗?开不了,通过调用底层C++本地方法start0()开启;

并发和并行

并发:多线程操作同一资源(CPU一核,交替进行)
并行:多个人一起行走(CPU多核,多线程同时执行):线程池

//获取cpu核数
System.out.println(Runtime.getRuntime().availableProcessors)

并发编程的本质:充分利用CPU资源

线程有几个状态

源码中有6个
NEW:新生
RUNNABLE:就绪
RUNNING:运行
BLOCKED:阻塞
WATTING:等待(死等)
TIMED_WATTING:超时等待
TERMINATED:终止

wait/sleep区别

  1. 来自的类不同:Object和Thread。企业中一般不会使用Thread.sleep(),而用TimeUnit.DAYS(SECONDS).sleep(1);
  2. 锁的释放:wait释放锁,sleep不会释放
  3. 适用范围:wait必须在同步代码块,sleep可以在任何地方
  4. 异常捕获:wait不需要,sleep必须捕获

三、Lock(重点)

实现买票

传统的Synchronized锁

public class saleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"C").start();
    }

}


//资源类  OOP面向对象,资源对象只有属性、方法
class Ticket {

    private int num = 30;

    public synchronized void sale() {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了" + num-- + "票,剩余" + num);
        }
    }

}

Lock锁

使用方式
在这里插入图片描述
实现Lock接口的类
在这里插入图片描述

ReentrantLock:可重入锁

有两个构造函数
在这里插入图片描述
公平锁:可以先来后到 (3h 3s那3s的线程就要等待3h)
非公平锁:可以插队(默认)

//Lock 三部曲
//1、 new ReentrantLock();
//2、 加锁 lock.lock(); 业务代码放在try中间
//3、 解锁 finally-》lock.unlock();

package com.zzz;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class saleTicketLock {

    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"C").start();
    }

}


//Lock 三部曲
//1、 new ReentrantLock();
//2、 加锁 lock.lock();   业务代码放在try中间
//3、 解锁 finally-》lock.unlock();
class Ticket2 {

    private Lock lock = new ReentrantLock();

    private int num = 30;

    public void sale() {
        lock.lock();
        try {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了" + num-- + "票,剩余" + num);
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }

}

Synchronized与Lock区别

  1. Synchronized内置关键字,Lock是一个Java类
  2. Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
  3. Synchronized会自动释放锁,Lock必须手动释放,如果不释放会产生死锁
  4. Synchronized 线程1(获得锁、阻塞)线程2(等待锁、死等),Lock 不会一直等待(lock.tryLock();)
  5. Synchronized 可重入锁,不可以中断,非公平。Lock可重入锁,可以判断锁,非公平(可自己设置)。
  6. Synchronized 适合锁少量代码同步问题,Lock适合锁大量的同步代码。

生产者消费者问题

线程通信
线程交替执行 A B操作同一变量

传统的Synchronized锁

// A num+1
// B num-1
public class ProductTest {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.add();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.del();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"B").start();
    }
}

//判断等待  业务  通知
class Data {
    private int num;

    public synchronized void add() throws InterruptedException {
        if (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + ":" + num);
        this.notifyAll();
    }

    public synchronized void del() throws InterruptedException {
        if (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + ":" + num);
        this.notifyAll();
    }
}

但是如果线程数量超过2个就会出现问题(虚假唤醒)
在这里插入图片描述
if 改为 while:因为if只会判断一次

JUC 版Lock锁

之前使用的是wait和notify通信
而Lock使用的是await和signal来通信
在这里插入图片描述

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//线程通信
//线程交替执行  A B操作同一变量
// A num+1
// B num-1
public class ProductLockTest {
    public static void main(String[] args) {
        Data2 data = new Data2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.add();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.del();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.add();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"C").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        data.del();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"D").start();
    }
}

//判断等待  业务  通知
class Data2 {
    private int num;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();


    public void add() throws InterruptedException {
        lock.lock();
        try {
            while (num != 0) {
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            lock.unlock();
        }

    }

    public void del() throws InterruptedException {
        lock.lock();

        try {
            while (num == 0) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + ":" + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }

    }
}

仍存在的问题:ABCD四个线程随机访问,想让有序,A-B-C-D,精准的通知

解决:新建3个Condition,执行完A后唤醒Condition2,通过num依次唤醒
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();

package com.zzz;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// ABCD线程有序输出
//当num=1执行A num=2 B 3 C
public class ProductLockOrdered {
    public static void main(String[] args) {
        Data3 data3 = new Data3();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    data3.printA();
                }
            }
        },"A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    data3.printB();
                }
            }
        },"B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    data3.printC();
                }
            }
        },"C").start();

    }
}
class Data3 {
    private int num = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();

        try {
            while (num!=1){
                condition1.await();
            }
            System.out.println("AAAA");
            num = 2;
            condition2.signal();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();

        try {
            while (num!=2){
                condition2.await();
            }
            System.out.println("BBBB");
            num = 3;
            condition3.signal();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();

        try {
            while (num!=3){
                condition3.await();
            }
            System.out.println("CCCC");
            num = 1;
            condition1.signal();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}

四、8锁现象

什么是锁?如何判断锁的是谁?

8锁其实就是锁的八个问题。

1、标准情况下两个线程先打印 发短信还是打电话?
答:发短信
2、sendMsg延迟4s,两个线程打印,中间休息1s,先打印谁?
答:发短信

原因:synchronized 锁的对象是方法的调用者:phone。两个方法用得是同一个锁,谁先拿到谁执行。


import java.util.concurrent.TimeUnit;

//8锁,就是锁的八个问题
//1、标准情况下,两个线程打印,中间休息1s,先打印谁?  发短信
//2、sendMsg延迟4s,两个线程打印,中间休息1s,先打印谁?  发短信
public class Q1 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{phone.call();},"B").start();
    }
}

class Phone{

    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

3、增加一个普通方法,先执行普通方法还是发短信?
答:普通方法
原因:hello方法非同步,没有锁

...
new Thread(()->{phone.hello();},"B").start();

...

public void hello(){
        System.out.println("hello");
    }
    
...
    

4、两个对象,两个同步方法,先执行打电话还是发短信?
答:打电话
原因:有两个对象,锁的对象不是同一个,不会占用锁

public class Q2 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();
        new Thread(()->{
            try {
                phone1.sendMsg();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();},"B").start();
    }
}

class Phone2{

    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }

    public void hello(){
        System.out.println("hello");
    }
}

5、给同步方法加上static,只有一个对象,先打印打电话还是发短信?
答:发短信
6、给同步方法加上static,两个对象,先打印打电话还是发短信?
答:发短信

原因:加上static(静态方法)类一加载就有了 ,锁的是Class ,Phone3只有全局唯一的对象

public class Q3 {
    public static void main(String[] args) throws InterruptedException {
//        Phone3 phone = new Phone3();
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(()->{
            try {
                phone1.sendMsg();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();},"B").start();
    }
}

class Phone3{

//    加上static(静态方法)类一加载就有了 ,锁的是Class ,Phone3只有全局唯一的对象
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }

}

7、一个静态同步方法,一个普通同步方法,一个对象,先打印打电话还是发短信?
答:打电话
8、一个静态同步方法,一个普通同步方法,两个对象,先打印打电话还是发短信?
答:打电话

原因:静态同步方法锁的是Class,普通锁的是对象。所以不会占用一个锁。

public class Q4 {
    public static void main(String[] args) throws InterruptedException {
//        Phone4 phone = new Phone4();
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
            try {
                phone1.sendMsg();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();},"B").start();
    }
}

class Phone4{
//    静态同步方法
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
//    普通同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }

}

小节

锁的有两个:new出来的对象和Class唯一的模板
关键看是否是同一个对象或者是否是static

五、集合的不安全

ArrayList

ArrayList在多线程不安全 报java.util.ConcurrentModificationException并发修改异常

解决方式:

  1. Vector:使用new Vector<>(),但是该方法面试不加分
  2. Collections.synchronizedList :使用Collections.synchronizedList(new ArrayList());
  3. CopyOnWriteArrayList : 使用juc中的 new CopyOnWriteArrayList<>(); CopyOnWrite写入时复制一份,避免多线程写入时被覆盖造成数据问题

public class ArrayListTest {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

问:CopyOnWriteArrayList为什么比Vector好?
答:Vector的add()方法源码中是使用synchronized修饰的,使用synchronized修饰的效率都会降低。CopyOnWriteArrayList使用的是Lock锁,执行了 取——复制——存 的操作。

Set

    //HashSet 不安全

// 解决方法 :1. 使用Collections 2.使用CopyOnWrite


import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetTest {

    public static void main(String[] args) {
//        HashSet<String> set = new HashSet<>();

//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString());
                System.out.println(set);
            }).start();
        }
    }
}

HashSet底层 就是HashMap
在这里插入图片描述
add方法本质就是map的put,所以Set的本质就是Map的key,是无法重复的
在这里插入图片描述

Map

HashMap有三个构造,默认HashMap = new HashMap<>(16,0.75) 初始容量16、 加载因子0.75

解决方法 :1. 使用Collections 2.ConcurrentHashMap

public class MapTest {

    public static void main(String[] args) {
//        HashMap<String,String> map = new HashMap<>();


        //HashMap 不安全
//        解决方法 :1. 使用Collections 2.ConcurrentHashMap

//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString());
                System.out.println(map);
            }).start();
        }
    }
}

六、Callable

有返回值,可以抛出异常

要实现implements Callable,此处的String即为返回值类型。

  • new Thread的构造中只能传入Runnable,不能传入Callable
  • 所以通过一个适配类来传入Callable,即FutureTask
  • 获取call返回结果通过FutureTask.get()获取,但是他会产生阻塞,要放到最后一行或通过异步通信来处理
public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();

        FutureTask<String> futureTask = new FutureTask<>(myThread); // 适配类
        new Thread(futureTask).start();
        new Thread(futureTask).start();
        String s = futureTask.get();
        System.out.println(s);
    }
}


class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "call()";
    }
}

七、常用的辅助类(高并发必会)

CountDownLatch

减法计数器,等待计数器归0后执行后续任务。

在这里插入图片描述
等待所有人都出教室后关门

  1. new CountDownLatch(6); 默认计数器为6
  2. 每执行一次,countDown 减一
  3. countDownLatch.await();等待计数器归0后执行后续任务,如果没有这句话计数器无用。
public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " out");
                countDownLatch.countDown();
            }).start();

        }

        countDownLatch.await();
        System.out.println("关门");

    }
}

CyclicBarrier

加法计数器在这里插入图片描述
CyclicBarrier有两个构造函数,

  1. 一个参数:第一个参数是需要几个线程await()
  2. 两个参数:第一个参数是需要几个线程await(),第二个参数是Runnable,表示满足几个线程后执行该Runnable。

集卡,集齐7卡可获得奖品

public class CyclicBarrierTest {

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("奖品发放");
        });
        for (int i = 0; i < 7; i++) {
            final int temp = i;

            new Thread(() -> {
                //   这里不能直接使用i,可以通过final来转接使用。因为作用域不同
                System.out.println(Thread.currentThread().getName() + temp + " 集卡成功");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }

    }
}

Semaphore

信号量。可以理解为通行证。限流

acquire() 得到,占有。如果已经满了,等待被释放为止。
release() 释放车位,将当前信号量+1,然后唤醒等待的线程

抢车位。

//抢车位
public class SemeaphoreTest {

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    //  acquire  得到
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位!");
                    TimeUnit.SECONDS.sleep(2);

                    // release 释放
                    System.out.println(Thread.currentThread().getName() + "离开了车位!");
                    semaphore.release();

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            },String.valueOf(i)).start();
        }

    }
}

八、读写锁

在这里插入图片描述
自定义一个缓存,模拟读写过程

未加读写锁之前:写被插队,期望成功写入后再下一个写。

在这里插入图片描述

加入读写锁后:保证了写入成功后再下一个写

  1. 新建ReentrantReadWriteLock():ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  2. 写操作加写锁:readWriteLock.writeLock().lock();并释放
  3. 读操作加读锁:readWriteLock.readLock().lock();并释放
public class ReadWriteLockTest {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        //写操作
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.put(Thread.currentThread().getName(), temp);
            },String.valueOf(i)).start();
        }

        //读操作
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                myCache.get(Thread.currentThread().getName());
            },String.valueOf(i)).start();
        }
    }

}

//自定义模拟缓存
class MyCache{

    private HashMap<String,Object> hashMap = new HashMap<>();

    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key,Object value){
        readWriteLock.writeLock().lock();

        try {
            System.out.println("写入"+key);
            hashMap.put(key,value);
            System.out.println("写入"+key + "成功");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public Object get(String key){
        readWriteLock.readLock().lock();

        try {
            System.out.println("读取"+key+",值为"+hashMap.get(key));
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.readLock().unlock();
        }
        return hashMap.get(key);
    }
}

在这里插入图片描述
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占用

九、阻塞队列

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

BlockingQueue

  1. 什么情况下使用它?
    多线程并发处理、线程池

  2. 如何使用它?
    添加、移除
    有四组API:抛出异常、不会抛出异常、阻塞等待、超时等待

方式抛出异常不抛异常,有返回值阻塞等待超时等待
添加add()offer()put()offer(,)
移除remove()poll()take()poll(,)
查看队首元素element()peek()-----------
  • 抛出异常
    /**
     * 抛出异常
     */
    public static void throwException(){
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.add("a"));
        System.out.println(queue.add("b"));
        System.out.println(queue.add("c"));
//      抛出异常java.lang.IllegalStateException: Queue full
//        System.out.println(queue.add("d"));

        System.out.println("----------");
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
//      抛出异常java.util.NoSuchElementException
//        System.out.println(queue.remove());
    }
  • 不抛出异常,有返回值
    offer返回false
    poll 返回null
/**
     * 抛出异常
     */
    public static void noThrowAndHasReturn(){
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("b"));
        System.out.println(queue.offer("c"));
//      不抛出异常,返回false
//        System.out.println(queue.offer("d"));

        System.out.println("----------");
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
//      不抛出异常,返回null
        System.out.println(queue.poll());
    }
  • 等待(一直阻塞)
    /**
     * 等待:一直阻塞
     */
    public static void waitAlways() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        queue.put("a");
        queue.put("b");
        queue.put("c");
//      一直阻塞
//        queue.put("d");

        System.out.println("----------");
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
//      一直阻塞
//        System.out.println(queue.take());
    }
  • 超时等待
    /**
     * 等待:超时退出
     */

    public static void waitTime() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
//      等待2秒
//      queue.offer("d",2, TimeUnit.SECONDS);

        System.out.println("----------");
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
//      等待2秒
//        System.out.println(queue.poll(2,TimeUnit.SECONDS));
    }
}

SynchronousQueue

同步队列
和其他BlockingQueue 不同,SynchronousQueue不存储元素
put了一个元素,必须先take取出来,否则不能put进值


public class SynchronousQueueTest {

    public static void main(String[] args) {
        SynchronousQueue<String> queue = new SynchronousQueue<>();

        new Thread(()->{
            try {
                System.out.println("A put 1");
                queue.put("1");
                System.out.println("A put 2");
                queue.put("2");
                System.out.println("A put 3");
                queue.put("3");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"A").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println("B get " + queue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println("B get " + queue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println("B get " + queue.take());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"B").start();
    }
}

十、线程池(重点)

面试题:三大方法、七大参数、四种拒绝策略

好处:

  1. 降低资源消耗
  2. 提高响应速度
  3. 方便管理
  4. 线程复用、可以控制最大并发数、管理线程

三大方法

  1. 单一线程池:Executors.newSingleThreadExecutor();
    在这里插入图片描述

  2. 固定线程池:Executors.newFixedThreadPool(5);
    在这里插入图片描述

  3. 缓存线程池:Executors.newCachedThreadPool();可伸缩,遇强则强,遇弱则弱
    在这里插入图片描述

线程池用完一定要关闭,为保证程序运行完毕,可以将其放到finnaly中。

public class ThreeMethod {

    public static void main(String[] args) {
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        ExecutorService threadPool = Executors.newCachedThreadPool();


        try {
            for (int i = 0; i < 100; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "  OK");
                });
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            threadPool.shutdown();
        }



    }
}

七大参数

在这里插入图片描述
原因是因为三大方法的源码都是调用了new ThreadPoolExecutor(),其实为封装的方法,但其中使用到的参数是Integer.MAX_VALUE,即大小约为21亿,会导致OOM,所以不允许使用。

图例
在这里插入图片描述

  • 超时等待即为所有人业务都办完了,除核心线程数外,超过超时时间后就会关闭窗口
  • 候客区即为阻塞队列,new
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                2,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),  //阻塞队列,最大为3
                Executors.defaultThreadFactory(),  //默认线程工程,一般不会动
                new ThreadPoolExecutor.AbortPolicy() //拒绝策略
        );

四种拒绝策略

AbortPolicy抛出异常

new ThreadPoolExecutor.AbortPolicy()

CallerRunsPolicy返回

new ThreadPoolExecutor.CallerRunsPolicy()
哪里来的去哪里
在这里插入图片描述

DiscardPolicy 丢掉新任务,不抛出异常

new ThreadPoolExecutor.DiscardPolicy()

DiscardOldestPolicy与最早来的竞争,不抛出异常

new ThreadPoolExecutor.DiscardPolicy()
如果竞争成功则处理,不成功丢弃

如何定义最大线程数

  1. CPU密集型。几核就是几,可以保证CPU效率最高。
    • 可以在任务管理器——性能——CPU——逻辑处理器查看
    • (常用)通过代码获取:Runtime.getRuntime().availableProcessors();
  2. IO密集型。程序中有几个大型任务执行IO操作(非常耗时),一般设置为IO任务数的2倍。

十一、四大函数式接口(必须掌握)

新时代程序员必会:lambda表达式、链式编程、函数式接口、Stream流

函数式接口:只有一个方法的接口
例如:Runnable、foreach

在这里插入图片描述
使用场景:Stream流中的函数
例如:
.filter(Predicate p)
.forEach(Consumer c)

Function

在这里插入图片描述

public class FuncitonTest {

    public static void main(String[] args) {
//        Function func = new Function<String,String>() {
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };

        Function<String,String> function = (str)->{
            return str;
        };

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

断定型接口 Predicate

有一个参数输入,返回值只能是boolean

public class PredicateTest {

    public static void main(String[] args) {
//        Predicate func = new Predicate<String>() {
//            @Override
//            public boolean test(String str) {
//                return str.isEmpty();
//            }
//        };

        Predicate<String> function = (str)->{
            return str.isEmpty();
        };

        System.out.println(function.test("aaa"));
    }
}

消费型接口 Consumer

public class ConsumerTest {

    public static void main(String[] args) {
//        Consumer<String> consumer = new Consumer<String>() {
//            @Override
//            public void accept(String str) {
//                System.out.println(str);
//            }
//        };

        Consumer<String> consumer = (str)->{
            System.out.println(str);
        };

    }
}

供给者接口 Supplier

public class SupplierTest {

    public static void main(String[] args) {
//        Supplier<String> supplier = new Supplier<String>() {
//            @Override
//            public String get() {
//                return "aaa";
//            }
//        };

        Supplier<String> supplier = ()->{
            return "aaa";
        };
        System.out.println(supplier.get());

    }
}

十二、Stream流

大数据时代基本的操作都是存储+计算
存储:集合、Mysql
计算:Stream

以下习题运用到了【新时代程序员必会:lambda表达式、链式编程、函数式接口、Stream流】

User:id\name\age
/**
 * 题目要求:一分钟内完成此题,只能用一行代码实现!
 * 现在有5个用户!筛选:
 * 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, "d", 25);
        User u6 = new User(6, "f", 26);

        List<User> list = Arrays.asList(u1, u2, u3, u4, u5, u6);
        //lambda表达式、链式编程、函数式接口、Stream流
        list.stream()
                .filter((u) -> u.getId() % 2 == 0)
                .filter((u) -> u.getAge() > 23)
                .map(u -> u.getName().toUpperCase())
                .sorted((uu1,uu2)->uu2.compareTo(uu1))
//                .sorted(Comparator.reverseOrder())
                .limit(1)
                .forEach(System.out::println);
    }
}

十三、ForkJoin

必须在大任务量的情况下使用。并行执行任务,提高效率
大任务拆分成小任务

在这里插入图片描述
特点:工作窃取
当线程B工作完成之后会将A未完成的部分偷过来继续执行
在这里插入图片描述
如何使用?
在这里插入图片描述
//如何使用ForkJoin
//1.通过ForkJoinPool执行
//2.计算任务 ForkJoinPool.execute(ForkJoinTask task)
//3.计算类要继承ForkJoin方法,实现compute方法 (RecursiveTask递归任务)

计算1-十亿的和
// 分3(普通计算) 6(ForkJoin) 9(Stream流)等


import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

// 分3(普通计算) 6(ForkJoin) 9(Stream并行流)等
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //普通计算
//        test1(); //时间:8340
//        test2();//6726
        test3();//422
    }

    public static void test1(){
        long start = System.currentTimeMillis();
        Long sum = 0L;
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.println((end - start));
    }

    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
        Long sum = submit.get();
        long end = System.currentTimeMillis();

        System.out.println(sum);
        System.out.println((end - start));
    }


    public static void test3(){
        long start = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(0, 10_0000_0000).parallel().reduce(0L, Long::sum);
        System.out.println(sum);
        long end = System.currentTimeMillis();
        System.out.println((end - start));
    }

}
//如何使用ForkJoin
//1.通过ForkJoinPool执行
//2.计算任务 ForkJoinPool.execute(ForkJoinTask task)
//3.计算类要继承ForkJoin方法,实现compute方法    (RecursiveTask递归任务)
public class ForkJoinDemo extends RecursiveTask<Long> {

    private Long start;
    private Long end;

    //临界值
    private Long temp = 10000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }


    @Override
    protected Long compute() {
        if(end - start < temp) {
            Long sum = 0L;
            for (Long i = start; i < end; i++) {
                sum +=i;
            }
            return sum;
        }else {
//            forkjoin
            long middle = (start + end) / 2;
//            拆分任务,把子任务压入队列
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork();
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork();
            long sum = task1.join() + task2.join();
            return sum;
        }
    }
}

十四、JMM

什么是JMM?
Java内存模型,不存在的东西,概念!约定!

关于JMM的一些同步约定:

  1. 线程解锁前,必须把共享变量立刻刷回主存
  2. 线程加锁前,必须读取主存中的最新值,到线程工作内存中
  3. 加锁跟解锁是同一把锁

线程的工作内存与主存之间的8个操作:
在这里插入图片描述
在这里插入图片描述

存在问题:线程B修改了Flag值,但A仍然在Flag为true的内存中不能及时可见。
在这里插入图片描述

十五、Volatile

Volatile是Java虚拟机提供的轻量级同步机制 .
1、保证可见性


//Volatile保证可见性,如果不加Volatile则程序不会停止,即num感知不到其他线程修改了值。
public class Demo01 {
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(num==0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(num);
    }
}

2、不保证原子性

不保证原子性,即加了Volatile结果也不是2w。加上Synchonized则结果正确。


public class Demo02 {
    private static volatile int num = 0;

    public static void main(String[] args) {
        //期望结果应该是2w
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(num);
    }

    public static void add(){
        num++;
    }
}

如果不加lock和Synchonized如何保证原子性

javap -p .class 反编译查看如下。
在这里插入图片描述
使用原子类、解决原子性问题 。这些类的底层都与操作系统挂钩,直接在内存中修改值(Unsafe类)。
在这里插入图片描述
例如int num的++,可以使用AtomicInteger

private static volatile AtomicInteger num = new AtomicInteger();
num.incrementAndGet();    //AtomicInteger +1方法, 使用的是CAS方法,在内存中修改值

3、禁止指令重排序
什么是指令重排?
源代码——编译器优化的重排——指令并行也可能重排——内存系统也会重排——执行

处理器在进行指令重排的时候,考虑:数据之间的依赖性!
在这里插入图片描述
所以Volatile可以保证禁止指令重新排序,通过添加内存屏障。在单例模式中使用最多

十六、单例模式

饿汉式

//饿汉式
public class HungryTest {
    private static HungryTest hungryTest = new HungryTest();
    private HungryTest(){}
    public static HungryTest getInstance() {
        return hungryTest;
    }
}

懒汉式

public class LazyTest {
    private LazyTest(){
        System.out.println(Thread.currentThread().getName() +" ok");
    }
    private static LazyTest lazyTest;

    public static LazyTest getInstance(){
        if(lazyTest == null){
            lazyTest = new LazyTest();
        }
        return lazyTest;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyTest.getInstance();
            }).start();
        }
    }
}

单线程下单例模式这样写没问题,多线程有问题。
在这里插入图片描述
应该采用DCL单例模式。即二次判断加锁,并防止指令重排加Volatile。

DCL单例模式

二次判断加锁,并防止指令重排加Volatile

在这里插入图片描述

public class LazyDCLTest {
    private LazyDCLTest(){
        System.out.println(Thread.currentThread().getName() +" ok");
    }
    private volatile static LazyDCLTest lazyTest;

    public static LazyDCLTest getInstance(){
        if(lazyTest == null){
            synchronized (LazyDCLTest.class){
                if(lazyTest == null){
                    lazyTest = new LazyDCLTest();//但是该行不是原子性操作
                    /*
                    我们期望它的步骤:
                    1.分配内存空间
                    2.执行构造方法,初始化对象
                    3.把这个对象指向这个空间
                    不是原子性操作原因是他有可能指令重排,步骤为132。当A线程执行了13时,B线程加入会判断到当前对象!=null,但此时还未完成构造
                    所以要在对象上加上Volatile
                     */
                }
            }
        }

        return lazyTest;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyDCLTest.getInstance();
            }).start();
        }
    }
}


结果正常
在这里插入图片描述

但是这些单例模式都是不安全的,是可以通过反射来破坏的。可以使用枚举单例来防反射。

public enum EnumTest {

    INSTANCE;

    private EnumTest(){}


    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumTest instance1 = EnumTest.INSTANCE;

        //获取无参构造,但是提示没有NoSuchMethodException,反编译后发现为有参
//        Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(null);
        Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumTest instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
        System.out.println(instance2);

    }
}

在这里插入图片描述

十七、深入理解CAS

什么是CAS?
compareAndSet,比较并交换
如果是这个值,就重新赋值。

public class CASDemo {
    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger(2000);
        System.out.println(atomicInteger.compareAndSet(2000, 3000));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2000, 3000));
        System.out.println(atomicInteger.get());
    }
}

在这里插入图片描述

源码:

atomicInteger.getAndIncrement();(num++):
在这里插入图片描述
在这里插入图片描述

Unsafe类

在这里插入图片描述
CAS缺点:

  1. 循环会耗时
  2. 一次性只能保证一个共享变量的原子性
  3. ABA问题

ABA问题(狸猫换太子)(乐观锁)
A拿到1,要替换为2。但在中间B也拿到了1,替换为了3,又替换为了1,此时A是毫不知情的,继续处理。
但是我们期望有人改了之后告诉别人——即引入原子引用
在这里插入图片描述

十八、原子引用

版本号的原子操作
原子引用
解决上述ABA问题

public class CASDemo2 {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(1,1);

        new Thread(()->{
            int stamp = atomicReference.getStamp();
            System.out.println("A,last:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(atomicReference.compareAndSet(1, 2, atomicReference.getStamp(), atomicReference.getStamp() + 1));
            System.out.println("A,modify1:" + atomicReference.getStamp());


            System.out.println(atomicReference.compareAndSet(2, 1, atomicReference.getStamp(), atomicReference.getStamp() + 1));
            System.out.println("A,modify2:" + atomicReference.getStamp());

        },"A").start();

        new Thread(()->{
            int stamp = atomicReference.getStamp();
            System.out.println("B,last:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(atomicReference.compareAndSet(1, 2, stamp, stamp + 1));
            System.out.println("B,modify1:" + atomicReference.getStamp());
        },"B").start();

    }
}

十九、各种锁

公平锁、非公平锁

公平锁:不能插队,必须先来后到。
非公平锁:可以插队,3s 3h(默认非公平)

Lock reentrantLock = new ReentrantLock();
Lock reentrantLock = new ReentrantLock(true);

可重入锁

又叫递归锁, 拿到外面的锁之后,就可以拿到里面的锁
所有的锁都是可重入锁

可重入锁的主要目的是解决在递归调用或嵌套代码中的锁定问题。当一个线程已经获得了锁,但在持有锁的代码块中又调用了另一个需要同样锁的方法时,如果使用非可重入锁,线程会因为无法再次获得同一个锁而陷入死锁状态。而可重入锁允许线程多次获得同一个锁,避免了死锁问题。

在这里插入图片描述

自旋锁

什么是自旋锁?
当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

为什么要使用自旋锁?
多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。

我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

在这里插入图片描述

死锁

在这里插入图片描述
制造死锁:

public class DeadLock {

    public static void main(String[] args) {
        MyThread myThreadA = new MyThread("lockA", "lockB");
        MyThread myThreadB = new MyThread("lockB", "lockA");

        new Thread(myThreadA,"A").start();
        new Thread(myThreadB,"B").start();
    }

}

class MyThread implements Runnable{

    String lockA = "";
    String lockB = "";

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + " lock: " + lockA + "=>want to get " + lockB);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + " lock: " + lockB + "=>get" + lockA);
            }
        }
    }
}

如何解决?
1、使用jps -l定位进程号
在这里插入图片描述
2、使用 jstack +进程号,找到死锁问题

在这里插入图片描述

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值