16_JUC并发编程

1、什么是JUC

JUC就是java.util下的三个工具包:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

2、线程和进程

2.1、线程和进程

进程:一个程序,QQ.exe等程序的集合

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

Java默认有个2个线程

  • main
  • GC

线程:开了一个Typora,写字、自动保存就是线程负责的

java的三种开启线程的方式:集成Thread类、实现Runnable接口、实现Callable接口

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();

2.2、并发、并行

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

  • CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替

并行:多个人一起行走

  • CPU多核,多个线程可以同时执行,线程池
public class Test {
    public static void main(String[] args) {
        //获取CPU核数
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

并发编程的本质:充分利用CPU的资源

2.3、线程的状态

public enum State {
    //新生
    NEW,
	//运行
    RUNNABLE,
	//阻塞
    BLOCKED,
	//等待,死死的等
    WAITING,
	//超时等待
    TIMED_WAITING,
	//终止
    TERMINATED;
}

2.4、wait和sleep的区别

  1. 来自不同的类

    wait ---> Object

    sleep ---> Thread

  2. 关于锁的释放

    wait会释放锁

    sleep睡觉了,抱着锁睡觉,不会释放锁!

  3. 使用的范围不同

    wait:必须在同步代码块中使用

    sleep:可以在任何地方使用

3、Lock锁(重点)

  • 传统的Synchronized

    在公司真正的多线程开发中,线程就是一个单独的资源类,没有任何附属的操作(类中只有属性和方法),为了降低耦合性,不会用类去实现接口,因为实现类接口就不是OOP编程了,而且实现了接口的话耦合性变高,如果让类实现了Runnable,这个类就只是一个线程类了

    如下代码,只是把Ticket作为了资源类,并没有让它实现Runnable接口

    public class TestSynchronized {
        public static void main(String[] args) {
            //并发:多线程操作同一个资源类
            Ticket ticket = new Ticket();
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "A").start();
    
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "B").start();
    
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "C").start();
        }
    }
    
    //资源类OOP
    class Ticket {
        private int number = 40;
    
        public synchronized void sale() {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余" + (--number) + "张票");
            }
        }
    }
    
  • Lock接口

    所有已知实现类:
    ReentrantLock(可重入锁) , ReentrantReadWriteLock.ReadLock (读锁), ReentrantReadWriteLock.WriteLock (写锁)

    公平锁:十分公平,先来后到,排队
    非公平锁:不公平,可以插队
    默认是非公平锁,是为了公平,比如一个线程要3s,另一个线程要3h,难道一定要让3h的锁先来就先执行吗

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class SaleTicketDemo02 {
        public static void main(String[] args) {
            //并发:多线程操作同一个资源类
            Ticket2 ticket = new Ticket2();
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "A").start();
    
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "B").start();
    
            new Thread(()->{
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }, "C").start();
        }
    }
    
    //资源类OOP
    class Ticket2 {
        private int number = 40;
    
        Lock lock = new ReentrantLock();
        public void sale() {
            lock.lock(); //加锁
            try {
                if (number > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余" + (--number) + "张票");
                }
            } finally {
                lock.unlock();  //解锁
            }
        }
    }
    
  • Synchronized与Lock的区别

    1. Synchronized是Java内置的关键字;Lock是一个Java类
    2. Synchronized无法判断获取锁的状态;Lock可以判断是否获取到了锁
    3. Synchronized会自动释放锁;Lock必须要手动释放锁,如果不是释放就死锁
    4. Synchronized线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
    5. Synchronized可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断锁,默认非公平(可以自己设置)
    6. Synchronized适合锁少量的代码同步问题;Lock适合锁大量的同步代码

4、生产者和消费者问题

  • synchronized版本,用wait()和notify()

    生产者消费者骨架:等待、执行业务、通知

    public class ProductAndConsumer {
        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 num = 0;
    
        public synchronized void increment() throws InterruptedException {
            if (num != 0) {
                //等待操作
                this.wait();
            }
            num++;
            //执行完++,通知其他线程我已经完成++操作
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll();
        }
    
        public synchronized void decrement() throws InterruptedException {
            if (num == 0) {
                //等待操作
                this.wait();
            }
            num--;
            //执行完--,通知其他线程我已经完成--操作
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll();
        }
    }
    

    执行结果:

    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
    

    synchronized版本存在的问题

    如果增加两个线程,即两个线程加,两个线程减,得到如下结果,并不能1,0交替,而且出现了2,3

    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();
    

    问题原因分析
    咱们看jdk1.8的官方文档,找到Object类的wait()方法

    导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。
    线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:

    while (<condition does not hold>)
        obj.wait(timeout);
    	... // Perform action appropriate to condition
    

    结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

    跟cpu调度有关,运气好没问题也是可能的。主要原因还是因为wait会释放锁,会导致两个num++/num–的线程同时进入wait队列。当线程被唤醒后,A先获得锁++,这时侯如果cpu调度到B,B就会继续往下执行++。如果调度是有序的(++,–,++,–),也不会有这种问题

    解决方法

    用if会出现,判断过了,但是拿到的是之前的值
    把if判断改成while判断等待,因为if判断进if之后不会停,用while判断的话,变量一旦被修改,另外一个线程拿到锁之后,就会等待,防止虚假唤醒

    class Data {//数字,资源类
        private int num = 0;
    
        public synchronized void increment() throws InterruptedException {
            while (num != 0) {
                //等待操作
                this.wait();
            }
            num++;
            //执行完++,通知其他线程我已经完成++操作
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll();
        }
    
        public synchronized void decrement() throws InterruptedException {
            while (num == 0) {
                //等待操作
                this.wait();
            }
            num--;
            //执行完--,通知其他线程我已经完成--操作
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll();
        }
    }
    
  • 用Lock锁完成生产者消费者问题

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ProductAndConsumerLock {
        public static void main(String[] args) {
            Data2 data = new Data2();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.increment();
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.decrement();
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.increment();
                }
            }, "C").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.decrement();
                }
            }, "D").start();
        }
    }
    
    class Data2 {
        private int num = 0;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
    
        public void increment() {
            try {
                lock.lock();//锁
                //业务代码
                while (num != 0) {
                    //等待操作
                    condition.await();
                }
                num++;
                //执行完++,通知其他线程我已经完成++操作
                System.out.println(Thread.currentThread().getName() + "=>" + num);
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    
        public void decrement() {
            try {
                lock.lock();
                while (num == 0) {
                    //等待操作
                    condition.await();
                }
                num--;
                //执行完--,通知其他线程我已经完成--操作
                System.out.println(Thread.currentThread().getName() + "=>" + num);
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    用Lock锁完成生产者消费者问题改进,Condition 精准的通知和唤醒线程,让ABCD四个线程交替执行

    任何一个新的技术,绝对不是仅仅只是覆盖了原来的技术,一定有优势和补充!

    Condition 精准的通知和唤醒线程

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ProductAndConsumerLock2 {
        public static void main(String[] args) {
            Data3 data3 = new Data3();
    
            new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    data3.printA();
                }
            }, "A").start();
            new Thread(() -> {
    
                for (int i = 0; i < 5; i++) {
                    data3.printB();
                }
    
            }, "B").start();
            new Thread(() -> {
    
                for (int i = 0; i < 5; i++) {
                    data3.printC();
                }
            }, "C").start();
        }
    }
    
    class Data3 {
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();
        private int state = 1;
    
        public void printA() {
            lock.lock();
            try {
                while (state != 1) {
                    condition1.await();
                }
                System.out.println(Thread.currentThread().getName() + "=>AAAAA");
                condition2.signal();//A执行完通知B
                state = 2;//改变state的状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void printB() {
            lock.lock();
            try {
                while (state != 2) {
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName() + "=>BBBBB");
                condition3.signal();//A执行完通知B
                state = 3;//改变state的状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void printC() {
            lock.lock();
            try {
                while (state != 3) {
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName() + "=>CCCCC");
                condition1.signal();//A执行完通知B
                state = 1;//改变state的状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    执行结果:

    A=>AAAAA
    B=>BBBBB
    C=>CCCCC
    A=>AAAAA
    B=>BBBBB
    C=>CCCCC
    A=>AAAAA
    B=>BBBBB
    C=>CCCCC
    A=>AAAAA
    B=>BBBBB
    C=>CCCCC
    A=>AAAAA
    B=>BBBBB
    C=>CCCCC
    

5、八锁现象

锁只会锁两个东西,一个是new出来的对象,一个是class模板

8锁,就是关于锁的8个问题

  • 问题1:标准情况下,先打印发短信,还是先打印打电话?

    import java.util.concurrent.TimeUnit;
    
    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 (Exception e) {
                e.printStackTrace();
            }
            new Thread(phone::call,"B").start();
        }
    }
    
    class Phone {
    
        public synchronized void send() {
            System.out.println("发短信");
        }
    
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    发短信
    打电话
    

    不能回答先调用A线程,这是错误的,不是先调用先执行,这是锁的问题,因为被Synchronized修饰的方法,锁的对象是方法的调用者,所以调用两个方法的对象都是phone,但是现在phone只有一个,也就是说这两个方法现在用的是同一把锁,谁先拿到,谁就先执行。

  • 问题2:给发短信方法加延时4s,程序的执行情况

    public synchronized void send() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    

    执行结果:

    发短信
    打电话
    

    因为是锁的同一个对象,A先拿到锁,所以要等4s后A执行完后,B在执行

  • 问题3:当调用普通方法,而不是synchronized方法时,先输出什么

    import java.util.concurrent.TimeUnit;
    
    public class Test2 {
        public static void main(String[] args) {
            Phone2 phone2 = new Phone2();
            new Thread(phone2::send,"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(phone2::sayHello,"B").start();
        }
    }
    
    class Phone2 {
        public synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public void sayHello() {
            System.out.println("hello");
        }
    }
    

    执行结果:

    hello
    发短信
    

    先执行普通方法,因为普通方法没有锁,不受锁的影响

  • 问题4:两个对象分别调用synchronized方法时,先输出什么

    import java.util.concurrent.TimeUnit;
    
    public class Test3 {
        public static void main(String[] args) {
            Phone3 phone1 = new Phone3();
            Phone3 phone2 = new Phone3();
            new Thread(phone1::send,"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(phone2::call,"B").start();
        }
    }
    
    class Phone3 {
    
        public synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    打电话
    发短信
    

    因为锁不一样,两把锁,所以耗时短的先输出

  • 问题5:一个对象,把两个synchronized方法改成静态synchronized方法,即static synchronized方法,先输出哪个?

    import java.util.concurrent.TimeUnit;
    
    public class Test4 {
        public static void main(String[] args) {
            new Thread(Phone4::send,"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(Phone4::call,"B").start();
        }
    }
    
    class Phone4 {
    
        public static synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    发短信
    打电话
    

    static方法在类加载时就有了,锁的对象是class模板。两个方法都被static修饰了,所以两个方法用的是同一个锁,且锁的是类模板,所以谁先拿到锁谁先输出

  • 问题6:2个对象,把两个synchronized方法改成静态synchronized方法,即static synchronized方法,先输出哪个?

    import java.util.concurrent.TimeUnit;
    
    public class Test5 {
        public static void main(String[] args) {
            Phone5 phone1 = new Phone5();
            Phone5 phone2 = new Phone5();
            new Thread(()->{
                phone1.send();
            },"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(()->{
                phone2.call();
            },"B").start();
        }
    }
    
    class Phone5 {
    
        public static synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    发短信
    打电话
    

    因为两个方法都被static修饰了,所以两个方法用的是同一个锁,且锁的是类模板。

  • 问题7:同一个对象,把两个synchronized方法中的一个改成静态synchronized方法,即static synchronized方法,另一个为普通synchronized方法,先输出哪个?

    import java.util.concurrent.TimeUnit;
    
    public class Test6 {
        public static void main(String[] args) {
            Phone6 phone = new Phone6();
            new Thread(()-> phone.send(),"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(phone::call,"B").start();
        }
    }
    
    class Phone6 {
    
        public static synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    打电话
    发短信
    

    锁的对象不一样,一个锁的是类模板,一个锁的是对象,后面调用的方法不需要去等待锁。

  • 问题8:两个对象,把两个synchronized方法中的一个改成静态synchronized方法,即static synchronized方法,另一个为普通synchronized方法,先输出哪个?

    import java.util.concurrent.TimeUnit;
    
    public class Test7 {
        public static void main(String[] args) {
            Phone7 phone1 = new Phone7();
            Phone7 phone2 = new Phone7();
            new Thread(()-> phone1.send(),"A").start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            new Thread(phone2::call,"B").start();
        }
    }
    
    class Phone7 {
    
        public static synchronized void send() {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
    
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    执行结果:

    打电话
    发短信
    

    还是锁的对象不一样,一个锁的是类模板,一个锁的是对象,后面调用的方法不需要去等待锁。

小结

  • 当同步方法不用static修饰的时候:锁的是对象
  • 当同步方法用static修饰的时候:锁的是类模板,是唯一的

6、线程安全的集合

6.1、CopyOnWriteArrayList

  • Arrarlist测试(单线程)

    public class ListTest {
        public static void main(String[] args) {
            List<String> list = Arrays.asList("1","2","3");
            list.forEach(System.out::println);
        }
    }
    

    执行结果:

    1
    2
    3
    
  • Arrarlist测试(多线程)

    现在我们创建10个线程来向List添加元素

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

    执行结果:出现并发修改异常

    [482c7, 9b801, 63cf5, 22c29]
    [482c7, 9b801, 63cf5, 22c29, 2f204, 5463c, 5cc98]
    [482c7, 9b801, 63cf5, 22c29, 2f204, 5463c, 5cc98, 23f55, 8e5b6, 8f366]
    [482c7, 9b801, 63cf5, 22c29, 2f204, 5463c]
    [482c7, 9b801, 63cf5, 22c29, 2f204]
    [482c7, 9b801, 63cf5, 22c29]
    [482c7, 9b801, 63cf5, 22c29]
    [482c7, 9b801, 63cf5, 22c29]
    [482c7, 9b801, 63cf5, 22c29, 2f204, 5463c, 5cc98, 23f55]
    Exception in thread "8" java.util.ConcurrentModificationException
    	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    	at java.util.ArrayList$Itr.next(ArrayList.java:851)
    	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
    	at java.lang.String.valueOf(String.java:2994)
    	at java.io.PrintStream.println(PrintStream.java:821)
    	at collections.TestList.lambda$main$0(TestList.java:13)
    	at java.lang.Thread.run(Thread.java:745)
    
  • 并发下 ArrayList 不安全的解决方案

    1. List list = new Vector<>();//vector默认是安全的
    2. List list = Collections.synchronizedList(new ArrayList<>());
    3. List list = new CopyOnWriteArrayList<>();//CopyOnWriteArrayList,写入时复制

    CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略
    多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
    在写入的时候避免覆盖,造成数据问题!
    读写分离(写入的时候复制一个数组出来,写入完之后再插入进去,保证线程安全)

  • CopyOnWriteArrayList 比 Vector 好在哪里?

    Vector的add方法有Synchronized修饰(看源码),有Synchronized修饰的方法,效率都比较低

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    

    CopyOnWriteArrayList的add方法用的是Lock锁(看源码)

    public boolean add(E e) {
        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();
        }
    }
    

6.2、CopyOnWriteSet

  • 多线程下HashSet安全测试

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

    执行结果:出现并发修改异常

    [098d4, ea923, 66567, 9b68a, 895f8, b2b02, ac661, d699c, e32b1, 9cc6b, a4e2f, f9b55, 5ce9c, c57b2, 00d73, 750d2, f4f5f]
    Exception in thread "1" Exception in thread "14" java.util.ConcurrentModificationException
    	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
    	at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
    	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
    	at java.lang.String.valueOf(String.java:2994)
    	at java.io.PrintStream.println(PrintStream.java:821)
    	at collections.TestSet.lambda$main$0(TestSet.java:13)
    	at java.lang.Thread.run(Thread.java:745)
    java.util.ConcurrentModificationException
    	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
    	at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
    	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
    	at java.lang.String.valueOf(String.java:2994)
    	at java.io.PrintStream.println(PrintStream.java:821)
    	at collections.TestSet.lambda$main$0(TestSet.java:13)
    	at java.lang.Thread.run(Thread.java:745)
    
  • 并发下 HashSet 不安全的解决方案

    1. Set set = Collections.synchronizedSet(new HashSet<>()); 通过工具类转化成Synchronized
    2. Set set=new CopyOnWriteArraySet<>(); 写入时复制,保证效率跟性能问题
  • HashSet的底层是什么?

    看源码可以看到,HashSet的底层就是HashMap

    public HashSet() {
        map = new HashMap<>();
    }
    
  • HashSet的add方法

    本质就是map的key,因为map的key是不能重复的,所以set是无序的,也是无法重复的

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

6.3、ConcurrentHashMap

 HashMap<String, String> objectObjectHashMap = new HashMap<>();

工作中不这样用!

map默认等价于:

HashMap<String, String> objectObjectHashMap = new HashMap<>(16,0.75);

其中16是初始容量,0.75是加载因子

注意是位运算:

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 多线程下HashMap的安全测试

    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
    

    执行结果:出现并发修改异常

    {11=e5348, 1=c7c63, 12=4967c, 2=4f4e8, 13=054fa, 14=1f1c7, 15=e14b9, 16=95beb, 17=6554e, 18=bba1d, 4=f139f, 5=385ff, 6=6577f, 8=40934, 9=8b63a, 10=89a11}
    {11=e5348, 1=c7c63, 12=4967c, 2=4f4e8, 13=054fa, 14=1f1c7, 15=e14b9, 16=95beb, 17=6554e, 4=f139f, 5=385ff, 6=6577f, 8=40934, 9=8b63a, 10=89a11}
    {11=e5348, 12=4967c, 13=054fa, 10=89a11, 19=82f1e, 1=c7c63, 2=4f4e8, 14=1f1c7, 4=f139f, 5=385ff, 6=6577f, 18=bba1d, 8=40934, 9=8b63a, 20=d8d2d}
    {11=e5348, 12=4967c, 13=054fa, 10=89a11, 19=82f1e, 1=c7c63, 2=4f4e8, 14=1f1c7, 4=f139f, 5=385ff, 6=6577f, 18=bba1d, 8=40934, 9=8b63a}
    Exception in thread "14" java.util.ConcurrentModificationException
    	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
    	at java.util.HashMap$EntryIterator.next(HashMap.java:1463)
    	at java.util.HashMap$EntryIterator.next(HashMap.java:1461)
    	at java.util.AbstractMap.toString(AbstractMap.java:531)
    	at java.lang.String.valueOf(String.java:2994)
    	at java.io.PrintStream.println(PrintStream.java:821)
    	at collections.TestMap.lambda$main$0(TestMap.java:12)
    	at java.lang.Thread.run(Thread.java:745)
    
  • 并发下 HashMap 不安全的解决方案

    1. Map<String, String> map= Collections.synchronizedMap(new HashMap<>());
    2. Map<String, String> map = new ConcurrentHashMap<>();

7、Callable

官方文档:Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而, Runnable不返回结果,也不能抛出被检查的异常。

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不同,run()/call()

根据底层源码:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

Thread 只能接受Runnable类型的参数,不能接受Callable类型的参数
但是,Runnable有一个实现类:

Class FutureTask<V>

FutureTask可以接受Callable类型的参数,所以我们可以通过new Thread启动Callable

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

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

        MyThread myThread = new MyThread();
        //适配类:FutureTask
        FutureTask futureTask = new FutureTask(myThread);

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

        //获取返回值,Callable的返回结果
        Integer result = (Integer) futureTask.get();
        /*get方法可能会产生阻塞,如果Callable是一个耗时的操作,get()会等线程执行完才获取结果
         * 所以,get方法一般放在最后,或者使用异步通信
         */
        System.out.println(result);
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "call()");
        return 1024;
    }
}

执行结果:

call()
1024

问题:如果new两个线程会打印几个call()?

答:1个
分析:结果会被缓存,效率高

8、常用的辅助类(必会)

8.1、CountDownLatch

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 -计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。

import java.util.concurrent.CountDownLatch;

//计数器
public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);//倒计时

        //倒计时等待所有线程执行完毕
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " GO OUT");
                countDownLatch.countDown();//减1,每走一个线程就让  countDownLatch减1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();//等待计数器归0,再向下执行
        System.out.println("关门");

    }
}

执行结果:

1 GO OUT
5 GO OUT
6 GO OUT
4 GO OUT
3 GO OUT
2 GO OUT
关门

countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!如果 countDown()没有减到0,后面的程序是不会执行的。

8.2、CyclicBarrier

如果把CountDownLatch看做一个减法计数器,那么CylicBarrier就是一个加法计数器。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("计数达到7,召唤神龙成功");
        });
        for (int i = 1; i <= 7; i++) {//计数器达到7才会召唤神龙成功,所以我们先创建7个线程
            final int temp = i;//我们想操作i,但是lambda表达式不能直接操作i,所以我们用一个中间变量
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集第" + temp + "颗龙珠");
                try {
                    cyclicBarrier.await();//计数器加到7才会向下执行
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

执行结果:

1收集第1颗龙珠
6收集第6颗龙珠
7收集第7颗龙珠
5收集第5颗龙珠
4收集第4颗龙珠
2收集第2颗龙珠
3收集第3颗龙珠
计数达到7,召唤神龙成功

8.3、Semaphore(信号量)

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

public class SemaphoreTest {
    public static void main(String[] args) {
        //模拟停车,假设现在有6辆车,但是只有3个停车位
        //在有限的情况下使其有秩序,限流的时候可以使用
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();//因为信号量是3,所以最开始有3个车可以进来,然后等进来的车出去,其他的车又可以进来
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();//释放
                }
            }, String.valueOf(i)).start();
        }
    }
}

执行结果:

2抢到车位
1抢到车位
3抢到车位
2离开车位
1离开车位
3离开车位
4抢到车位
6抢到车位
5抢到车位
5离开车位
6离开车位
4离开车位

semaphore.acquire(); 获得,假设如果已经满了,等待,等待被释放为止!

semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!

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

9、读写锁

A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有作者。 write lock是独家的。所有ReadWriteLock实现必须保证的存储器同步效应writeLock操作(如在指定Lock接口)也保持相对于所述相关联的readLock 。 也就是说,一个线程成功获取读锁定将会看到在之前发布的写锁定所做的所有更新。

读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(因此读取器线程)。从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。

9.1、不加锁

import java.util.HashMap;
import java.util.Map;

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp + "");
                myCache.read(temp + "");
            }, "线程" + i).start();
        }
    }
}

class MyCache {
    private Map<String, Object> map = new HashMap<>();

    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "开始读取");
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成");
    }

    public void write(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完成");
    }
}

执行结果:

线程1开始写入
线程3开始写入
线程3写入完成
线程5开始写入
线程4开始写入
线程4写入完成
线程4开始读取
线程2开始写入
线程2写入完成
线程2开始读取
线程4读取完成
线程5写入完成
线程5开始读取
线程5读取完成
线程3开始读取
线程3读取完成
线程1写入完成
线程1开始读取
线程1读取完成
线程2读取完成

9.2、使用Lock

我们希望写入的时候能够互斥的,就是对于同一个myCache,一个线程写完了,才允许另一个线程接着写。

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

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp + "");
                myCache.read(temp + "");
            }, "线程" + i).start();
        }
    }
}

class MyCache {
    private Map<String, Object> map = new HashMap<>();
    Lock lock = new ReentrantLock();

    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "开始读取");
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成");
    }

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

执行结果:

线程1开始写入
线程1写入完成
线程1开始读取
线程3开始写入
线程1读取完成
线程3写入完成
线程3开始读取
线程2开始写入
线程2写入完成
线程3读取完成
线程2开始读取
线程2读取完成
线程4开始写入
线程4写入完成
线程4开始读取
线程4读取完成
线程5开始写入
线程5写入完成
线程5开始读取
线程5读取完成

9.3、使用ReadWriteLock

在读的时候,我们希望多个线程可以一起读的,而且要求写的时候也是可以读的,那么我们需要使用到粒度更细的ReadWriteLock。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 独占锁(写锁) 一次只能被一个线程占有
 * 共享锁(读锁) 多个线程可以同时占有
 * ReadWriteLock
 * 读-读  可以共存!
 * 读-写  不能共存!
 * 写-写  不能共存!
 */
public class ReadWriteLockDemo2 {
    public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();
        for (int i = 1; i <= 5; i++) {
            int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp + "");
                myCache.read(temp + "");
            }, "线程" + i).start();
        }
    }
}

// 加锁的
class MyCacheLock {
    private volatile Map<String, Object> map = new HashMap<>();
    // 读写锁: 更加细粒度的控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 存,写入的时候,只希望同时只有一个线程写
    public void write(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 取,读,所有人都可以读!
    public void read(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

执行结果:

线程2写入2
线程2写入OK
线程3写入3
线程3写入OK
线程1写入1
线程1写入OK
线程4写入4
线程4写入OK
线程5写入5
线程5写入OK
线程5读取5
线程2读取2
线程2读取OK
线程5读取OK
线程1读取1
线程1读取OK
线程3读取3
线程4读取4
线程4读取OK
线程3读取OK

10、阻塞队列

队列什么时候会阻塞?

答:队满,无法写入元素,如果队列满了,就必须阻塞等待
如果队列是空的,必须等待插入元素

blockingqueue与list和set是同级的接口

什么情况会使用阻塞队列?

多线程(A调用B,必须等B先执行,B没有执行完,A就会挂起或者等待)
线程池(出了弹性大小之外,一般会用一个队列去维护里面的大小)

学会使用队列

添加,移出

10.1、BlockingQueue的四组API

  1. 抛出异常
  2. 不会抛出异常
  3. 阻塞等待
  4. 超时等待
方式 抛出异常 有返回值,不抛出异常 阻塞等待 超时等待
添加 add() offer() put() offer(,,)
移除 remove() poll() take() poll(,)
检测队首元素 element() peek() - -

通过观察源码,我们会发现,add和remove是用offer 和poll 实现的,而element()的底层是用peek()实现的

1、抛出异常

/**
 * 抛出异常
 */
public static void test1(){
    // 队列的大小
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    System.out.println(blockingQueue.add("a"));
    System.out.println(blockingQueue.add("b"));
    System.out.println(blockingQueue.add("c"));
    // IllegalStateException: Queue full 抛出异常!
    // System.out.println(blockingQueue.add("d"));

    System.out.println("=============");

    System.out.println(blockingQueue.element()); // 查看队首元素是谁
    System.out.println(blockingQueue.remove());


    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());

    // java.util.NoSuchElementException 抛出异常!
    // System.out.println(blockingQueue.remove());
}

执行结果:

true
true
true
=============
a
a
b
c

2、有返回值,不会抛出异常

/**
 * 有返回值,没有异常
 */
public static void test2(){
    // 队列的大小
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    System.out.println(blockingQueue.offer("a"));
    System.out.println(blockingQueue.offer("b"));
    System.out.println(blockingQueue.offer("c"));

    System.out.println(blockingQueue.peek());
    // System.out.println(blockingQueue.offer("d")); // false 不抛出异常!
    System.out.println("============================");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll()); // null  不抛出异常!
}

执行结果:

true
true
true
a
============================
a
b
c
null

3、阻塞等待

/**
 * 等待,阻塞(一直阻塞)
 */
public static void test3() throws InterruptedException {
    // 队列的大小
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    // 一直阻塞
    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    // blockingQueue.put("d"); // 队列没有位置了,一直阻塞
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞
}

执行结果:

a
b
c

4、超时等待

/**
 * 等待,阻塞(等待超时)
 */
public static void test4() throws InterruptedException {
    // 队列的大小
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    blockingQueue.offer("a");
    blockingQueue.offer("b");
    blockingQueue.offer("c");
    // blockingQueue.offer("d",2,TimeUnit.SECONDS); // 等待超过2秒就退出
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    blockingQueue.poll(2, TimeUnit.SECONDS); // 等待超过2秒就退出
}

执行结果:

a
b
c

10.2、同步队列

  • 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素

  • put了一个元素,必须从里面先take取出来,否则不能在put进去值!

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();

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

执行结果:

T1 put 1
T2=>1
T1 put 2
T2=>2
T1 put 3
T2=>3

11、线程池

程序运行的本质是占用系统资源,为了优化资源的使用,引入了池化技术。比如线程池、连接池、内存池、对象池。

jdbc连接池会有一个最小的池(就是默认有几个连接的),有一个最大的池。
因为连接跟关闭的时候是非常消耗资源的。

池化技术:事先准备好一些资源,有人要用,就来拿,用完就归还。

线程池的好处

  • 降低资源消耗
  • 提高响应速度
  • 方便管理线程
  • 线程可以复用
  • 可以控制最大并发数

三大方法 7大参数 4种拒绝策略

在大厂线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors各个方法的弊端:

  1. newFi xedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  2. newCachedThreadPool和newSchedul edThreadPool:主要问题是线程数最大数是Integer.MAX__VALUE,可能会创建数量非常多的线程,甚至OOM。

11.1、三大方法

  • Executors.newSingleThreadExecutor(),创建单个线程池

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class PoolDemo01 {
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newSingleThreadExecutor();
            try {
                for (int i = 0; i < 100; i++) {
                    threadPool.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + " ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown();// 线程池用完,程序结束,关闭线程池
            }
        }
    }
    

    执行结果:

    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    pool-1-thread-1 ok
    ...
    
  • Executors.newFixedThreadPool(size),创建一个固定的线程池的大小

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class PoolDemo01 {
        public static void main(String[] args) {
    		ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
            try {
                for (int i = 0; i < 100; i++) {
                    threadPool.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + " ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown();// 线程池用完,程序结束,关闭线程池
            }
        }
    }
    

    执行结果:

    pool-1-thread-2 ok
    pool-1-thread-3 ok
    pool-1-thread-1 ok
    pool-1-thread-4 ok
    pool-1-thread-5 ok
    pool-1-thread-2 ok
    pool-1-thread-2 ok
    pool-1-thread-2 ok
    pool-1-thread-4 ok
    pool-1-thread-3 ok
    pool-1-thread-5 ok
    pool-1-thread-2 ok
    ...
    
  • Executors.newCachedThreadPool(),根据cpu的性能,创建尽可能多的线程,可伸缩的,遇强则强,遇弱则弱。

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class PoolDemo01 {
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
            try {
                for (int i = 0; i < 100; i++) {
                    threadPool.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + " ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown();// 线程池用完,程序结束,关闭线程池
            }
        }
    }
    

    执行结果:

    pool-1-thread-7 ok
    pool-1-thread-3 ok
    pool-1-thread-7 ok
    pool-1-thread-8 ok
    pool-1-thread-15 ok
    pool-1-thread-13 ok
    pool-1-thread-19 ok
    pool-1-thread-6 ok
    pool-1-thread-19 ok
    pool-1-thread-1 ok
    pool-1-thread-11 ok
    pool-1-thread-19 ok
    pool-1-thread-9 ok
    pool-1-thread-1 ok
    pool-1-thread-9 ok
    pool-1-thread-11 ok
    pool-1-thread-1 ok
    pool-1-thread-18 ok
    pool-1-thread-18 ok
    pool-1-thread-13 ok
    ...
    

11.2、七大参数

newSingleThreadExecutor()源码分析:

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

newFixedThreadPool()源码分析:

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

newCachedThreadPool()源码分析:

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

可以看到,三个方法的底层都是new ThreadPoolExecutor:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

我们点进去this:

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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

11.3、拒绝策略

如下图,银行柜台和候客区都满了,现在还有客户来,就只能拒绝了

手动创建一个线程池

模拟上面的银行业务
核心线程大小设为2:就是一直工作的窗口
最大线程设为5:就是银行最多的工作窗口
keepAliveTime设置为1小时:如果1小时都没有业务,就关闭窗口
候客区:new LinkedBlockingQueue(3),假设候客区最多3个人
线程工厂:就用默认的,Executors.defaultThreaFactory()
拒绝策略: 可以发现有4种拒绝策略,用默认的AbortPolicy()//银行满了,但是还有人进来,就不处理这个人,并抛出异常

工作中只会用 ThreadPoolExecutor,因为Executors不安全

自定义线程池:

当只有4个任务的时候,没有触发最大线程,即没有额外开银行窗口

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class PoolDemo02 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 1; i <= 4; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-1 ok

增加到6个人的时候,触发了一个额外窗口

for (int i = 1; i <= 6; i++) {
    threadPool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + " ok");
    });
}

执行结果:

pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-1 ok

增加到8个人的时候,触发了全部窗口,即一共5个线程

for (int i = 1; i <= 8; i++) {
    threadPool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + " ok");
    });
}

执行结果:

pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok
pool-1-thread-2 ok

增加到10个人就抛出异常了,超过了最大承载。(最大承载 = 最大核心线程池大小 + 阻塞队列大小)

for (int i = 1; i <= 10; i++) {
    threadPool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + " ok");
    });
}

执行结果:

pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok
java.util.concurrent.RejectedExecutionException: Task pool.PoolDemo02$$Lambda$1/1096979270@7ba4f24f rejected from java.util.concurrent.ThreadPoolExecutor@3b9a45b3[Running, pool size = 5, active threads = 4, queued tasks = 0, completed tasks = 4]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at pool.PoolDemo02.main(PoolDemo02.java:20)

现在改变拒绝策略,ThreadPoolExecutor.CallerRunsPolicy(),被拒绝的由主线程执行

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    2,
    5,
    3,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy());

执行结果:

pool-1-thread-2 ok
pool-1-thread-4 ok
main ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-1 ok

ThreadPoolExecutor.DiscardPolicy(),队列满了,不抛出异常,也不处理任务,可以看到,只处理了8个任务

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    2,
    5,
    3,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardPolicy());

执行结果:

pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-5 ok

ThreadPoolExecutor.DiscardOldestPolicy(),队列满了,就尝试和最早的任务竞争,如果竞争失败,这个任务就没了,也不会抛出异常,如果竞争成功,这个任务就可以执行

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    2,
    5,
    3,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardOldestPolicy());

执行结果:

pool-1-thread-1 ok
pool-1-thread-5 ok
pool-1-thread-4 ok
pool-1-thread-3 ok
pool-1-thread-5 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-1 ok

策略小结

  • new ThreadPoolExecutor.AbortPolicy() : 银行满了,还有人进来,不处理这个人的,抛出异常
  • new ThreadPoolExecutor.CallerRunsPolicy() : 哪来的去哪里!
  • new ThreadPoolExecutor.DiscardPolicy():队列满了,丢掉任务,不会抛出异常!
  • new ThreadPoolExecutor.DiscardOldestPolicy():队列满了,尝试去和最早的竞争,也不会抛出异常!

11.4、线程池的最大数量如何设定

最大线程到底该如何定义
1、CPU 密集型,几核就是几,可以保持CPU的效率最高!(你的电脑是几个核心的(用程序获取),你的最大线程数就设置成几,可以保持CPu的效率最高)
2、IO 密集型,判断你程序中十分耗IO的线程!(你的程序里面有15个任务很占用IO资源,就用15个线程去执行,所以最大线程数量大于这个15就好了,一般是大型IO任务数量的2倍)

获取CPU的核数:

System.out.println(Runtime.getRuntime().availableProcessors());

12、四大函数式接口

新时代程序员(JDK8新特性):lambda 表达式、链式编程、函数式接口、Stream 流式计算

函数式接口:只有一个方法的接口,比如 Runnable 接口

优点:简化编程,在新版本的框架底层大量使用

剩下的都是一些复合类型。

12.1、Function 函数型接口

传入参数T,返回类型R

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    ...
}

代码演示:

import java.util.function.Function;

public class FunctionDemo01 {
    public static void main(String[] args) {
        Function<String,String> function = (str)-> str;
        System.out.println(function.apply("aaaaa"));
    }
}

执行结果:

aaaaa

测试效果:传入什么,返回什么,像是一个工具类

12.2、Predicate 断定型接口

只能传入一个参数,返回值是 boolean

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ...
}

代码演示:

import java.util.function.Predicate;

public class PredicateDemo {
    public static void main(String[] args) {
        Predicate<String> predicate = (str)->{
            return str.isEmpty();
        };
        System.out.println(predicate.test("aaa"));
        System.out.println(predicate.test(""));
    }
}

执行结果:

false
true

测试效果:我们可以利用这一特性,对传入的参数进行判断,返回 true or false,

12.3、Suppier 供给型接口

没有输入,只有返回值,输出

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

代码演示:

import java.util.function.Supplier;

public class SupplierDemo {
    public static void main(String[] args) {
        Supplier<String> supplier = ()->{
            return "1024";
        };
        System.out.println(supplier.get());
    }
}

执行结果:

1024

测试效果:这个实例展示输出的返回值

12.4、Consummer 消费型接口

只输入,没有返回值

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    ...
}

代码演示:

import java.util.function.Consumer;

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

执行结果:

abc

测试结果:这里我们实例就是打印一下消费了什么内容

12.5、小结:函数式接口的作用

简化编程模型,这里的接口只是起一个规范,具体怎么来编写业务代码,需要我们自己重写里面的方法。

13、Stream 流式计算

大数据:存储+计算

集合、MySQL 本质就是存储数据,而计算应该交给流来操作

首先查看 package java.util.stream包下的 interface Stream<T> 接口

可以看到,Stream 中的大量方法都是用了函数式接口:

题目要求:

  1. ID必须是偶数
  2. 年龄必须大于23岁
  3. 用户名转为大写字母
  4. 用户名倒着排序
  5. 只输出一个用户

根据上述需求,编写演示代码。

public class User {
    private int id;
    private String name;
    private int age;

    public User() {
    }

    public User(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class StreamDemo {
    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(5, "e", 25);
        // 集合用于存储
        List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
        // 计算交给Stream流
        // 一行代码使用了 lambda表达式  链式编程  函数式接口  Stream流式计算
        list.stream()
                .filter(user -> user.getId() % 2 == 0)
                .filter(user -> user.getAge() > 23)
                .map(user -> user.getName().toUpperCase())
                .sorted(Comparator.reverseOrder())
                .limit(1)
                .forEach(System.out::println);
    }
}

执行结果:

D

理解分析:

  • Collection有一个方法 stream() 可以返回 Stream 流,然后我们使用 Stream 调用接口里的方法开启流式计算

  • 理解这些方法需要我们首先理解函数式接口,然后对照 API 查看各个方法的含义

    比如,第一个方法 filter

    Stream<T> filter(Predicate<? super T> predicate);
    
  • 这个方法的参数需要我们使用 Predicate 断定型接口的实例,因此我们重写这个接口方法并使用了 lambda 表达式

    user -> {return user.getId() % 2 == 0;}
    
  • 流中的所有user只有满足ID为偶数的才会返回true,通过筛选,filter返回的流中只有ID为偶数的user

  • 这里的流在计算判断时,会将所有的user全部都过一遍,然后按照我们的业务规则进行筛选

  • 这些代码就是通过泛型统一的元素类型为 User

总结一下:

  • list转为流,流调用方法,方法里的参数用函数式接口,函数式接口有我们定义业务规则,然后方法返回的就是符合条件的元素构成的流,继续链式编程。
  • 这种方式不仅简化代码,底层优化也使得代码效率得到提高,是不是感觉很强大!

14、ForkJoin 分之合并

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

ForkJoin 特点:工作窃取

实现原理:双端队列!从上面和下面都可以去拿到任务进行执行!

14.1、如何使用ForkJoin

用法:

使用要点:

  1. 通过 ForkJoinPool 来执行,把 ForkJoinTask 作为参数丢进去
  2. 计算任务 execute(ForkJoinTask<?> task)(execute是同步执行,submit是异步执行)
  3. 写一个计算类要去继承 RecursiveTask(底层还是ForkJoinTask),重写方法,定义计算规则

准备计算类:

import java.util.concurrent.RecursiveTask;

public class ForkJoinDemo extends RecursiveTask<Long> {
    private long start;     // 1
    private long end;       // 20_0000_0000
    private long temp = 1_0000;

    public ForkJoinDemo(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        // 小于临界值则直接求和
        if (end - start < temp) {
            long sum = 0;
            for (long i = start; i < end; i++) {
                sum += i;
            }
            return sum;
        } else {
            // 大于临界值则采用分支合并计算
            long middle = (end + start) / 2;  //中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork(); // 拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork();// 拆分任务,把任务压入线程队列
            return task1.join() + task2.join();
        }
    }
}

写测试类:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

public class ForkJoinTest {
    private static final long SUM = 20_0000_0000;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();
        test2();
        test3();
    }

    // 使用普通方法
    public static void test1() {
        long star = System.currentTimeMillis();
        long sum = 0;
        for (int i = 0; i < SUM; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.println("时间:" + (end - star));
        System.out.println("---------------");
    }

    // 使用 ForkJoin 方法
    public static void test2() throws ExecutionException, InterruptedException {
        long star = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0, SUM);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long along = submit.get();
        System.out.println(along);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end - star));
        System.out.println("---------------");
    }

    // 使用流计算
    public static void test3() {
        long star = System.currentTimeMillis();
        long sum = LongStream.range(0, SUM).parallel().reduce(0, Long::sum);
        System.out.println(sum);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end - star));
        System.out.println("-------------");
    }
}

执行结果:

1999999999000000000
时间:938
---------------
1999737855999656614
时间:328
---------------
1999999999000000000
时间:297
-------------

流式计算的效率最高。

15、异步调用

Future 设计的初衷: 对将来的某个事件的结果进行建模

Java也可以实现异步调用,与ajax是一个道理

我们平时都使用 CompletableFuture。

  • 没有返回值的runAsync异步回调

    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    
    public class CompletableFutureDemo01 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 发起一个请求
            System.out.println(System.currentTimeMillis());
            System.out.println("-----------");
    
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                // 发起一个异步任务
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "......");
            });
    
            System.out.println(System.currentTimeMillis());
            System.out.println("-------------------------");
            System.out.println(future.get());//获取执行结果
        }
    }
    

    执行结果:

    1629563996353
    -----------
    1629563996400
    -------------------------
    ForkJoinPool.commonPool-worker-1......
    null
    
  • 有返回值的异步回调supplyAsync

    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    
    public class CompletableFutureDemo02 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
                System.out.println(Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return 1024;
            });
            System.out.println(completableFuture.whenComplete((t, u) -> {
    //            success 回调
                System.out.println("t=>" + t);//正常的返回结果
                System.out.println("u=>" + u);//抛出异常的错误信息
            }).exceptionally((e) -> {
    //            error 回调
                System.out.println(e.getMessage());
                return 404;
            }).get());
        }
    }
    

    执行结果:

    ForkJoinPool.commonPool-worker-1
    t=>1024
    u=>null
    1024
    

    whenComplete: 有两个参数,一个是t 一个是u

    T:是代表的 正常返回的结果;

    U:是代表的 抛出异常的错误信息;

    如果发生了异常,get可以获取到exceptionally返回的值;

16、JMM

  • JMM: JAVA 内存模型,不存在的东西,是一个概念,也是一个约定!

  • 关于JMM的一些同步的约定:

    1. 线程解锁前,必须把共享变量立刻刷回主存;
    2. 线程加锁前,必须读取主存中的最新值到工作内存中;
    3. 加锁和解锁是同一把锁;
  • 线程中分为 工作内存、主内存

  • JMM约定有八种操作:

    • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
    • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
    • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
    • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
    • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
    • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
    • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
    • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

  • 问题来了,b刷新了主内存,a没有及时发现,因此,JMM规定了八个规定:

    1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
    2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
    3. 不允许一个线程将没有assign的数据从工作内存同步回主内存
    4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
    5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    6. 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    7. 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
    8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存

17、volatile

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

三个特性:

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

17.1、保证可见性

import java.util.concurrent.TimeUnit;

public class VolatileDemo01 {
    // 如果不加 volatile 程序会死循环
    // 加了volatile 是可以保证可见性的
    private volatile static Integer number = 0;

    public static void main(String[] args) {// main 线程
        // 子线程1
        new Thread(() -> {
            while (number == 0) {
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number = 1;
        System.out.println(number);
    }
}

测试运行:输出1,并结束程序

加了volatile 是可以保证可见性的。

如果将 volatile 去掉,主线程将number修改为1,子线程不知道,程序不会结束,还在按照 number=0一直做死循环。

17.2、不保证原子性

原子性:不可分割

线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。

public class VolatileDemo02 {
    private static int number = 0;

    public static void add() {
        number++;
        // ++ 不是一个原子操作,是2~3个操作
    }

    public static void main(String[] args) {
        // 理论上 number === 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        // java 默认有两个线程 main 和 gc  超过2个说明还有其他线程存活
        // 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + ",num=" + number);
    }
}

运行测试,理论上number应该为20000,实际上达不到,因为number++不是原子操作,存在并发问题

main,num=18973

解决办法之一就是给 add 加锁

public synchronized static void add(){
    number++;
}

执行结果:main,num=20000

去掉synchronized ,给 number 加 volatile

private volatile static int number = 0;

运行测试,发现总数达不到20000,说明 volatile不能保证原子性

main,num=18406

如果不加 lock 和 synchronized,如何保证原子性?

  1. 首先分析 number++的底层实现

    通过命令行进入到class文件所在目录,执行指令,查看他的字节码指令逻辑

    javap -c VolatileDemo02.class
    

    说明 number++ 在底层有多步骤实现

  2. 使用原子类解决原子性问题

    查看API会发现:

    使用原子类定义 number,修改 number++

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class VolatileDemo03 {
        private static final AtomicInteger number = new AtomicInteger();
    
        public static void add() {
            // ++ 不是一个原子操作,是2~3个操作
            number.incrementAndGet();//底层是 CAS 保证原子性
        }
    
        public static void main(String[] args) {
            // 理论上 number === 20000
            for (int i = 1; i <= 20; i++) {
                new Thread(() -> {
                    for (int j = 1; j <= 1000; j++) {
                        add();
                    }
                }).start();
            }
            // java 默认有两个线程 main 和 gc  超过2个说明还有其他线程存活
            // 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + ",num=" + number);
        }
    }
    

    执行结果:

    main,num=20000
    
  3. 为什么要使用原子类?

    因为他比锁要高效很多,这些类的底层都直接和操作系统挂钩!是在内存中修改值。Unsafe 类是一个很特殊的存在。

17.3、禁止指令重排

  • 什么是指令重排?

    我们写的程序,计算机并不是按照我们自己写的那样去执行的

    源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行

    int x=1; //1
    int y=2; //2
    x=x+5;   //3
    y=x*x;   //4
    
    //我们期望的执行顺序是 1_2_3_4  可能执行的顺序会变成2134 1324
    //可不可能是 4123? 不可能的
    

    因为,处理器在进行指令重排的时候,会考虑数据之间的依赖性!

    这个例子是指令重排没有造成影响,也有造成影响的时候。

    比如,假设abxy默认都是0,执行以下线程操作:

    线程A 线程B
    x = a y = b
    b = 1 a = 2

    正常的结果应该是 x = 0; y =0;

    然而可能会出现以下的执行顺序

    线程A 线程B
    b = 1 a = 2
    x = a y = b

    可能在线程A中会出现,先执行b=1,然后再执行x=a;

    在B线程中可能会出现,先执行a=2,然后执行y=b;

    那么就有可能结果如下:x=2; y=1。

    这种情况可能很难出现,但是还是有一定概率,需要注意

  • volatile可以避免指令重排

    volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序

    内存屏障:CPU指令。

    作用:

    1. 保证特定的操作的执行顺序;
    2. 可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

17.4、小结

  • volatile 可以保证可见性
  • 不能保证原子性
  • 由于内存屏障,可以保证避免指令重排的现象产生
  • 面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

18、玩转单例模式

18.1、饿汉模式

public class SingleHungryDemo {
    /*
     * 可能会浪费空间
     * */
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];

    private SingleHungryDemo() {}

    private final static SingleHungryDemo hungry = new SingleHungryDemo();

    public static SingleHungryDemo getInstance() {
        return hungry;
    }
}

程序一加载就可能占用大量资源

18.2、懒汉模式

// 懒汉式单例,单线程安全
public class LazyMan {
    // 构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private static LazyMan lazyMan;

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

    // 多线程会出现并发问题
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

执行结果:但是多线程下有并发问题,这里仍然出现了多个线程,本应该只有一个

Thread-0ok
Thread-6ok
Thread-4ok
Thread-5ok
Thread-1ok
Thread-3ok
Thread-2ok

18.3、DCL 懒汉式

因此我们需要加锁,DCL:双重检测模式

// 懒汉式单例,单线程安全
public class LazyMan {
    // 构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private static LazyMan lazyMan;

    // 双重检测模式 简称 DCL
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    // 多线程会出现并发问题
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

执行结果:Thread-0ok

但是 DCL 懒汉式仍然有问题,极端情况下会出现问题

lazyMan = new LazyMan(); 不是原子性操作,里面会经历三步操作:

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

这就有可能出现指令重排问题:

  • 比如执行的顺序是 1 3 2 等
  • 另一个线程也可能会干扰这个线程,导致比如返回的 lazyman为空

为了安全,我们要避免指令重排,我们就可以添加 volatile 保证指令重排问题

private volatile static LazyMan lazyMan;

18.4、静态内部类实现单例

public class Holder {
    // 单例模式一定是构造器私有
    private Holder() {}

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

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

18.5、反射可以破坏单例

但是目前,单例仍然不安全,因为可以使用反射

import java.lang.reflect.Constructor;

// 懒汉式单例
public class LazyMan {
    // 构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private volatile static LazyMan lazyMan;

    // 双重检测模式 简称 DCL
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    // 反射
    public static void main(String[] args) throws Exception {
        LazyMan instance1 = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 无视私有构造器
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行发现,两个对象不一样,不是单例

mainok
mainok
single.LazyMan@74a14482
single.LazyMan@1540e19d

如何解决这种问题?我们可以在构造器中加锁判断,防止别人使用反射破坏单例

private LazyMan() {
    synchronized (LazyMan.class) {
        if (lazyMan != null) {
            throw new RuntimeException("不要试图使用反射破坏异常");
        }
    }
    System.out.println(Thread.currentThread().getName() + "ok");
}

执行结果:

mainok
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
	at single.LazyMan.main(LazyMan.java:36)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
	at single.LazyMan.<init>(LazyMan.java:11)
	... 5 more

但是,如果两个instance都是通过反射创建的,如何判断呢?

LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();

执行结果:单例又被破坏

mainok
mainok
single.LazyMan@74a14482
single.LazyMan@1540e19d

我们可以设置一个标志位,来判断对象是否已经被创建了无需再创建,添加标志位,将构造器改造为:

private static boolean flag = false;

// 构造器私有
private LazyMan() {
    synchronized (LazyMan.class) {
        if (flag) {
            throw new RuntimeException("不要试图使用反射破坏异常");
        } else {
            flag = true;
        }
    }
    System.out.println(Thread.currentThread().getName() + "ok");
}

执行结果:

mainok
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
	at single.LazyMan.main(LazyMan.java:41)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
	at single.LazyMan.<init>(LazyMan.java:13)
	... 5 more

只要构造器被调用,标志位就会变化,然后无法再次创建对象,除非对方有反编译器看到了标志位,否则仅仅使用反射也不能创建多例,标志也可以再进一步加密,进一步提高了安全性。

假设对手更高级,找到了标志位,并通过反射将标志位破坏了

public static void main(String[] args) throws Exception {
    Field flag = LazyMan.class.getDeclaredField("flag");
    flag.setAccessible(true);

    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);// 无视私有构造器
    LazyMan instance1 = declaredConstructor.newInstance();
    // 通过反射破坏标志位
    flag.set(instance1, false);

    LazyMan instance2 = declaredConstructor.newInstance();

    System.out.println(instance1);
    System.out.println(instance2);
}

执行结果:这样就又一次破坏了单例

mainok
mainok
single.LazyMan@1540e19d
single.LazyMan@677327b6

最终得出结论:道高一尺,魔高一丈。

都是反射惹的祸,这就需要引入枚举

18.6、枚举

反射不能破坏枚举

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

执行结果:

INSTANCE
INSTANCE

如果还是要用反射操作呢?

查看target目录中 EnumSingle 编译后的 class 代码

发现有无参构造,因此我们尝试再次使用反射

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, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 破除私有
        EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

执行结果:

Exception in thread "main" java.lang.NoSuchMethodException: single.EnumSingle.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at single.Test.main(EnumSingle.java:17)

异常的含义是,我们的枚举类中没有空参数构造器

但是我们在IDEA中 查看 EnumSingle.class 显示有无参构造,为什么?

我们使用命令行反编译查看一下,还是无参构造,还是有问题,这个代码也欺骗了我们,因为异常枚举类没有无空参构造

我们使用反编译神器 jad.exe

把它复制到,class文件所在目录,在这个目录下执行命令行jad -sjava EnumSingle.class,把它反编译成Java文件

打开这个Java文件,得到枚举类型的最终反编译源码:

public final class EnumSingle extends Enum {

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

    public static EnumSingle valueOf(String name) {
        return (EnumSingle) Enum.valueOf(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
        });
    }
}

发现这里使用了一个有参构造器:

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

所以我们再次改造上面的反射创建枚举对象的代码:

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, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        // 通过反编译器发现,枚举使用的是有参构造
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);// 破除私有
        EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

执行结果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
	at single.Test.main(EnumSingle.java:20)

枚举不能通过反射创造对象,所以,枚举不能被反射破坏单例

19、深入理解CAS

为什么深入理解CAS?大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构

19.1、什么是CAS

CAS:compareAndSet 比较并交换

传统原子类

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // boolean compareAndSet(int expect, int update)
        // 第一个参数:期望值,第二个参数:更新值
        // 如果实际值 和 我期望值相同,那么就更新
        // 如果实际制 和 我期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        // 因为期望值是 2020 ,实际值却变成了2021 所以会修改失败
        // CAS 是 CPU 的并发原语
        atomicInteger.getAndIncrement();//++操作
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

执行结果:

true
2021
false
2022

CAS 是 CPU 的并发原语,意思就是CPU的指令

进入 getAndIncrement 查看

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

进入 unsafe

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

Unsafe 是什么?

  • Java无法操作内存
  • Java可以调用C++ 的本地方法 native,操作内存
  • Unsafe 就相当于Java 的后门,通过这个类的方法来操作内存

valueOffset:内存地址的偏移值,而且value也被volatile修饰,保证不被指令重排

Unsafe 类提供的都是本地方法(native),包括上面的 getAndAddInt(+1的操作)

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

这个的方法的含义:如果var1这个对象,对应的值var2,是var5,那么就给var5+var4

这是一个内存操作,效率很高

Java 的 CAS 底层就是上述这个CAS

do {
    var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

这段代码本身也是一个自旋锁。

19.2、小结

CAS:

  • 比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。

CAS 缺点:

  • 循环会耗时(即使这样,也比Java操作好);
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

19.3、ABA 问题

  • 了解到CAS,就一定要知道 ABA 问题
  • 一句话解释:狸猫换太子

解释一下:

A 期望这是一块蛋糕,如果是,A就进行更新值,这是线程B来操作了蛋糕,换成了面包,然后又换回蛋糕,这个过程A是不知道的,A继续操作这个蛋糕,并且认为这还是以前的蛋糕。

看代码演示:

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo02 {
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // 捣乱的线程
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        // 期望的线程
        System.out.println(atomicInteger.compareAndSet(2020, 666));
        System.out.println(atomicInteger.get());
    }
}

执行结果:

true
2021
true
2020
true
666

可以看到A不知情,并且正常进行了更新值,(这也是乐观锁的原理)

这是我们不期望的,如何解决这个问题?

20、原子引用

解决 ABA 问题,对应的思想:就是使用了乐观锁

解决办法:带版本号的原子操作!

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo03 {
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args) {
        // 给值的同时 添加一个版本号,这里我们定为1
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
        // 开启两个线程
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();// 获得版本号
            System.out.println("a1=>" + stamp);
            // 为保证两者的版本号是同一个,我们暂停一下
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 开始CAS 操作
            // 参数1:期望值,参数2:更新值,参数3:版本号,参数4:版本号下一步的操作
            System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2=>" + stamp);
            // 再将期望值改回去,但是版本号还在累加
            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2=>" + stamp);
        }, "a").start();
        // b线程也对atomicStampedReference操作
        // 和乐观锁原理相同
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();// 获得版本号
            System.out.println("b1=>" + stamp);
            // 为保证两者的版本号是同一个,暂停时间更长一些
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("b2=>" + stamp);
        }, "b").start();
    }
}

这样,在操作过程中,即使期望值发生了变化,我们也可以通过版本号得知发生了变化

执行结果:

a1=>1
b1=>1
true
a2=>1
true
a2=>1
true
b2=>1

这里有个小坑:

  • AtomicStampedReference<Integer>
  • Integer 使用了对象缓存机制,默认范围是 -128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new ,因为 valueOf 使用了缓存,而 new 一定会创建新的对象分配新的内存空间。
  • 所以我们的期望值包装类,需要使用范围在 -128~127 才确保是同一个内存空间
  • 当然实际使用时可能是个对象,不会有这个问题
  • 即使是数字的话,我们也可重写equals来解决这个问题

21、各种锁的理解

21.1、公平锁、非公平锁

  • 公平锁:非常公平,先来后到,不允许插队
  • 非公平锁:非常不公平,允许插队,默认都是非公平
public ReentrantLock() {
    sync = new NonfairSync(); //无参默认非公平锁
}
// 重载方法可以传参
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
}

21.2、可重入锁(递归锁)

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁,而且是自动获得的

  • synchronized版本的可重入锁

    public class Demo01 {
        public static void main(String[] args) {
            TestPhone phone = new TestPhone();
            new Thread(() -> {
                //在调用sendMessage的方法时已经为phone加上了一把锁
                //而call方法又为其加上了一把锁
                phone.sendMessage();
            }, "A").start();
            new Thread(() -> {
                phone.sendMessage();
            }, "B").start();
        }
    }
    
    class TestPhone {
        public synchronized void sendMessage() {
            System.out.println(Thread.currentThread().getName() + "=> sendMessage");
            call();// 这里也有锁
        }
    
        public synchronized void call() {
            System.out.println(Thread.currentThread().getName() + "=> call");
        }
    }
    

    执行结果:

    A=> sendMessage
    A=> call
    B=> sendMessage
    B=> call
    

    按照正常逻辑,A执行完发短信,应该释放锁,B也有机会在A打电话之前就能发短信,

    然而运行测试发现,每次都是A发短信、打电话之后,B才有机会开始发短信

    这说明,A进入到内层锁的时候仍然保留着外层的锁

  • Lock 版本的锁

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Demo02 {
        public static void main(String[] args) {
            Phone2 phone2 = new Phone2();
            new Thread(() -> {
                phone2.sms();
            }).start();
            new Thread(() -> {
                phone2.sms();
            }).start();
        }
    }
    
    class Phone2 {
        Lock lock = new ReentrantLock();
    
        public void sms() {
            lock.lock();//细节:这个两把锁,两个钥匙
            // lock 锁必须配对,否则就是死锁在里面,配对指的就是每加一把锁就要对应一个解锁
            try {
                System.out.println(Thread.currentThread().getName() + "=> sms");
                call();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void call() {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "=> call");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    运行测试,也会得到同样的效果,需要注意的是lock/unlock必须成对出现,否则程序会死在里面

    Thread-0=> sms
    Thread-0=> call
    Thread-1=> sms
    Thread-1=> call
    

21.3、自旋锁

在上面的案例中,已经看到自旋锁

do {
    var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

不断地的循环去尝试,直到成功为止

手写代码实现一个自旋锁:

import java.util.concurrent.atomic.AtomicReference;

public class SpinlockDemo {
    // 默认
    // int为 0
    // thread 为 null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    // 加锁
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "===> mylock");
        //自旋锁
        while (!atomicReference.compareAndSet(null, thread)) {
            System.out.println(Thread.currentThread().getName() + " ==> 自旋中~");
        }
    }

    // 解锁
    public void myUnlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "===> myUnlock");
        atomicReference.compareAndSet(thread, null);
    }
}

开始使用这个自旋锁,t2直到发现满足条件才 可以执行,一直在自旋:

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

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
        // 这是Java提供的锁,我们暂时不用
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        reentrantLock.unlock();
        // 使用CAS实现我们写的自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        new Thread(() -> {
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        }, "t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        }, "t2").start();
    }
}

21.4、死锁

比如说,两个线程互相抢夺资源,谁都不释放锁就会形成死锁

怎么排除死锁,让死锁的四个条件中至少有一个不成立

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
import java.util.concurrent.TimeUnit;

public class DeadLock {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new MyThread(lockA, lockB), "t1").start();
        new Thread(new MyThread(lockB, lockA), "t2").start();
    }
}

class MyThread implements Runnable {
    private String lockA;
    private 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() + " lock" + lockA + "===>get" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + " lock" + lockB + "===>get" + lockA);
            }
        }
    }
}

程序卡死,两个线程都拿着对方想要的锁不释放

t2 locklockB===>getlockA
t1 locklockA===>getlockB

解决方法:

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

执行结果:

t1 locklockA===>getlockB
t2 locklockB===>getlockA
t1 locklockB===>getlockA
t2 locklockA===>getlockB

当程序卡着不输出,如何排查是死锁问题?

使用Java自带工具:

  1. 使用 jps 定位进程号

    在命令行输入 jps -l 查看目前运行的Java进程

  2. 使用 jstack 进程号查看进程信息,找到死锁问题

面试中,工作如何排查

大部分人会说查看日志,但是你可以说查看堆栈信息!

posted @ 2021-08-23 12:01  蓝色空间号  阅读(56)  评论(0)    收藏  举报