JUC并发编程学习笔记(一)

前言:学习B站UP主狂神说视频笔记整理视频链接

什么是JUC

在这里插入图片描述
Thread:普通线程类
Runable:没有返回值,效率比Callable相对较低

什么是JUC

JUC是
java.util.concurrent包名的简写,是关于并发编程的API。
与JUC相关的有三个包:
java.util.concurrent、
java.util.concurrent.atomic、
java.util.concurrent.locks。

线程和进程

进程是一个程序的集合 如QQ.exe
进程往往包含多个线程,至少包含一个

Java默认有几个线程

2个 ; main GC

Java真的可以开启线程吗?

开不了!

阅读源码:

 public synchronized void start() {
    
    
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);//加入队列

        boolean started = false;
        try {
    
    
        //调用本地方法
            start0();
            started = true;
        } finally {
    
    
            try {
    
    
                if (!started) {
    
    
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
    
    
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    //本地方法 调用底层C++ Java无法直接操作硬件
    private native void start0();

并发,并行

并发:多个线程操作同一个资源

  • CPU一核,模拟出来多线程,基于CPU时间片快速调度

并行:多个人一起行走

  • CPU多核,多个线程可以同时执行
//在Java中可以通过此方法 查看CPU核心数
Runtime.getRuntime().availableProcessors()

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

多线程

线程六大状态

阅读源码可知,线程有六大状态

 public enum State {
    
    
        //新生
        NEW,

       //运行
        RUNNABLE,

       //阻塞
        BLOCKED,

       //等待
        WAITING,

       //超时等待
        TIMED_WAITING,

       //死亡
        TERMINATED;
    }

wait/sleep区别

1.来自不同的类

wait来着Object
sleep来着线程类

//在实际工作中,不会使用sleep让线程睡眠
//使用TimeUnit让线程睡眠
TimeUnit.DAYS.sleep(1);//睡一天
TimeUnit.SECONDS.sleep(2);//睡两秒

2.关于锁的释放

wait会释放锁
sleep不会释放锁

3.使用范围不同
wait必须在同步代码块中使用
sleep任何地方都可以睡眠

4.是否需要捕获异常

wait不需要捕获异常
sleep必须捕获异常

synchronized

简介

synchronized是解决线程不安全的关键,它的实现原理就是 队列和锁

由于我们可以通过private关键词来保证数据变量(对象),只能被方法访问,所以我们只需要针对方法提出一套机制.这套机制就是synchronized关键字.

实现原理

synchronized方法控制对对象的访问,每个对象对应一把锁,每一个synchronized方法都必须获得调用该方法对象的锁才能执行,否则会线程阻塞,方法一旦执行,就独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续往下执行

缺陷:如果一个大的方法被申明synchronized将会影响效率

具体用法

它的用法有两种:
1.synchronized同步方法

//同步方法
public synchronized void method(int ages){
    
    }
  1. synchronized方法 锁的是对象本身this
  2. 谁获得这个对象的锁才能执行这个方法

2.synchronized同步块

synchronized(obj){
    
    }

同步块锁的是obj
Ojb称为同步监视器:

  1. 他可以是任何对象,但是推荐使用共享资源对象
  2. 同步方法种无需指定同步监视器,因为同步方法的同步监视器就是this就是这个对象的本身

Lock锁

简介

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition

Lock默认有三个实现类
在这里插入图片描述

  1. 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  2. java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  3. ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock(可重复锁),可以显式加锁、释放锁。

ReentrantLock()源码:

在这里插入图片描述
公平锁:先来后到,不可插队
非公平锁: 可插队

Lock简单案例

public class ThreadTest {
    
    

    public static void main(String[] args) {
    
    
        Tikect tikect = new Tikect();
        new Thread(() -> {
    
     for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
        new Thread(() -> {
    
     for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
        new Thread(() -> {
    
     for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
    }
}

class Tikect{
    
    

    private Integer num =20;

    //通过显示声明锁 进行手动加锁 和解锁
    private Lock lock =new ReentrantLock();
    public void numTest(){
    
    
        lock.lock();//加锁
        try {
    
    
            if (num >0) {
    
    
                num--;
                System.out.println(num);
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();//解锁
        }

    }
}

synchronized与Lock区别

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

ReadWriteLock读写锁

在这里插入图片描述
ReadWriteLock只有唯一的一个实现类ReentrantReadWriteLock

//自定义缓存
class myCache{
    
    

    private Map<String,Object> map = new HashMap<>();
    //读写锁
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //存的时候只有一个线程 存 取的时候可以多个线程取

    //存
    public void put(String key,Object value){
    
    
        lock.writeLock();//写锁
        try {
    
    
            //业务代码
            map.put(key,value);

        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.writeLock().unlock();//释放锁
        }
    }

    //取
    public Object get(String key){
    
    
        lock.readLock();//读锁

        System.out.println(3);
        try {
    
    
            //业务代码
            return map.get(key);
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.readLock().unlock();
        }
        return null;
    }
}

生产者与消费者

在这里插入图片描述

传统synchronized版

public class ThreadTest {
    
    

    public static void main(String[] args) {
    
    
        Tikect tikect = new Tikect();

        new Thread(()  -> {
    
    
            for (int i = 0; i < 20; i++) {
    
    
                try {
    
    
                    tikect.increment();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        },"A");

        new Thread(()  -> {
    
    
            for (int i = 0; i < 20; i++) {
    
    
                try {
    
    
                    tikect.decrement();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        },"B");
    }
}

class Tikect{
    
    

    private int num =0;

    public synchronized void increment() throws InterruptedException {
    
    
        if (num!=0) {
    
    
            this.wait();//睡眠
        }
        System.out.println(num++);
        this.notifyAll();//唤醒睡眠的线程
    }

    public synchronized void decrement() throws InterruptedException {
    
    
        if (num==0) {
    
    
            this.wait();
        }
        System.out.println(num--);
        this.notifyAll();
    }
}

如果只是两个线程之间进行通信是没有问题的,如果多个线程之间四个五个六个之间并发执行就会产生虚假唤醒
因为if()只会判断一次
在这里插入图片描述

我们应该根据官方文档,改成while()循环判断,来防止虚假唤醒

class Tikect{
    
    

    private int num =0;

    public synchronized void increment() throws InterruptedException {
    
    
        //防止虚假唤醒 使用while判断
        while (num!=0) {
    
    
            this.wait();//睡眠
        }
        System.out.println(num++);
        this.notifyAll();//唤醒睡眠的线程
    }

    public synchronized void decrement() throws InterruptedException {
    
    
        while (num==0) {
    
    
            this.wait();
        }
        System.out.println(num--);
        this.notifyAll();
    }
}

Lock版

通过Condition替代了Object中睡眠唤醒方法
在这里插入图片描述
通过lock找到Condition
在这里插入图片描述

class Tikect{
    
    

    private int num =0;

    private Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void increment()  {
    
    
        lock.lock();
        try{
    
    
            //防止虚假唤醒 使用while判断
            while (num!=0) {
    
    
                condition.await();//睡眠
            }
            num++;
            condition.signalAll();//唤醒睡眠的线程
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock(); 
        }
       
    }

    public void decrement()  {
    
    
        lock.lock();
        try{
    
    
            while (num==0) {
    
    
                this.wait();
            }
            num--;
            this.notifyAll();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
        
    }
}

Condition实现精准通知唤醒,让线程顺序执行

public class ConditionTest {
    
    
    // A 执行完 唤醒B B执行完 唤醒C 顺序唤醒
    public static void main(String[] args) {
    
    
        TestCondition condition = new TestCondition();
        new Thread(() -> {
    
     condition.prinltTest1(); },"A").start();
        new Thread(() -> {
    
     condition.prinltTest2(); },"B").start();
        new Thread(() -> {
    
     condition.prinltTest3(); },"C").start();
    }
}
class TestCondition{
    
    

    private Lock lock = new ReentrantLock();
    //准备三个监视器
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    private int number =1;
    //使用同一把锁进行加锁
    public void prinltTest1(){
    
    
        lock.lock();
        try {
    
    
            // 判断是否执行-> 执行具体业务 -> 唤醒其他线程
            while (number!=1){
    
    
                condition1.await();//睡眠
            }
            //业务
            number++;
            System.out.println("A执行了");
            //唤醒B
            condition2.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }

    public void prinltTest2(){
    
    
        lock.lock();
        try {
    
    
            // 判断是否执行-> 执行具体业务 -> 唤醒其他线程
            while (number!=2){
    
    
                condition2.await();//睡眠
            }
            //业务
            number++;
            System.out.println("B执行了");
            //唤醒C
            condition3.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }

    public void prinltTest3(){
    
    
        lock.lock();
        try {
    
    
            // 判断是否执行-> 执行具体业务 -> 唤醒其他线程
            while (number!=1){
    
    
                condition3.await();//睡眠
            }
            //业务
            number=1;
            System.out.println("C执行了");
            //唤醒A
            condition1.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }
}

实现精准唤醒-> 需要准备多个监视器,通过condition1.signal();唤醒指定的监视器

集合类不安全

在多线程下操作集合容易出现
java.util.concurrentModificationException并发修改异常!

List

在多线程并发情况下,ArrayList不再具有安全性,此时我们应该使用安全的List

解决方案一:Vector

使用线程安全的类

      //解决方案:
        List<String> list = new Vector<>();

Vector在底层源码中使用synchronized来进行修饰的.,所以它是线程安全的在这里插入图片描述

解决方案二:Collections工具类

使用工具类转化线程不安全的类

        //Collections转化线程安全的类
        List<String> list = Collections.synchronizedList(new ArrayList<>());

解决方案三:CopyOnWriteArrayList

CopyOnWriteArrayList是JUC下线程安全的类

 List<Object> objects = new CopyOnWriteArrayList<>();

CopyOnWrite的意思是:

写入时复制List 简称COW,它是计算机程序设计领域的一种优化策略
多个线程调用时,List读取的时候,固定的,产生写入(覆盖)

copyOnWrite 的解决方案是 在写入时进行复制 避免覆盖 造成的数据问题

CopyOnWriteArrayList比Vector好在哪里呢?

我们查看底层源码可知:
在CopyOnWriteArrayList中使用的是高性能lock锁
在这里插入图片描述

Set

hashSet的底层是什么?

查看底层源码会发现hashSet本质上是一个hashMap
在这里插入图片描述
add方法的本质就是往map中put值
在这里插入图片描述

解决方案一:Collections工具类

与list相似,我们可以使用工具类的方式来转换线程不安全的类

 Set<Object> set = Collections.synchronizedSet(new HashSet<>());

解决方案二:CopyOnWriteArraySet

Set<Object> set1 = new CopyOnWriteArraySet<>();

Map

解决方案一:Hashtable

Map<Object, Object> objectHashtable = new Hashtable<>();

HashTable是线程安全的:
HashTable和HashMap的实现原理几乎一样,
差别:1.HashTable不允许key和value为null;

HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞
在这里插入图片描述

解决方案二:Collections工具类

Collections.synchronizedMap(new HashMap<>());

解决方案三:ConcurrentHashMap

Map<Object, Object> map = new ConcurrentHashMap<>();

JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
在这里插入图片描述
从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,总结如下:

1、JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)

2、JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了

3、JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

4、JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:

  • 低粒度加锁方式,synchronized并不比ReentrantLock差,
    粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  • JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
  • 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存

Callable

简介

在这里插入图片描述
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。

Callable区别:
1.有返回值
2.可以抛出异常
3.方法不同 run()/call()

测试使用

public class CallableTest {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        MyCall call = new MyCall();
        //寻常 多线程是如下方式开启 ,但是在Thread中不能直接传入Callable
       /* new Thread(new Runnable() {
            @Override
            public void run() {

            }
        }).start();*/

        //那么如何执行Callable呢?
        //我们知道在Runnable的实现类FutureTask 构造方法中可以传入Callable 于是可以如下使用
        FutureTask<String> futureTask = new FutureTask<>(call);
        new Thread(futureTask).start();
        //如何获取结果呢?
        String s = futureTask.get();//使用get参数 获取获取结果

        //如果多条线程执行呢?
        new Thread(futureTask).start();
        new Thread(futureTask).start();
        //会发现方法打印只执行了一次
        //原因是Callable有缓存
    }
}

class MyCall implements Callable<String> {
    
    

    @Override
    public String call() throws Exception {
    
    
        System.out.println("执行方法");
        //调用结果时可能会产生阻塞,因为方法内部逻辑的执行是一个耗时操作
        return "ok";
    }
}

在这里插入图片描述

常用辅助类

CountDownLatch减法计数器

在这里插入图片描述

public class Test1 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch count = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
    
    
            new Thread(() -> {
    
    
                System.out.println(">>>"+Thread.currentThread().getName());
                count.countDown();//数量-1
            },String.valueOf(i)).start();
        }
        count.await();//等待计数器归零后才往下执行
        System.out.println("这段代码会等待所有线程执行完毕以后,再执行");
    }
}

CyclicBarrier加法计数器

在这里插入图片描述
CyclicBarrier有两个构造方法,一个只用于计数,一个是计数完可以执行一个线程
在这里插入图片描述

public class Test2 {
    
    
    public static void main(String[] args) {
    
    
        //计数器加到7时 执行线程
        CyclicBarrier barrier = new CyclicBarrier(7,() -> {
    
    
            System.out.println("计数完执行的线程");
        });

        //创建7个线程
        for (int i = 0; i < 7; i++) {
    
    
            new Thread(() -> {
    
    
                System.out.println(Thread.currentThread().getName());

                try {
    
    
                    barrier.await();//线程等待 计数器+1
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

注意点:如果无法计数到指定数量,那么线程会卡死,一直等待下去

Semaphore信号量

在这里插入图片描述

public class Test3 {
    
    
    public static void main(String[] args) {
    
    
        //Semaphore 主要应用于限流
        Semaphore semaphore = new Semaphore(3);
        
        //Semaphore的位置只有3个 现在有6个线程 先拿到通行证的线程先执行 其他线程必须等待释放以后执行
        for (int i = 0; i < 6; i++) {
    
    
            new Thread(() -> {
    
    
                try {
    
    
                    semaphore.acquire();//获取通行证
                    System.out.println(Thread.currentThread().getName());
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("释放通行证");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }finally {
    
    
                    semaphore.release();//释放
                }
            },String.valueOf(i)).start();
        }
    }
}

阻塞队列

在这里插入图片描述

BlockingQueue

在这里插入图片描述
BlockingQueue不是一个新东西,它是跟List,Set同级的集合框架
在这里插入图片描述

四组API

方式 抛出异常 有返回值,不抛异常 等待 超时等待
添加 add offer() put offer(“a”,2, TimeUnit.SECONDS)
移除 remove poll() take poll(2,TimeUnit.SECONDS);
判断队列队首 element peek

抛出异常


    //抛出异常
    public static void a(){
    
    
        //构造方法 需要传入队列大小
        ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);

        //往队列放入
        b.add("a");
        b.add("b");
        b.add("c");
        //队列已满 如果继续放会报错 java.lang.IllegalStateException: Queue full 队列已满异常
        //b.add("d");
        b.element();//查看队首的对象是谁
        b.remove();//弹出
        b.remove();
        b.remove();

        //队列已空 如果继续取出 会报错 java.util.NoSuchElementException
       // b.remove();
    }

不抛出异常,有返回值:

b.element();//查看队首的对象是谁

public static void b(){
    
    
        //构造方法 需要传入队列大小
        ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);

        //往队列放入
        b.offer("a");
        b.offer("a");
        b.offer("a");
        //队列已满 继续放入 返回值为false 没有异常
        b.offer("a");
       b.peek();//查看队首
        b.poll();
        b.poll();
        b.poll();
        //队列已空 如果继续取出 为null  没有异常
        b.poll();

    }

等待

//等待 阻塞(一直阻塞)
    public void c() throws InterruptedException {
    
    
        //构造方法 需要传入队列大小
        ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);

        //往队列放入
        b.put("a");
        b.put("a");
        b.put("a");
        //b.put("a"); 队列已满 一直阻塞

        b.take();
        b.take();
        b.take();
        // b.take(); 队列已空 一直阻塞
    }

等待超时

  //等待 超时等待
    public void d() throws InterruptedException {
    
    
        //构造方法 需要传入队列大小
        ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);

        //往队列放入
        b.offer("a");
        b.offer("a");
        b.offer("a");
        b.offer("a",2, TimeUnit.SECONDS);//超时等待两秒 两秒之后退出

        //取出
        b.poll(2,TimeUnit.SECONDS);
        b.poll(2,TimeUnit.SECONDS);
        b.poll(2,TimeUnit.SECONDS);
        b.poll(2,TimeUnit.SECONDS);//超时等待两秒 取不到值就退出
    }

SynchronousQueue

没有容量,
进去一个元素,必须等待取出来之后,才能再往里面放一个元素!

public static void main(String[] args) {
    
    
        //同步队列
        SynchronousQueue<String> queue = new SynchronousQueue<>();

        new Thread(() -> {
    
    
            try {
    
    
                queue.put("");
                System.out.println("存");
                queue.put("");
                System.out.println("存");
                queue.put("");
                System.out.println("存");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
    
    
            try {
    
    
                queue.take();
                System.out.println("取");
                queue.take();
                System.out.println("取");
                queue.take();
                System.out.println("取");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }).start();
    }

猜你喜欢

转载自blog.csdn.net/weixin_46684099/article/details/118015089
今日推荐