JAVA笔记 | JUC并发编程

自己的学习笔记,材料源于网上教程

基础概念

JUC是什么?

java.util.concurrent包,用于更好地支持高并发任务,在进行多线程编程时减少竞争条件和死锁的问题

并发编程本质是充分利用CPU,并发问题可以理解为多个线程同时操作一个资源类产生的问题

进程与线程区别

进程:写的一个程序集,例如菜谱

线城:进程地具体一次运行,例如根据菜谱进行一次做菜的过程,是操作系统调度的基本单位

java默认有两个线程:main主方法 gc垃圾回收

java是否真的能开启线程:java运行在虚拟机之上无法直接操作硬件,start()方法实际上时调用本地C++ native方法开启线程

并发与并行

并发:CPU单核,多线程共用一个资源,同一时间段内,快速交替执行

并行,CPU多核,同一时间段内,多个线城同时执行 

获取CPU核数

Runtime.getRunTime().availableProcessors();

线程状态

新生态NEW, 运行态RUNNABLE, 阻塞态BLOCKED,等待态WAITING,超时等待TIMED_WAITING,终止态TERMINATED

wait与sleep

wait是Object类的方法,且会释放锁,只能在同步代码块中使用,不需要异常捕获

sleep是线程类Thread的方法,不会释放锁,什么地方都能调用,需要异常捕获

JUC构成

tools工具类:

  • CountDownLatch闭锁:使一个线程等待其他线程都执行完毕后再执行
  • CyclicBarrier栅栏:使一组线程互相等待,当所有线程都达到某个屏障点后再执行后续操作
  • Semaphore信号量:共享锁,线程通过调用acquire()获取信号量许可,当信号量中含有可用许可,线程就能获取,否则线程需要一直等待,直到有可用信号许可位置,线程通过release()释放所持有的信号量许可证

executor执行者:

执行线程的工具,真正的线程池接口为ExecutorService

  • ScheduledExecutorService 解决需要任务重复执行的问题
  • ScheduledThreadPoolExecutor 周期性任务调度
  • atomic 原子性包,一组原子操作类。AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,均被volatile修饰,保证每次线程拿到的值都是最新的

locks锁包:

  • ReentrantLock 独占锁,同一时间点只能被一个线程锁获取
  • ReentrantReadWriteLock 包括子类ReadLock共享锁,WriteLock独占锁
  • LockSupport 可阻塞线程和解除阻塞线程且不会引发死锁

collections集合类:

提供线程安全的集合,常见集合类对应的高并发类

  • ArrayList - CopyOnWriteArrayList
  • HashSet - CopyOnWriteArraySet
  • HashMap - ConcurrentHashMap 等

使用

多线程创建的常规方法

继承Thread类

class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread的线程创建");
    }
}

//使用
ThreadTest test = new ThreadTest();
test.start();

实现runnable接口,Callable接口效率更快

class ThreadTest2 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口的创建线程");
    }
}

//使用
Thread thread = new Thread(new ThreadTest2());
thread.start();

锁Lock

Synchronized同步锁

  • 修饰代码块,同步语句块,作用范围为该代码块
  • 修饰方法,同步方法,范围为该方法,作用于调用该方法的对象
  • 修饰静态方法,范围为该静态方法,作用于这个类所有对象
  • 修饰类,范围Synchronized括号起来的部分,作用于这个类的所有对象

Lock接口ReentrantLock

ReentrantLock接口 可重入独占锁

公平锁:先来后到排队

非公平锁:可以插队(默认)

public class ReentrantLock implements Lock, java.io.Serializable {
......
    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

Synchronized与Lock区别

Synchronized:java关键字,无法获取锁状态,自动释放锁,一个线程获取到锁其他线程均需要等待,不可中断,非公平可重入锁,适合解决少量代码同步问题

Lock:类,可判断是否获取锁,需要手动释放锁,不然会出现死锁问题,可以通过tryLock()方法尝试获取锁,不让此线程一直等待,可中断,可以设置公平性的可重入锁,适合解决大量的代码同步问题

ReentrantLock使用举例

模拟简单场景:电影院热门电影抢票,同一时间多个人买了同一张票,假设共20张,编号分别为1-20号

情况1:不使用锁,排队(单线程)购买票(30人一个渠道排队购买20张票)

public class Cinema {
    static class TicketOffice {
        Integer ticketNum = 20;
        public void sale(){
            if(this.ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
            }
        }
    }
    public static void main(String[] args) {
        //资源
        TicketOffice ticketOffice = new TicketOffice();
        //一个渠道 30人排队购买20张票
        new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();

    }
}

此时票按顺序正常售出,结果为:

A购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
A购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
A购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
A购买渠道售出13号票当前还剩余12张票
A购买渠道售出12号票当前还剩余11张票
A购买渠道售出11号票当前还剩余10张票
A购买渠道售出10号票当前还剩余9张票
A购买渠道售出9号票当前还剩余8张票
A购买渠道售出8号票当前还剩余7张票
A购买渠道售出7号票当前还剩余6张票
A购买渠道售出6号票当前还剩余5张票
A购买渠道售出5号票当前还剩余4张票
A购买渠道售出4号票当前还剩余3张票
A购买渠道售出3号票当前还剩余2张票
A购买渠道售出2号票当前还剩余1张票
A购买渠道售出1号票当前还剩余0张票

情况2:不使用锁,开启多个渠道(多线程)购买票(多个渠道多抢购20张票)

public class Cinema {
    static class TicketOffice {
        Integer ticketNum = 20;
        public void sale() {
            try{
                if(this.ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
                }
                //增加每次购买的时间差,提高同时抢购的错误率
                Thread.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        //资源
        TicketOffice ticketOffice = new TicketOffice();
        //A B渠道各30人准备抢购20张票
        new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();
        new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"B购买渠道").start();
    }
}

此时出现,相同座位号的票出售给多人,造成错误结果

B购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
B购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
B购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
B购买渠道售出13号票当前还剩余12张票
B购买渠道售出12号票当前还剩余11张票
A购买渠道售出11号票当前还剩余10张票
B购买渠道售出10号票当前还剩余9张票
A购买渠道售出10号票当前还剩余8张票
B购买渠道售出8号票当前还剩余7张票
A购买渠道售出8号票当前还剩余6张票
B购买渠道售出6号票当前还剩余5张票
A购买渠道售出6号票当前还剩余5张票
A购买渠道售出5号票当前还剩余4张票
B购买渠道售出5号票当前还剩余4张票
A购买渠道售出4号票当前还剩余3张票
B购买渠道售出3号票当前还剩余2张票
A购买渠道售出2号票当前还剩余1张票
B购买渠道售出2号票当前还剩余1张票
B购买渠道售出1号票当前还剩余0张票
A购买渠道售出1号票当前还剩余0张票

情况3:基于情况2增加锁,确保线程安全,抢票系统正常售票

(1)采用传统方式加Synchronized解决并发可能产生的问题

//......
//将资源中的出售方式加上同步锁
public synchronized void sale() {
    if(this.ticketNum > 0) {
        System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号 
        票" + "当前还剩余" + --ticketNum + "张票");
     }
}
//......

(2)采用Lock锁(可重入锁ReentrantLock)

public class Cinema {
    static class TicketOffice {
        Lock lock = new ReentrantLock();
        Integer ticketNum = 20;
        public void sale() {
            lock.lock();
            try{
                if(this.ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出" + ticketNum + "号票" + "当前还剩余" + --ticketNum + "张票");
                }
                //增加每次购买的时间差,提高同时抢购的错误率
                Thread.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        //资源
        TicketOffice ticketOffice = new TicketOffice();
        //A B渠道各30人准备抢购20张票
        new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"A购买渠道").start();
        new Thread(() -> {for(int i = 0; i < 30 ; i++){ticketOffice.sale();}},"B购买渠道").start();
    }
}

此时双渠道正常售票

A购买渠道售出20号票当前还剩余19张票
A购买渠道售出19号票当前还剩余18张票
A购买渠道售出18号票当前还剩余17张票
A购买渠道售出17号票当前还剩余16张票
A购买渠道售出16号票当前还剩余15张票
A购买渠道售出15号票当前还剩余14张票
A购买渠道售出14号票当前还剩余13张票
B购买渠道售出13号票当前还剩余12张票
B购买渠道售出12号票当前还剩余11张票
B购买渠道售出11号票当前还剩余10张票
B购买渠道售出10号票当前还剩余9张票
B购买渠道售出9号票当前还剩余8张票
B购买渠道售出8号票当前还剩余7张票
B购买渠道售出7号票当前还剩余6张票
B购买渠道售出6号票当前还剩余5张票
B购买渠道售出5号票当前还剩余4张票
B购买渠道售出4号票当前还剩余3张票
B购买渠道售出3号票当前还剩余2张票
B购买渠道售出2号票当前还剩余1张票
B购买渠道售出1号票当前还剩余0张票

生产者消费者问题

背景:当库存为0时候需要生产产品,当库存不为0时候需要卖出产品

使用Synchronized解决生产者消费者

public class Test {
    public static void main(String[] args) {
        //资源
        Products products = new Products();
        new Thread(() -> {
            for(int i = 0;i < 10;i++){
                try {
                    products.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A生产者线程").start();
        new Thread(() -> {
            for(int i = 0;i < 10;i++){
                try {
                    products.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B消费者线程").start();

    }
}
//资源类
class Products {
    //库存
    int stockNum = 0;
    //生产 +1
    public synchronized void produce() throws InterruptedException {
        //当库存不为0时就不生产,从而进行等待,商品满了
        if(stockNum  > 0){
            this.wait();
        }
        //为0的时候就生产1个
        stockNum ++ ;
        System.out.println(Thread.currentThread().getName() + "生产了1个产品,当前库存" + stockNum);
        this.notifyAll();
    }
    //售货 -1
    public synchronized void sale() throws InterruptedException {
        //当库存为0时候就不售出,从而进行等待
        if(stockNum <= 0){
            this.wait();
        }
        //不为0正常售出
        stockNum -- ;
        System.out.println(Thread.currentThread().getName() + "卖出了1个产品,当前库存" + stockNum);
        this.notifyAll();
    }
}

运行结果正常

A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0
A线程生产了1个产品,当前库存1
B线程卖出了1个产品,当前库存0

但当新增线程C生产者线程,D消费者线程后则结果就会出现问题

......
D线程卖出了1个产品,当前库存0
B线程卖出了1个产品,当前库存-1
D线程卖出了1个产品,当前库存-2
C线程生产了1个产品,当前库存-1
C线程生产了1个产品,当前库存0
......

将if改为while即可解决该虚假唤醒问题,while代码省略

虚假唤醒

使用用if或造成虚假唤醒问题,故一般使用while替换if,避免if + wait()

虚假唤醒:理解为,当所有线程都在等待的时候,突然资源允许一个线程来操作,但是此时所有线程均被唤醒,所以有效唤醒为1,其他视为虚假唤醒。

为什么if会造成虚假唤醒?

可能存在情况,比如当库存为0时,B生产者,D消费者均到达sale()方法,此时因为库存为0,于是就执行wait挂起,准备生产者生产后继续售卖,但是期间可能只生产了1个,此时唤醒B,D线程均被唤醒准备售卖,此时两个线程if不再判断,直接执行--操作,故导致库存-1的情况。

换成while后,在B,D被唤醒后,两线程均会在进行一次判断,即  while(stockNum <= 0),此时会有序微小的先后偏差,故一个减1后,库存为0,另一个就会重新被挂起,不会再售卖,避免了虚假唤醒。

JUC中的Condition监视器

相比于synchronized(wait notify),Lock中有Condition监视器(await,signal)

Condition精准的通知唤醒线程,使线程唤醒变得有序(如A线程等待则唤醒B线程,B线程等待唤醒C线程)

通过对同一锁构造不同的Condition监视器,对不同的线程程进行包装标识(自己的理解),即可达到精准唤醒指定的处于等待状态的线程。

用法举例,资源类中,有三个执行方法 A ,B,C 要求A执行完执行B,B执行完执行C(流水线操作)

//资源类
class Products {
    //创建锁
    Lock lock = new ReentrantLock();
    //创建3个Condition监视器
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    //当number= 1执行A 2执行B 3执行C 控制顺序
    int number = 1;

    public void produceA(){
        lock.lock();
        try{
            //number不等于对应的标识则挂起等待
            while(number != 1){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "A执行");
            number = 2;
            condition2.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock()
        } 
        
    }
    public void produceB(){
        lock.lock();
        try{
            //number不等于对应的标识则挂起等待
            while(number != 2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "B执行");
            number = 3;
            condition3.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock()
        } 
        
    }
    public void produceC(){
        lock.lock();
        try{
            //number不等于对应的标识则挂起等待
            while(number != 3){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "C执行");
            number = 1;
            condition1.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock()
        }  
    }  
}

使用JUC解决生产者消费者问题 

public class Test2 {
    public static void main(String[] args) {
        //资源
        Products products = new Products();
        new Thread(() -> {for (int i = 0; i < 10; i++) {products.produce();}}, "A线程").start();
        new Thread(() -> {for (int i = 0; i < 10; i++) { products.sale();}}, "B线程").start();
        new Thread(() -> {for (int i = 0; i < 10; i++) {products.produce();} }, "C线程").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) { products.sale(); } }, "D线程").start();
    }
    
    //资源类
    static class Products {
        //库存
        int stockNum = 0;
        Lock lock = new ReentrantLock();
        Condition conditionProduce = lock.newCondition();
        Condition conditionSale = lock.newCondition();
        //生产 +1
        public  void produce() {
            lock.lock();
            try {
                //当库存不为0时就不生产,从而进行等待,商品满了
                while (stockNum > 0) {
                    conditionProduce.await();
                }
                //为0的时候就生产1个
                stockNum++;
                System.out.println(Thread.currentThread().getName() + "生产了1个产品,当前库存" + stockNum);
                conditionSale.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        //售货 -1
        public void sale() {
            lock.lock();
            try {
                //当库存为0时候就不售出,从而进行等待
                while (stockNum <= 0) {
                    conditionSale.await();
                }
                //不为0正常售出
                stockNum--;
                System.out.println(Thread.currentThread().getName() + "卖出了1个产品,当前库存" + stockNum);
                conditionProduce.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

8锁现象

8锁现象:关于锁的8个问题

问题1:两个同步方法,同一个对象调用,谁先执行?

先拿到锁,谁先执行。

如下”生产“打印后,时隔5秒后打印“售卖”。

synchronized 锁住的对象是方法的调用者,此处公用一个资源products,该对象只有一把锁,故谁先拿到锁谁就会先执行。此处如果在生产方法中加入sleep 5秒同样是应为生产先拿到锁,所以produce()会先执行。

public class Test3 {
    public static void main(String[] args) {
        Products products = new Products();
        new Thread(() -> { products.produce();}).start();
        try {
            //sleep 5秒
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {products.sale();}).start();
    }

    //资源
    static class Products {
        public synchronized void produce() {
            System.out.println("生产");
        }
        public synchronized void sale() {
            System.out.println("售卖");
        }
    }
}

问题2:两个同步方法,两个对象调用,谁先执行

如果调用均无等待延迟,则按照调用顺序执行,如果设置等待时间,则按照各自的等待时间结束执行,但实际上两个互不影响。

如下,按照执行顺序,先调用.produce(),但方法中延迟了5秒,此时主方法中延迟1秒后调用sale(),直接打印"售卖",等produce()睡眠时间结束后答应"生产"。

public class Test3 {
    public static void main(String[] args) {
        Products products = new Products();
        Products products2 = new Products();
        new Thread(() -> { products.produce();}).start();
        try {
            //sleep 1秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {products2.sale();}).start();
    }

    //资源
    static class Products {
        public synchronized void produce() {
            try {
                //sleep 5秒
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("生产");
        }
        public synchronized void sale() {
            System.out.println("售卖");
        }
    }
}

问题3:一个同步方法,一个普通方法,一个对象调用,谁先执行

普通方法的调用不受锁影响,正常调用资源方法

如下,sale()方法为非同步方法,即使同步方法produce()对资源products上锁,但对于sale()来说,这个锁对它并无作用,故可以先调用,而不是等produce()方法等待时间过后,执行后再执行,与问题1对比。

public class Test3 {
    public static void main(String[] args) {
        Products products = new Products();
        Products products2 = new Products();
        new Thread(() -> { products.produce();}).start();
        new Thread(() -> {products2.sale();}).start();
    }

    //资源
    static class Products {
        public synchronized void produce() {
            try {
                //sleep 5秒
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("生产");
        }
        public void sale() {
            System.out.println("售卖");
        }
    }
}

问题4:两个同步方法,一个对象调用,其中先调用的方法设置延迟时间,谁先执行?

同一对象调用,先获取到锁的方法先执行,无论sleep多久,应为sleep不释放锁,故后者也等待前者释放资源,此问题类同问题1

如下,produce()设置延迟时间10秒,sale方法也会跟着等待,等10秒后produce()执行完后再执行

public class Test3 {
    public static void main(String[] args) {
        Products products = new Products();
        new Thread(() -> { products.produce();}).start();
        new Thread(() -> {products.sale();}).start();
    }

    //资源
    static class Products {
        public synchronized void produce() {
            try {
                //sleep 10秒
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("生产");
        }
        public synchronized void sale() {
            System.out.println("售卖");
        }
    }
}

问题5:两个静态同步方法,一个对象调用,谁先执行

静态方法锁得对象是Class类,所以先拿到锁先执行

如下,produce()先执行,后执行sale()

public class Test3 {
    public static void main(String[] args) {
        Products1 products = new Products1();
        new Thread(() -> {products.produce();}).start();
        new Thread(() -> {products.sale();}).start();
    }
}

//资源
class Products1 {
    public static synchronized void produce() {
        try {
            //sleep 5秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产");
    }
    public static synchronized void sale() {
        System.out.println("售卖");
    }
}

问题6:两个静态同步方法,两个对象调用,谁先执行

同问题5,静态方法锁得对象是Class类,所以先拿到锁先执行

如下,produce()先执行,后执行sale()

public class Test3 {
    public static void main(String[] args) {
        Products1 products = new Products1();
        Products1 products2 = new Products1();
        new Thread(() -> { products.produce();}).start();
        new Thread(() -> {products2.sale();}).start();
    }

}

//资源
class Products1 {
    public static synchronized void produce() {
        try {
            //sleep 5秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产");
    }
    public static synchronized void sale() {
        System.out.println("售卖");
    }
}

问题7:一个静态同步方法,一个普通同步方法,一个对象调用,谁先执行

静态方法锁类对象,普通方法锁实例化对象,两个不同得锁,互不影响,故按正常执行

如下 produce()因为有延迟,故sale()先执行

public class Test3 {
    public static void main(String[] args) {
        Products1 products = new Products1();
        new Thread(() -> { products.produce();}).start();
        new Thread(() -> {products.sale();}).start();
    }

}
//资源
class Products1 {
    public static synchronized void produce() {
        try {
            //sleep 5秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产");
    }
    public synchronized void sale() {
        System.out.println("售卖");
    }
}

 问题8:一个静态同步方法,一个普通同步方法,两个对象调用,谁先执行

 同问题7,静态方法锁类对象,普通方法锁实例化对象,两个不同得锁,互不影响,故按正常执行

public class Test3 {
    public static void main(String[] args) {
        Products1 products = new Products1();
        Products1 products2 = new Products1();
        new Thread(() -> { products.produce();}).start();
        new Thread(() -> {products2.sale();}).start();
    }

}

//资源
class Products1 {
    public static synchronized void produce() {
        try {
            //sleep 5秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产");
    }
    public synchronized void sale() {
        System.out.println("售卖");
    }
}

集合类并发安全

通常单线程开发中集合类均无问题,但是在并发情况下则会报并发修改异常java.util.ConcurrentModificationException,故要考虑解决

CopyOnWriteArrayList

解决方案1 用vector代替ArrayList,效率低不建议用

List<String> list = new Vector<>();

解决方案2 利用Collections.sysnchronizedList

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

解决方案3 利用JUC CopyOnWriteArrayList解决,最优方案

List<String> list = new CopyOnWriteArrayList<>();

写入时复制

COW,计算机程序设计的一种优化策略,在写入修改操作的时候,会复制原集合副本,在副本进行修改,此时如果同步读取的话,还是会访问旧集合,等副本修改完成后,最终集合指向副本完成修改

优点:多线程调用时,解决读取问题,读写分离,写入时避免覆盖

缺点:同步读取时候,读到的数据并不是最新的

CopyOnWriteHashSet

解决方案1

Set<String> set = Collections.sysnchronizedSet(new HashSet<>());

解决方案2

Set<String> set = new CopyOnWriteHashSet<>();

ConcurrentHashMap

在并发环境下使用HashMap可能导致形成环状链表,get操作时,使得cpu空转,很危险

相对于HashTable线程安全,内部使用synchronized,但当表足够大时,导致效率低下 

方案:Map<String,String> map = ConcurrentHashMap<>();

HashTable与ConcurrentHashMap区别:HashTable锁住整张表,而后者分段锁

 Callable

类似于Runable,但Callable可以有返回值,可以抛出异常,Runable不行,Callable中call()取代run()

但是Thread类只能接受Runable,不能直接接受Callable,故需要通过Runable来完成调用.

(Runnable与Callable都需要通过Thread来装载)

Runnable的实现

class MyThread implements Runable{
...
    @override
    public void run(){
    }
...
}

new Thread(new Mythread()).start();

Callable的实现

class MyThread implements Callable<Integer>{
...
    @override
    public Integer call (){

        System.out.println(“运行”);

        return 1;
    }
}

//调用

MyThread myThread = new MyThread();

FutureTask futureTask = new FutureTask(myThread);

new Thread(futureTask,"A").start();

//可以获取到返回值,此方法可能引起阻塞,可放在代码最后一行或者异步处理

Integer i = (Integer)futureTask.get();

三大常用辅助类

CountDownLatch

计数器,一个或者多个线程执行完再执行某个包含此对象的线程

 countDownLatch.countDown(); 数量-1

 countDownLatch.await();等待计数器的值为0,唤醒当前线程继续执行

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i =1;i <= countDownLatch.getCount();i++){
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "执行");
                //计时器-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();

        }
        //等上面线程都执行完后执行主线程,不然主线程会随机执行
        countDownLatch.await();
        System.out.println("主线程,最终执行");
    }
}

CyclicBarrier

栅栏,指定一个属数n,当执行的线程到达n后才执行某个线程的内容

如代码,i如果达不到3,则栅栏中的代码不会执行,且执行完一个线程需要调用cyclicBarrier.await()来计数,否则栅栏中的代码也不会执行

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3,() -> {
            System.out.println("到达3后 执行");
        });
        for(int i =1 ; i <= 3 ;i ++){
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "线程执行");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

Semaphore

计数信号量,指定一个信号量,为当前需要执行的线程数,当线程数达到该信号量,其他线程则需要等待前几个资源使用完并释放信号后才能继续执行

acquire获得凭证,如果已经满员,则等待至释放为止

release 释放凭证,信号量+1,唤醒等待的线程

public class SemaphoreTest {
    public static void main(String[] args) {
        //设置初始信号量 2 允许两个线程执行
        Semaphore semaphore = new Semaphore(2);
        //4个线程 acquire获得凭证 release 释放凭证
        for (int i = 1; i < 4 ;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();
                }
            },String.valueOf(i)).start();
        }
    }
}

读写锁ReadWriteLock

保证一个线程写入的时候,不被另一个写入操作的线程插队。

如果不加锁,如下 多线程读取的情况,5个线程写入,5个线程获取对应写入的内容

public class ReadWriteLockTest {
    public static void main(String[] args) {
        Cache cache = new Cache();
        //写
        for(int i = 1 ; i <= 5 ; i++){
            final int temp = i;
            new Thread(() -> {
                cache.put(temp+"",temp + "");
            },String.valueOf(i)).start();
        }
        //取
        for(int i = 1 ; i <= 5 ; i++){
            final int temp = i;
            new Thread(() -> {
                cache.get(temp+"");
            },String.valueOf(i)).start();
        }
    }


}
class Cache {
    private volatile Map<String,Object> map = new HashMap<>();
    //写
    public void put(String key,Object value){
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName() + "写入完成");
    }
    //读
    public void get(String key){
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成");
    }
}

结果如下,其中1写入的时候,被5等插队,存在此类情况,古考虑添加读写锁,使读的时候不受限制,写的时候一个线程写完再写入

1写入1
5写入5
4写入4
3写入3
3写入完成
2写入2
2写入完成
4写入完成
1读取1
....

增加读写锁,允许多线程同时读取,不能边读边写,不能边写边写

读写锁:即为互斥锁也是共享锁,两种模式

read:互斥锁 - 写锁 一次只能被一个线程占有

write:共享锁 - 读锁  多个线程可以同时占有

class Cache {
    private volatile Map<String,Object> map = new HashMap<>();
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //写
    public void put(String key,Object value){
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName() + "写入完成");
        readWriteLock.writeLock().unlock();
    }
    //读
    public void get(String key){
        readWriteLock.readLock().lock();
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成");
        readWriteLock.readLock().unlock();
    }
}

结果:写入均需要一个线程写入完成后再进行写入,读取则不限制,如果此处读取不加共享锁,则会导致一个线程还没写完就读取这个资源,就会出现问题

2写入2
2写入完成
3写入3
3写入完成
1写入1
1写入完成
.....
1读取1
1读取完成
2读取2
3读取3
3读取完成
......

读取不加共享锁的情况,5还未写入就读取5

......

5读取5
5读取完成
4读取4
4读取完成
5写入5
5写入完成

阻塞队列

队列

先进先出,队列(双端队列deque,非阻塞队列AbstractQueeu,阻塞队列BlockingQueue(ArrayBlockingQueue,LinkedBlockingQueue))

阻塞队列

基于ReentrantLock的队列,常用与生产消费者模式,实现web长连接聊天等。

阻塞情况

1.当队列为空时,需要阻塞等待新元素入队

2.当队列满之后,阻塞等待队列有位置再入队

ArrayBlockingQueue

常用API

//指定容量为2

ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(2);

(1)抛异常组

add 添加,成功返回true,超出报错

remove 移除,无元素报错

element 获取队首元素

(2)不抛异常组

offer 入队,成功返回true,队满返回false

poll 出队,获取当前队首元素,空队返回null

peek 获取队首元素

(3)一直阻塞等待组

put 入队,如果入队数量大于容量将一直等待直到有元素出队后入队

take 出队,获取当前队首元素,如果队列无元素则一直等待,直到有元素后获取

(4)超时等待组

超过等待的时间后则不继续等待,当前线程中断

blockingQueue.offer("A",2,TimeUnit.SECONDS)

blockingQueue.poll(2,TimeUnit.SECONDS)

LinkedBlockingQueue

两把锁控制入队,出队,只允许一个线程入队,一个线程出队,但是入队出队互不影响,可同时进行,为了维持线程安全,使用了原子安全类AtomicInteger类型标识当前队列元素个数,确保出队入队两个线程之间的线程安全。

可指定容量,不允许元素为null,如果队列满了,则会调用notFull的await()方法将该线程加入Condition等待队列中。当队列为空,则加入到notEmpty()的等待队列中。

同步队列SynchronousQueue

相当于容量为1的阻塞队列,只允许入队一个元素,等待出队后才能再次入队

put 入队,如果入队数量大于容量将一直等待直到有元素出队后入队

take 出队,获取当前队首元素,如果队列无元素则一直等待,直到有元素后获取

线程池

池化技术

提前准备好资源,在请求量大的时候优化应用性能,减低资源的消耗,提高响应速度,管理方便。

线程池

实现启动若干数量的线程,并让这些线程处于睡眠状态,当需要某个线程工作时则唤醒,完成后继续处于睡眠(非销毁线程)

优点:线程复用,控制最大并发数,管理线程

四个方法

Executors.newSingleThreadExecutor();//单线程
Executors.newFixedThreadPool(5);//固定线程数
Executors.newCachedThreadPool();//可伸缩线程数,无固定大小

...

newSingleThreadExecutor() 单个线程的线程池

public class PoolTest {
    public static void main(String[] args) {
        ExecutorService threadPool =  Executors.newSingleThreadExecutor();// 单线程
        try{
            for (int i = 0 ; i < 4 ; i++){
                //使用线程池创建线程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //线程池使用完要关闭
            threadPool.shutdown();
        }
    }
}

运行结果,同一个线程调用

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

Executors.newFixedThreadPool(5); Executors.newCachedThreadPool();用法同上,只是创建的线程数量不同。

七个参数自定义线程池

ThreadPoolExecutor参数

public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小
                              int maximumPoolSize, //最大核心线程池大小
                              long keepAliveTime, //空闲最大存活时间
                              TimeUnit unit,//超时单位
                              BlockingQueue<Runnable> workQueue, //阻塞队列
                              ThreadFactory threadFactory,  //线程工厂
                              RejectedExecutionHandler handler)  //拒绝策略
public class PoolTest {
    public static void main(String[] args) {
        ExecutorService threadPool =  new ThreadPoolExecutor(
                2, 5, 3,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());//阻塞吼的处理策略,AbortPolicy-》抛出异常
        try {
            for (int i = 1; i <= 11; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "线程执行");
                });
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

建议不要直接使用Executors创建线程池,通过ThreadPoolExecutor创建

ExecutorService threadPool =  new ThreadPoolExecutor();创建线程池,并且通过自定义参数,定制线程池。

threadPool.execute()线程执行

corePoolSize,  //核心线程池大小 基础工作的线程数
maximumPoolSize, //最大核心线程池大小,当达到条件,开启附加线程进行工作
keepAliveTime, //超时等待,空闲最大存活时间,超过时间,则关闭附加线程
TimeUnit unit,//超时时间单位设置
BlockingQueue<Runnable> workQueue, //阻塞队列,用于存放当前需要被线程操作的资源,指定个数,当达到最大值,则出发附加线程进行工作
ThreadFactory threadFactory,  //线程工厂 一般不做改动
RejectedExecutionHandler handler)  //拒绝策略,当需要运行的线程数量大于线程池最大的线程容量,需要做的事。线程池承载量=核心线程数+附加线程数+阻塞队列的容量

如定义线程池核心线程为3,最大核心线程池大小为5,则附加线程为5-2=3个,核心线程用于处理线程,而附加线程睡眠,资源入阻塞队列等待被操作,当阻塞到达一定情况,则触发附加线程一起工作。

4个拒绝策略

ThreadPoolExecutor.AbortPolicy()  满了抛异常ThreadPoolExecutor.CallerRunsPolicy()  哪里来回哪去,由调用的线程去处理ThreadPoolExecutor.DiscardPolicy()  队列满了不抛异常,丢弃多余任务ThreadPoolExecutor.DiscardOldestPolicy()  队列满了,尝试与第一个线程竞争资源

ForkJoin

并行执行任务

工作窃取:某线程工作执行完毕后会去执行其他线程的工作

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值