JUC详解

JUC

1.什么是JUC

JUC实际上就是java.util下的一个工具包,多线程中的Lock类,Callable接口都在此工具包下

2.回顾进程与线程

  • 程序:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

  • 进程:进程则是执行程序的一次过程,它是一个动态的概念。是系统资源分配的单位。

  • 线程:线程是CPU调度和执行的的单位。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。

java默认有2个线程:main,GC

java可以开启线程吗?

java没有权限开启线程,只能调用底层的方法,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 */
            }
        }
    }
	
	//调用本地的方法启动线程
    private native void start0();

并发与并行:

  • 并发:同一个时间周期内,处理多件事情
  • 并行:在同一时间,同时进行2件或多件事情

单核的cpu在快速交替时可以做到并发,但不能做到并行

public static void main(String[] args) {
        //获取cpu的核数
        //CPU密集型,IO密集型
        System.out.println(Runtime.getRuntime().availableProcessors());
}

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

线程状态(源码层面):

   public enum State {
       	//新生状态
        NEW,
		
       	//运行状态
        RUNNABLE,

        //阻塞状态
        BLOCKED,

        //等待状态,会一直等待
        WAITING,

        //超时等待状态,等待超过一定时间后,就会退出等待状态
        TIMED_WAITING,
		
        //终止状态
        TERMINATED;
    }

sleep和wait方法的区别:

  • sleep =>Thread wait=>Object
  • sleep使用时不限制位置,wait必须在同步代码块中
  • sleep不会释放锁,wait会释放锁

3.Synchronized与Lock锁

1.Lock锁

​ 首先,Lock是一个接口,它有三个实现类,ReentrantLock(可重入锁),ReentrantReadWriteLock.ReadLock(可读写锁.读锁),ReentrantReadWriteLock.WriteLock(可读写锁.写锁),常用是ReentrantLock(可重入锁)

ReentrantLock(可重入锁)的方法

  • 加锁:lock()
  • 尝试获取锁:tryLock()
  • 解锁:unlock

公平锁与非公平锁:

  • 公平锁:先来后到,一定要排队执行
  • 非公平锁:可以插队
  • 可重入锁默认使用非公平锁,因为公平,例:性能倒置问题(3h的线程排在3s的线程前时)

2.两者的比较

多线程卖票产生的并发问题,使用synchronized和lock锁的解决方式

package com.lzl;

//使用Synchronized关键字
public class demo1 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"A").start();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"B").start();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"C").start();
    }

}


class Ticket{
    
    int ticketNum = 30;
    
    public synchronized void saleTicket(){
        if(ticketNum>0){
            System.out.println("卖出了第"+ticketNum--+"张票!");
        }
    }

}

package com.lzl;

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

//使用lock锁
public class demo2 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"A").start();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"B").start();
        new Thread(()->{ for (int i = 0; i < 60; i++) ticket.saleTicket(); },"C").start();
    }

}


class Ticket2{
    int ticketNum = 30;

    //默认是非公平锁,传入true使用公平锁,false非公平锁
    Lock lock = new ReentrantLock();

    public  void saleTicket(){
        //加锁
        lock.lock();
        try {
            if(ticketNum>0){
                System.out.println("卖出了第"+ticketNum--+"张票!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }

    }

}

我们通过Lock锁或者Synchronized关键字,都可以解决多线程的并发问题,下面说下它们的区别、优缺点

  • Synchronized是java内置的一个关键字,Lock是工具包下java提供的一个接口
  • Synchronized无法判断锁的状态,Lock可以判断是否获取到了锁
  • Synchronized会自动释放锁,Lock必须要手动释放锁,否则会产生死锁
  • Synchronized遇到线程阻塞时会一直等待,Lock锁不一定会等待下去
  • Synchronized是已设置好的可重入锁、不可中断、非公平锁;Lock是可以手动设置的可重入锁、中断(是否),公平(是否)
  • 正是由于Synchronized的全自动性,对于少量代码时,Synchronized更适用,Lock适用于大量代码内容

4.生产者消费者问题

1.传统解决方式

​ 在加入同步锁的情况下,A线程负责生产,B线程负责消费时,使用if不使用whlie进行判断,程序也可以正常进行,但当生产者或消费者有多个对象时,if判断会产生虚假唤醒的问题,应当使用while判断。

package com.lzl;

/**
 * 测试生产者、消费者问题
 */
public class Demo3 {
    public static void main(String[] args) {
        Data data = new Data();

        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 Data{

    private Integer num=0;

    public synchronized void increment(){
        while (num!=0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"-->"+num);
        //唤醒所有线程
        notifyAll();
    }


    public synchronized void decrement(){
        while(num == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"-->"+num);
        notifyAll();

    }

}

2.JUC版的解决方式

传统方式:synchronized --> wait() --> notifyAll()

JUC: lock(),unlock(),lock.newCondition() --> condition.await() --> condition.signalAll()

package com.lzl;

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

/**
 * 测试JUC解决生产者、消费者问题
 */
public class Demo4 {
    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 Integer num=0;

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

    public void increment(){
        lock.lock();
        try {
            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(){
        lock.lock();
        try {
            while(num == 0){
               condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"-->"+num);
            //唤醒全部线程
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

问题:上述两种方式,可以做到生产者、消费者交替执行,但是无法精准定位谁进行生产?谁进行消费?

package com.lzl;

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

/**
 * 利用Condition监视器精准唤醒某一线程
 */
public class Demo5 {
    public static void main(String[] args) {
        Data3 data = new Data3();
        new Thread(()->{ for (int i = 0; i < 10; i++) data.printA();}).start();
        new Thread(()->{ for (int i = 0; i < 10; i++) data.printB();}).start();
        new Thread(()->{ for (int i = 0; i < 10; i++) data.printC();}).start();
    }
}


class Data3{

    private int num=1;
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();
    
    public void printA(){
        lock.lock();
        try {
            if(num != 1){
                conditionA.await();
            }
            System.out.println("A线程开始工作。。。。。。。");
            num=2;
            //精准唤醒某一个线程
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            if(num != 2){
                conditionB.await();
            }
            System.out.println("B线程开始工作。。。。。。。");
            num=3;
            //精准唤醒某一个线程
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        try {
            if(num != 3){
                conditionC.await();
            }
            System.out.println("C线程开始工作。。。。。。。");
            num=1;
            //精准唤醒某一个线程
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

5.8锁问题

结论:锁,锁的只有两个东西,一个是对象,一个是class模板

下述四组八个问题:

package com.lzl.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 问题1:在不加延迟的情况下,同一个对象调用两个方法 ---》发短信、打电话
 * 问题2:在发短信方法加了延迟的情况下,同一个对象调用两个方法 ---》 发短信、打电话
 * 结论:两个方法由同一个对象调用,使用同一把锁,谁先获得锁,谁先调用
 */
public class Demo1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        //问题1、2
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone.call() ).start();
    }

}

class Phone{
    public synchronized void sendSms(){
        try {
            //睡4s
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}

package com.lzl.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 问题1:同一个对象调用加锁和不加锁两个的方法 --》hello、发短信
 * 问题2:不同对象调用两个加锁的方法 --》打电话、发短信
 * 结论:未加锁或两个对象使用的不是同一把锁,没有延迟的方法先执行
 */
public class Demo2 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        Phone2 phone2 = new Phone2();

        //问题1
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone.hello() ).start();

        //问题2
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone2.call() ).start();
    }

}


class Phone2{

    public synchronized void sendSms(){
        try {
            //睡4s
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

    public void hello(){
        System.out.println("hello");
    }
}
package com.lzl.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 问题1:同一个对象的两个方法都加了static静态修饰时 --》发短信、打电话
 * 问题2:两个对象的两个方法都加了static静态修饰时 --》发短信、打电话
 * 结论:由于方法被静态修饰,在类一加载时就会产生,此时锁的是class模板,无论一个还是多个对象,都共用一把锁
 *
 */
public class Demo3 {

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

        //问题1
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone.call() ).start();
        //问题2
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone2.call() ).start();
    }
}

class Phone3{
    public static synchronized void sendSms(){
        try {
            //睡4s
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call(){
        System.out.println("打电话");
    }

}
package com.lzl.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 问题1:同一个对象调用两个静态方法,一个加锁,一个不加锁 --》打电话、发短信
 * 问题2:两个个对象调用两个静态方法,一个加锁,一个不加锁 --》打电话、发短信
 * 结论:没有锁的情况下,没有延迟的先执行
 */
public class Demo4 {
    public static void main(String[] args) {
        Phone4 phone = new Phone4();
        Phone4 phone2 = new Phone4();

        //问题1
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone.call() ).start();

        //问题2
        new Thread(()-> phone.sendSms() ).start();
        new Thread(()-> phone2.call() ).start();
    }


}

class Phone4{

    public static synchronized void sendSms(){
        try {
            //睡4s
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public  static void call(){
        System.out.println("打电话");
    }

}

6.不安全集合类

1.CopyOnWriteArrayList

多个线程向List集合中插入数据时,java.util.ConcurrentModificationException 并发修改异常
解决方式:
1.使用Vector
2.使用Collections.synchronizedList
3.使用JUC包下的CopyOnWriteArrayList
CopyOnWrite 写入时复制,cow计算机程序设计领域的一种优化策略
相较于Vector的优势,CopyOnWriteArrayList使用lock锁的方式,效率更高

package com.lzl.unsafe;

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

/**
 * 测试多线程环境下List的不安全及解决方案
 */
public class ListTest {

    public static void main(String[] args) {
        //原生集合
        //List<String> list = new ArrayList<>();
        //1.Vector
        //List<String> list = new Vector<>();
        //2.使用工具类对集合进行转换
        //List<String> list = Collections.synchronizedList( new ArrayList<>());
        //3.使用JUC包下的CopyOnWriteArrayList
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }).start();
        }

    }
}

2.CopyOnWriteArraySet

多个线程向Set集合中插入数据时,java.util.ConcurrentModificationException 并发修改异常

解决方式:

  1. 使用工具类转换 Collections.synchronizedSet()
  2. 使用JUC包下CopyOnWriteArraySet
package com.lzl.unsafe;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 测试并发下set集合的不安全及解决方案
 */
public class SetTest {
    public static void main(String[] args) {
        //原始set
        //Set<String> set = new HashSet<>();
        //使用工具类转换
        //Set<String> set = Collections.synchronizedSet(new HashSet<>());
        //使用JUC包下CopyOnWriteArraySet
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }

    }
}

3.ConcurrentHashMap

同样,多个线程向Map中插入数据时,也会产生java.util.ConcurrentModificationException 并发修改异常

解决方式:

  1. JUC包下的ConcurrentHashMap
package com.lzl.unsafe;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 测试并发下不安全的Map及解决方案
 */
public class MapTest {
    public static void main(String[] args) {
        //1.原生Map
        //Map<String,String> map = new HashMap<>();
        //2.JUC包下的ConcurrentHashMap
        Map<String,String> map = new ConcurrentHashMap<>();

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

7.Callable接口

Callable接口特点:

  1. 有返回值,接口泛型的类型 == 返回值类型
  2. Thread类 和 Runnable接口使用run()方法,Callable接口使用call()方法
  3. 使用get()方法时,可能会产生阻塞,一般代码置后或异步获取
  4. 由于缓存的存在,开启多条线程调用call()方法,只会执行一次
  5. 线程的启动通过FutureTask类来过渡
package com.lzl.callAble;

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

/**
 * 测试使用callable接口开启线程
 */
public class CallAbleTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * callable接口开启线程的过程:
         * 首先开启线程的方式只有一个:new Thread().start();
         * FutureTask 是 Runnable接口的一个实现类,而FutureTask的构造函数参数可以是Callable类型的
         * 因此使用callable接口开启线程,需要用到FutureTask类过度
         *
         */
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask<String>(myThread);
        new Thread(futureTask,"A").start();
        //开启两个线程调用callable接口时,由于缓存的存在,只会输出一次
        new Thread(futureTask,"B").start();
        //获取callable接口的返回值
        // 此方法可能会产生阻塞,一般放在代码的最后,或者异步调用
        String str = (String)futureTask.get();

        System.out.println(str);

    }

}

class MyThread implements Callable<String>{

    @Override
    public String call() {
        System.out.println("call()");
        return "hello,callable";
    }
}

8.辅助工具类

1. CountDownLatch

减法计数器,当待执行任务数量置零时,执行后续任务,否则处于阻塞状态

方法:

  • countDownLatch.countDown() --> 计数器-1
  • countDownLatch.await() -->阻塞,等待计数器归0
package com.lzl.util;

import java.util.concurrent.CountDownLatch;

/**
 * 线程的减法计数器
 */
public class TestCountDownLatch {

    public static void main(String[] args) throws InterruptedException {
        //6次计数后输出
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName());
                //计数器-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();

        }
        //等待计数器归零
        countDownLatch.await();
        System.out.println("计数器已归零");

    }

}

2.CyclicBarrier

加法计数器,当任务数量达到指定数量时,执行后续任务,否则处于阻塞状态

方法:

  • cyclicBarrier.await() --> 等待任务数量达到指定数量
package com.lzl.util;

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

/**
 * 加法计数器
 */
public class TestCyclicBarrier {

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(6, () -> System.out.println("计数完毕!"));

        for (int i = 0; i < 6; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(temp);
                try {
                    //等待
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

3. Semaphore

信号量,限制可同时执行任务量,其他待执行任务等待信号量释放执行

方法:

  • semaphore.acquire() --> 从信号量中获取许可证,获取不到,等待
  • semaphore.release() --> 释放许可证到信号量中
package com.lzl.util;

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

/**
 * 信号量
 */
public class TestSemaphore {
    public static void main(String[] args) {
        //设置可用信号量为3
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    //从信号量中获取许可证
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"获取到了许可证!");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"释放了许可证!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放给定数量的许可证到信号量中
                    semaphore.release();
                }
            },i+"").start();
        }
    }

}

9.读写锁

适用场景:单个写入,多个读取数据

ReentrantReadWriteLock:相较于lock锁可以更加细粒度的控制

ReentrantReadWriteLock().writeLock() : 写锁,也叫独占锁

ReentrantReadWriteLock().readLock() : 读锁,也叫共享锁

package com.lzl.readWriteLock;

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

/**
 * 测试读写锁
 */
public class TestReadWriteLock {

    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.put(""+temp,""+temp);
            }).start();
        }

        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.get(""+temp);
            }).start();
        }
    }


}

/**
 * 自定义缓存类
 */
class MyCache{

    private volatile Map<String,String> map =  new HashMap<>();
    //读写锁,相较于lock锁可以更加细粒度的控制
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key,String value){
        //加锁 读写锁.写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(key+"开始写入");
            map.put(key,value);
            System.out.println(key+"写入完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            readWriteLock.writeLock().unlock();
        }

    }

    public void get(String key){
        //加锁 读写锁.读锁
        readWriteLock.readLock().lock();
        try {
            System.out.println(key+"开始读取");
            map.get(key);
            System.out.println(key+"读取完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }

    }
}

10.阻塞队列

1.概念及关系图

队列:一种遵循先入先出、后入后出(FIFO)的线性表

阻塞:

  • 队列已满的情况下,再次向队列中写值时
  • 队列为空的情况下,从队列中取值时

队列的实现类在java中与List,set的关系图:

2.四组api介绍

方式 无返回值,抛异常 有返回值 阻塞等待 超时等待
添加 add offer put offer(时间参数)
取出 remove poll take poll(时间参数)
获取队首元素 element peek -- --
package com.lzl;

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

/**
 * 测试阻塞队列的四组api
 */
public class queueTestBlockingQueue {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test1(){
        //add、remove方法,当队列已满继续插入或队列为空继续移除时,会抛出异常
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        //检测当前的队首元素,队列为空时,抛出异常
        System.out.println(blockingQueue.element());
        //System.out.println(blockingQueue.add("d"));
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
       // System.out.println(blockingQueue.remove());
    }

    public static void test2(){
        //offer、poll,当队列已满继续插入或队列为空继续移除时,返回false或null
        BlockingQueue<String> 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.offer("d"));
        //检测当前的队首元素,队列为空时,返回null
        System.out.println(blockingQueue.peek());

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

    public static void test3() throws InterruptedException {
        //put、take,当队列已满继续插入或队列为空继续移除时,程序会一直处于阻塞等待的状态
        BlockingQueue<String> 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());
    }

    public static void test4() throws InterruptedException {
        //put、take带有参数调用时,当队列已满继续插入或队列为空继续移除时,超过定义的时间后会结束阻塞状态
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        blockingQueue.offer("d",2, TimeUnit.SECONDS);

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));

    }
    
}

3.同步队列

同步队列不会存储元素,添加一个必须取出一个,并且此处的添加和取出方法,应当使用阻塞等待,否则元素无法加入队列中

package com.lzl.queue;

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

/**
 * 测试同步队列
 */
public class TestSynchionzeQueue {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new SynchronousQueue<>();

        new Thread(()->{
            try {
                System.out.println("添加1");
                queue.put("1");
                System.out.println("添加2");
                queue.put("2");
                System.out.println("添加3");
                queue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println("取出"+queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println("取出"+queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println("取出"+queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

   }
}

11.线程池

1.使用原因及其优点

程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段

池化技术:提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。

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

它的主要特点为:线程复用,控制最大并发数,管理线程。

第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

2.使用方式

线程池的创建:三大方式、七大参数、四种拒绝策略

1.三大创建方式

  • 单一线程线程池:Executors.newSingleThreadExecutor()
  • 固定数量的线程池:Executors.newFixedThreadPool(num)
  • 可伸缩大小的线程池: Executors.newCachedThreadPool()
package com.lzl.pool;

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

/**
 * 测试创建线程池的三大方式
 */
public class TestThreadPool {
    public static void main(String[] args) {
        //创建单一线程的线程池
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //创建固定数量的线程池
        //ExecutorService threadPool =Executors.newFixedThreadPool(5);
        //创建一个可伸缩大小的线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ==> ok");
                });
            }
        } finally {
            //关闭线程池
            threadPool.shutdown();
        }
    }
}

2.七大参数

三种创建方式都调用的源码分析:

 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;
    }

实际开发中,不要使用工具类提供的三种创建方式,而是自定义线程池,阿里巴巴代码规约中有明确说明

8. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

自定义线程池:

package com.lzl.pool;

import java.util.concurrent.*;

/**
 * 自定义线程池
 * 模拟银行办理业务场景:
 * 总窗口5个,核心营业窗口2个,候客等待位置3个
 * 通过调整开启的线程数量,获取运行结果,可以得出:
 * 线程池的能容纳的最大线程数量为:最大线程数+待执行任务队列数 5+3
 */
public class TestCustomThreadPool {
    public static void main(String[] args) {
        //自定义一个核心线程数为2,最大线程数为5,线程池任务队列为3的线程池
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                //默认的线程创建工厂
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
                );

        try {
            for (int i = 0; i < 9; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

3.四种拒绝策略

当待执行任务数大于线程池的最大容量时,线程池的拒绝策略

  1. AbortPolicy :抛出异常提示
  2. CallerRunsPolicy:此任务从哪个线程进来的,会被退回到哪个线程执行,如main方法进入,此任务交由main()线程执行
  3. DiscardPolicy:任务会被忽略
  4. DiscardOldestPolicy :与线程池中已有的任务进行竞争

4.如何定义最大线程数量

分为两种方式:

  1. CPU密集型
  2. IO密集型

CPU密集型,获取当前电脑的最大线程数量,作为最大线程数量的参数即可

Runtime.getRuntime().availableProcessors()

IO密集型: 当程序中调用IO资源较多时,最大线程数量应大于IO任务数量,最好为2倍。 如IO任务数量为15时,最大线程数量定义为30。

12.四大函数式接口

函数式接口:有且只有一个方法的接口

新时代程序猿必备四个技能:lambda表达式、链式编程、函数式接口、Stream流式计算

四个函数式接口原型:Function,Predicate,Consumer,Supplier

1.函数型接口

有参数,有返回值,泛型定义参数及返回值类型

package com.lzl.function;

import java.util.function.Function;

/**
 * 函数型接口:传入参数及返回值类型
 */
public class TestFunction {
    public static void main(String[] args) {
//        Function function = new Function<String, String>() {
//            @Override
//            public String apply(String str) {
//                return str;
//            }
//        };

        //定义参数及返回值得类型
        Function<String, String> function =(str)->{return str;};
        System.out.println(function.apply("asd"));

    }
}

2.断定型接口

有参数,又返回值,泛型定义参数类型,返回值为固定的布尔值

package com.lzl.function;

import java.util.function.Predicate;

/**
 * 断定型接口:传入参数类型,返回布尔值
 */
public class TestPredicate {
    public static void main(String[] args) {
       Predicate<String> predicate = (str)->{return str.isEmpty();};
        System.out.println(predicate.test("asd"));
    }
}

3.消费型接口

有参数,无返回值,泛型定义参数类型

package com.lzl.function;

import java.util.function.Consumer;

/**
 * 消费型接口:只有参数,无返回值
 */
public class TestConsumer {
    public static void main(String[] args) {
        Consumer<String> consumer = (str)->{
            System.out.println(str);
        };
        consumer.accept("asd");
    }
}

4.供给型接口

无参数,又返回值,泛型定义返回值类型

package com.lzl.function;

import java.util.function.Supplier;

/**
 * 供给型接口:定义返回值类型,只有返回值,无参数
 */
public class TestSupplier {
    public static void main(String[] args) {
        Supplier<Integer> supplier =()->{return 1024;};
        System.out.println(supplier.get());
    }
}

13.Strem流式计算

将存储的数据转换为流,进行计算处理

package com.lzl.stream;

import java.util.Arrays;
import java.util.List;

/**
 * 流式计算、链式编程综合题目
 * 题目:请按照给出数据,找出同时满足以下条件的用户
 * 也即以下条件:
 * 1、全部满足偶数ID
 * 2、年龄大于24
 * 3、用户名转为大写
 * 4、用户名字母倒排序
 * 5、只输出一个用户名字 limit
 */
public class TestStream {
    public static void main(String[] args) {

        User u1 = new User(11, "a", 23);
        User u2 = new User(12, "b", 24);
        User u3 = new User(13, "c", 22);
        User u4 = new User(14, "d", 28);
        User u5 = new User(16, "e", 26);

        //数据存储在集合中
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);

        //转换为流进行条件过滤
        //Strem部分流方法使用、链式编程
        list.stream()
                .filter((u)->{return u.getId()%2 == 0;})
                .filter((u)->{return u.getAge()>24;})
                .map((u)->{return u.getUserName().toUpperCase();})
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1); })
                .limit(1)
                .forEach(System.out::println);
    }

}

package com.lzl.stream;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private int id;
    private String userName;
    private int age;
}

14.分支合并

什么是ForkJoin

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是将一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

并行执行任务,大数据量!提高效率!

特点:工作窃取

​ 简单理解,就是一个工作线程下会维护一个包含多个子任务的双端队列。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度较快,很快就把所有任务消耗完了。此时,该线程就会从其他队列中窃取任务来执行。

package com.lzl.forkJoin;

import java.util.stream.LongStream;

/**
 * 场景:求和1~100_0000_0000之间的所有数字
 */
public class TestDemo {
    static long start = 1L;
    static long end = 100_0000_0000L;
    static long sum = 0L;

    public static void main(String[] args) {
        //test1(); //3586
        //test2();//1124
        test3();//797
    }

    //普通求和
    public static void test1(){
        long timeStart = System.currentTimeMillis();
        for (long i = start; i <= end; i++) {
            sum+=i;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("结果:"+sum+",时间:"+(timeEnd-timeStart));
    }
    //forkJoin任务
    public static void test2(){
        long timeStart = System.currentTimeMillis();
        sum = new MyForkJoinTask(start,end).compute();
        long timeEnd = System.currentTimeMillis();
        System.out.println("结果:"+sum+",时间:"+(timeEnd-timeStart));
    }
    //并行stream流
    public static void test3(){
        long timeStart = System.currentTimeMillis();
        sum = LongStream.rangeClosed(start,end).parallel().reduce(0,Long::sum);
        long timeEnd = System.currentTimeMillis();
        System.out.println("结果:"+sum+",时间:"+(timeEnd-timeStart));
    }

}

package com.lzl.forkJoin;

import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * 自定义forkjoin任务
 */
public class MyForkJoinTask  extends RecursiveTask<Long> {
    long start ;
    long end;


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

    @Override
    protected Long compute() {
        //临界值,可以调整此值,优化效率
        long critical = 10000L;
        //判断是否拆分完毕
        long length = end-start;
        if(length <= critical){
            long sum = 0L;
            //如果相加数的首尾项小于临界值,直接计算
            for (long i = start; i < end; i++) {
                sum+=i;
            }
            return sum;
        }else{
            //拆分任务
            //计算首尾项的中间值
            long middle = (start+end)/2;
            MyForkJoinTask task1 = new MyForkJoinTask(start,middle);
            MyForkJoinTask task2 = new MyForkJoinTask(middle,end);
            //拆分任务,把任务压入线程队列中
            task1.fork();
            task2.fork();
            return task1.join()+task2.join();
        }
    }
}

15.异步回调

java.util.concurrent.Future CompletableFuture

package com.lzl.future;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 测试异步任务回调
 */
public class TestCompletableFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //没有返回值得异步回调
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("异步回调方法。。。");
        });
        //先打印外部输出
        System.out.println("1111");
        //阻塞 待任务中程序执行完毕
        future.get();

        //供给型接口 有返回值的异步输出
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("有返回值型接口。。");
            int a=10/0;
            return 1024;
        });


        System.out.println(future1.whenComplete((u, t) -> {
            //程序执行成功时
            System.out.println("u=>"+u);
            System.out.println("t=>"+t);
        }).exceptionally((u) -> {
            //程序执行错误时
            System.out.println("u=>"+u);
            return 233;
        }).get());


    }

}

16.Volatile与JMM

  1. 什么是JMM?

JMM本身是一种抽象的概念,并不真实存在,是JAVA中的一种约定,规范

  1. JMM关于同步的规定
  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁
  1. Volatile

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

三大特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排
  1. JMM的内存模型

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。此时,就需要上述的JMM规定来保证多个线程操作的安全性。

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

JMM对这八种指令的使用,制定了如下规则:

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

import java.util.concurrent.TimeUnit;

/**
 * 测试Volatile的可见性
 */
public class TestVolatile {

    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        //如果num为0,此线程一直执行
        new Thread(()->{
            while (num == 0){

            }
        }).start();

        //为保证上述线程启动,此处延迟1s
        TimeUnit.SECONDS.sleep(1);

        //main线程修改主内存中的变量值,如果num变量线程间可见,程序会停止,否则会一直死循环
        System.out.println("main...");
        num++;

    }

}
  1. 原子性

原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。

volatile不保证原子性的验证:

package com.lzl.jmm;

/**
 * 测试Volatile是否能够保证原子性
 */
public class TestVolatile2 {

    private volatile static int num=0;

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

    public static void main(String[] args) {

        //此处开启20个线程去执行叠加操作
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        //主线程获取num的值,如果正常叠加,num值为20000
        System.out.println(Thread.currentThread().getName()+"-----"+num);

    }
}
  1. 怎样保证原子性

上述程序,在不使用lock锁、synchronized的情况下,怎样保证正确求和呢?

    private volatile  static AtomicInteger num = new AtomicInteger();
    public static void add(){
        num.getAndIncrement();
    }

使用原子类定义数字,调用其方法,此类的方法调用并不是简单的叠加操作,涉及到操作系统,比加锁的效率高很多。

  1. 指令重排

什么是指令重排:程序的执行顺序可能不是按照你的书写顺序去执行!

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

int x=1;//1
int y=2;//2
x = x+5;//3
y = x*x;//4

//书写执行顺序:1234 
//指令重排后执行的可能顺序:2134 1324
//但不可能是4123这种 因为处理器在进行指令重排时,会考虑数据之间的依赖性

指令重排理论上一定存在,但实际写很多次代码,也不一定可以重现!

为什么volatile可以禁止指令重排?

系统中的CPU指令,内存屏障。它的作用:

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性( 利用这些特性volatile实现了可见性 )

.

17.单例模式

1.饿汉式单例

package com.lzl.singleCase;

/**
 * 单例模式 -- 饿汉式
 * 程序一开始的时候,就初始化对象,当类中有比较占用空间的操作时,如下数组,比较浪费空间!
 */
public class HungryMan {

    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];
    private byte[] data4 = new byte[1024];

    private HungryMan(){
    }

    private final static HungryMan hungryMan = new HungryMan();

    public static HungryMan getInstance(){
        return hungryMan;
    }

}
  1. 懒汉式单例
package com.lzl.singleCase;

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

/**
 * 单例模式 -- 懒汉式
 */
public class LazyMan {

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

    private volatile static  LazyMan lazyMan;

    //方式一:可以实现单例,延迟加载,但多线程下不安全
    public static LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }


    //方式二:双重检测锁模式 多线程下安全 也称为DCL懒汉式
    public static LazyMan getInstance2(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                    //new对象不是一个原子性操作
                    /*
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把这个对象指向这个空间
                     *
                     * 所以完整的双重检测锁模式 需要使用volatile关键字去修饰
                     * */
                }
            }
        }
        return lazyMan;
    }

    //使用反射去破坏上述单例模式
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //方式一测试 构造器中输出执行的线程名称发现会有多个线程
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
               getInstance();
            }).start();
        }

        //方式二测试
        LazyMan instance2 = getInstance2();
        LazyMan instance21 = getInstance2();
        System.out.println(instance2);
        System.out.println(instance21);

        //使用反射去破坏上述的懒汉式单例
        LazyMan instance22 = LazyMan.getInstance2();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //关闭私有属性的代码检测
        declaredConstructor.setAccessible(true);
        LazyMan instance = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance22);
    }
}

关于单例模式的其他介绍参考:单例详解

关于利用反射破解的详细介绍本章视频:玩转单例模式

18.CAS

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V
  • 旧的预期值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

相关介绍:乐观锁与悲观锁

package com.lzl.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 关于cas的理论介绍
 *
 * @author liangzilong
 * @date 2022-11-16 9:59
 */
public class TestCas {
	public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(1024);
        //public final boolean compareAndSet(int expect, int update)
        //如果expect是期望的值,则结果更新的update这个值
        //此方法会返回布尔值,如果获得到了期望值并且更新成功了,返回true;否则返回false
        System.out.println(integer.compareAndSet(1024, 2048));
        //结果是2048
        System.out.println(integer.get());
        //未获取到期望值的结果
        System.out.println(integer.compareAndSet(1024, 2048));
        System.out.println(integer.get());
         //原子性变量的++操作
        integer.getAndIncrement();
        System.out.println(integer);
         /*
        源码分析:
        Unsafe类:首先需要明确,java是无法直接操作内存的,但是java可以操作底层的c++ 如开启线程的native方法;unSave类属于java开的一个后门,可以通过这					 个类操作底层

        public final int getAndIncrement() {
            this:当前值
            valueOffset:当前值在内存中的偏移量
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }

        public final int getAndAddInt(Object var1, long var2, int var4) {
            //定义变量var5
            do {
                //获取当前对象在内存中的值
                var5 = this.getIntVolatile(var1, var2);
                比较 var1 var2 与 var5的内存值,如果var5的值是期望的值,执行var5+var4(1) +1操作,内存层面的+1 效率很高
                                              否则,在此处进行循环(自旋锁)
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
              return var5;
        }

        */
    }
}

19.原子引用解决ABA问题

什么是ABA问题:假设t1线程工作时间为10秒,t2线程工作时间为2秒,那么可能在A的工作期间,主内存中的共享变量 A已经被t2线程修改了多次,只是恰好最后一次修改的值是该变量的初始值,虽然用CAS判定出来的结果是期望值,但是却不是原来那个了=======》“狸猫换太子”

原子引用解决ABA问题:实际就是通过增加版本号,类似于乐观锁中的版本号机制

package com.lzl.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * ABA问题的还原
 */
public class TestABA {
    private static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        //t2线程执行的程序
        atomicInteger.compareAndSet(1,2);
        System.out.println(atomicInteger.get());
        atomicInteger.compareAndSet(2,1);
        System.out.println(atomicInteger.get());

        //t1线程执行的程序
        atomicInteger.compareAndSet(1,66);
        //此时的输出结果是66 但实际上比较值中的1已经被t2线程替换过了
        System.out.println(atomicInteger.get());
        
    }
}

关于包装类比较的一个坑:

.

package com.lzl.test;

public class TestDemo {

    public static void main(String[] args) {
        Integer a=1;
        Integer b=1;
        System.out.println(a==b);//true
        System.out.println(a.equals(b));//true

        Integer c=129;
        Integer d=129;
        System.out.println(c==d);//false
        System.out.println(c.equals(d));//true
    }
}
package com.lzl.cas;

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

/**
 * 原子引用解决ABA问题
 */
public class TestABA2 {

    //关于包装类比较的一个坑 泛型为Integer时,此处的期望值需要在-127~128区间,否则期望值一直无法匹配成功
    private static AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<Integer>(1,1);

    public static void main(String[] args) {

        //t2线程执行的操作
        atomicReference.compareAndSet(1,2, 1,
                atomicReference.getStamp()+1);
        System.out.println(atomicReference.getReference());
        atomicReference.compareAndSet(2,1, 2,
                atomicReference.getStamp()+1);
        System.out.println(atomicReference.getReference());

        //t1线程执行的操作
        //由于此时的版本号已经被改为了3,此处比较无法通过
        atomicReference.compareAndSet(1,66, 1 ,
                atomicReference.getStamp()+1);
        System.out.println(atomicReference.getReference());
        
    }

}

20.可重入锁

什么是可重入锁:可重入锁又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。

package com.lzl.lock;

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

/**
 * 测试可重入锁
 */
public class TestReenTrantLock {
    public static void main(String[] args) {

        Phone2 phone = new Phone2();

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

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

    }

}

class Phone{

    public synchronized static void sms(){
        System.out.println(Thread.currentThread().getName()+"--->sms");
        call();
    }

    public synchronized static void call(){
        System.out.println(Thread.currentThread().getName()+"--->call");
    }

}

class Phone2{
    public  static void sms(){
        Lock lock = new ReentrantLock();
        try {
            //锁需要配对,如果此处锁了2次,下面只解锁一次,就会产生死锁
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"--->sms");
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }

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

}

21.自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

package com.lzl.lock;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 自定义自旋锁
 */
public class TestSpinLock {

    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public  void myLock(){
        //获取当前线程
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"--->myLock");
        //如果当前线程不为空,在此处循环自旋
        while (!atomicReference.compareAndSet(null,thread)){

        }
    }

    public  void myUnLock(){
        //获取当前线程
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"--->myUnLock");
        //传入当前线程,结束自旋
        atomicReference.compareAndSet(thread,null);
    }

}


package com.lzl.lock;

import java.util.concurrent.TimeUnit;

/**
 * 测试自定义自旋锁
 */
public class TestSpinLockDemo {

    public static void main(String[] args) {
        TestSpinLock spinLock = new TestSpinLock();


        new Thread(()->{
            spinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.myUnLock();
        },"A").start();


        new Thread(()->{
            spinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.myUnLock();
        },"B").start();

    }
}

代码分析:当A线程先拿到自定义的自旋锁时,B线程虽然也可以输出mylock,但此时只能等待A线程释放锁,B线程才能获取到锁。

22.死锁排查

package com.lzl.lock;

import java.util.concurrent.TimeUnit;

/**
 * 创建死锁进程
 */
public class TestDeadLock {
    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{

    String lockA;
    String lockB;

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

    @Override
    public void run() {

        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"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);
            }
        }
    }
}

使用命令jsp -l,在Terminal查看当前所有进程

.

使用命令jstack查看栈信息

.

posted @ 2022-11-21 19:21  梁小白123  阅读(2713)  评论(0编辑  收藏  举报