JUC高并发编程

目录

一、JUC简介

什么是JUC

进程和线程的概念

进程状态

wait()/sleep()的区别

并发与并行

管程

用户线程和守护线程

二、Lock接口

三、线程间通信

四、集合的线程安全

五、多线程锁

锁的八个问题

公平锁和非公平锁

可重入锁(递归锁)

死锁

六、Callable&Future接口

七、JUC三大辅助类

减少计数CountDownLatch

循环栅栏CyclicBarrier

信号灯Semaphore

八、读写锁

九、阻塞队列

BlockingQueue简介

BlockingQueue核心方法

常见的BlockingQueue

十、ThreadPool线程池

线程池简介

线程参数说明

线程池的种类与创建

底层工作原理

自定义线程池

十一、Fork/Join

十二、CompletableFuture


一、JUC简介

什么是JUC

JUC是java.util.concurrent工具包的简称,是处理线程的工具包,从java1.5开始出现

进程和线程的概念

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程是操作系统能够进行运算调度最小单位。被包含在进程中,是进程中的实际运作单位。

进程状态

NEW(新建):调用new()创建一个线程后,处于新建状态。此时该线程仅有Java虚拟机为其分配内存并初始化其成员变量,还没有表现出任何线程的动态特征。

RUNNABLE(就绪):当线程对象调用start()后,线程处于就绪状态。java虚拟机为其创建方法调用栈好热程序计数器,等待JVM里的线程调度器调度。一旦获得cpu,则执行run()方法中的执行体。

BLOCKED(阻塞):线程执行过程被中断,等待某个监视器锁的线程。当发生如下情况时,线程进入阻塞状态

WAITING(等待):一个正在等待另一个线程执行的某个操作的线程处于这一状态

TIME_WAITING(超时等待):一个正在限时等待另一个线程执行某一动作的线程处于这一状态

TERMINATED(终止):线程完成执行进入终止状态

wait()/sleep()的区别

sleep()是Thread的静态方法,wait()是Object的方法,任何对象实例都能调用

sleep(),它不需要占用锁。而wait()会释放锁,并通过notify()/notifyALL()重新获取锁

sleep()可以在任何地方调用,而wait()只能在同步方法或者同步代码块中调用

并发与并行

串行模式:串行表示所有任务都按先后顺序进行。串行是一次只能取得一个任务,并执行这个任务

并行模式: 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。

并发:并发指的是多个程序可以同时运行的现象,更细化的是多线程可以同时运行或多指令可以同时运行

管程

是保证了同一时刻只有一个线程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用,但是这样并不能保证进程以设计的顺序执行

JVM 中同步是基于进入和退出管程 (monitor) 对象实现的,每个对象都会有一个管程 (monitor) 对象,管程 (monitor) 会随着 java 对象一同创建和销毁。

执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程。

用户线程和守护线程

用户线程:普通线程,自定义线程

守护线程:运行在后台,是一种特殊的线程,比如垃圾回收。

二、Lock接口

Synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有

        修饰代码块:被修饰的代码块称为同步语句块,起作用范围是{}括起来的代码,作用的对象是调用这个代码块的对象;

        修饰一个方法:被修饰的方法称为同步方法,其作用范围是整个方法。作用对象是调用这个方法的对象

        修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

        修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。

Lock和Synchronized不同

        Lock是一个接口,而synchronized是java中的一个关键字,synchronized是内置语言的实现

        synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象;但是Lock在发生异常时,必须手动释放锁,很可能造成死锁现象,因此必须在finally中手动释放锁

        lock可以让等待锁的线程响应中断,而synchrionized却不行,使用synchronized时,等待的线程会一直等待下去,不能响应中断

        Lock可以知道线程有没有成功获取锁,而synchronized则无法办到

        Lock可以提高多个线程指向读操作的效率

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源

非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized

多线程的编程步骤

创建资源类,创建属性和操作方法

创建多线程调用资源类的方法

为了控制多线程执行顺序,在操作方法中:判断,干活,通知

三、线程间通信

Object notifyAll() 方法用于唤醒在该对象上等待的所有线程。

三、线程间通信

四、集合的线程安全

线程不安全集合案例:ArrayList

UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,是由一个十六位的数字组成,表现出来的 形式。

package com.company;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Main {
    public static void main(String[] args) {
            List list = new ArrayList();
            for (int i = 0; i < 30; i++) {
                new Thread(() -> {
                    list.add(UUID.randomUUID().toString());
                    System.out.println(list);
                }, "线程" + i).start();
            }
            // write your code here
        }
}

报并发修改异常:Exception in thread “线程5” java.util.ConcurrentModificationException

原因:ArrayList的add()方法并没有Synchronized修饰,未加锁

解决方案:Vector ,Collections,CopyOnWriteArrayList

Vector 是矢量队列

Collections提供了方法synchronizedList保证list是同步线程安全的

CopyOnWriteArrayList:相当于是线程安全的ArrayList是个就、可变数组有以下特性:

独占锁效率低:采用读写分离思想解决

复制思想:**当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容

器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素

之后,再将原容器的引用指向新的容器。

package com.company;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class ArrayDemo {
    public static void main(String[] args) {
        //List list = new Vector();

        //Collections解决办法
        //List list=Collections.synchronizedList(new ArrayList<>());

        //CopyOnWriteArrayList解决办法
        List list=new CopyOnWriteArrayList();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString());
                System.out.println(list);
            }, "线程" + i).start();
        }
    }
}

五、多线程锁

锁的八个问题

class Phone {
    public synchronized void SMS() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendSMS");
    }
    public synchronized void Email(){
        System.out.println("sendEmail");
    }
    public void Hello(){
        System.out.println("sendHello!");
    }
}
public class Eclock{
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.SMS();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();

        new Thread(()->{
            phone.Email();
        },"BB").start();
    }
}

标准同步方法先打印短信还是邮件

sendSMS

sendEmail

停留4秒在短信方法内,先打印短信还是邮件

sendSMS

sendEmail

新普通的Hello方法,停四秒在短信方法内,先打印短信还是Hello

sendHello

sendSMS

sendEmail

现在是俩个手机对象,停4秒在短信方法内,先打印短信还是邮件

sendEmail

sendSMS

两个静态同步方法,一部手机,先打印短信还是邮件

两个静态同步方法,两部手机,先打印短信还是邮件

一个静态同步方法,一个普通同步方法,一部手机,先打印短信还是邮件

一个静态同步方法,一个普通同步方法,两部手机,先打印短信还是邮件

结论;

一个对象里面如果有多个synchronized方法,某个一个时刻内,只要一个线程去调用其中一个synchronized方法,其他线程就只能等待,只能有唯一线程去访问这些synchronizd方法。锁的是当前对象this,被锁定后,其他线程不能进入到当前对象的其它synchronzied方法。

加入普通方法后和同步锁无关

换成两个对象后,不是同一把锁,synchronixed实现同步的基础:Java中的每一个对象都可以作为锁。

具体表现为3种形式

对于普通方法,锁是当前实例对象。对于静态同步方法,锁是当前类的Class对象。对于同步方法快,锁是synchronized括号里配置的对象。

当一个线程试图访问同步代码块时,首先必须得到锁,退出或者抛出异常时必须释放锁

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,

所有的静态同步方法用的也是同一把锁——类对象本身,两把锁时两个不同的对象,所以静态同步方法与非静态同步方法之间时不会有竞态条件的。

一旦一个静态方法获取锁后,其他静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们是同一类的实例对象

公平锁和非公平锁

ReentranLock()为例

ReentranLock类的有参构造和无参构造

public ReentrantLock() {
     sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
}

无参构造默认为非公平锁,可以通过有参构造来决定使用公平还是非公平

公平和非公平锁区别:

非公平锁:可能导致线程饿死,但是效率高

公平锁:不会导致线程饿死,但是效率低,会询问

可重入锁(递归锁)

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

死锁

package com.company;

import java.util.concurrent.TimeUnit;

public class DeadLock {
    static Object a=new  Object();
    static Object b=new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (a){
                System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println(Thread.currentThread().getName()+"获取锁b");
                }
            }
        },"A").start();
        new Thread(()->{
            synchronized (b){
                System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println(Thread.currentThread().getName()+"获取锁b");
                }
            }
        },"B").start();
    }


}

JUC高并发编程_第1张图片

死锁产生原因:资源不足,线程推进顺序不当,资源分配不当

六、Callable&Future接口

创建线程池的四种方法:

继承Thread类

实现Runnable接口

实现Callable接口

线程池创建

Callable和Runnable接口的区别

Runnable接口重写run()方法;Callable接口重写call()方法

run方法没有返回值;call()有返回值,当计算不出返回值时,报异常

不能用Callable直接替换Runnable,因为Thread类的构造方法根本没有Callable

创建新类 MyThread 实现 runnable 接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
新类 MyThread2 实现 callable 接口
class MyThread2 implements Callable{
@Override
public Integer call() throws Exception {
return 200; }
}

FutureTast

不能用Callable直接替换Runnable,但又想要返回结果,所以可以通过FutureTask来实现

FutureTask时Runnable接口的实现类,FutureTask的构造函数中用到了Callable

七、JUC三大辅助类

JUC提供了三种常用辅助类,这些辅助类可以很好的解决线程数量多时Lock锁频繁操作。三种辅助类为:

CountDownLatch减少计数

CyclicBarrier:循环栅栏

Semaphore;信号灯

减少计数CountDownLatch

CountDownLatch类可以设置一个计数器,然后通过conuntDown方法来进行减一的操作,使用await方法等待计数器不大于零,然后继续执行await方法之后的语句

CountDownLatch主要有两个方法,当一个或者多个线程调用await方法时,这些线程会被阻塞

当计数器的值变成0时,因await方法阻塞的线程会被唤醒,继续执行

package com.company;
import sun.font.FontRunIterator;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6); //初始化计数器值
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"离开了教室!");
                countDownLatch.countDown(); //计数器减一
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); //当计数器大于0时,线程阻塞
        System.out.println("教室关门!");
    }
}

循环栅栏CyclicBarrier

CyclicBarrier在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行cyclicBarrier一次障碍书会加以,如果达到目标障碍数,才会执行cycilcBarrier.await()之后的语句

package com.company;

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    private static final int NUMBER=7;//创建固定值
    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier=new  CyclicBarrier(NUMBER,()->{
            System.out.println("召集七颗龙珠");
        });
        for (int i=1;i<=7;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"星龙收集到了");
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

信号灯Semaphore

Semaphore的构造方法中传入第一个参数是最大的信号量,每个信号量初始化为组多之呢个发一个许可证。使用acquire方法获取许可证,relaese方法释放许可证

package com.company;

import com.sun.jmx.snmp.ThreadContext;

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

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(1);
                    System.out.println(Thread.currentThread().getName()+"----离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

八、读写锁

JAVA的并法包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,成为共享锁;一个是写相关的锁,称为排他锁或独占锁

线程进入读锁的前提条件:没有其他线程的写锁;没有写请求,或者有写请求,但调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:没有其他线程的读锁;没有其他线程的写锁

九、阻塞队列

BlockingQueue简介

Concurrent包中,BlockingQueue很好的解决了多线程中如何高效安全传输数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的的多线程程序带来极大的便利。

阻塞队列:通过共享的队列,可以使数据由一端输入,从另一端输出

JUC高并发编程_第2张图片

当队列是空的,从队列中获取元素的操作会被阻塞

当队列是满的,从队列添加元素的操作将会被阻塞

试图从空的队列获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素试图向已满的队列中添加新元素的线程将被阻塞,直到其他线程从队列中移除一个或者多个元素或者完全清空,使队列变得空闲起来并后续新增

BlockingQueue核心方法

JUC高并发编程_第3张图片

常见的BlockingQueue

ArrayBlockingQueue

由数组组成的有界阻塞队列

LinkedBlockingQueue

有链表结构组成的有界阻塞队列

DelayQueue

使用优先级队列实现的延迟无解阻塞队列

PriorityBlockingQueue

基于优先级的阻塞队列,并不会阻塞数据的生产者,而只会在没有可消费的数据时,阻塞数据的消费者

SynchronouseQueue

不存储元素的阻塞队列

LinkedTransferQueue

由链表组成的无界阻塞队列

LinkedBlockingDeque

由链表做成的双向阻塞队列

十、ThreadPool线程池

线程池简介

线程池:一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建和销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

线程池的优势:线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executora,ExecutorService,ThreadPoolExecutor这几个类

JUC高并发编程_第4张图片

线程参数说明

常用参数

JUC高并发编程_第5张图片

当提交的任务数大于(workQueue.size()+maximumPoolSize),就会触发线程的拒绝策略

拒绝策略

CallerRunsPolicy:调用者模式,当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过于快,可能导致程序阻塞,性能效率上必然的损失较大

AbortPolicy:丢弃任务,并抛出拒绝执行 RejectedExecutionException,异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行

DiscardPolicy:直接丢弃

DiscardOldestPolicy:当初发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列workQueue中最老的一个任务,并将新任务加入

线程池的种类与创建

newCachedThreadPool(线程池根据需求创建,可扩容)

创建一个可重用固定线程数的线程池

package com.company;

import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TreadPoolDemo {
    public static void main(String[] args) {
        //一池多线程
        // ExecutorService threadPool = Executors.newFixedThreadPool(5);//在后边打上var就可以直接出来前边
        //一池一线程
        //ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
        //一池可扩容线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
        //十个请求
        try{
            for (int i = 0; i < 20; i++) {
            threadPool3.execute(()->{System.out.println(Thread.currentThread().getName()+"办理业务");});
        }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool3.shutdown();
        }

    }
}

JUC高并发编程_第6张图片  

newFixedThreadPool(一池N线程)

创建一个可缓存线程池

package com.company;
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TreadPoolDemo {
    public static void main(String[] args) {
        //一池多线程
        ExecutorService threadPool = Executors.newFixedThreadPool(5);//在后边打上var就可以直接出来前边
        //十个请求
        try{
            for (int i = 0; i < 10; i++) {
            threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"办理业务");});
        }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

newSingleThreadExecutor(一池一线程)

创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。

package com.company;

import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TreadPoolDemo {
    public static void main(String[] args) {
        //一池多线程
       // ExecutorService threadPool = Executors.newFixedThreadPool(5);//在后边打上var就可以直接出来前边
        //一池一线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
        //十个请求
        try{
            for (int i = 0; i < 10; i++) {
            threadPool2.execute(()->{System.out.println(Thread.currentThread().getName()+"办理业务");});
        }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool2.shutdown();
        }

    }
}

JUC高并发编程_第7张图片

通过ThraedPoolExecutor完成线程池的创建

底层工作原理

JUC高并发编程_第8张图片

1.在创建了线程池后,线程池中的线程数为零。

2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

        如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务

        如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入对队列

        如果这个队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

        如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行

3.当一个线程完成任务时,他会从队列中取下一个任务来执行

4.当一个线程无事可做超过一定的时间时,线程会判断

        如果当前运行的线程数大于corePoolSize,那么线程会停止

        线程池的所有任务完成后,最终会收缩corePoolSize的大小

自定义线程池

package com.company;
import sun.invoke.empty.Empty;
import java.util.concurrent.*;
public class ThraedPoolDemo2 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

十一、Fork/Join

Fork/Join可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。

Fork:把复杂任务进行分拆,大事化小

Join:八分拆任务的结果进行合并

//递归累加
class MyTask extends RecursiveTask{

    private int begin;
    private int end;
    private long result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if ((end-begin)<=10){ //10个为一组进行计算
            for (int i = begin; i <= end; i++) {
                //累加
                result+=begin;
            }
        }else {
            //切分为2部分,递归计算
            int middle = (begin+end)/2;
            MyTask myTask01 = new MyTask(begin, middle);
            MyTask myTask02 = new MyTask(middle+1, end);
            //执行异步
            myTask01.fork();
            myTask02.fork();
            //同步阻塞获取结果
            result = myTask01.join()+myTask02.join();
        }
        //计算完返回
        return result;
    }
}
public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //定义任务
        MyTask myTask = new MyTask(1, 100);
        //定义执行对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //加入任务执行
        ForkJoinTask result = forkJoinPool.submit(myTask);
        //输出结果
        System.out.println(result.get());
        forkJoinPool.shutdown();
    }
}

十二、CompletableFuture

异步编程意味着非阻塞,任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息

异步调用没有返回值.runAsync

异步调用有返回值.supplyAsync

你可能感兴趣的:(java,开发语言,后端)