讨伐Java多线程与高并发——JUC篇

本文是学习Java多线程与高并发知识时做的笔记。

这部分内容比较多,按照内容分为5个部分:

  1. 多线程基础篇
  2. JUC篇
  3. 同步容器和并发容器篇
  4. 线程池篇
  5. MQ篇

本篇为JUC篇。

目录

1 什么是JUC?

2 并发与并行

3 使用Callable创建线程

4 Lock

4.1 Lock锁

4.2 公平锁和非公平锁

4.3 Lock锁的生产者和消费者问题

4.4 读写锁

5 线程八锁

6 常用的辅助类

6.1 CountDownLatch

6.2 CyclicBarrier

6.3 Semaphore


1 什么是JUC?

java.util.concurrent,Java并发工具包。主要包括:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

2 并发与并行

并发:多个线程快速地轮换执行,使得在宏观上具有多个线程同时执行的效果。

并行:多核CPU下,多个线程同时执行。

3 使用Callable创建线程

在java.util.concurrent中提供了一个Callable接口用来创建线程。

比起实现Runnable接口创建线程,实现Callable接口创建的线程:

  • 可以有返回值
  • 可以抛出异常

代码演示:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CreateThread {
    static class MyCallable implements Callable<Integer> { //泛型规定返回值类型
        @Override
        public Integer call() throws Exception { //重写call()方法,类似于Runnable接口中的run()方法
            System.out.println("Hello MyCallable");
            return 1024;
        }
    }
 
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask futureTask = new FutureTask(myCallable); //适配器模式
        new Thread(futureTask).start(); //调用start()方法启动线程
        //打印返回值
        Integer result = (Integer) futureTask.get();
        System.out.println(result);
    }
}

需要注意的是,Integer result = (Integer) futureTask.get(); 这行代码可能会导致线程阻塞。

4 Lock

4.1 Lock锁

除了使用synchronized关键字,JUC中提供了另一种线程同步方法——Lock锁。

Lock是java.util.concurrent.locks包中的接口,它共有3个实现类:

  • ReentrantLock,可重入锁
  • ReentrantReadWriteLock.ReadLock,可重入读锁
  • ReentrantReadWriteLock.WriteLock,可重入写锁

使用Lock锁的3个步骤:

  1. Lock lock = new ReentrantLock()
  2. lock.lock()
  3. lock.unlock(),如果不调用unlock()方法释放锁资源,会造成死锁,一般放在finally代码块中。

代码演示:

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

public class Test {
    public static void main(String[] args) {
        MyLock myLock = new MyLock();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                myLock.test();
            }).start();
        }
    }
}

class MyLock {
    private int number = 10;
    Lock lock = new ReentrantLock();

    public void test() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + " 操作后,number = " + --number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

Thread-0 操作后,number = 9
Thread-3 操作后,number = 8
Thread-4 操作后,number = 7
Thread-5 操作后,number = 6
Thread-7 操作后,number = 5
Thread-1 操作后,number = 4
Thread-2 操作后,number = 3
Thread-6 操作后,number = 2
Thread-8 操作后,number = 1
Thread-9 操作后,number = 0

4.2 公平锁和非公平锁

公平锁:多个线程完全按照申请锁的顺序去获得锁,新来的线程会直接进入等待队列去排队。

非公平锁:新来的线程会尝试获取锁,如果获取不到,再进入等待队列。

synchronized锁是非公平锁;lock锁默认是非公平锁,也可以使用公平锁。

lock使用公平锁:

Lock lock = new ReentrantLock(true); 

4.3 Lock锁的生产者和消费者问题

synchronized锁使用Object类中的监视器方法(wait、notify、notifyAll)实现了线程之间的通信,java.util.concurrent.locks包中提供了类似的监视器方法。

使用Lock锁监视器的步骤:

  1. Lock lock = new ReentrantLock()
  2. Condition condition = lock.newCondition()
  3. lock.lock()
  4. condition.await(),线程等待,总是出现在循环中。
  5. condition.signalAll(),通知其它线程。
  6. lock.unlock(),一般写在finally代码块中。

生产者和消费者问题:

由多个线程同时操作同一个变量number:

  • 生产者每次操作,number++
  • 消费者每次操作,number--

当number == 0时,消费者不能对其进行操作。

代码演示:

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

public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        int n = 4; //消费者数目
        int k = 5; //每个消费者的消费额
        new Thread(() -> {
            for (int i = 0; i < n * k; i++) {
                data.increment();
            }
        }, "Producer").start(); //生产者
        for (int id = 0; id < n; id++) {
            new Thread(() -> {
                for (int i = 0; i < k; i++) {
                    data.decrement();
                }
            }, "Consumer" + id).start(); //消费者们
        }
    }
}

class Data {
    private int number = 0;
    Lock lock = new ReentrantLock(); //创建锁
    Condition condition = lock.newCondition(); //创建监视器

    public void increment() {
        try {
            lock.lock();
            while (number > 3) {
                condition.await(); //线程等待
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            condition.signalAll(); //通知其它线程
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        try {
            lock.lock();
            while (number <= 0) {
                condition.await(); //线程等待
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            condition.signalAll(); //通知其它线程
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4.4 读写锁

读写锁ReadWriteLock本质上是一种细粒度的Lock锁,又分为读锁和写锁:

  • 读锁:又叫共享锁,可以被多个线程同时占有。用于只读操作。
  • 写锁:又叫独占锁、排他锁,只允许被一个线程占有。用于写操作。

在多个线程对同一个资源进行读写操作时:

  • 允许多个线程同时执行读操作
  • 不允许多个线程同时执行写操作
  • 不允许读操作和写操作同时执行

使用读写锁可以满足上述要求。

使用读锁的步骤:

  • ReadWriteLock readWriteLock = new ReentrantReadWriteLock()
  • readWriteLock.readLock().lock()
  • readWriteLock.readLock().unlock(),一般写在finally代码块中。

使用写锁的步骤:

  • ReadWriteLock readWriteLock = new ReentrantReadWriteLock()
  • readWriteLock.writeLock().lock()
  • readWriteLock.writeLock().unlock(),一般写在finally代码块中。

代码演示:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * ReadWriteLock
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp);
            }).start();
        }
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "读取结果:" + myCache.read(temp + ""));
            }).start();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void write(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public Object read(String key) {
        Object o;
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取");
            o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完成");
        } finally {
            readWriteLock.readLock().unlock();
        }
        return o;
    }
}

运行结果:

Thread-0写入
Thread-0写入完成
Thread-2写入
Thread-2写入完成
Thread-1写入
Thread-1写入完成
Thread-3写入
Thread-3写入完成
Thread-4写入
Thread-4写入完成
Thread-5读取
Thread-5读取完成
Thread-9读取
Thread-9读取完成
Thread-5读取结果:0
Thread-6读取
Thread-6读取完成
Thread-8读取
Thread-8读取完成
Thread-9读取结果:4
Thread-7读取
Thread-7读取完成
Thread-7读取结果:2
Thread-8读取结果:3
Thread-6读取结果:1

5 线程八锁

线程八锁并不是八把实际的锁,而是八个关于锁的经典问题。

第一问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        new Thread(() -> {
            client.actionA();
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client.actionB();
        }).start();
    }
}

class Client {
    public synchronized void actionA() {
        System.out.println("执行操作A");
    }

    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作A。毫无疑问。

第二问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        new Thread(() -> {
            client.actionA();
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client.actionB();
        }).start();
    }
}

class Client {
    public synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2); //线程阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }

    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作A。虽然线程A阻塞了2秒,但在2秒前就已经拿到了锁资源。

第三问:下面代码中,先执行操作A、操作B还是操作C?

import java.util.concurrent.TimeUnit;

public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        new Thread(() -> {
            client.actionA();
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client.actionB();
        }).start();
        new Thread(() -> {
            client.actionC();
        }).start();
    }
}

class Client {
    //同步方法
    public synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }
    //同步方法
    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
    //普通方法
    public void actionC() {
        System.out.println("执行操作C");
    }
}

答:先操作C,然后操作A,最后操作B。普通方法不用拿到锁资源就可以执行。

第四问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Client client1 = new Client();
        Client client2 = new Client();
        new Thread(() -> {
            client1.actionA(); //client1调用
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client2.actionB(); //client2调用
        }).start();
    }
}

class Client {
    public synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }

    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作B。线程加锁的目标是对象,不同对象之间不会出现锁冲突。

第五问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        new Thread(() -> {
            client.actionA();
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client.actionB();
        }).start();
    }
}

class Client {
    //静态同步方法
    public static synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }
    //静态同步方法
    public static synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作A。需要注意的地方是静态方法上锁的对象是Client.class。

第六问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Client client1 = new Client();
        Client client2 = new Client();
        new Thread(() -> {
            client1.actionA(); //client1调用
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client2.actionB(); client2调用
        }).start();
    }
}

class Client {
    //静态同步方法
    public static synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }
    //静态同步方法
    public static synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作A。静态方法上锁的对象是Client.class。

第七问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        new Thread(() -> {
            client.actionA();
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client.actionB();
        }).start();
    }
}

class Client {
    //静态同步方法
    public static synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }
    //同步方法
    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作B。毫无疑问。

第八问:下面代码中,先执行操作A还是操作B?

import java.util.concurrent.TimeUnit;

public class Test8 {
    public static void main(String[] args) throws InterruptedException {
        Client client1 = new Client();
        Client client2 = new Client();
        new Thread(() -> {
            client1.actionA(); //client1调用
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            client2.actionB(); //client2调用
        }).start();
    }
}

class Client {
    //静态同步方法
    public static synchronized void actionA() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行操作A");
    }
    //同步方法
    public synchronized void actionB() {
        System.out.println("执行操作B");
    }
}

答:操作B。毫无疑问。

6 常用的辅助类

6.1 CountDownLatch

CountDownLatch是一种通用的同步工具。它的核心思想是:

允许一个或多个线程等待,直到在其它线程中的一组操作完成。

CountDownLatch类中的关键方法有:

  • countDown(),每次调用countDown(),CountDownLatch计数器的值-1。
  • await(),当前线程等待,直到CountDownLatch计数器归零后,唤醒当前线程。

代码演示:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int count = 5;
        CountDownLatch countDownLatch = new CountDownLatch(count); //设置计数器countDownLatch的初始值

        for (int i = 0; i < count; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName());
                countDownLatch.countDown(); //countDownLatch的值-1
            }).start();
        }
        countDownLatch.await();//等待countDownLatch归零,再往下执行

        System.out.println("ok");
    }
}

6.2 CyclicBarrier

CyclicBarrier也是一种通用的同步工具。它的核心思想是:

允许一组线程全部等待彼此达到共同屏障点。

CyclicBarrier类中的关键方法有:

  • await()

代码演示:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int count = 7;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(count, () -> { //设置计数器cyclicBarrier的阈值
            System.out.println("召唤神龙成功");
        });

        for (int i = 0; i < count; i++) {
            //lambda表达式不能直接操作 i
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集到" + (temp + 1) + "星球");
                try {
                    cyclicBarrier.await(); //cyclicBarrier的值+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "继续工作");
            }).start();
        }
    }
}

运行结果:

Thread-0收集到1星球
Thread-4收集到5星球
Thread-2收集到3星球
Thread-3收集到4星球
Thread-1收集到2星球
Thread-5收集到6星球
Thread-6收集到7星球
召唤神龙成功
Thread-6继续工作
Thread-0继续工作
Thread-4继续工作
Thread-3继续工作
Thread-2继续工作
Thread-5继续工作
Thread-1继续工作

6.3 Semaphore

Semaphore,信号量,通常用于限制线程数。

信号量维持一组许可证。如果有必要,每个线程都会阻塞,直到获得许可证。

Semaphore类中的关键方法有:

  • acquire(),当前信号量-1,如果当前信号量为0,则等待。
  • release(),当前信号量+1。

代码演示:

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {
        int count = 2;
        Semaphore semaphore = new Semaphore(count);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "得到许可,开始了它的表演");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "表演结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}

运行结果:

Thread-0得到许可,开始了它的表演
Thread-1得到许可,开始了它的表演
Thread-1表演结束
Thread-0表演结束
Thread-2得到许可,开始了它的表演
Thread-3得到许可,开始了它的表演
Thread-2表演结束
Thread-3表演结束
Thread-4得到许可,开始了它的表演
Thread-4表演结束
 

学习视频链接:(这个讲的挺好的)

https://www.bilibili.com/video/BV1B7411L7tE

加油!(ง •_•)ง

猜你喜欢

转载自blog.csdn.net/qq_42082161/article/details/113868802