Java多线程进阶(JUC)

2023-11-10

1.什么是JUC

JUC实际上是Java包的缩写:java.util.concurrent包

2.回顾线程和进程

1.进程:一个程序,例如QQ.exe,进程是程序的集合,进程是CPU调度的基本单位。一个进程可以有多个线程,至少包含一个。Java默认有两个线程,一个是main线程,另外一个是GC线程。

2.线程:线程是依附于进程的,进程可以启动线程,一个进程可以有多个线程;比如QQ同时跟多人聊天;迅雷同时下载多个不同的文件,每个下载都是一个线程;如果主线程结束了,但是还有其他线程,那么进程也不会结束;

3.线程创建方式:继承Thread、实现Runnable、实现Callable; Runnable 没有返回值 效率相比Callable较低

4.并发:并发是指一个cpu在一个时间片里面轮换的执行进程,假的同时,实际上是一个执行一会儿。

5.并行:并行是多核cpu在同时间执行不同的任务,真正的同时。

6.wait和sleep的区别:

  • wait是Java中Object类中的方法,sleep是Thread类中的方法。

  • wait是会释放锁的,而sleep是不会释放锁的,sleep就像是一个人抱着锁睡觉,但是wait则是把锁放在旁边进行等待。

  • wait只能在同步代码块中使用,sleep可以在任何地方使用。

3.管程的概念

管程实际上就是一个监视器(Monitor),就是我们所说的锁,换句话说:锁就是监视器。监视器本身是一种同步的机制,保证同一时间只有一个线程访问特定的数据。

jvm同步基于进入和退出的,使用管程对象实现的,每个对象都会有一个Monitor对象,Monitor会随着java对象的创建而创建。

4.用户线程和守护线程的概念

用户线程:平时所用到的线程,基本上都是用户线程。

守护线程:后台所用到的线程,比如垃圾回收机制(GC),当用户线程销毁,守护线程也会销毁。可以这样理解:人(用户线程)在塔在,人不在了,塔(守护线程)也就没了。

5.Lock接口

首先介绍一下多线程编程的一般步骤:

  1. 创建资源类,在资源类中创建属性和操作方法

  2. 在资源类操作方法

    • 判断

    • 干活

    • 通知

  3. 创建多个线程,调用资源类的操作方法

Lock简介(来自官方文档):

  1. Lock实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。这里的condition是用来线程通讯的,相当于wait()和notify()。

  2. Lock的基本使用方法(来自官方文档):

    只需要在资源类里面,增加一个lock的成员变量,然后调用对应的上锁和解锁方法就能实现。

    注意:上锁之后,业务代码建议放在try{}finally{}中,要保证不管业务实现与否,都要释放锁,不然会造成死锁

    Lock l = ...; //创建一个lock锁
         l.lock();//加锁
         try {
             // access the resource protected by this lock
         } finally {
             l.unlock();//解锁
         }

  3. Lock的三个实现类:

    • ReentrantLock:一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

    • ReentrantReadWriteLock.WriteLock:可重用写锁

    • ReentrantReadWriteLock.ReadLock:可重用读锁

  4. 可重入锁:

    可以重复使用的锁。例如上厕所,一个人进入测试然后上锁,出来后解锁,另外一个人又进去上锁,然后出来解锁。。。这就叫可重入锁。

  5. 买票小案例:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    ​
    /**
     * 这是静态资源类
     */
    class Ticket{
        //票的数量
        private int number = 30;
        //声明一个可重用锁
        private final Lock lock = new ReentrantLock();
    ​
        public void sale(){
            lock.lock();//上锁,保证只有一个线程能操作
            try {
                if (number>0)
                    number--;
                    System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+number);
            } finally {
                lock.unlock();//在finally里面解锁,让其他线程能争抢锁
            }
        }
    }
    public class SaleTicket {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();//创建一个资源类的对象
            new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"AA").start();
            new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"BB").start();
            new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"CC").start();
        }
    }

    运行结果:

6.线程通讯

问题:消费者和生产者问题,生产一个,则消费一个。对一个资源变量(初始为0)进行加一和减一的操作。一个线程加一,另一个线程减一。

如果不使用线程之间的通讯,则无法实现上诉功能,基本思路是:A线程加一之后,通知B线程来减一。B线程减一之后,通知A线程来加一。

两种实现方法:

  1. 使用synchronized锁配合Object类中的wait()和notify()实现

  2. 使用lock锁配合condition对象的await()和condition.signal()实现

    condition对象的获取:

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

这里使用第二种方式

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
class MyData{
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
​
    public void add() throws InterruptedException {
        lock.lock();
        try {
            if (num != 0) {
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+"减少了--"+num);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
​
    public void sub() throws InterruptedException {
        lock.lock();
        try {
            if (num == 0) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"减少了--"+num);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}
public class PC {
    public static void main(String[] args) {
        MyData myData = new MyData();
        //用于增加的线程
        new Thread(()->{for (int i = 0; i < 10; i++) {
            try {
                myData.add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        },"AA").start();
        //用于减少的线程
        new Thread(()->{for (int i = 0; i < 10; i++) {
            try {
                myData.sub();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        },"BB").start();
    }
}

运行结果:

可以看到,是0101交替执行的

问题:这里只有两个线程,所以没事,但是如果是同时有4个线程来操作,两个线程加,两个线程减。这种情况则会出现一个问题——虚假唤醒的问题。

四个线程运行结果:

可以看到4个线程运行结果,并不是0101交替执行

虚假唤醒问题:

产生原因:上诉例子中,假设A线程和C线程都是增加的线程,其中一个线程进入if判断之后,然后休眠了。

下次被唤醒的时候,则不会再进行if判断了,因为wait的机制是,在哪里休眠,就在哪里唤醒。所以下次唤醒的时候,不管num是否为0,都会继续执行后面的代码。

解决方案:通过Java官方文档的内容,要避免产生虚假唤醒问题,则需要将if判断换成while判断。下面代码来自官方文档:

//等待应总是发生在循环中,如下面的示例:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
     }

7.线程间的定制化通讯

问题:A线程唤醒B线程,B线程唤醒C线程,C线程唤醒A线程。

这种通讯并不是和以前一样,唤醒所有的线程或者是唤醒其中一个线程,而是唤醒指定的线程。

实现思路:

package com.wang.lock;
​
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
//创建资源类
class ShareData{
    //定义标志位
    private int flag = 1;
    private final Lock lock = new ReentrantLock();
    //创建三个Condition
    private final Condition c1 = lock.newCondition();
    private final Condition c2 = lock.newCondition();
    private final Condition c3 = lock.newCondition();
​
    public void doItA() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (flag!=1){
                c1.await();
            }
            System.out.println(Thread.currentThread().getName()+"--");
            //通知
            flag = 2;
            c2.signal();
        }finally {
            lock.unlock();
        }
    }
    public void doItB() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (flag!=2){
                c2.await();
            }
            System.out.println("    "+Thread.currentThread().getName()+"--");
            //通知
            flag = 3;
            c3.signal();
        }finally {
            lock.unlock();
        }
    }
    public void doItC() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (flag!=3){
                c3.await();
            }
            System.out.println("    "+"    "+Thread.currentThread().getName()+"--");
            //通知
            flag = 1;
            c1.signal();
        }finally {
            lock.unlock();
        }
    }
}
​
public class ThreadDemo3 {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        new Thread(()->{
            try {
                for (int i = 0; i < 3; i++) {
                    shareData.doItA();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        new Thread(()->{
            try {
                for (int i = 0; i < 3; i++) {
                    shareData.doItB();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();
        new Thread(()->{
            try {
                for (int i = 0; i < 3; i++) {
                    shareData.doItC();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"C").start();
    }
}

运行结果

可以看到ABC三个线程是交替执行的

ArrayList、HashSet、HashMap的线程不安全问题

我们都知道ArrayList是线程不安全的,当我们用多个线程去操作ArrayList的时候,就会产生并发安全异常。

解决方案:

  1. 使用Vector

    • 实际开发中并不建议使用,这个方法是java1.0时候的,其原理就是给其中的方法加上synchronized关键字。属于无脑解决线程安全问题。

    • 实际效率不高

  2. 使用Collections工具类

    • 使用其synchronizedList()方法返回一个线程安全的List

    • List<String> list = Collections.synchronizedList(new ArrayList<>());

    • 也不推荐使用

  3. 使用CopyOnWriteArrayList解决

    • 这个类是JUC里面的类

    • 原理是:写时复制技术,读的时候并发读,写的时候独立写。

      • 首先读的时候是很多个线程一起读

      • 但是写的时候,先复制一份跟之前的内容相等的集合,然后在新的集合开始写,写完之后再跟以前的进行合并

      • 再读的时候,就是读的新的集合

    • 好处是:既进行了并发读,又可以进行独立写,没有并发安全

    • //Java源码是这样的:
      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();
              }
          }

同理:当我们用多个线程去操作HashSet的时候,就会产生并发安全异常。

解决方案:使用CopyOnWriteArraySet解决。

同理:当我们用多个线程去操作HashMap的时候,就会产生并发安全异常。

解决方案:使用ConcurrentHashMap解决。

多线程锁

  1. 公平锁和非公平锁

    • 非公平锁:会造成线程被“饿死”的情况,可能出现就是一个线程把活都干了,其他线程没有做事情的情况。大致可以这样理解,非公平锁在线程释放锁之后,又继续参与抢锁,然后又抢到了锁。

    • 公平锁就是线程都会拿到“活”干

    • 两个锁的优缺点:

      公平锁:阳光普照,各个线程都能分配到任务,效率相对较低。因为在执行任务的时候,公平锁会先判断一下是否有被占用,如果被占用则进行排队,如果没有再进去。

      非公平锁:线程饿死,效率高。

  2. 可重入锁:

    • synchronized(隐式)和Lock(显示)都是可重入锁

    • 介绍:可重入锁就是如果进入一个锁之后,里面还有锁,则可以随便进入,实际上就是加的同一把锁,开了一次锁就不用再开了。例如我回家了,大门有个锁,开锁之后进了大门,里面的卧室门就不需要开开锁了。

      public class Demo1 {
          public static void main(String[] args) {
              Object o = new Object();
              new Thread(()->{
                  synchronized (o){
                      System.out.println(Thread.currentThread().getName()+"--外层");
      ​
                      synchronized (o){
                          System.out.println(Thread.currentThread().getName()+"--中层");
      ​
                          synchronized (o){
                              System.out.println(Thread.currentThread().getName()+"--内层");
                          }
                      }
                  }
              },"T1").start();
          }
      }

      可以看到,锁里面还有锁,但是T1线程可以自由的进入

死锁

什么是死锁:

两个或者两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待现象,如果没有外力干涉,他们无法继续执行下去。

造成死锁的原因:

  1. 系统资源不足

  2. 进程推进顺序不合适

  3. 资源分配不当

1 . 产生死锁的必要条件:

(1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。

(2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。

(3)不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 (4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

Callable接口实现创建线程

除了通过继承Thread类和实现Runnable接口,还有另外一种创建线程的方式。

通过Callable接口和Runnable的区别:

  • Callable可以实现线程在完成时,有返回的结果方法call()。

  • call()方法可以引发异常,而run方法不能

  • 实现Callable接口,必须重写call方法

Callable创建线程:

  1. 实现Callable接口并且重写call方法

  2. 借助FutureTask创建线程

    class MyThread2 implements Callable {
    ​
        @Override
        public Integer call() throws Exception {
            return 200;
        }
    }
    ​
    public class Demo1 {
        public static void main(String[] args) {
            //Runnable创建线程
            new Thread(new MyThread1(),"AA").start();
            //Callable创建线程
    ​
            //1.借助FutureTask创建线程
            FutureTask<Integer> task = new FutureTask<>(new MyThread2());
            //2.使用lam表达式
            FutureTask<Integer> task1 = new FutureTask<>(()->{
               return 111;
            });
            
            new Thread(task,"BB").start();
            new Thread(task1,"CC").start();
            System.out.println(task.get());
        }
    ​
    }

    通过FutureTask的get方法,能够拿到线程的返回值,如果没有则一直等待。

    通过FutureTask的isDone方法,能够判断线程是否已经执行完毕。

JUC中的三大辅助类

减少计数类:CountDownLatch

  • CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减一操作。还有一个await方法,这个方法表示一个等待,线程会阻塞,只有到计数器里面的数变为0的时候,才会继续下面的代码。

    • 当一个或者多个线程调用await方法时,这些线程会阻塞

    • 其他线程调用countDonw方法会将计数器减一

    • 当计数器的值变为0的时候,被await阻塞的线程就会被唤醒,继续执行

案例:放学了,有3个同学还在办公室做作业,只有当所有同学做完作业,离开办公室之后,门卫才可以锁门,否则门卫只能一直处于等待状态。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建countDownLatch类来实现计数
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //模拟有三个同学
        for (int i = 1; i <=3; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"--出教室了");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        //主线程模拟门卫
        System.out.println("我锁门了");
    }
}

执行结果

作用:可以用来线程直接的通讯。

循环栅栏CyclicBarrier类

  • 官方文档:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

  • CyclicBarrier可以看作是循环阻塞的意思,在使用中CyclicBarrier的构造方法可以传入一个目标障碍数,每次执行CyclicBarrier一次,障碍数会增加,如果达到了目标障碍数,才会执行CyclicBarrier中await之后的代码。可以将CyclicBarrier理解为加一的操作

    • public class CyclicBarrierDemo {
          public static void main(String[] args) {
              CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
                  System.out.println("阻塞达到了七次后输出这句话");
              });
              for (int i = 0; i < 7; i++) {
                  new Thread(()->{
                      System.out.println(Thread.currentThread().getName()+"被阻塞了...");
                      try {
                          cyclicBarrier.await();
                      } catch (InterruptedException | BrokenBarrierException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName()+"出阻塞了");
                  },String.valueOf(i)).start();
              }
          }
      }

      执行结果:

    • 通过代码我们可以发现,只有其余线程,总共调用了7次await方法,上面定义在CyclicBarrier类中的代码才会执行;如果删掉cyclicBarrier.await(),则不会执行那句话。

    • 当数量达到7次之后,被阻塞的7个线程又会从wait后面继续执行

信号灯Semaphore

  • 官方文档:一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

  • 有多个线程执行任务,然后都要获得Semaphore的许可证,许可证可以有多个,只有拥有许可证的线程才能够继续执行后面的业务代码,没获得许可证的就不能继续执行,只能等待别人释放许可证。

    public class SemaphoreDemo {
        public static void main(String[] args) {
            //创建一个信号灯类,然后给里面初始化三张许可证
            Semaphore semaphore = new Semaphore(3);
            //创建六个线程去争抢许可证
            for (int i = 0; i < 6; i++) {
                new Thread(()->{
                    try {
                        //抢占许可证
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName()+"抢到了许可证");
    ​
                        //模拟三秒钟的业务时间
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //释放许可证
                        semaphore.release();
                    }
                }).start();
            }
        }
    }

    运行结果:

只有三个线程抢到了许可证,剩余的线程则在等待,三秒后前三个释放许可证,后三个抢到许可证

JUC读写锁

首先先介绍几种锁:

悲观锁:

悲观锁就是,线程在操作某个数据的时候,首先先上锁。然后别的线程只能阻塞。顾名思义,悲观锁很悲观,他认为我在操作数据的时候,一定有人来更改数据,所以一开始就上锁了。优点:能解决并发的各种问题;缺点:效率低;

乐观锁:

乐观锁就是,线程在操作某个数据的时候,先不上锁,拿到该数据的版本号,等线程操作完之后判断版本号是否有改变,如果没有改变,则操作成功,如果失败则操作失败。操作成功更改版本好

乐观锁,就很乐观,认为我在操作数据的时候,不会有人来动我的数据,所以我第一时间并不上锁,等我操作完毕要提交的时候,再判断一下有人改过我数据了吗,如果没有改过则操作成功,反之操作失败。

表锁:

表锁就是,假设对数据库的一张表进行操作,如果线程对一张表里面的一条记录进行操作,但是把整个表都锁了,这就是表锁。表锁不会发生死锁。

行锁:

操作一条记录只对该记录对应的行上锁,就是行锁。行锁可能会发生死锁

读锁:共享锁,发生死锁。

写锁:独占锁,发生死锁

java通过ReentrantReadWriteLock来获得读写锁

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
​
class MyCatch{
    private final Map<String, Integer> map = new HashMap<>();
​
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
​
​
    public void put(String key,Integer value) {
        rw.writeLock().lock();//写锁上锁
        try {
            System.out.println(Thread.currentThread().getName()+"正在写操作"+key);
            TimeUnit.SECONDS.sleep(3);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rw.writeLock().unlock();//写锁解锁
        }
    }
    public int get(String key) {
        rw.readLock().lock();//读锁上锁
        int integer = -1;
        try {
            System.out.println(Thread.currentThread().getName()+"正在读操作"+key);
            TimeUnit.SECONDS.sleep(3);
            integer = map.get(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rw.readLock().unlock();//读锁解锁
        }
        return integer;
    }
}
​
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCatch myCatch = new MyCatch();
​
        for (int i = 0; i < 5; i++) {
            int num = i;
            new Thread(()->{
                myCatch.put(num+"",num);
            }).start();
        }
​
        for (int i = 0; i < 5; i++) {
            int num = i;
            new Thread(()->{
                myCatch.get(num+"");
            }).start();
        }
    }
}

运行结果:

可以看到,写锁是一个一个去写的,一个线程在写的时候,别人不能进入。

但是读锁是一起读的。

读写锁的深入

读写锁:一个资源可以被多个读的线程访问,或者可以被一个写的线程访问,但是不能同时存在读写操作,读写是互斥的,读读是共享的。

演变:

锁降级:

将写入锁降级为读锁

  • 先获取到写锁

  • 再获取读锁

  • 释放写锁

  • 完成锁降级

  • 释放读锁

阻塞队列

  • 概念:阻塞队列是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另一端输出;

    • 当队列是空的,从队列中获取元素将被阻塞
    • 当队列是满的,从队列中添加元素将被阻塞

  • 阻塞队列的分类:

    • ArrayBlockingQueue:数组结构组成的一个有界队列

    • LinkedBlockingQueue:由链表组成的一个有界队列

    • DelayQueue:使用优先级队列实现的延迟无界队列

    • PriorityBlockingQueue:支持优先级排序的无界阻塞队列

    • 。。。

  • 阻塞队列的核心方法:

    • add:添加

    • remove:取出

    • element:判断是否存在

    • offer:添加,返回true和false

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java多线程进阶(JUC) 的相关文章

随机推荐