文章目录

JUC并发编程

JUC简介

来源于 java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks 这三个包(简称JUC ),在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

进程、线程的关系
进程:一个程序,QQ.exe Music.exe 程序的集合。
线程:一个进程往往可以包含多个线程,至少包含一个!
Java默认有两个线程
mian 线程
GC 线程
线程的六种状态
新生 NEW
运行 RUNNABLE
阻塞 BLOCKED等待
死死地等 WAITING
超时等待 TIMED_WAITING
终止 TERMINATED
并发、并行的关系:
并发(多线程操作同一个资源)
并行:(多个人一起行走) CPU 多核 ,多个线程可以同时执行;
并发编程的本质:充分利用CPU的资源
Lock锁

Lock锁是一个接口,其所有的实现类为:

ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可重入读写锁中的读锁)
ReentrantReadWriteLock.WriteLock(可重入读写锁中的写锁)
 Lock lock = new ReentrantLock();
 
 public  void doTicket(){ 
    lock.lock();  //加锁
    try {
       System.out.println(Thread.currentThread().getName());
    } catch (Exception e) {
       e.printStackTrace();
    } finally {
       lock.unlock();  // 解锁
    }
 }
Synchronized 和 Lock的区别

来源不同
synchronize => java的内置关键字,在jvm层;Lock =>java的一个接口
获取锁得方式不同
synchronize => 自动获取锁,不能判断锁得状态;Lock => 手动获取锁,可判断是否获取到锁
线程阻塞方面
synchronize => 线程1阻塞会导致线程2永远等待;Lock=>不一定会等下去
锁得类型不同
synchronize=>可重入锁、不可中断、非公平;Lock=>可重入锁、可判断锁、非公平(可设置成公平)
使用范围不同
synchronize=>适用于少量代码块同步;Lock=>适合锁大量的同步代码块

8锁现象

8锁现象,实际对应的就是8个问题。

掌握了这8个问题后:可以清楚判断锁的是谁!永远的知道什么是锁,锁到底锁的是谁!

问题一 : 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test1 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{phone.seedMsg();}, "A").start();
        // 4秒延迟
        TimeUnit.SECONDS.sleep(4);
        // 线程B
        new Thread(()->{phone.call();}, "B").start();
    }
}
class Phone{
    public synchronized void seedMsg(){
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果是:先打印发短信,然后再打电话!

问题二 : 在发短信方法中,延迟4秒,两个线程先打印 发短信 还是 打电话?
public class Test2 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep();
        // 线程B
        new Thread(()->{phone.call();}, "B").start();
    }
}
class Phone{
    public synchronized void seedMsg() throws Exception{
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:还是先打印发短信。

首先知道 锁的对象是谁?因为 synchronized 加在方法上,所以锁的对象是 方法的调用者,所以两个方法用的是同一个锁,谁先拿到谁先执行!

解释:

phone对象,就是方法的调用者,也就是手机,它可以打电话和发短信。
现在有两个人线程A 和 线程B,他们一个想打电话,一个想发短信。
线程A,先拿到锁(也就是手机),抱着锁(手机)睡了4秒。
线程B肯定拿不到锁(手机),需要等待。

问题三 : Phone类增加一个普通方法,线程B调用,那么两个线程先打印 发短信 还是 打电话?
public class Test3 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{ phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        // 线程B
        new Thread(()->{ phone.hello();}, "B").start();
    }
}class Phone{
    //同步方法
    public synchronized void seedMsg(){
        // 1秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //普通方法
    public void hello(){
        System.out.println("hello");
    }
}

结果:先打印hello,然后打印发短信。

解释:

锁的是方法的调用者
现在线程B 调用普通方法,相当于可以远程操控,不需要接收消息

问题四 : 创建两个 phone对象,线程调用不同对象的方法,那么两个线程先打印 发短信 还是 打电话?
public class Test4 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{ phone1.seedMsg(); }, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone2.call(); }, "B").start();
    }
}class Phone{
    //同步方法
    public synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
    //普通方法
    public void hello(){
        System.out.println("hello");
    }
}

结果:先打印打电话。

解释:synchronized用在方法上,那么锁的是方法的调用者。现在有两个调用者,所以互不影响。

问题五 : 将方法变为静态同步方法,那么两个线程先打印 发短信 还是 打电话?
问题六 : 现在有两个对象,调用不同对象的,那么两个线程先打印 发短信 还是 打电话?
public class Test5 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{ phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        // 线程B
        new Thread(()->{ phone.call(); }, "B").start();
        
        // 问题6
        // Phone phone1 = new Phone();
        // Phone phone2 = new Phone();
        // new Thread(()->{ phone1.seedMsg();}, "A").start();
        // new Thread(()->{ phone2.call();}, "B").start();
    }
}class Phone{
   // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 静态同步方法
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印发短信。

解释:5、6的问题一样,对于static静态方法来说,对于整个类Class只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!

问题七 : 资源类中,一个是静态同步方法,一个普通同步方法,那么两个线程先打印 发短信 还是 打电话?
public class Test7 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        new Thread(()->{ phone.seedMsg(); }, "A").start();
         // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone.call(); }, "B").start();
    }
}// 手机
class Phone{
    // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 普通同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印打电话,然后打印发短信

解释:因为一个锁的是Class类,一个锁的是对象调用者。后面那个打电话不需要等待发短信,可以直接运行。

问题八: 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test8 {
    public static void main(String[] args) throws Exception{
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{ phone1.seedMsg(); }, "A").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone2.call(); }, "B").start();
    }
}class Phone{
    // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 普通同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印打电话,然后打印发短信

解释:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。

总结:
new 和 this 是一个对象。
static 和 Class 是唯一的一个模板。
集合类不安全
List不安全

ArrayList 在并发情况下是不安全的!

解决:

Vector就是线程安全的
使用Collections.synchronizedList(new ArrayList<>());
使用 List arrayList = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList:写入时复制, 是计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

Set不安全

和List、Set同级的还有一个BlockingQueue 阻塞队列;

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;
解决:

使用Collections工具类的synchronized包装的Set类
使用CopyOnWriteArraySet 写入复制的JUC解决方案
Map不安全

HashMap基础类也存在并发修改异常!
解决:

使用Collections.synchronizedMap(new HashMap<>());处理;
使用ConcurrentHashMap进行并发处理

创建线程

之前创建线程:
new Thread(()->{ System.out.println("创建handsome线程");},"handsome").start();
Callable创建线程:

Java库具有FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。

注意:使用Callable进行多线程操作,多个线程结果会被缓存,效率高。这个get 方法可能会产生阻塞!把他放到 最后,或者使用异步通信来处理!
Callable接口与Runnable接口的区别:

是否有返回值
是否抛出异常
一个是call(),一个是run()

JUC 常用的辅助类

CountDownLatch

这个类使一个线程等待其他线程各自执行完毕后再执行。

主要方法
countDown 减一操作。
await 等待计数器归零。
public static void main(String[] args) throws InterruptedException {
    //总数6个
    CountDownLatch countDownLatch = new CountDownLatch(6);

    for (int i = 1; i <= 6; i++) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() +" 执行do");
            //每个线程都数量-1
            countDownLatch.countDown();
        },String.valueOf(i)).start();
    }
    //等待计数器归零
    countDownLatch.await();
    System.out.println("必须其他线程都执行完,在执行这里");
    //最后执行的...
}
CyclickBarrier

用于对多个线程任务进行同步执行。

主要方法

await 在所有线程任务都到达之前,线程任务都是阻塞状态

public static void main(String[] args) {
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
        System.out.println("召唤神龙的线程~");
    });

    for (int i=1;i<=7;i++){
        int atI = i;
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" 收集了第" + atI +"颗龙珠");
                cyclicBarrier.await(); //加法计数 等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        },"线程"+i).start();
    }
}
应用:

CyclickBarrier可以根据基于子线程进行处理其他线程的结果,处理比较复杂的业务。并且可以通过reset方法重新执行方法。
CountDownLoatch则必须在主线程才能处理,一般用于任务执行初始化数据

Semaphore
信号量,在信号量定义两种操作:

acquire(获取)当一个线程调用acquire操作,它通过成功获取信号量(信号量-1),有阻塞,直到有线程释放信号量,或者超时。
release(释放)实际上将信号量的值+1,然后唤醒等待的线程。

public static void main(String[] args) {
    //停车位为3个
    Semaphore semaphore = new Semaphore(3);
    // 参数设为1即相当于Synchronized,即可设置占用锁的时间。
    // Semaphore semaphore = new Semaphore(1);

    for (int i=1 ; i<=10; i++){
        int atI = i;
        new Thread(()->{
            try {
                semaphore.acquire(); //得到
                System.out.println(Thread.currentThread().getName() + "  抢到停车位" + atI);
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "  离开停车场");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                semaphore.release();  //释放
            }
        },"线程"+i).start();
    }
}

作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!

读写锁ReadWriteLock

读写锁:更加细粒度的锁
读-读:可以共存
读-写:不能共存
写-写:不能共存
JUC的目的,就是将锁的粒度变的更细,提高并发效率;至少读-读,可以共存

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache mycache = new MyCache();
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i;
            new Thread(()->{ 
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

class MyCache{

    private volatile Map<String,String> map = new HashMap<>();
    //普通锁
    //private Lock lock = new ReentrantLock();
    //使用读写锁
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(String key,String value){
        //写锁
        lock.writeLock().lock();
        try {
            //写入
            System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+" 线程 写入完成");
        } finally {
            lock.writeLock().unlock();
        }
    }

    public String get(String key){
        //读锁
        lock.readLock().lock();
        String o;
        try {
            System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
            o = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 线程 读取完成");
        } finally {
            lock.readLock().unlock();
        }
        return o;
    }
}

对于读取,我们运行多个线程同时读取,也能在一定程度上提高效率。

阻塞队列

ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

线程池
优点:

降低资源消耗
提高响应速度
提高线程的可管理性

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

线程池的使用
Executors:
// 创建线程池
public static void main(String[] args) {
    // 单个线程
    // ExecutorService executorService = Executors.newSingleThreadExecutor(); 
    // 创建一个固定的线程池的大小
    // ExecutorService executorService = Executors.newFixedThreadPool(5);   
    // 可伸缩的
    ExecutorService executorService = Executors.newCachedThreadPool();   
    try {
        for (int i=1; i<=80; i++){
            //使用线程池之后创建线程
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+ " ok");
            });
        }
    } finally {
        executorService.shutdown();
    }
}
源码分析:线程池的真正实现类是ThreadPoolExecutor,有7大参数

阿里巴巴开发手册

ThreadPoolExecutor
自定义创建线程池,参数类型就是7大参数,分别是 核心线程池大小、最大的线程池大小、超时了没有人调用就会释放、超时单位、阻塞队列、线程工厂 创建线程的 一般不用动、拒绝策略
public static void main(String[] args) {
    // 注意:
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() //拒绝策略
    );
    try {
        for (int i=1; i<=80; i++){
            //使用线程池之后创建线程
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+ " ok");
            });
        }
    } finally {
        threadPoolExecutor.shutdown();
    }
}
等待队列已经满了,塞不下新任务,同时,线程池中max线程也达到了,无法继续新任务。这个时候就需要拒绝策略机制
拒绝策略有4种:

AbortPolicy:如果阻塞队列满了,直接抛出异常阻止系统正常运行,队列容量大小 + maxPoolSize
CallerRunsPolicy:如果阻塞队列满了,该策略不会抛弃任务,也不抛出异常,而是将任务回退给调用者
DiscardPolicy:如果阻塞队列满了,丢弃无法处理的任务,不抛出异常,如果允许任务丢失,是最好的策略
DiscardOldestPolicy:如果阻塞队列满了,抛弃队列中等待最久的任务,把当前任务加入队列中再次提交

如何去设置线程池的最大大小如何去设置?

CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小Runtime.getRuntime().availableProcessors() // 获取CPU核数
I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

Java内置核心四大函数式接口

我的学习论坛

HandsomeForum:用Java编写的学习论坛,打造我们自己的圈子!(http://huangjunjie.vip:66)
文章链接:http://huangjunjie.vip:66/blog/read/q6s628g3bfcg79mhug

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐