JUC并发编程学习文档

JUC并发编程

1,什么是JUC(java.util.concurrent)

java.util工具包

image-20201207145215073

业务:普通的线程代码:Thread

Runnable 没有返回值,效率相对callable较低

image-20201207150815959

以及lock锁

image-20201207150815959

2,线程和进程

线程,进程

线程:

  • 一个程序,WeChat.exe,QQ.exe,程序的集合

  • 一个进程可以包含多个线程,至少包含一个

  • java默认有两个线程,一个是main线程,一个是负责垃圾回收的GC线程进程

线程:

  • 例如开启了一个进程(腾讯视频),播放声音是一个线程,播放音乐是一个线程,每一个同时执行的任务都是不同的线程负责的,

  • 对于Java而言:Thread,Runnable,Callable

  • Java是运行在虚拟机上的,本质上不能直接开启线程,我们创建的线程通过.start()方法调用后,是走了start0()这个native方法调用底层的C++实现的,Java本质上不能直接操作硬件

        public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
    
            /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
            group.add(this);
    
            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                    /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
                }
            }
        }
    	//本地方法,调用底层C++,Java不能直接操作硬件
        private native void start0();
    

并发,并行

并发(多个线程操作同一个资源)

  • 一核CPU,模拟出来多条线程,线程间快速交替,产生并行的假象

并行(多个人一起行走)

  • 多核CPU,多个线程同时运行执行 , 线程池

    public class ThreadTest {
        public static void main(String[] args) {
            //查看电脑的CPU核数,CPU密集型,IO密集型
            System.out.println(Runtime.getRuntime().availableProcessors());
        }
    }
    
  • 并发编程的本质:充分利用CPU的资源

线程有几个状态

以下代码来自Thread.State源码

     * @since   1.5
     * @see #getState
     */
    public enum State {
		//新生
        NEW,
		//运行
        RUNNABLE,
		//阻塞
        BLOCKED,
		//等待
        WAITING,
		//超时等待
        TIMED_WAITING,
		//终止
        TERMINATED;
    }

Java定义了六种线程状态

  • 新生,运行,阻塞,等待,超时等待,终止

wait/sleep的区别

  • 来自不同的类,wait来自Object,sleep来自Thread
  • 使用范围不同,sleep可以在任何地方使用,wait只能在同步代码块中使用
  • 不会释放锁,sleep不会释放锁,wait会释放锁
  • 自JDK1.8以后,两个方法都需要抛出InterruptedException这个中断异常

3,Lock锁

传统的Synchronized

在方法返回值之前加上synchronized关键字

public class SellTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //JDK1.8之后的lambda表达式
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"A").start();
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"B").start();
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"C").start();
    }

}

//OOP
class Ticket {
    //属性,方法
    private int ticketNumber = 30;

    public synchronized void sellTicket(){
        if(ticketNumber>0){
            System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketNumber--+"张票,还剩下"+ticketNumber+"张票");
        }
    }

}

synchronized本质上就是队列,锁,本质上就是让线程排队,让所有不讲规矩的人规规矩矩的排队

Lock接口

image-20201207161609373

image-20201207161842545


package com.hai.demo03;

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

/**
 * @author Muzi
 * @version 3.6.3
 * @package com.hai.demo03
 * @date 2020/12/7 16:02
 * @project juc
 **/
public class SellTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //JDK1.8之后的lambda表达式
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"A").start();
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"B").start();
        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sellTicket();
            }
        },"C").start();
    }

}

//OOP

//三部曲
//1.new ReentrantLock()     创建锁
//2.lock.lock()             上锁
//3.lock.unlock()           解锁
class Ticket {
    //属性,方法
    private int ticketNumber = 30;
    Lock lock = new ReentrantLock();
    public void sellTicket(){
        lock.lock();
        try {
            if(ticketNumber>0){
                System.out.println(Thread.currentThread().getName()+"卖出了第"+ticketNumber--+"张票,还剩下"+ticketNumber+"张票");
            }
        }finally {
            lock.unlock();
        }
    }

}

Synchronized和Lock锁的区别

  1. synchronized是java内置关键字,Lock是一个类
  2. synchronized会自动释放锁,Lock必须手动释放锁
  3. synchronized在其他线程阻塞的情况下,傻等释放锁,Lock阻塞时会尝试获取锁
  4. synchronized不能判断锁的状态,Lock可以判断时候获取到了锁
  5. synchronized可重入锁,不可中断的,非公平锁,Lock可重入锁,可以判断锁,公平锁(可以自己设置)
  6. synchronized适合锁少量代码块,Lock适合锁大量同步代码块

锁是什么,如何判断锁的是谁

4,生产者和消费者问题

生产者和消费者问题,synchronized版


//生产者消费者问题,实现线程之间的通信
public class PC {

    public static void main(String[] args) {
        Date date = new Date();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.increment();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.decrement();
            }
        },"B").start();

    }
}

class Date{
    private int number = 0;

    //+1
    public synchronized void increment(){

        if(number!=0){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务
        number++;
        System.out.println(Thread.currentThread().getName()+"==>"+number);
        //唤醒
        this.notifyAll();
    }

    //-1
    public synchronized void decrement(){

        if(number==0){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务
        number--;
        System.out.println(Thread.currentThread().getName()+"==>"+number);
        //唤醒
        this.notifyAll();
    }
}

在只有两个线程的情况下,这是没有问题的,那要是三个四个线程呢?看效果

image-20201207165411435

可见数据产生了负数,如果用到线上环境发生这种情况,那你基本要蹲号子了,

还引发了一个问题,程序并没有结束运行,也没有任何输出,线程之间的通信做不好甚至会引起死锁的情况

image-20201207170416215

问题发生的原因:

  • 是因为我们做了if判断,因为if判断只判断一次,在两个线程都进入的情况下,两个线程看到的数据都是可进入的数据,两个线程都将数据进行了操作,

问题解决,将if判断时候while循环,让它一致判断,JDK帮助文档明显指出使用if判断可能存在虚假唤醒的问题,应该将if换成while

image-20201207170010691

使用while判断后,4个线程同时执行

//生产者消费者问题,实现线程之间的通信
public class PC {

    public static void main(String[] args) {
        Date date = new Date();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.increment();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.decrement();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.increment();
            }
        },"C").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date.decrement();
            }
        },"D").start();

    }
}

class Date{
    private int number = 0;

    //+1
    public synchronized void increment(){

        while (number!=0){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务
        number++;
        System.out.println(Thread.currentThread().getName()+"==>"+number);
        //唤醒
        this.notifyAll();
    }

    //-1
    public synchronized void decrement(){

        while (number==0){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务
        number--;
        System.out.println(Thread.currentThread().getName()+"==>"+number);
        //唤醒
        this.notifyAll();
    }
}

修复问题后家庭幸福美满

image-20201207170528025

juc版的生产者和消费者问题

通过Lock找到Condition

image-20201207170947305

image-20201207171033990

代码实现:

package com.hai.providerandcosumer;

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

/**
 * @author Muzi
 * @version 3.6.3
 * @package com.hai.providerandcosumer
 * @date 2020/12/7 17:15
 * @project juc
 **/
public class PC2 {


    public static void main(String[] args) {
        Date2 date = new Date2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                date.increment();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                date.decrement();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                date.increment();
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                date.decrement();
            }
        }, "D").start();

    }
}

class Date2 {
    private int number = 0;

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

    //+1
    public void increment() {
        lock.lock();
        try {
            while (number != 0) {
                //等待
                condition.await();
            }
            //业务
            number++;
            System.out.println(Thread.currentThread().getName() + "==>" + number);
            //唤醒
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //唤醒
    }

    //-1
    public void decrement() {
        lock.lock();
        try {
            while (number == 0) {
             //等待
                condition.await();
            }
            //业务
            number--;
            System.out.println(Thread.currentThread().getName() + "==>" + number);
            //唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}


运行效果:

image-20201207172320134

运行效果可见,确实是010101运行了,但是我想让他有序ABCD这样运行该怎么做呢??

Condition精准唤醒

代码测试:

package com.hai.providerandcosumer;

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

/**
 * @author Muzi
 * @version 3.6.3
 * @package com.hai.providerandcosumer
 * @date 2020/12/7 17:31
 * @project juc
 **/
public class PC3 {
    public static void main(String[] args) {
        Date3 date3 = new Date3();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                date3.printA();
            }
        }, "A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date3.printB();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                date3.printC();
            }
        },"C").start();

    }
}

class Date3 {
    private Lock lock = new ReentrantLock();
    //构建三个监视器监视不同的资源
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();
    private int num = 1;//1A,2B,3C

    public void printA() {

        lock.lock();
        try {
            while (num != 1) {
                //等待
                conditionA.await();
            }
            num = 2;
            System.out.println(Thread.currentThread().getName()+"==>"+num);
            conditionB.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (num != 2) {
                conditionB.await();
            }
            num = 3;
            System.out.println(Thread.currentThread().getName()+"==>"+num);
            conditionC.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void printC() {

        lock.lock();
        try {
            while (num!=3) {
                conditionC.await();
            }
            num=1;
            System.out.println(Thread.currentThread().getName()+"==>"+num);
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行效果:

image-20201207174352286

5,八锁现象

什么是锁,

锁只会锁两个东西,一个是对象,一个是类的Calss模板

new this 是一个具体的对象

static class 是一个全局唯一的类模板

代码:无

6,集合类不安全

List不安全

代码:

public class ListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }

}

以上代码看似没什么问题,但是运行时却出现了.ConcurrentModificationException并发修改异常

image-20201207181138404

因此我们可以看出,List在多线程的情况下是不安全的,那么怎样使它变得安全呢

有以下三种解决方案:

1. 使用     new Vector<>();
2. 使用     Collections.synchronizedList(new ArrayList<>());
3. 使用     new CopyOnWriteArrayList<>();
  1. Vector的add方法上了synchronized,解决了并发修改异常,但是加synchronized代表线程需要进行排队,降低了效率

    源码:

     public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
     }
    
  2. Collections.synchronizedList(new ArrayList<>())的这是Java提供的一个将list转换为安全集合的方法

  3. new CopyOnWriteArrayList<>() 操作数据的方法是通过Lock锁锁上的

    • CopyOnWrite写入时复制,COW计算机设计领域的一种是优化策略
    • 在写入的时候避免覆盖,造成数据问题

    源码:

     public boolean add(E e) {
         	//构建Lock锁
            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();
            }
        }
    

三种方式均能解决List集合并发问题

image-20201207183523102

Set不安全

跟List同理,在多线程并发情况下,Set同样是不安全的,也同样会抛出并发修改异常

image-20201208191610235

解决方案:

  1. 使用Collections工具类转化

    Set<String> set = Collections.synchronizedSet(new HashSet<>());
    
  2. 使用写入前复制的CopyOnWriteSet<>()

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

代码:

public class SetTest {

    public static void main(String[] args) {
//        Set<String> set = new HashSet<>();
//        Set<String> set = new CopyOnWriteArraySet<>();
        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

hashSet的本质是什么?

本质就是一个HashMap,从源码可以看出,底层就是new了一个HashMap

public HashSet() {
        map = new HashMap<>();
}

为什么HashSet存储的值是去重的,因为set的add方法就是调用了Map的put方法,将set的值作为Map的键存进去,而Map的键是不能重复的,HashSet就是将HashMap做了一个封装

 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
 }

Map不安全

public class MapTest {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(()->{
             map.put(Thread.currentThread().getName(),String.valueOf(finalI));
                System.out.println(map);
            }).start();
        }
    }
}

还是我们熟悉的异常信息

image-20201208193357645

这里的解决方案与之前的List和Set有一个不同

解决方案:

  • Collections工具类转化

    Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    
  • 使用ConcurrentHashMap

    Map<String, String> map = new ConcurrentHashMap<>();
    

Map<String, String> map = new HashMap<>();等价于什么?

  • 在没有任何参数情况下,new HashMap<>()默认的参数有加载因子0.75,默认容量16,通过1>>4位运算得来
  • 等价于 Map<String, String> map = new HashMap<>(16,0.75f);
  • image-20201208194151227

7,Callable

image-20201208194816106

  1. 可以有返回值
  2. 可以抛出异常
  3. 使用call()方法调用

线程代码:

class MyThread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

问题:

  • 线程启动得方法有且仅有一个,就是.start()方法,这个方法归属于Thread类,但是Thread类里只能有Runnable这个参数,那该如何启动我们的线程?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2MGl37pc-1608011313176)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20201208200315123.png)]

解决:通过JDK文档,我们找到了Runnable得实现类Futuretask

image-20201208201059365

而且futureTask的构造函数时可以传入一个Callable线程的,

image-20201208201229798

因此我们可以将futureTask作为媒介类来启动我们的Callable线程

public class CallableTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread);
        new Thread(futureTask).start();
    }

}

class MyThread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

线程完美启动并成功调用call()方法

image-20201208201457912

那么返回值呢?

通过futureTask调用get()方法获取

image-20201208201649540

结果如期而至,但是值得一提的是,这个get方法需要等到call方法结束后才能接收到返回值,如果call方法执行的是比较耗时的任务时,get方法会阻塞,所以一般get方法用在最后一行或者使用异步通信

当多条线程执行时,会发现call方法只执行了一次,由此可以得出,callable的结果为了提高效率是有缓存的

image-20201208202059918

8,常用辅助类

8.1,CountDownLatch(减法计数器)

模拟一个任务完成关机行为

public class CountDownLatchDemo {

    public static void main(String[] args) {


        for (int i = 1; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"号任务结束");

            },String.valueOf(i)).start();
        }

        System.out.println("线程任务已执行结束,正在关机");
    }
}

在不适用CountDownLatch的情况下,可见结果并不理想

image-20201208203038462

使用后:

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(5);

        for (int i = 1; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"号任务结束");
                count.countDown();//计数器-1
            },String.valueOf(i)).start();
        }
        count.await();//等待计数器归零,执行以下代码
        System.out.println("线程任务已执行结束,正在关机");
    }
}

完成要求

image-20201208203321982

8.2,CyclicBarrier(加法计数器)

官方的话语过于官方,简单来说就是,多少条线程执行结束之后,在执行指定线程

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5,()->{
            System.out.println("车辆已经全部让行,救护车通过前方道路");
        });
        for (int i = 1; i <= 5; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"号车正在让行,塞车中...");
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

结果

image-20201208204834692

8.3,Semaphore(信号量)

一般用于限流,多个共享资源互斥使用等

public class SemaphoreDemo {
    public static void main(String[] args) {
        //将服务器请求限制为2个
        Semaphore semaphore = new Semaphore(2);

        //共有10个用户请求我的服务器
        for (int i = 0; i < 10; 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 {
                    //完成后释放,信号量+1
                    semaphore.release();
                }
            },String.valueOf(i+1)).start();
        }
    }
}

效果:

image-20201208205845960

9,读写锁

ReadWruteLock

image-20201209125055422

缓存资源类

class MyCacheLock{

    private volatile Map<String,Object> map;

    public MyCacheLock(){
        map=new HashMap<>();
    }
    public void put(String k,Object v){
        System.out.println(Thread.currentThread().getName()+"写入"+k);
        map.put(k, v);
        System.out.println(Thread.currentThread().getName()+"写入OK");
    }
    public void get(String k){
        System.out.println(Thread.currentThread().getName()+"读取");
        map.get(k);
        System.out.println(Thread.currentThread().getName()+"读取OK");
    }
}

线程操作结果

image-20201209125849688

在不加锁的情况下,发生了如上情况,使用其他的锁也能达到目的,但是相对使用读写锁来说,更细粒度的控制,从而提高效率

加锁后:

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCacheLock lock = new MyCacheLock();

        //十条线程写入
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(()->{
                lock.put(finalI +"", finalI +"");
            },String.valueOf(i)).start();
        }
        //读取
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(()->{
                lock.get(finalI+"");
            },String.valueOf(i)).start();
        }
    }
}

class MyCacheLock{

    private volatile Map<String,Object> map;
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public MyCacheLock(){
        map=new HashMap<>();
    }
    public void put(String k,Object v){
        try {
            //加写锁
            readWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"写入"+k);
            map.put(k, v);
            System.out.println(Thread.currentThread().getName()+"写入OK");
        } finally {
            //解锁
            readWriteLock.writeLock().unlock();

        }
    }
    public void get(String k){

        try {
            //加读锁
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"读取");
            map.get(k);
            System.out.println(Thread.currentThread().getName()+"读取OK");
        } finally {
            //解锁
            readWriteLock.readLock().unlock();
        }
    }
}

效果:

image-20201209130400938

  • 读锁(共享锁)多个线程可以同方式操作,线程间可以共存,
  • 写锁(独占锁)只能有一个线程操作,其他线程需要等待,线程间不能共存

10,阻塞队列

image-20201209130842558

阻塞队列:

image-20201209131033052

BlockingQueue 大家对它其实并不陌生,它和List跟Set一样都是从Collection衍生过来的,

image-20201209131734043

看一下类图

image-20201209132312312

什么时候使用阻塞队列:并发处理,线程池等

学会使用队列

添加,移除

BlockingQueue对应四组API

方式抛出异常不抛异常,有返回值阻塞等待超时等待
添加add()offer()put()offer(value,time,TimeUnit)
移除remove()poll()take()poll(time,TimeUnit)
获得首元素element()peek()

测试代码:

/**
 * 会抛出异常
 **/
public static void test1(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    blockingQueue.add("1");
    blockingQueue.add("2");
    blockingQueue.add("3");

    //获得队列首元素
    System.out.println(blockingQueue.element());
    //Exception in thread "main" java.lang.IllegalStateException: Queue full    队列满
    //blockingQueue.add("1");

    System.out.println("==============");
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    //Exception in thread "main" java.util.NoSuchElementException       队列空
    //System.out.println(blockingQueue.remove());
}

/**
 * 不会抛出异常,有返回值
 **/
public static void test2(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("1"));
    System.out.println(blockingQueue.offer("2"));
    System.out.println(blockingQueue.offer("3"));
    System.out.println(blockingQueue.offer("4"));//false

    //获得队列的首元素
    System.out.println(blockingQueue.peek());

    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());//null
}

/**
 * 阻塞等待,死等
 **/
public static void test3() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    blockingQueue.put("1");
    blockingQueue.put("2");
    blockingQueue.put("3");
    //程序阻塞
    //blockingQueue.put("4");
    System.out.println("======");
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    //同样阻塞
    //System.out.println(blockingQueue.take());
}

/**
 * 超时等待,规定时间内等不到就结束不等了
 **/
public static void test4() throws InterruptedException {

    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("1"));
    System.out.println(blockingQueue.offer("2"));
    System.out.println(blockingQueue.offer("3"));
    //等待2秒放不进去就放弃
    System.out.println(blockingQueue.offer("4",2,TimeUnit.SECONDS));
    System.out.println("===============");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    //等待2秒取不出来就放弃
    System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
}
  1. 抛出异常
  2. 不会抛出异常
  3. 阻塞等待
  4. 超时等待

SynchronousQueue 同步队列

没有容量,一次只能放一个,必须等到放进去put的元素取出来take才能放入下一个元素

测试代码:

public class SynchronousQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "存入" + 1);
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "存入" + 2);
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "存入" + 3);
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "取==>" + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "取==>" + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "取==>" + synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();
    }

}

image-20201209182136338

可见在多线程 情况下,必须将元素取出来才能重新存储,类似于我们的semaphore信号量

11,线程池

线程池:三大方法,七大参数四种拒绝策略

池化技术

程序运行的本质,占用系统资源,使用池化技术减少系统资源的消耗,优化系统资源的使用

线程池,连接池,内存池,对象池等。频繁的创建和销毁,十分的浪费系统内资源

池化技术:实现准备好资源,用就来拿,用完就还给我

线程池的好处

  • 减少资源消耗

  • 提高响应速度

  • 管理更加方便

  • 线程服用,可以控制最大并发数,管理线程

线程池:三大方法

//遇强则强,可伸缩的线程池大小
ExecutorService executorService = Executors.newCachedThreadPool();
//单个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

以上就是线程池创建的三大方法。但是我们发现其底层都是将ThreadPoolExecutor进行了封装,

new ThreadPoolExecutor(nThreads, nThreads,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>());

阿里巴巴开发手册明确指出,不允许使用者三个方法创建线程,我们应该通过ThreadPoolExecutor自定义一个线程池,

image-20201209184415010

源码

image-20201209184616900

线程池启动

image-20201209184934938

使用完记得归还,不然会让程序卡死

public class ExecutorsTest {
    public static void main(String[] args) {
//        ExecutorService executorService = Executors.newCachedThreadPool();
//        ExecutorService executorService = Executors.newSingleThreadExecutor();
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        try {
            for (int i = 0; i < 10; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"号线程执行");
                });
            }
        } finally {
            //使用完归还
            executorService.shutdown();
        }
    }
}

七大参数

源码分析:

Executors.newCachedThreadPool();

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Executors.newSingleThreadExecutor();

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

Executors.newFixedThreadPool(5);

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

通过者三个方法的线程创建方式我们可以看到,底层都是new了一个ThreadPoolExecutor,那他到底是个什么东西?

本质:ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                          int maximumPoolSize,//最大线程池大小
                          long keepAliveTime,//超时无用自动释放
                          TimeUnit unit,//超时单位
                          BlockingQueue<Runnable> workQueue,//阻塞队列
                          ThreadFactory threadFactory,//线程工厂
                          RejectedExecutionHandler handler//拒绝策略
                         ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

手动创建一个线程池

 ExecutorService executorService = new ThreadPoolExecutor(
                2,//核心2个
                3,//最大3个
                3,//超时3秒释放
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),//BlockingQueue
                Executors.defaultThreadFactory(),//使用默认线程工厂
                new ThreadPoolExecutor.AbortPolicy()//
        );

当队列大小和最大线程大小之和小于了当前线程数时,AbortPolicy()这个决绝策略会抛出异常

image-20201209191554086

四种拒绝策略

image-20201209191056461

  • AbortPolicy,超出处理数量抛出异常

    image-20201209191715239

  • DiscardPolicy,队列满了,就丢弃任务,不抛出异常

    image-20201209191903435

  • CallerRunsPolicy,谁放进来的谁来处理(main)

    image-20201209192032518

  • DiscardOldestPolicy,队列满了,尝试与最早的任务竞争,也不会抛出异常

    image-20201209192311459

如何定义最大线程数(了解,调优)

  • CPU密集型

    根据电脑的CPU核数分配最大的线程池大小,每个电脑配置不一样,CPU核数应该动态获取,而不是写死

    ExecutorService executorService = new ThreadPoolExecutor(
                    2,//核心2个
                    Runtime.getRuntime().availableProcessors(),//获取设备可用处理器
                    3,//超时3秒释放
                    TimeUnit.SECONDS,
                    new LinkedBlockingDeque<>(3),//BlockingQueue
                    Executors.defaultThreadFactory(),//使用默认线程工厂
                    new ThreadPoolExecutor.AbortPolicy()//
            );
    
  • IO密集型

    根据程序大型IO资源消耗的任务开启线程池大小,如果IO资源消耗过大的任务有10个,那我们一般默认开启的线程池的大小时它的两倍

12,四大函数式接口(JDK1.8新特性)

JDK1.8四大新特性:函数式接口,链式编程,Lambda表达式,Stream流式计算

作用:简化编程模型,新框架底层大量应用

函数型接口

public class FunctionDemo {
    public static void main(String[] args) {
        Function<String, Integer> function = new Function<String, Integer>() {
            @Override
            public Integer apply(String str) {
                return str.isEmpty() ? 1 : 0;
            }
        };

        //lambda
//        Function<String, Integer> function =(str)-> str.isEmpty() ? 1 : 0;


        System.out.println(function.apply("111"));
    }

}

Function,泛型规约参数和返回值

image-20201209194605652

断定型接口

public class PredicateDemo {
    public static void main(String[] args) {
//        Predicate<String> stringPredicate = new Predicate<String>() {
//            @Override
//            public boolean test(String str) {
//                return str.isEmpty();
//            }
//        };

        //lambda
        Predicate<String> stringPredicate = String::isEmpty;

        System.out.println(stringPredicate.test("02"));


    }

Predicate,泛型规约参数类型,返回值为布尔值,可用做非空工具类判断

image-20201209195138412

消费型接口

public class ConsumerDemo {
    public static void main(String[] args) {
//        Consumer consumer = new Consumer<String>() {
//            @Override
//            public void accept(String username) {
//                System.out.println(username+"调用消费型接口");
//            }
//        };
        //lambda
        Consumer consumer =(username)->{
            System.out.println(username+"调用消费型接口");
        };
        consumer.accept("海金");
    }

}

Consumer,泛型规约参数类型,顾名思义消费者,只有支出没有收入,只有参数,没有返回值

image-20201209200527963

供给型接口

public class SupplierDemo {
    public static void main(String[] args) {
//        Supplier supplier = new Supplier<String>() {
//            @Override
//            public String get() {
//                return null;
//            }
//        };
        //lambda
        Supplier supplier = ()-> "供给型接口";

        System.out.println(supplier.get());

    }

}

Supplier,泛型规约返回值类型,不需要参数,他是个供应商,只负责提供

image-20201209200922838

可以看到都是@since都是1.8,这代表着最早使用到此类的JDK版本,这都是JDK1.8的新特性

13,Stream流式计算

什么是Stream流式计算

大数据时代,本质就是存储和计算,计算都应该交给流来操作

image-20201210125746634

测试代码

public class StreamDemo {
    /**
     * 题目要求:根据条件查找用户名
     * 1.id为偶数
     * 2.年龄大于23
     * 3.倒序排列
     * 4.用户名字转换大写
     * 5.只显示一个用户
     **/

    public static void main(String[] args) {
        User user1 = new User("u", 22, 3);
        User user2 = new User("v", 12, 7);
        User user3 = new User("x", 25, 2);
        User user4 = new User("y", 72, 4);
        User user5 = new User("z", 13, 9);

        //解:
        long start = System.currentTimeMillis();
        List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
        list.stream().filter(u -> u.getId() % 2 == 0)
                .filter(u -> u.getAge() > 23)
                .map(u -> u.getUsername().toUpperCase())
                .sorted((u1,u2)->{ return u2.compareTo(u1);})
                .limit(1)
                .forEach(System.out::println);//X
        long end = System.currentTimeMillis();
        System.out.println("流式计算耗时"+(end-start)+"ms");


         long start1 = System.currentTimeMillis();
        //将多个对象转换为List集合
        List<User> list2 = Arrays.asList(user1, user2, user3, user4, user5);
        ArrayList<User> users = new ArrayList<>();
        for (User user : list2) {
            //过滤掉id基数和年龄小于23的
            if (user.getId() % 2 == 0 && user.getAge() > 23) {
                //将名字转换为大写,并添加到新的List集合,
                user.setUsername(user.getUsername().toUpperCase());
                users.add(user);
            }
        }
        //因为List集合是有序的,相当于已经给我们自动排好序了,只不过是正序,而我们只需要取一个用户,那就是最后一个
        System.out.println(users.get(users.size()-1).getUsername());
        long end1 = System.currentTimeMillis();
        System.out.println("普通计算耗时"+(end1-start1)+"ms");

    }

}

运行结果:少数据量的情况下,流式计算好像没起到什么作用,反而更鸡肋了

image-20201210132151293

14,ForkJoin

在JDK1.7退出,并行执行,提高效率,适合大数据量

什么是ForkJoin

分支合并,将一个任务进行拆分,最后合并结果并返回

image-20201210132638722

ForkJoin特点:工作窃取,当A,B两条线程执行不同任务时,B线程执行完了自己的任务,他会偷A线程没有完成的任务去执行,双端队列,两头都能取

找到它的实现类

image-20201210190310762

测试代码:

public class ForkJoinDemo extends RecursiveTask<Long> {

    private final Long startV;
    private final Long endV;

    public ForkJoinDemo(Long startV, Long endV) {
        this.startV = startV;
        this.endV = endV;
    }

    @Override
    protected Long compute() {
        if ((endV - startV) > 10_0000) {
            long middle = (endV + startV) / 2;
            ForkJoinDemo task1 = new ForkJoinDemo(startV, middle);
            ForkJoinDemo task2 = new ForkJoinDemo(middle+1, endV);
            //开始计算
            task1.fork();
            task2.fork();
            //合并结果
            return task1.join()+task2.join();

        }
        long sum= 0L ;
        for (Long i = startV; i < endV; i++) {
            sum+=i;
        }
        return sum;
    }
}

工作原理就是递归拆分任务,最后将结果合并返回调用者,

  • 继承RecursiveTask类,泛型约束任务方法的返回值
  • 开始递归调用,自己创建自己

15,异步回调

future设计的初衷就是为了为未来建模

image-20201210191742586

异步回调,,类似于前端的Ajax

public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //没有返回值的
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync");
        });
        System.out.println("执行");
        System.out.println(future.get());//获取结果时会阻塞但是任务是后台执行的,类似于Ajax

        //有返回值的
        System.out.println(CompletableFuture.supplyAsync(() -> {
            int i = 1 / 0;
            return 1024;
        }).whenComplete((t, u) -> {
            System.out.println("u" + u);//u是异常信息
            System.out.println("t" + t);//t是返回值
        }).exceptionally((e) -> {
            e.getMessage();
            return 500;//发生错误返回状态码
        }).get());
    }
}

16,JMM(Java Memory Model)

JMM内存模型,它不是真实存在的,它是一种约定概念

关于JMM的一些同步约定

  1. 线程加锁前必须把主存中的最新值加载到工作内存中
  2. 线程解锁前必须把共享变量立刻刷回主存
  3. 加锁跟解锁是同一把锁

image-20201210195318335

线程操作变量时,并不是直接修改变量的值,而是先将主存的变量加载到自己的工作内存,再提供给执行引擎,最后返还给内存,然后在写入主存中

JMM有八大原子性操作

关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。

  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

  • 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。

  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

测试:

public class VolatileDemo {
    private static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (num == 0) {}
        }).start();
        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(num);
    }
}

可以看到,程序并没有停止运行,直到我打完这行字,他也没有停止,因为线程拿到的变量一直是0;

image-20201210200547962

问题:当线程A在使用变量时,线程B将变量做出了修改,但是线程A没能收到通知,这个问题该如何解决?volatile

17,Volatile

volatile是什么?

volatile是Java虚拟机提供的轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1,保证可见性

public class VolatileDemo {
    //不加volatile程序就会死循环
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (num == 0) {}//线程的工作内存不知道主存已经发生了变化
        }).start();
        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(num);
    }
}

2,不保证原子性

public class VDemo02 {
    private volatile static int num = 0;

    private static void add(){
        num++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){}
        //正常结果应该等于20000
        System.out.println(Thread.currentThread().getName()+"===>"+num);
    }
}

结果应该是20000,但是输出结果却是少于20000,说明了即使添加了volatile关键字,线程修改还是不能保证原子性,还是会被其他线程插入,那应该怎么解决呢?,除了使用synchronized和Lock以外,我们还能使用原子类这个包下的类

image-20201210203711876

为什么一个num++还不能保证原子性呢?,因为在我们看来就是一个+1操作,但是在计算机那里,分为了几个指令,我们可以通过javap -c 查看字节码文件

image-20201211132747742

计算机指令给它分为了四步走。

public class VDemo02 {
    private volatile static AtomicInteger num = new AtomicInteger();

    private static void add(){
        num.getAndIncrement();
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){}
        System.out.println(Thread.currentThread().getName()+"===>"+num);
    }
}

3,禁止指令重排

在我们写的程序中,计算机并不是直接按照你写的那样去执行的,程序从编译到运行,要经过很多步骤

image-20201211175609809

而在这些步骤中,指令是可能发生的,但是概率极低,并不排除不会发生的可能性

int a = 1;	//1
int b = 1;	//2
a = a + 2;	//3
b = a * a;	//4

我们希望程序1234依次执行,但是程序可能会执行:2134,1324等,

但是不可能是4123,因为那样会改变程序的运行结果,处理器在进行指令重排的时候,要考虑数据依赖性的,

volatile是怎么禁止指令重排的呢?

内存屏障,CPU指令,

作用:

1,保证特定的操作的执行顺序

2,可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

volatile可以保证可见性,但是不能保证原子性,由于内存屏障,很好的解决了指令重排的问题

18,单例模式

饿汉式单例

public class HungryMan {
    
    private HungryMan(){}
    
    private static final HungryMan hungryMan = new HungryMan();
   
    public HungryMan getInstance(){
        return hungryMan;
    }
}

会存在空间的浪费,但是对象构建速度是很快的

懒汉式单例

public class LazyMan{
    private LazyMan(){}

    private static LazyMan lazyman;

    public static LazyMan getInstance(){
        if(lazyman==null){
            lazyman = new LazyMan();
        }
        return lazyman;
    }
}

在单线程情况下,普通懒汉式单例是安全的,但是在多线程情况下,可能会创建多个实例,

DCL懒汉式单例

public class LazyMan{
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    //避免指令重排
    private volatile static LazyMan lazyman;

    //双重检测锁模式
    public static LazyMan getInstance(){
        if(lazyman==null){
            synchronized (LazyMan.class){
                if(lazyman==null){
                    lazyman = new LazyMan();//构建对象时可能会发生指令重排,加上volatile关键字
                }
            }
        }
        return lazyman;
    }
}

解决了多线程下单例模式失效的问题。

静态内部类单例也能解决:

public class Holder {
    private Holder() {
    }

    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }
}

单例真的安全吗?反射破坏单例

public class ReflectSingle {

    private ReflectSingle(){}

    private static ReflectSingle reflectSingle;

    public static ReflectSingle getInstance(){
        if(reflectSingle==null){
            synchronized(ReflectSingle.class){
                if(reflectSingle==null){
                    reflectSingle = new ReflectSingle();
                }
            }
        }
        return reflectSingle;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<ReflectSingle> constructor = ReflectSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        ReflectSingle reflectSingle1 = constructor.newInstance();
        ReflectSingle reflectSingle2 = getInstance();
        System.out.println(reflectSingle1);
        System.out.println(reflectSingle2);

    }
}

测试结果:

image-20201211190817937

可见在反射下,我们的单例模式是不安全的,难道真的没有办法保证单例的安全性吗?

枚举

通过反射的newInstance()方法的源码我们发现,枚举是不能被破坏的

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
     //如果是ENUM就Cannot reflectively create enum objects抛出异常
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

真的不能吗?,我想试试看。。

测试代码:

package com.hai.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

}

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle enumSingle=EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingle enumSingle1 = constructor.newInstance();
        System.out.println(enumSingle);
        System.out.println(enumSingle1);
    }
}

通过编译的.class字节码文件还原后,发现有个无参构造器

image-20201211191818493

但是通过反射抛出的异常却告诉我们没有无参的构造器,而不是我们预想中的Cannot reflectively create enum objects这个异常信息

image-20201211191637862

好像IDEA骗了我,通过javap反编译后

image-20201211192113371

那为什么??通过jad将class转换为jad源码

// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingle.java

package com.hai.single;


public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/hai/single/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

存在的构造器确实不是无参的,而是带着两个参数

image-20201211192525101

那我们将这两个参数传入

image-20201211192655915

结果抛出了newInstance()方法预先声明的异常,说明枚举确实是不能被反射破坏的,我们也因此得知了枚举没有无参构造,而是有两个参数的有参构造

19,CAS理解

CAS比较并交换

public class CASTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2000);
        //CAS比较并交换
        //如果是期望值,那么就更新并返回true,如果不是就返回false
        System.out.println(atomicInteger.compareAndSet(2000, 2002));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(2000, 2002));
        System.out.println(atomicInteger);

    }

}

结果

image-20201211194352656

源码:

image-20201211194741890

直接修改内存中的值,是一个循环判断内存偏移量,自旋锁

CAS:比较当前工作内存中的值和主存中的值,如果是期望值,就执行操作,不是就一直循环

缺点:一次性只能保证一个共享变量的原子性,循环比较耗时,存在ABA问题

ABA问题

image-20201211200653578

20,原子引用

解决ABA问题

带版本号的原子引用,类似于乐观锁

image-20201214184754098

原子引用解决ABA问题

public class ABADemo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(10,1);
        new Thread(()->{
            System.out.println("A::"+reference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reference.compareAndSet(10, 11, reference.getStamp(), reference.getStamp() + 1);
            System.out.println("A:::"+reference.getStamp());
            System.out.println(reference.compareAndSet(11, 10, reference.getStamp(), reference.getStamp() + 1));
        },"A").start();

        new Thread(()->{

            int stamp = reference.getStamp();
            System.out.println("B::"+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(reference.compareAndSet(10, 11, stamp, reference.getStamp() + 1));
            System.out.println("B:::"+reference.getStamp());
            System.out.println("reference = " + reference.getReference());
        },"B").start();
    }

}

运行结果:

image-20201214190242718

可见A线程将10改成了11,又把11,改回了10,但是它的版本号也随之增加,当B线程拿到10时,检查了版本号的变化,发现版本号和与其不同,发现这个10是被人修改过的,是的,它不纯洁了,B线程发现之后,对它很失望,于是绝对不对它的值进行修改,直接返回一个false,此原子引用解决了ABA问题,貌似很简单

21, 各种锁

1,可重入锁

可重入锁(递归锁)

image-20201214191346237

代码测试:

synchronized版

public class LockDemo {
    public static void main(String[] args) {
        MyLock lock = new MyLock();

        new Thread(lock::call,"A").start();

        new Thread(lock::call,"B").start();
    }

}
class MyLock{
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"call");
        sms();
    }
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"sms");
    }
}

结果:

image-20201214191954441

call()方法在调用sms()方法时,也可以获得sms()方法的锁,因为他们存在包含关系,可重入的

Lock版

public class LockDemo02 {
    public static void main(String[] args) {
        MyLock2 lock = new MyLock2();

        new Thread(lock::call, "A").start();

        new Thread(lock::call, "B").start();
    }

}

class MyLock2 {
    Lock lock = new ReentrantLock();
    public void call() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"call");
            sms();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void sms() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "sms");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Lock锁注意加锁和解锁,必须成对出现,否则可能造成死锁现象

2,公平锁/非公平锁

公平锁:很公平,不能插队执行

非公平锁:不公平,可以插队执行,(默认都是非公平锁)

实现方式:

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

3,自旋锁

使用cas定义一个锁

public class SpinLock {
    AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();

    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===Lock");
        while (threadAtomicReference.compareAndSet(null,thread)){

        }
    }

    public void unLock(){
        Thread thread = Thread.currentThread();
        threadAtomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName()+"===unLock");
    }
}

测试:

class Test{
    public static void main(String[] args) {
        SpinLock lock = new SpinLock();
        new Thread(()->{
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unLock();
            }
        },"A").start();

        new Thread(()->{
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unLock();
            }
        },"B").start();

    }
}

结果:

image-20201214193917997

A解锁后B才能解锁

4,死锁

产生死锁的情况

image-20201214195420654

public class DeadLock {
    public static void main(String[] args) {
        new Thread(new MyThread("lock-A","lock-B"),"线程1").start();
        new Thread(new MyThread("lock-B","lock-A"),"线程2").start();
    }
}

class MyThread implements Runnable {
    String lockA;
    String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() +
                    ",得到锁:" + lockA + ",get:" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() +
                        ",得到锁:" + lockB + ",get:" + lockA);
            }
        }
    }
}

发生死锁

image-20201214201623723

死锁排查jps-l

D:\T248\juc>jps -l
58976 com.hai.lock.DeadLock
68144 org.jetbrains.jps.cmdline.Launcher
81632 org.jetbrains.jps.cmdline.Launcher
7892 sun.tools.jps.Jps
90708 org.jetbrains.jps.cmdline.Launcher
50552

查看进程堆栈信息jstack 进程号

"线程2":
        at com.hai.lock.MyThread.run(DeadLock.java:39)
        - waiting to lock <0x00000000d670cc90> (a java.lang.String)
        - locked <0x00000000d670ccc8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"线程1":
        at com.hai.lock.MyThread.run(DeadLock.java:39)
        - waiting to lock <0x00000000d670ccc8> (a java.lang.String)
        - locked <0x00000000d670cc90> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

相关代码笔记:GitHub代码仓库


版权声明:本文为weixin_47949485原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。