多线程进阶复习JUC并发编程

文章目录

  • 1、什么是JUC?
  • 2、Lock锁(重点)
    • 2.1传统的使用synchronized锁的方式
    • 2.2使用lock锁的方式
    • 2.3lock锁和synchronized锁的区别
  • 3、生产者消费者问题
    • 3.1synchronized 传统版
    • 3.2lock 版
  • 4、什么是锁?
  • 5、不安全的集合类
    • 5.1List
    • 5.2Set
    • 5.3Map
  • 6.走进Callable
  • 7.常用的辅助类(必会)
    • 7.1CountDownLatch
    • 7.2CyclicBarrier
    • 7.3Semaphore(常用)
  • 8、ReadWriteLock读写锁
  • 9、阻塞队列(BlockingQueue)
    • 9.1概述
    • 9.2常用场景
    • 9.3队列之间的继承关系
    • 9.4学会使用四种api
  • 10、 同步队列(SynchronousQueue)
  • 11、线程池
    • 11.1池化技术(重点)
    • 11.2Executor工具类三大方法
    • 11.3线程池的7大参数
    • 12、定义最大线程数
  • 12、四大函数式接口
    • 12.1函数式接口
    • 12.2断定型接口
    • 12.3消费型接口
    • 12.4供给型接口
  • 13、stream流式计算
  • 14、forkJoin
  • 15、异步回调
    • 15.1没有返回值的异步回调
    • 15.2有返回值的异步回调
  • 16、JMM
    • 16.1、什么是JMM?
    • 16.2、JMM的一些约定
    • 16.3、JMM的经典问题
  • 17、Volatile
    • 17.1什么是Volatile?
    • 17.2Volatile可见性验证
    • 17.3Volatile不保证原子性验证
    • 17.4禁止指令重排
  • 18、五种单例模式
  • 19、深入理解CAS
    • 19.1什么是CAS:
    • 19.原子引用解决ABA问题
  • 20、各种锁的理解:

1、什么是JUC?

JUC指的是全称叫java.util.concurrent包下的工具类。用中文概括一下,JUC的意思就是java并发编程工具包。其下包含三个最常用的并发编程的工具类:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。
其中并发的三大特性:原子性,可见性,有序性。

2、Lock锁(重点)

2.1传统的使用synchronized锁的方式

public class JUC {
     

    public static void main(String[] args) throws InterruptedException {
     
        // 并发:多线程操作同一个资源类,
        //传统模式通过使一个类继承或实现Runnable接口变成纯粹的一个线程类,然后在main方法里通过new Thread(new 自定义线程类).start()开启线程
        //这样就会造成耦合度较高,不符合Java的OOP思想,为了解耦,通过配合使用jdk8新特性lambda表达式实现新的线程启动方法
        //使得类纯粹就是自己而不是一个线程类
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函数式接口
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                //把资源类丢入线程
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                ticket.sale();
            }
        }, "C").start();
    }
}

class Ticket {
     
    //假设车站还有40张票
    private int number = 40;

    // 卖票的方式---加synchronized锁保证线程的安全
    public synchronized void sale() {
     
        if (number > 0) {
     
            System.out.println(Thread.currentThread().getName() + "卖出了" + (number-(--number) + "票,剩余:" + number));
        }
    }

}

部分结果演示:

A卖出了1,剩余:24
A卖出了1,剩余:23
B卖出了1,剩余:22
B卖出了1,剩余:21
B卖出了1,剩余:20
B卖出了1,剩余:19
B卖出了1,剩余:18
B卖出了1,剩余:17
B卖出了1,剩余:16
B卖出了1,剩余:15
B卖出了1,剩余:14
B卖出了1,剩余:13
B卖出了1,剩余:12
B卖出了1,剩余:11
B卖出了1,剩余:10
B卖出了1,剩余:9
B卖出了1,剩余:8
B卖出了1,剩余:7
B卖出了1,剩余:6
B卖出了1,剩余:5
B卖出了1,剩余:4
C卖出了1,剩余:3
C卖出了1,剩余:2
C卖出了1,剩余:1
C卖出了1,剩余:0

Process finished with exit code 0


2.2使用lock锁的方式

我们通过jdk官方文档可以看到,实现lock锁的实现类有三个:
多线程进阶复习JUC并发编程_第1张图片
我们在这里演示用的是ReentrantLock重入锁,通过点击源码进去查看,我们可以发现,使用重入锁默认用的是非公平的形式,如下图所示:
多线程进阶复习JUC并发编程_第2张图片
公平锁的意思简单来说就是十分公平,可以先来后到;而非公平锁的意思就是允许有插队的情况存在;
下面演示使用Lock锁实现火车站卖票的例子:

public class lock {
     
    public static void main(String[] args) throws InterruptedException {
     
        Ticket2 ticket2 = new Ticket2();
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                ticket2.sale();
            }
        }, "A").start();
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                ticket2.sale();
            }
        }, "B").start();
        new Thread(() -> {
     
            for (int i = 1; i < 20; i++) {
     
                ticket2.sale();
            }
        }, "C").start();
    }
}

class Ticket2 {
     

    private int number = 40;
    //1.new出新的lock锁,实现类为重入锁,可以在重入锁的参数添加true或false改变锁的公平性
    Lock lock = new ReentrantLock();

    public void sale() {
     

        //2.加锁。一般在try方法前加锁
        lock.lock();
        try {
     
            //3.编写业务代码
            if (number > 0) {
     
                System.out.println(Thread.currentThread().getName() + "卖出了" + (number-(--number) + "票,剩余:" + number));
            }
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            //4.解锁
            lock.unlock();
        }
    }
}

2.3lock锁和synchronized锁的区别

Lock synchronized
一个java类 内置的java关键字
可以判断锁的状态,是否获得锁 无法判断锁的状态
手动开关锁 ,不释放锁会造成死锁 自动加锁释放锁
不一定会等待下去,通过tryLock尝试获取锁,等不到就结束 线程一获得锁后假如阻塞了,线程二会一直等待
可重入锁,不可中断,非公平锁 可重入锁,可中断,可改变公平性
适合锁少量的代码同步问题 适合锁大量的同步代码

3、生产者消费者问题

3.1synchronized 传统版

public class ProducerAndConsumer {
     

    public static void main(String[] args) {
     

        Data data = new Data();
        //编写两个线程类
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }

}

class Data {
     //数字资源类

    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
     
    		//如果number不等于0,等待消费,停止生产
        while (number != 0) {
     //使用if循环可能会造成虚假唤醒的情况
            //业务代码
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //通知其他线程
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
     
    		//如果number等于0,停止消费,等待生产
        while (number == 0) {
     
            //业务代码
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //通知其他线程
        this.notifyAll();
    }
}

执行结果就不在这里演示了。但是要提的一个问题是,假如我现在不止两个线程才运行,我多两个线程运行,运行结果会是怎么样的呢?

new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "D").start();

运行结果:

A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
D=>1
C=>2
D=>3
C=>4
D=>5
C=>6
D=>7
C=>8
D=>9
C=>10
D=>11
C=>12
D=>13
C=>14
D=>15
C=>16
D=>17
C=>18
D=>19
C=>20

那为什么会这样呢?这里就涉及到一个问题:虚拟唤醒的问题。这是因为在循环出我们用的是if循环,而if循环所带来的直接影响就是当wait被唤醒后直接从this.wait(),后面的语句继续执行,不是重新执行本方法,简单来说就是if循环只会判断一次,而while循环会不断的判断当时的情况,因而造成出现上述情况。我们来看看官方文档的描述:
在这里插入图片描述
因此,我们只需要将上述代码if循环改成while循环就好啦

3.2lock 版

使用synchronized锁的时候我们通过wait方法让线程等待,通过notifyAll的方法唤醒线程,那么我们使用Lock锁的时候应该用什么让线程等待,又是用什么让线程唤呢?
这里我们使用的是condition的方法去替代wait,notifyAll,他们的本质意思是一样的。

public class ConditionLock {
     
    public static void main(String[] args) {
     

        Data2 data = new Data2();
        //编写两个线程类
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

class Data2 {
     //数字资源类

    /**
     * 使用synchronized锁的时候我们通过wait方法让线程等待,通过notifyAll的方法唤醒线程
     * 那么我们使用Lock锁的时候应该用什么让线程等待,又是用什么让线程唤醒呢?
     * 这里我们使用的是condition的方法去替代wait,notifyAll,他们的本质意思是一样的
     * */

    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    //+1
    public void increment() throws InterruptedException {
     
        lock.lock();//加锁
        try {
     
            while (number != 0) {
     
                //业务代码
                condition.await();//替代synchronized的wait
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            //通知其他线程
            condition.signalAll();//唤醒所有,替代synchronized的notifyAll
        }catch (Exception e){
     
            e.printStackTrace();
        }
        finally {
     
          lock.unlock(); //释放锁
        }
    }

    //-1
    public void decrement() throws InterruptedException {
     
        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();
       }
    }
}

既然synchronized实现的方法和我们用lock锁实现的效果基本都是一致的,并且方法都几乎是相同的,无非就是使用lock的时候我们通过condition来实现唤醒和等待,那么使用lock的优势又是在哪里呢?
很显然,我们如果运行上面的代码会发现,它的唤醒是无序的,即他没有一个特定的顺序,假如说我们想让A执行完之后执行B,B执行完之后只从C,即:A->B->C->A这样的一个顺序,我们这时候就可以通过condition精确唤醒。

演示代码如下:

public class ConditionLock2 {
     
    public static void main(String[] args) {
     

        Data3 data = new Data3();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.printA();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.printB();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.printC();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }, "C").start();
    }
}


class Data3 {
     
    private int number = 1;
    private Lock lock = new ReentrantLock();
    //新建三个监视器来分别监视唤醒不同的线程
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();


    //A
    public void printA() throws InterruptedException {
     
        lock.lock();
        try {
     
            //业务代码
            while (number != 1) {
     //如果不是1都等待,只有当为1的时候输出AAAA
                condition1.await();
            }
            number = 2;//让condition2唤醒
            System.out.println(Thread.currentThread().getName() + "=>AAAAAAAAAAA");
            //通知2线程,注意这里不是signalAll
            condition2.signal();

        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }

    }

    //B
    public void printB() throws InterruptedException {
     
        lock.lock();
        try {
     
            //业务代码
            while (number != 2) {
     
                condition2.await();
            }
            number = 3;
            System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBBBB");
            condition3.signal();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }

    }

    //C
    public synchronized void printC() throws InterruptedException {
     
        lock.lock();
        try {
     
            //业务代码
            while (number != 3) {
     
                condition3.await();
            }
            number = 1;
            System.out.println(Thread.currentThread().getName() + "=>CCCCCCCCCC");
            condition1.signal();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

4、什么是锁?

八种锁的现象深入浅出的理解锁

代码片段一:我们为Phone类的send,call方法加锁,然后先调用发短信功能,然后休眠一秒,再调用打电话功能,那么,实现打电话呢还是先发短信呢?

public class Test1 {
     

    public static void main(String[] args) {
     
        Phone phone = new Phone();

        new Thread(() -> {
     
                phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
                phone.call();
        }, "B").start();
    }
}

class Phone {
     
	//发短信
    public synchronized void send() {
     
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    //打电话
    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

以上代码执行的时候输出的结果是什么呢?

A发短信
B打电话

结果无论怎么运行,一定是先发短信再打电话,为什么呢?那如果我们让发短信的方法休眠4秒呢?

public class Test1 {
     

    public static void main(String[] args) {
     
        Phone phone = new Phone();

        new Thread(() -> {
     
                phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
                phone.call();
        }, "B").start();
    }
}

class Phone {
     
    public synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

结果无论如何运行,一定是发短信先,原因就在于synchronized
锁的是方法的调用者,我们的调用者只有一个,是phone,也就是说,无论是send方法还是call方法,他们用的都是同一个锁,都是phone对象的锁,因此,谁先拿到锁,谁就先使用。


代码片段二:我们再phone类新增了一个普通方法hello,然后再执行这段代码,这次让b输出hello方法,其他不变,那么结果是什么呢?

public class Test2 {
     

    public static void main(String[] args) {
     
        Phone2 phone = new Phone2();

        new Thread(() -> {
     
                phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
                phone.hello();
        }, "B").start();
    }
}

class Phone2 {
     
    public synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }
	//新增一个普通方法
    public void hello(){
     
        System.out.println("hello!");
    }

}

结果:

这次是hello先输出。因为新增的方法是普通方法,没有锁,不是同步方法,不受锁的影响,因此线程B并不会等待A释放锁再去执行。

hello!
A发短信

那如果我新new出一个对象phone,现在有两个对象,两个同步方法,一个发短信,一个打电话,那么是谁先执行呢?
代码如下:

public class Test2 {
     

    public static void main(String[] args) {
     
    //现在这里有两个对象,两把锁,两个调用者
        Phone2 phone = new Phone2();
        //新new出一个对象
        Phone2 phone2 = new Phone2();

        new Thread(() -> {
     
                phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone2.call();
        }, "B").start();
    }
}

class Phone2 {
     
    public synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

    public void hello(){
     
        System.out.println("hello!");
    }

}

结果如下:

因为这次new了两个对象,两个同步方法,这两个对象各自拥有各自的锁,互不影响,因此休眠时间短的先输出,因此B先打电话,过了四秒之后,A再发短信。

B打电话
A发短信

代码片段三:
如果我们这次给phone类的两个方法加上static变为静态方法呢?输出的结果会是什么呢?

public class test3 {
     

    public static void main(String[] args) {
     
        Phone3 phone = new Phone3();

        new Thread(() -> {
     
            phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone.call();
        }, "B").start();
    }
}

class Phone3 {
     
	//添加static变为静态方法
    public static synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    public static synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

输出结果:

A发短信
B打电话

那如果我这时候,又new了一个对象呢,两个对象,两个同步代码块呢?输出结果是什么呢?

public class test3 {
     

    public static void main(String[] args) {
     
        Phone3 phone = new Phone3();
        //new多一个新的对象
        Phone3 phone3 = new Phone3();

        new Thread(() -> {
     
            phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone3.call();
        }, "B").start();
    }
}

class Phone3 {
     
    public static synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    public static synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

输出结果:

A发短信
B打电话

原因是因为通过static修饰的方法它是属于静态方法,它是属于这一个class类,我们知道静态方法可以直接调用而不需要new出一个新的实例对象去调用。这里得意思就有点像这个,因为用的是static修饰的静态方法,因此synchronized 锁的是这个class类本身,而不是这个方法的调用者,因此无论你new多少个实例出来,class类只有一个,他还是只有一把锁,锁的是这个class类,因此,谁先拿到锁,就谁先输出结果。


代码片段四:这次如果我们一个方法是普通同步方法,一个方法是静态同步方法,只有一个对象,那么我们先输出打电话呢,还是先输出发短信呢?

public class test4 {
     

    public static void main(String[] args) {
     
        Phone4 phone = new Phone4();

        new Thread(() -> {
     
            phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone.call();
        }, "B").start();
    }
}

class Phone4 {
     
    //静态同步方法
    public static synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    //普通同步方法
    public  synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

结果如下:

B打电话
A发短信

那如果我现在有两个对象,一个对象调用的是静态同步方法,一个调用的是普通同步方法呢?

public class test4 {
     

    public static void main(String[] args) {
     
        Phone4 phone = new Phone4();
        Phone4 phone4 = new Phone4();

        new Thread(() -> {
     
            phone.send();
        }, "A").start();

        //休眠一秒
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone4.call();
        }, "B").start();
    }
}

class Phone4 {
     
    //静态同步方法
    public static synchronized void send() {
     
        //休眠四秒
        try {
     
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"发短信");
    }
    //普通同步方法
    public  synchronized void call() {
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }

}

结果:

B打电话
A发短信

这是因为同步方法锁的是调用者,静态同步方法锁的是这个class,这是两个锁,因此谁的时间段谁先执行。

5、不安全的集合类

5.1List

多个线程操作同一个ArrayList集合的时候会有不安全的现象,因此ArrayList是线程不安全的。

public class unsafeList {
     

    public static void main(String[] args) {
     
        List<String> list = new ArrayList<String>();
        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();
        }
    }

}

报的异常:

java.util.ConcurrentModificationException并发修改异常

解决方案有三种,最常用的是使用CopyOnWriteArrayList:

public class unsafeList {
     

    public static void main(String[] args) {
     
        //解决方案1.使用vector集合。底层其实用的还是synchronized锁,用这个锁效率就会低
        //List list =

        //解决方案2.使用collections的工具类,将不安全的集合ArrayList转换成安全的synchronizedList即可
        //List list =  Collections.synchronizedList(new ArrayList());

        //解决方法3.使用JUC包下的CopyOnWriteArrayList。最常用的方法。
        //CopyOnWrite 写入时复制,写入完之后再插入。避免写入覆盖

        //CopyOnWrite相比于vector方法
        List<String> list = new CopyOnWriteArrayList<>();

        //List 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();
        }
    }

}

5.2Set

Set的不安全例子和List大同小异,直接放代码:

public class unsafeSet {
     
    //解决方案一,通过collections工具类.  Set set = Collections.synchronizedSet(new HashSet());
    //解决方法二. 通过 Set set = new CopyOnWriteArraySet();
    public static void main(String[] args) {
     
        Set<String> set = new CopyOnWriteArraySet<String>();
        for(int i = 0; i < 1000; i++) {
     
            new Thread(()->{
     
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

5.3Map

同上

    public class unsafeMap {
     
        //解决方案一,通过collections工具类.  Map map =  Collections.synchronizedMap(new HashMap());
        //解决方法二. 通过 Set set = new CopyOnWriteArraySet();
        public static void main(String[] args) {
     
            Map<String,String> map =  new ConcurrentHashMap<String, String>();
            for(int i = 0; i < 1000; i++) {
     
                new Thread(()->{
     
                    map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                    System.out.println(map);
                },String.valueOf(i)).start();
            }
        }
    }

6.走进Callable

Callable: 返回结果并且可能抛出异常的任务。
优点:
可以获得任务执行返回值。
通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。
Callable可以抛出异常。

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

        MyThread thread = new MyThread();
        //适配类,接收实现Callable的线程类的实例对象
        FutureTask<String> FutureTask = new FutureTask<>(thread);
        //通过Thread开启线程
        new Thread(FutureTask,"A").start();
        //new一个新的线程,因为有缓存的存在,只会打印一个语句。用于提高效率
        new Thread(FutureTask,"B").start();

        //通过调用适配类的get方法,FutureTask.get()获取到返回值,但是可能存在阻塞现象
        //因为在Call方法内的业务逻辑代码可能会是耗时的操作,就会造成一直等待的现象,所以一般把他放在最后一行
        System.out.println(FutureTask.get());

    }
}

//传递的参数类型,就是我们需要返回时的参数类型
class MyThread implements Callable<String> {
     
    public String call() {
     
        System.out.println("成功调用call方法");
        return "1024";
    }
}

7.常用的辅助类(必会)

7.1CountDownLatch

可以理解为减法计数器,主要有两个方法:
countDownLatch.countDown();//数量-1
countDownLatch.await();//等待计数器归0再往下执行
一般常用于一些必须要完成的线程任务
代码演示:

public class CountDownLatchTest01 {
     
    public static void main(String[] args) throws InterruptedException {
     
        //执行任务的数量
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i <= 6; i++) {
     
            new Thread(() -> {
     
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown();//数量-1
            }, String.valueOf(i)).start();
        }
        //计数器不归0,不会执行以下操作
        countDownLatch.await();
        System.out.println("Close door");
    }
}

7.2CyclicBarrier

有减法计数器,就会有加法计数器,这里的CyclicBarrier就相当于加法计数器,只有当所有线程都执行完操作才可以进行下一步

public class CyclicBarrierTest01 {
     
    public static void main(String[] args) {
     
        //只有7个线程都完成才可以执行召唤神龙的操作
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
     
            System.out.println("集齐了七龙珠召唤神龙!");
        });

        for (int i = 1; i <= 7; i++) {
     
            int temp = i;
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName()+"拿到了第了"+temp+"颗龙珠");

                try {
     
                    cyclicBarrier.await();//没有完成就继续等待
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
     
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

7.3Semaphore(常用)

信号量,相当于一个通行证。
主要方法:
semaphore.acquire();获取,假设满了,等待,直到释放
semaphore.release();释放,当前信号量+1,唤醒等待线程
主要用于共享资源互斥使用,并发限流,控制最大线程数

public class SemaphoreTest01 {
     
    public static void main(String[] args) {
     
        //信号量,在这里可以理解为通信证,没有通行证不能占用资源:理解为停车位
        Semaphore semaphore = new Semaphore(3);
        //假设有6个线程6台车,但只有3个停车位
        for (int i = 1; i <= 6; i++) {
     
            new Thread(() -> {
     
                try {
     
                    //获取
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    //停车3秒
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println("三秒钟后" + Thread.currentThread().getName() + "离开车位");

                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }finally {
     
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

8、ReadWriteLock读写锁

使用方法:
通过ReentrantReadWriteLock实例生成对象,直接调用即可。如:如果想调用写锁:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock()
使用方法上和lock没什么区别。

public class ReadWriteLock {
     

    public static void main(String[] args) throws InterruptedException {
     
        myCache2 myCache = new myCache2();

        //1-5个线程分别只进行写的操作,写的时候只有一个线程写
            for (int i = 1; i <= 5; i++) {
     
                int temp = i;
                new Thread(() -> {
     
                    myCache.put(temp + "", temp + "");
                }, String.valueOf(i)).start();
            }
            TimeUnit.SECONDS.sleep(3);
        //1-5个线程分别只进行读的操作,读的时候可以有多个线程读
        for (int i = 0; i <= 5; i++) {
     
            int temp = i;
            new Thread(()->{
     
                myCache.get(temp+"");
            },String.valueOf(i)).start();
        }

    }

}

/*
 * 自定义缓存--加锁
 * */

class myCache2{
     
    public volatile Map<String,Object> map = new HashMap<>();
    final  ReentrantReadWriteLock lock = new ReentrantReadWriteLock();


    //存,写
    public void put(String key,Object value){
     
        lock.writeLock().lock();
        try {
     
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入成功");
        }catch (Exception e){
     
            e.printStackTrace();
        }
        finally {
     
            lock.writeLock().unlock();
        }

    }


    //取,读
    public void get(String key){
     
        lock.readLock().lock();
        try {
     
            System.out.println(Thread.currentThread().getName()+"取出"+key);
            map.get(key);
            System.out.println(Thread.currentThread().getName()+"取出成功");
        }catch (Exception e){
     
            e.printStackTrace();
        }
        finally {
     
            lock.readLock().unlock();
        }
    }
}


/*
 * 自定义缓存--不加锁
 * */

class myCache{
     
    public 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()+"取出成功");
    }
}

9、阻塞队列(BlockingQueue)

9.1概述

阻塞队列可以分开理解为阻塞和队列。在写入时候如果队列满了,则阻塞等待。在读取时如果队列是空的,则阻塞等待生产。

9.2常用场景

常用于多线程并发处理、线程池!

9.3队列之间的继承关系

多线程进阶复习JUC并发编程_第3张图片

9.4学会使用四种api

操作 抛出异常 有返回值不抛出异常 阻塞等待 超时等待,有返回值
添加 add;异常种类:java.lang.IllegalStateException: Queue full offer put offer(添加的内容,睡眠时间, 睡眠种类)
删除 remove;异常种类: java.util.NoSuchElementException poll take poll(添加的内容,睡眠时间, 睡眠种类)
获取队首 element;异常种类:java.util.NoSuchElementException peek - -

第一种:抛出异常的情况

public class fourApi {
     

    public static void main(String[] args) {
     
        test1();
    }

    //测试普通的抛出异常的方式
    public static void test1(){
     
        //参数表示queue队列的长度
        ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
        //添加元素
        queue.add("a");
        queue.add("b");
        queue.add("c");
        //演示多一个增加异常情况
        //queue.add("d");

        System.out.println("===================");
        //打印队首
        System.out.println("队首元素是"+queue.element());
        System.out.println("队列的长度是"+queue.size());
        System.out.println("===================");
        //取出元素

        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        //演示多一个删除异常情况
        //System.out.println(queue.remove());

//        Iterator iterator = queue.iterator();
//        while (iterator.hasNext()){
     
//            System.out.println(iterator.next());
//        }
    }
}

第二种:有返回值不抛出异常的情况

public static void test2(){
     
        //参数表示queue队列的长度
        ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
        //添加元素
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        //演示多一个增加异常情况
        //queue.offer("d");

        System.out.println("===================");
        //打印队首
        System.out.println("队首元素是"+queue.peek());
        System.out.println("队列的长度是"+queue.size());
        System.out.println("===================");
        //取出元素

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        //演示多一个删除异常情况
        System.out.println(queue.poll());
    }

第三种:阻塞等待

public static void test3() throws InterruptedException {
     
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        queue.put("a");
        queue.put("b");
        queue.put("c");
        //一直阻塞,进入等待不会停止
        //queue.put("d");


        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        //一直阻塞,进入等待不会停止
        //queue.take();
    }

第四种:超时等待

public static void test4() throws InterruptedException {
     
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a",2, TimeUnit.SECONDS));
        System.out.println(queue.offer("b",2, TimeUnit.SECONDS));
        System.out.println(queue.offer("c",2, TimeUnit.SECONDS));
        //有返回值,返回值是布尔类型,如果等待超时则推出等待
        System.out.println(queue.offer("d",2, TimeUnit.SECONDS));

        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        //
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
    }

10、 同步队列(SynchronousQueue)

同步队列是一种相对来说较为特殊的队列,他自己本身不存储值,它的容量为1。存入之后必须等待取出才能进行下一次存入。

public class TestSynchronousQueue {
     
    public static void main(String[] args) {
     

        SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();

        new Thread(()->{
     
            try {
     
                System.out.println("放入一个元素a");
                synchronousQueue.put("a");
                System.out.println("放入一个元素b");
                synchronousQueue.put("b");
                System.out.println("放入一个元素c");
                synchronousQueue.put("c");
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
     
            try {
     
                TimeUnit.SECONDS.sleep(3);
                System.out.println("移除一个元素");
                System.out.println(synchronousQueue.take());

                TimeUnit.SECONDS.sleep(3);
                System.out.println("移除一个元素");
                System.out.println(synchronousQueue.take());

                TimeUnit.SECONDS.sleep(3);
                System.out.println("移除一个元素");
                System.out.println(synchronousQueue.take());

            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }).start();
    }
}

11、线程池

11.1池化技术(重点)

1、什么是池化技术?
答:简单理解就是事先准备一部分资源,使用就拿走,用完就还给池。
2、使用线程池的好处?
答:线程池其实就相当于我们缓冲区的概念,线程池中会先启动若干数量的线程,这些线程都处于睡眠状态。当客户端有一个新的请求时,就会唤醒线程池中的某一个睡眠的线程,让它来处理客户端的这个请求,当处理完这个请求之后,线程又处于睡眠的状态。
1)这样最大的好处就是避免了繁琐的创建线程销毁线程,降低了资源的损耗
2)通过预备好的线程去执行服务,提高了响应速度。
3)方便对线程进行统一的管理。
4)实现了线程的复用,可用于控制并发数,更好的管理线程。

11.2Executor工具类三大方法

SingleThreadExecutor
FixedThreadPool
CachedThreadPool

public class Executor {
     

    public static void main(String[] args) {
     
        //通过Executors创建只有单个线程的线程池
        //ExecutorService ThreadPool = Executors.newSingleThreadExecutor();
        //通过Executors创建只有固个线程的线程池
        ExecutorService ThreadPool = Executors.newFixedThreadPool(5);
        //通过Executors创建可伸缩的动态线程池
        //ExecutorService ThreadPool = Executors.newCachedThreadPool();
        try {
     

            for (int i = 1; i <= 100; i++) {
     
                int temp = i;
                //开启线程,通过调用execute方法
                ThreadPool.execute(()->{
     
                    System.out.println(Thread.currentThread().getName()+"执行了"+temp+"线程");
                });
            }

        }catch (Exception e){
     
            e.printStackTrace();
        }finally {
     
            //线程使用完之后一定要关闭线程
            ThreadPool.shutdown();
        }
    }
}

11.3线程池的7大参数

无论是SingleThreadExecutor,还是FixedThreadPool,或者是CachedThreadPool,我们通过点进去它的源码可以查看到,它的底层本质上都是使用了一个叫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;
    }

阿里巴巴开发者手册里面明确规定:
多线程进阶复习JUC并发编程_第4张图片
因此,在日后的开发中,我们不会使用上面的三大方法来创建线程池,而是通过最原始的方法ThreadPoolExecutor方法来创建线程池。在通过使用ThreadPoolExecutor来创建线程池例子演示的时候,先通过一幅图来更好的了解七大参数。
maximumPoolSize–最大线程数:我们假设有一个银行,这个银行对应的就是我们的线程池,这一个个柜台就相当于我们的线程,由图我们不难看到,maximumPoolSize最大线程数就是5,因为有5个柜台。
corePoolSize–核心线程数:一般我们去银行办理业务的时候,如果人不是很多,一般不是每个柜台都开启服务的,一般只开几个柜台,在我们下图这里。红色的柜台表示暂不服务,白色的柜台表示服务。不管怎么样,不管人多人少,一定有两个柜台在服务,这两个一定服务的柜台(线程)就叫核心线程。
BlockingQueue workQueue–阻塞队列:在这里就可以理解为候客区
多线程进阶复习JUC并发编程_第5张图片
keepAliveTime–线程存活时间:假设有一天两个柜台(线程在服务),候客区的人 (请求)已经满了,这个时候还有人想进来等待被服务,那么这个时候平时不服务的柜台(红色标注)会打开来进行服务
多线程进阶复习JUC并发编程_第6张图片
多线程进阶复习JUC并发编程_第7张图片
当服务完之后,人已经不多了,也不需要这么多柜台进行服务了,在经过一段等待时间后还是没有人来办理业务,那么这些柜台又会重新关闭,这段等待的时间就叫最大等待时间。
多线程进阶复习JUC并发编程_第8张图片
TimeUnit unit–单位:最大等待时间的单位
ThreadFactory threadFactory–线程池工厂:可以理解为建造银行(线程池)的建筑商
RejectedExecutionHandler–拒绝策略:可以理解为,当银行里面所有柜台都在服务了,候客区也满了,这个时候还想有人来办理业务,明显这个时候是处理不了的,太繁忙了,银行在这个时间段里告诉你先别服务,这就叫拒绝策略。
多线程进阶复习JUC并发编程_第9张图片
拒绝策略有四种:
1)多出来的线程,直接抛出异常
new ThreadPoolExecutor.AbortPolicy()
2)谁开启的这个线程,就让这个线程返回给谁执行。比如main线程开启的,那就返回给main线程执行
new ThreadPoolExecutor.CallerRunsPolicy()
3)如果队列线程数量满了以后,直接丢弃,不抛出异常
new ThreadPoolExecutor.DiscardPolicy()
>4)队列满了以后,尝试去和最早的线程竞争,也不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy

下面展示自定义线程池:

public class MyPool {
     

    public static void main(String[] args) {
     
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        2,
        5,
        3,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());

        try {
     
            for (int i = 1; i <= 2; i++) {
     
                int temp = i;
                threadPoolExecutor.execute(()->{
     
                    System.out.println(Thread.currentThread().getName()+"执行了"+temp+"线程");
                });
            }

        }catch (Exception e){
     
            e.printStackTrace();
        }finally {
     
            threadPoolExecutor.shutdown();
        }
        
    }
}

可以看到,当阻塞队列,即候客区没满的时候,只有两个核心线程在执行。
多线程进阶复习JUC并发编程_第10张图片
当候客区满了,核心线程忙不过来了的时候,3号服务窗口开始服务。多线程进阶复习JUC并发编程_第11张图片
当我们的要求服务的人达到了9个,可以看到,我们使用了AbortPolicy抛出了异常
多线程进阶复习JUC并发编程_第12张图片,具体异常为:java.util.concurrent.RejectedExecutionException


12、定义最大线程数

1、CPU密集型:根据电脑运行的核数去定义最大线程数,通过Runtime.getRuntime().availableProcessors()获得本机电脑CPU核数。
2、IO密集型:根据程序中消耗IO的线程数,乘于2就是我们定义的最大线程数

//电脑本机CPU最大核数
        System.out.println(Runtime.getRuntime().availableProcessors());
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        2,
         Runtime.getRuntime().availableProcessors(),//电脑本机CPU最大核数
        3,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());

关于IO密集型与CPU密集型的描述–转载自Java技术栈


12、四大函数式接口

12.1函数式接口

//传入类型T和返回类型R
public interface Function<T, R> {
     
    R apply(T t);
}
public class hanshu {
     

    public static void main(String[] args) {
     
//        lambda表达式                         //传入的参数
//        Function function = (str)->{
     
                  //返回的字符串
//                return str;
//        };
//        System.out.println(function.apply("asd"));
        
        //匿名内部类方式的函数式接口,Function接口体的两个参数表示传入的参数和返回的参数类型
        Function function = new Function<String,String>() {
     
            @Override
            public String apply(String o) {
     
                return o;
            }
        };
        System.out.println(function.apply("test"));
    }

}

12.2断定型接口

@FunctionalInterface
//传入类型T,返回布尔类型
public interface Predicate<T> {
     
    boolean test(T t);
}
public class duanding {
     

    public static void main(String[] args) {
     

        //使用lambda表达式进行简化书写
        Predicate<String> predicate = (s)->{
     return s.isEmpty();};

        System.out.println(predicate.test(""));
    }

}

12.3消费型接口

@FunctionalInterface
//只有输入没有返回值
public interface Consumer<T> {
     
    void accept(T t);
}

public class consmuer {
     
    public static void main(String[] args) {
     
        Consumer<String> consumer = (s)->{
     
            System.out.println(s);
        };
        consumer.accept("aaa");
    }
}

12.4供给型接口

@FunctionalInterface
//没有参数,只有返回值
public interface Supplier<T> {
     
    T get();
}
public class gongji {
     

    public static void main(String[] args) {
     

        Supplier<String> supplier = ()->{
     
          return "1024";
        };
        System.out.println(supplier.get());
    }

}

13、stream流式计算

来道题目感受一下:
题目要求:

使用Stream进行筛选计算
输出的id必须为偶数
年龄必须大于23岁
用户名转为大写字母
用户名字字母倒着排序
只能输出一个用户

public class stream {
     
    public static void main(String[] args) {
     
        User user1 = new User(1, "a", 21);
        User user2 = new User(2, "b", 22);
        User user3 = new User(3, "c", 23);
        User user4 = new User(4, "d", 24);
        User user5 = new User(6, "e", 25);

        //使用List集合,存储五个用户
        List<User> list = new ArrayList<User>();
        list.add(user1);
        list.add(user2);
        list.add(user3);
        list.add(user4);
        list.add(user5);


        //使用Stream流进行计算
        list.stream()
                //filter返回由与此给定参数匹配的此流的元素组成的流,即返回对象。
                .filter((u) -> {
     return u.getId() % 2 == 0; })
                .filter((u) -> {
     return u.getAge() > 23;})
                //map返回由给定函数应用于此流的元素的结果组成的流,即返回函数处理后返回地值。
                .map((u -> {
     return u.getName().toUpperCase();}))
                //将结果逆序排列
                .sorted((u1, u2) -> {
     return u2.compareTo(u1);})
                //System.out::println打印的是传入的参数,也就是Customer的参数泛型T,
                // 这个T就是从list的流对象获取的T,最终都是List对象创建时指定的泛型,即User
                .forEach(System.out::println);
    }
}

14、forkJoin

fork有叉子的意思,叉子大家都见过,一个主体分成两个尖尖的“牙”,join有加入得意思。从字面上意思就很好理解,先分开再合并。因此,forkJoin的思想就一目了然,它的主要作用就是把一个大任务分割成若干个小任务,然后再把每个小任务得到的结果汇总得到大任务的结果。用一张图表示就是:
多线程进阶复习JUC并发编程_第13张图片
特点:工作窃取算法:
即当一个大任务被拆分成小任务的时候,有的线程可能提前完成了任务,此时闲着就会去帮助其他没完成工作的线程。将别的线程的任务窃取过来完成。为了减少竞争,一般线程采取的是双端队列。但是当双端队列只有一个任务的时候,窃取就会造成资源竞争,消耗更多的资源。

代码演示ForkJoin:
1、继承自RecursiveTask<>,类型为重写compute方法的类型
2、通过fork拆分任务
3、通过join合并任务

//自定义的forkjoin类
//继承自RecursiveTask,泛型为compute方法的参数类型
public class ForkJoinDemo extends RecursiveTask<Long> {
     
	//定义初始加法的值
    private long start;	
    //定义最有一个加法的值
    private long end;

    //临界值,这个值的上下执行不同的方法
    private long temp = 1000000;
    public ForkJoinDemo(Long start, Long end) {
     
        this.start = start;
        this.end = end;
    }

    //继承类中的抽象方法,我们在里面编写我们的计算方法体
    @Override
    protected Long compute() {
     
		//如果最后一个值与第一个值之间的差小于临界值,执行普通的方法
        if ((end - start) < temp) {
     
            long sum = 0L;
            for (Long i = start; i <= end; i++) {
     
            //求和运算
                sum += i;
            }
            //方法的返回值需要与继承类的类型相同
            return sum;
            //否则使用forkjoin拆分任务求和
        } else {
     
        	//定义中间值拆分成两个任务
            long middle = (start + end) / 2;

            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);

            //把一个任务拆分为两个小任务,并且将两个任务压入队列中
            task1.fork();
            task2.fork();

            //返回计算以后的结果
            return task1.join() + task2.join();
        }
    }
}

测试代码:

public class ForkJoinTest01 {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        test01();
        test02();
        test03();
    }

    //使用最基本的循环遍历计算
    public static void test01() {
     
        long start = System.currentTimeMillis();
        long sum = 0;
        for (long i = 0; i <= 10_0000_0000L; i++) {
     
            sum += i;
        }
        long end = System.currentTimeMillis();

        System.out.println("基本循环花费的时间是:" + (end - start) + "结果是" + sum);
    }

    //使用ForkJoin进行计算
    public static void test02() throws ExecutionException, InterruptedException {
     
        //起始时间
        long start = System.currentTimeMillis();
        //创建一个ForkJoinPool池,创建过程类似于线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //调用自己定义的类
        ForkJoinDemo task = new ForkJoinDemo(0L, 10_0000_0000L);
        //将自己的任务,提交至ForkJoinPool池中
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        long sum = submit.get();
        long end = System.currentTimeMillis();

        System.out.println("forkJoin花费的时间是:" + (end - start) + "结果是" + sum);
    }

    //使用stream分支流计算
    public static void test03() {
     
        long start = System.currentTimeMillis();
        //parallel返回平行的等效流。 可能会返回自己,因为流已经是并行的,或者因为底层流状态被修改为并行。 
        		//reduce,将区间里的数求和
        long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("stream分支流花费的时间是:" + (end - start) + " 结果是" + sum);
    }
}

补充LongStream中上述方法的用法:

   @Test
    public void rangedClosedTest() {
     
        LongStream ls = LongStream.rangeClosed(2L, 5L);
        long[] lsA = ls.toArray();
        for (long l : lsA) {
     
            System.out.println(l);
        }
    }
    /**
 * 输出结果为,方法是返回其实参数到末尾参数中间的所有的值
 * 2
 * 3
 * 4
 * 5
 * */

15、异步回调

15.1没有返回值的异步回调

public class future {
     

    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        //没有返回值的runAsync异步回调,使用CompletableFuture.runAsync方法,通过lambda实现线程
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
     
            try {
     
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            } finally {
     
                System.out.println(Thread.currentThread().getName()+"==>OK");
            }
        });
        //输出123表明异步线程并不会阻塞其他线程执行
        System.out.println("123");
        //阻塞获取结果,因为异步方法调用还没开始,就已经执行到get方法了,因此这里的get方法肯定是会阻塞的
        completableFuture.get();
    }

}

15.2有返回值的异步回调


public class FutureTest01 {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        //有返回值(t, u)的supplyAsync异步回调
        //返回值是错误的信息,返回类型是integer类型
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
     
            //添加一个异常语句
            int i = 10/0;
            System.out.println(Thread.currentThread().getName() + "==>OK");
            return 1024;
        });
        
        //获取异步回调对象的返回值(t, u),whenComplete对应成功时候的返回值,否则就是失败的返回值
        System.out.println(completableFuture.whenComplete((t, u) -> {
     		//t为正常执行的信息
            System.out.println("t=>" + t);
            //u为错误的信息
            System.out.println("u=>" + u);
        }).exceptionally((e) -> {
     
            //输出错误信息:java.lang.ArithmeticException: / by zero
            System.out.println(e.getMessage());
            return 333;
        }).get());
    }
}

16、JMM

16.1、什么是JMM?

JMM(Java Memory Model)Java内存模型,是一种规范和约定,他并不是真实存在的。
由于每个线程运行的时候,JVM都会在运行时数据区为其创建一个线程私有的Java栈,用于存储线程的私有数据,包括局部变量,基本类型等,在这里我们把它称之为工作内存。而JMM中则规定所有变量都存储在主存中,而主存是共享内存的,所有线程都可以访问。因此,在JMM中线程如果要对变量进行操作,就必须从主存中拷贝变量到自己的工作空间,在自己的工作空间对变量进行操作完成后,再把变量写回主存,不能直接操作主存中的变量。下面用一幅图来展示这个过程:
多线程进阶复习JUC并发编程_第14张图片
JMM八大操作名称解释:
1:lock: 把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写。
2:unlock:解锁一个主内存变量。
3:read: 把一个主内存变量值读入到线程的工作内存,强调的是读入这个过程。
4:load: 把read到变量值保存到线程工作内存中作为变量副本,强调的是读入的值的保存过程。
5:use: 线程执行期间,把工作内存中的变量值传给字节码执行引擎。
6:assign(赋值):字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量。
7:store: 把工作内存中的变量值传送到主内存,强调传送的过程。
8:write: 把store传送进来的变量值写入主内存的变量中,强调保存的过程。

16.2、JMM的一些约定

1、线程解锁前,必须把共享变量立刻刷回主存
2、线程加锁前,必须读取主存中的最新值到工作内存中
3、加锁和解锁必须是同一把锁
4、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
5、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

6、不允许一个线程将没有assign的数据从工作内存同步回主内存

7、一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

8、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

9、如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

10、 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

11、对一个变量进行unlock操作之前,必须把此变量同步回主内存

16.3、JMM的经典问题

依旧用一张图解释
多线程进阶复习JUC并发编程_第15张图片
假设现在有两个线程A和B,线程B从主存拿到了变量的副本并将Flag的值修改成了False,写回了主存中,而此时线程A拿到的还是最初的Flag=True的副本(个人认为这里可以理解为脏读),这个时候就可能会英发一些不安全的操作。下面来一段代码演示:

/**
 * 这里的main线程相当于上图的B线程,Thread1相当于线程A
 *按照我们的理解主线程修改num=false了,而Thread1不知道num的值修改了,因为线程与线程之间
 * 在此时是不可见的,Thread1并不知道Main线程已经修改了主存,因此Thread1里面的线程依然再循环,并没有停止
 * */
public class JMMTest {
     
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
     
        new Thread(()->{
     
            while (flag) {
     
                //while循环里如果打印循环会停止,暂时不知道为什么
            }
        },"ThreadA").start();

        //确保让开启的其他线程先执行
        TimeUnit.SECONDS.sleep(2);
        
        //修改变量的值,观察线程的变化
        flag = false;
        System.out.println(flag);
    }
}

那么,我们怎么让线程之间保证可见性呢?接下来引出我们的下一个小节Volatile。


17、Volatile

17.1什么是Volatile?

首先,volatile是Java中的一个关键字。它是轻量级的Synchronized锁。它保证了变量的可见性,但是没有保证原子性,禁止指令重排(有序性)。如果一个变量被声明为volatile,那么Java线程内存模型所有线程看到的这个变量都是一致性的。

17.2Volatile可见性验证

这里对上一小节遗留的问题进行代码修改。

public class JMMTest {
     
    private static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
     
        new Thread(()->{
     
            while (flag) {
     
                //while循环里如果打印循环会停止,暂时不知道为什么
                //上网查询了下原因是因为system.out.println底层使用的是synchronized锁?可能是因为这个原因
            }
        },"ThreadA").start();

        //确保让开启的其他线程先执行
        TimeUnit.SECONDS.sleep(2);
        
        //修改变量的值,观察线程的变化
        flag = false;
        System.out.println(flag);
    }
}

运行之后可以明显地看到,Thread1不会再出现死循环的现象,这说明volatile保证了可见性。


17.3Volatile不保证原子性验证

原子性:不可被中断的一个或一系列操作。线程A在执行的时候不能被分割或被停止,要么同时成功,要么同时失败。

代码验证:

public class atomicTest {
     
	//保证变量在各个线程可见
    private static volatile int num = 0;
    //加synchronized可以保证结果
    private static void add() {
     
        num++;
    }
    public static void main(String[] args) throws InterruptedException {
     

        //开启10个线程,每一个线程中执行100次方法
        for (int i = 0; i < 10; i++) {
     
            new Thread(()->{
     
                for (int j = 0; j < 100; j++) {
     
                    add();
                }
            }).start();
        }

        //避免我们开启的线程还没有结束就执行主线程
        while (Thread.activeCount() > 2){
     
            Thread.yield();
        }
        System.out.println(num);
    }

}

输出的结果正确的应该是1000,但是试了很多次结果都没有到1000,说明没有保证了原子性。num++并不是原子性操作。我们通过JClasslib插件可以看到:
多线程进阶复习JUC并发编程_第16张图片
num++这个操作其实它的底层字节码是不止一行的,是有可能被多个线程操作的,那么我们除了使用lock锁或者synchronized锁解决以外,还有什么办法呢?
我们可以使用java.util.concurrent.atomic 包下的AtomicInteger类。

改造后的方法:

public class atomicTest2 {
     

    //使用java.util.concurrent.atomic包下的原子类
    private static AtomicInteger num = new AtomicInteger();

    private static void add() {
     
        //使用原子类的增加方法,不是单纯的+1操作,用的是底层的CAS
        //关于CAS在接下来会进行描述
        num.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
     
        for (int i = 0; i < 10; i++) {
     
            new Thread(() -> {
     
                for (int i1 = 0; i1 < 100; i1++) {
     
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
     
            Thread.yield();
        }
        System.out.println(num);
    }

}

原子类的操作底层都和操作系统挂钩,在内存中修改值,关于这一部分的知识点会在接下来的CAS小节讲解复习。


17.4禁止指令重排

我们得先明确,什么是指令重排:
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(前提是指令之间没有数据依赖性)。但是,保证结果一致是对于单线程而言的!在多线程操作下,就会有可能影响并发执行的正确性。

如:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
     
  sleep()
}
doSomethingwithconfig(context);
/**
 *上面的代码里面指令之间没有依赖性
 *假设我先执行语句2,线程2条件满足跳出休眠
 *线程2里面又要用到语句1的初始化参数,语句1没有初始化,这个时候就会报错
 * */

此时使用volatile关键字就可以保证代码执行的有序性。由于其内存屏障的原因,可以避免指令重排的现象。
多线程进阶复习JUC并发编程_第17张图片


18、五种单例模式

五种单例模式测试


19、深入理解CAS

19.1什么是CAS:

public class CAS01 {
     
		//CAS:比较当前工作内存的值和主存中的值,如果值是期望的,那么执行操作,否则就一直循环
        public static void main(String[] args) {
     
            //AtomicInteger原子类,参数为初始值,不赋值则为0
            AtomicInteger atomicInteger = new AtomicInteger();

            //compareAndSet(int expect, int update),第一个参数是期望值,如果达到期望值,则更新为第二个参数
            //CAS是CPU的并发原语!
            System.out.println(atomicInteger.compareAndSet(0, 2021));//true符合预期更新为2021
            //输出当前的值
            System.out.println(atomicInteger.get());

           atomicInteger.getAndIncrement();//相当于I++操作

            System.out.println(atomicInteger.compareAndSet(2022, 2023));
            System.out.println(atomicInteger.get());
        }
}

底层源码窥探:多线程进阶复习JUC并发编程_第18张图片
我们都知道Java无法直接操作本地计算机的内存,虽然说可以通过本地方法native调用C++操作内存。但是Java给自己留了一个后门,可以通过Unsafe类操作内存。上面的代码中,AtomicInteger的getAndIncrement方法中的value参数(内存地址偏移量)就是通过unsafe类的objectFieldOffset获取的,底层用了do,while自旋锁方式,通过weakCompareAndSet(CAS)进行操作。
多线程进阶复习JUC并发编程_第19张图片

19.原子引用解决ABA问题

在这里我们得先明确CAS的核心就是比较原来的值和期望值有没有变化,如果没有变化则更新,如果变化了就不更新,这一点我们可以从示例代码很清楚的看到。那么问题来了,CAS它只检测值是不是原来那个值,它并不知道这个值是否被改动过,就好比如下图:
多线程进阶复习JUC并发编程_第20张图片
线程A从主存复制了工作副本准备进行CAS(1,2)操作,这个时候A也是1,如果正常执行那么就会通过CAS把值变为2;但是在线程A执行之前,线程B抢先一步操作把主存A的值从1变为3然后再变为1,此时线程A从主存拷贝工作副本的时候依旧认为这个1是原来的1,没有发生改变,其实已经改变过了没有被发现而已,这就是ABA问题。

代码展示:

public class CAS02 {
     

        public static void main(String[]args){
     
            AtomicInteger atomicInteger = new AtomicInteger(0);

            // ============== 捣乱的线程 ==================
            System.out.println(atomicInteger.compareAndSet(0, 1));
            System.out.println(atomicInteger.get());
            System.out.println(atomicInteger.compareAndSet(1, 0));
            System.out.println(atomicInteger.get());


            // ============== 期望的线程 ==================
            System.out.println(atomicInteger.compareAndSet(0, 3));
            System.out.println(atomicInteger.get());
        }
}

那么我们如何解决这个问题呢?JDK1.5以后atomic包下提供了一个AtomicStampedReference类可以解决这个问题。通过这个类添加版本号,实现版本控制,每更改一次就更新版本号。本质上是乐观锁思想的一种体现

public class ABAReslove {
     
    //初始值是1,初始版本号是1
    //这里用的是Integer包装类,java为了提高效率缓存了-128~127之间的数字来实现复用,超过这个范围的会在堆上new出一个新的对象
    //工作中主要是比较对象
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

    public static void main(String[] args) {
     

        new Thread(() -> {
     
            int stamp = atomicStampedReference.getStamp(); //获取当前版本号
            System.out.println("a1=>" + stamp);//输出当前版本号
            try {
     
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            //期望值,更新值,当前的版本号,更新的版本号
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);

            //打印最新版本信息
            System.out.println("a2=>" + atomicStampedReference.getStamp());

            //判断是否更新成功,返回布尔类型
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            //打印最新版本信息
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();


        new Thread(() -> {
     
            int stamp = atomicStampedReference.getStamp();
            //线程b当前的版本号
            System.out.println("b1=>" + stamp);
            try {
     
                //睡眠3秒确保线程A先执行完并更改
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));

            //由于在线程B执行之前A已经更改过数值,版本号发生了改变,因此更新失败
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }


    }

20、各种锁的理解:

锁的一些相关补充

你可能感兴趣的:(java,juc)