JUC的多线程进阶

进程和线程

程序运行起来后就是一个进程,进程包含多个线程,java默认有两个线程:main,g(c)线程

Java是没有权限开启线程的,需要调用native调用本地的方法

并发和并行

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

  • cpu一核,cpu快速交替模拟出多核的效果

并行

  • cep多核,多个线程可以同时执行

并发编程的本质:想要充分利用cpu的资源

wait()和sleep()的区别

  • wait会释放锁,sleep不会释放锁

  • wait只能在同步代码块中使用

Lock锁(重点)

  1. 创建锁

  2. 加锁

  3. 解锁

    Lock lock = new ReentranLock();
    lock.lock();
    Try{
    ...
    }catch(){
    	lock.unLock();
    }
    
/**
 * 真正的多线程开发公司不会像new Thread(new Runnable)这种方式实现创建线程
 * 线程就是一个资源类没有多余的操作,资源和对资源的操作
 * new Thread(()->{  // 资源类的方法调用 }) // 函数式接口的Lambda简写
 * 并发只的就是多个线程对同一个资源的操作,所以资源调用,就用同一个类的对象去调用
 * */
public class TestLock {
    public static void main(String[] args) {
        Resources resources = new Resources();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                resources.buy();
            }
        },"我").start();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
            resources.buy();
        }},"可恶的黄牛党").start();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
            resources.buy();
        }},"一样悲催的别人").start();
    }
}

// 购票资源类       这种线程作为单独的资源类称为OOP的思想
class Resources{
    private int num = 30;

    // buy
    @SneakyThrows
    public synchronized void buy() {
            if (num <= 0) return;
            System.out.println(Thread.currentThread().getName()+"购买了第" + num-- + "张票; 剩余:" + num + "张票。");
    }
}

Synchronized和Lock区别

  • Synchronized无法获取锁的状态
  • Synchronized会自动释放锁,Lock必须手动释放,不释放会形成死锁
  • Synchronized 线程1(获得锁,阻塞)、线程2(等待,继续等待);lock锁不会一直等待下去,因为Lock里面有tryLock()方法尝试获取锁,等不到就结束了。
  • Synchronized可重入锁,非公平锁,不可以中断;Lock锁,可重入锁,非公平锁(可以自己设置,在创建锁时ReentranLock()加入参数boolean)
  • Synchronized适合少量的代码的块

生产者和消费者问题

Synchronized版本(传统):判断资源的状态是否需要线程等待--执行业务--唤醒另外的线程 (两个线程)

  • 有多个线程的时候生产者消费者会出现问题(虚假唤醒),判断资源状态,决定线程是否等待时只判断了一次,用while()循环每次进程被唤醒时再次进行判 断就可以解决虚假唤醒问题了

JUC版(lock,Condition):Conditionl类中的方法await(),让进程等待;signal(),唤醒进程; lock.newCondition();

Condition可以实现对进程的精准唤醒

如现在要实现生产者A唤醒消费者B,B唤醒C,C唤醒D,D唤醒A;

conditionA.await();//让A线程等待

conditionA.signal();//唤醒A线程

condtionB.signal(); // 唤醒B线程

// 进程之间的通信,生产者消费者问题
public class TestShop {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                buffer.pushA();
            }

        }, "生产者AAA号").start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                buffer.popB();
            }
        }, "消费者BBB号").start();

        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                buffer.pushC();
            }
        }, "生产者CCC号").start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                buffer.popD();
            }
        }, "消费者DDD号").start();

    }
}

//JUC解决方案
class Buffer {
    Lock lock = new ReentrantLock();
    // 监视器
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();
    Condition conditionD = lock.newCondition();


    int num = 0;

    //生产者添加商品
    public void pushA() {
        lock.lock();
        // 产品满了停止生产等待消费者消费
        try {
            while (num >= 1) {
                conditionA.await();
            }
            //生产,通知消费者
            num++;
            System.out.println(Thread.currentThread().getName() + "生产了" + num + "份产品");
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void popB() {
        lock.lock();
        //没有商品等待生产
        try {
            while (num <= 0) {
                conditionB.await();
            }
            //消费,通知生产者生产
            num--;
            System.out.println(Thread.currentThread().getName() + "消费了,剩余" + num + "份产品");
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    //生产者添加商品
    public void pushC() {
        lock.lock();
        // 产品满了停止生产等待消费者消费
        try {
            while (num >= 1) {
                conditionC.await();
            }
            //生产,通知消费者
            num++;
            System.out.println(Thread.currentThread().getName() + "生产了" + num + "份产品");
            conditionD.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void popD() {
        lock.lock();
        //没有商品等待生产
        try {
            while (num <= 0) {
                conditionD.await();
            }
            //消费,通知生产者生产
            num--;
            System.out.println(Thread.currentThread().getName() + "消费了,剩余" + num + "份产品");
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

线程的8锁问题

  1. 两个方法的共用同一把锁,先抢到锁的方法先执行
  2. 锁的对象就是,方法的调用者
  3. 当资源是两个对象时,两个对象的锁互不相关
  4. 方法用static修饰后,方法锁的对象是类的Class对象,每个类的Class对象只有一个。所以多个static修饰方法时,锁只有一个。

集合类不安全

在单线程中集合类是安全的,但在多线程中是不安全的:例如:多个线程同时对集合类添加数据;

容易出现ConcurrentModificationException(迸发修改异常)

  1. 通过工具类Collection转换
  2. 写入时复制
  • List:解决方案:

    1. List list = new Vector();

    2. List<String> list = Collection.synchronizedList(new ArrayList());//  利用了同步锁的方法,但执行效率变低
      
    3. List<String> list = new CopyOnWriteArrayList(); // 利用lock锁
      
  • set:

    1. Set set= Collection.synchronizedSet(new hashSet<>());// 利用了同步锁的方法,但执行效率变低

    2. Set set = new CopyOnWriteArraySet(new HashSet());
      
  • HashMap

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

1.Callable

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

启动callable线程:

new Thread(new FutureTask(new Callble1())).start();

回顾Callable接口

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callble1 callble1 = new Callble1();
        //1.创建内部有五个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        Future<String> submit = service.submit(callble1);
        try {
            submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        service.shutdownNow();
        FutureTask futureTask = new FutureTask(new Callble1());
        new Thread(futureTask).start();
        String o = (String) futureTask.get(); // 获取返回的内容
        System.out.println(o);
    }
}
class Callble1 implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println("CallableTest");
        return "test sccesion";
    }
}

2.JUC的常用辅助类

  • 减法计数器CountDownLatch: 一般用在有些线程必须执行的时候使用,使用后,计数器-1,程序等待计数器归零(countDownLatch.await();)及线程执行了才继续运行

    两个方法:

    1. countDownLatch.countDown(); // -1
    2. countDownLatch.await(); //等待计数器归零在执行下面操作

    实现:

        // 计数器CountDownLatch
    public class CountDownLatchTest {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(5); // 总数是5
            for (int i = 0; i < 5; i++) {
                new Thread(()->{
                    System.out.println("线程执行");
                    countDownLatch.countDown();    // -1
                }).start();
            }
    
            countDownLatch.await(); //  等待计数器归零在执行下面操作
            System.out.println("多个线程执行完毕");
    
        }
    }
    
  • 加法计数器CyclicBarrier:只有一个await()方法,等待一次,就+1;

    // CyclicBarrier有两个构造器,一个参数的int 单纯的计数;
    // 两个参数,计数达到第一个参数的值时,执行第二个参数(Runnable接口)的线程;无法达到时,线程就会死在等待的地方

    实现:

    // 加法计数器
    public class CyclicBarrierTest {
        public static void main(String[] args) {
            // CyclicBarrier有两个构造器,一个参数的int 单纯的计数;
            //  两个参数,计数达到第一个参数的值时,执行第二个参数(Runnable接口)的线程
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
                System.out.println("召唤神龙");
            });
            for (int i = 1; i <= 7; i++) {
                final int temp = i ;
                new Thread(()->{
                    System.out.println("集齐了"+temp+"颗龙珠");
                    try {
                        cyclicBarrier.await();    // 从这个地方看await();等待一次,计数器就会+1等待多次达到计数器值时就执行线程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    
  • 计数信号量:Semaphore // 运用在限流方面,在有限的容量里让线程有序,容量满了其他线程等待,有线程释放,则其他线程才能获取

    两个方法:

    1. acquire() // 获取 ,一直阻塞,直到有可用的信号量,即可获取
    2. release() // 释放 , 释放所占有的信号量

    实现:抢车位(3个车位,6个车)

    // 抢车位,6抢3
    // Semaphore信号量
    public class SemaphoreTest {
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3);  // 有3个信号量
            for (int i = 1; i <= 6; i++) {
                new Thread(()->{
                    // 获取信号量
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName()+"号车获取到了车位");
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        // 释放信号量
                        System.out.println(Thread.currentThread().getName()+"号车离开了车位");
                        semaphore.release();
                    }
                },""+i).start();
            }
        }
    }
    

3. 读写锁(ReadWriteLock)

实现类:ReentrantReadWriteLock

允许多个线程读取,写的时候只允许一个线程写;相比Lock锁,更细了一点点

实现:多个线程排队写入,多个线程同时读取

  1. reentrantReadWriteLock.readLock().lock(); // // 获取读锁,加锁

reentrantReadWriteLock.writeLock().lock();  // // 获取写锁,加锁
public class ReadWriteLockTest {
    public static void main(String[] args) throws InterruptedException {
        Resources resources = new Resources();
        // 写东西
        for (int i = 1; i <= 7; i++) {
            final String  temp= ""+i;
            new Thread(()->{resources.write(temp,temp);},temp).start();
        }
        TimeUnit.SECONDS.sleep(3);
        for (int i = 8; i <= 14; i++) {
            final String  temp= ""+(i-7);
            new Thread(()->{resources.read(temp);},temp).start();
        }


    }
}

// 自定义缓冲区,线程锁操作的资源
class Resources{
    Map<String,Object> map = new HashMap();
    //读写锁
    ReentrantReadWriteLock lock= new  ReentrantReadWriteLock();

    //读取
    public void read(String i){
        // 获取读锁,加锁
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"正在读取");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"读取OK"+"读取了"+map.get(i));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            lock.readLock().unlock();
        }
    }
    //写入
    public void write(String key,String value){
        // 获取写锁,加锁
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"正在写入");
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            lock.writeLock().unlock();
        }
    }
}

4.阻塞队列(BlockingQueue)

  • 写入:队列满了的时候,阻塞
  • 取出:队列为空的时候,阻塞

一般用在多线程并发处理,线程池里面用队列去维护里面的线程

学会使用队列: 添加,移除!

BlockingQueue的四组API

每种方法对应的响应结果:

队列满时添加,队列为空时移除

四组API的测试:

public class BlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> os = new ArrayBlockingQueue<>(2); //队列长度为2
        //添加测试 add(e),offer(e),put(e),offer(e,time,unit)
        os.put("a");
        os.put("b");   //队列已满
        //os.put("c");  // 队列满时,用put()继续添加,会进入等待状态,程序不会退出
        //System.out.println(os.add("c"));   // 队列满了add()方法抛出异常
        //System.out.println(os.offer("c"));*/ //  队列满了offer(e)再添加时返回false
        //System.out.println(os.offer("c",2, TimeUnit.SECONDS));*/ //offer(e,time,unit)等待超过参数的时间还未添加成功,就结束等待

        //移除测试 remove() poll() take() poll(time,unit)
        System.out.println(os.remove());
        System.out.println(os.remove());  // 队列为空了
        //System.out.println(os.remove());          // remove()抛出异常
        //System.out.println(os.poll());            // poll() 返回false
        //System.out.println(os.take());            //   take()进入等待状态,程序还在运行,不会退出
        System.out.println(os.poll(1,TimeUnit.SECONDS));  //  poll(time,unit) 等待超过参数的时间后,移除方法就会结束,退出程序

        // 检测队首元素
        //System.out.println(os.element());    //队首为空,element()抛出异常
        System.out.println(os.peek());         // 队首为空,peek(),返回null
    }
}

5.同步队列(SynchronousQueue)

没有容量,进入一个元素后,必须取出来才能继续放元素

put(),take();

验证:从输出的结果看,添加和取出的线程是交替进行的(进入一个元素后,必须取出来才能继续放元素)

image-20210818141628454

)

public class SynchronousQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> synchronousQueue = new SynchronousQueue();

        new Thread(() -> {
            try {
                System.out.println("放入第1个");
                synchronousQueue.put("1");
                TimeUnit.SECONDS.sleep(1);
                System.out.println("放入第2个");
                synchronousQueue.put("2");
                TimeUnit.SECONDS.sleep(1);
                System.out.println("放入第3个");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                System.out.println("取出了" + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println("取出了" + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println("取出了" + synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

6.线程池(重点)

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

池化技术:

  1. 优化程序对系统资源的使用
  2. 事先准备好一些资源,给程序备用,用完后归还

好处:

  1. 降低资源的消耗
  2. 提高响应的速度
  3. 方便管理

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

一、线程池的三大方法

Executors 工具类、三大方法

及线程池运用Executors 工具类的三种创建方法

  1. executorService = Executors.newSingleThreadExecutor();  //创建只有一个线程的线程池
    
  2. executorService =Executors.newFixedThreadPool(5);//创建有5个线程的线程池
    
  3. executorService = Executors.newCachedThreadPool(); //可伸缩,遇强则强,遇弱则弱
    

创建和效果实现:

public class ThreadNew {
    public static void main(String[] args) {
        ExecutorService executorService ;
        //executorService = Executors.newSingleThreadExecutor();  //创建只有一个线程的线程池
        executorService =Executors.newFixedThreadPool(5);//创建有5个线程的线程池
        //executorService = Executors.newCachedThreadPool(); //可伸缩,遇强则强,遇弱则弱

        try {
            for (int i = 0; i < 30; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"执行");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown(); // 关闭线程池
        }


    }
}

二、七大参数

阿里巴巴的开发 手册,说明,不要运用上面的Executors 工具类的三种创建方式创建线程池,

Executors 工具类创建的线程池不安全,工作中用ThreadPoolExecutor()自定义线程池

  1. 因为这前两种方式允许的队列长度为为Integer.MAX_VALUE约等于21亿,可能导致大量请求堆积,从而OOM(内存用完了)

  2. 第三种CachedThreadPool()方式允许创建的最大线程数为Integer.MAX_VALUE,可能导致大量创建线程,从而OOM

  3. 建议通过底层的ThreadPoolExecutor()去创建线程池。上面三种方式都是返回这个底层类的对象

    七大参数

    public ThreadPoolExecutor(int corePoolSize,                   //核心线程池大小
                                  int maximumPoolSize,            //最大核心线程池大小
                                  long keepAliveTime,             //超时了没人调用就会释放
                                  TimeUnit unit,                  // 超时单位
                                  BlockingQueue<Runnable> workQueue,//阻塞队列
                                  ThreadFactory threadFactory,      //线程工厂;创建线程的,一般不会改变这个
                                  RejectedExecutionHandler handler) { // 拒绝策略 程序有该接口的实现类四种策略
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }	
    

三、四种拒绝策略

自定义线程池测试四种拒绝策略

1.AbortPolicy() //抛出异常
2.CallerRunsPolicy() //从哪儿来的就返回哪儿执行,如果从主线程main来的就由main线程来执行这个线程的代码
3.DiscardPolicy() //不会抛出异常,丢掉该任务,不执行
4.DiscardOldestPolicy()//尝试和最早进入线程池的线程竞争,如果刚好结束就接上,没有就放弃,丢掉任务,第三个的升级版

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        // 自定义线程池
        /** 四种拒绝策略在最大线程和阻塞队列都满了以后该做什么操作
         * 1.AbortPolicy()   //抛出异常
         * 2.CallerRunsPolicy() //从哪儿来的就返回哪儿执行,如果从主线程main来的就由main线程来执行这个线程的代码
         * 3.DiscardPolicy()  //不会抛出异常,丢掉该任务,不执行
         * 4.DiscardOldestPolicy()//尝试和最早进入线程池的线程竞争,如果刚好结束就接上,没有就放弃,丢掉任务
        */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                (2,
                        5,
                        3,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),  //默认的线程工厂,一般不会改动
                        //new ThreadPoolExecutor.AbortPolicy()   //抛出异常
                        //new ThreadPoolExecutor.CallerRunsPolicy() //从哪儿来的就返回哪儿执行,如果从主线程main来的就由main线程来执行这个任务的代码
                        //new ThreadPoolExecutor.DiscardPolicy()  //不会抛出异常,丢掉该任务,不执行
                        new ThreadPoolExecutor.DiscardOldestPolicy()//尝试和最早进入线程池的线程竞争,如果刚好结束就接上,没有就放弃,丢掉任务
                );

        for (int i = 0; i < 9; i++) {
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"执行");
            });
        }
    }
}

自定义的线程池的最大线程应该如何定义

  1. CPU密集型:cpu几核就是几,可以保持cpu的效率最高,代码获取电脑的cpu核数

    Runtime.getRuntime().availableProcessors());// 获取电脑的cpu核数
    
  2. IO密集型:判断程序中有多少个十分耗IO的线程有多少个,大于即可,一般两倍

  3. 调优的用处

7.四大函数式接口(必须掌握)

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

// 简化编程模型,在新版本的框架底层大量应用

一、Interface Function<T,R> //一个T参数类型,一个R返回值类型

简单实现:

Function<Object,Object> function = str -> {return str;}; // Lamda表达式简化函数式接口
public class FunctionTest {
    public static void main(String[] args) {
       /* Function<Object, Object> function = new Function<Object, Object>(){  //匿名内部类
            @Override
            public Object apply(Object o) {
                return o;
            }
        };*/
        Function<Object,Object> function = str -> {return str;}; // Lamda表达式简化函数式接口
        System.out.println(function.apply(9));
    }
}
二、Interface Predicate

断定行接口

​ 有一个输入参数,返回值只能是boolearn

// 比如用来实现判断传入的字符串是否为龙空之类的

三、Interface Consumer

消费型接口 // 只有一个输入参数,没有返回值

四、Interface Supplier

供给型接口 // 只有返回值,没有参数

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

8.Stream流式计算

什么是Stream流式计算

大数据 = 存储 + 计算

集合,数据库用来存储,计算就由Stream流式计算来完成

运用四个新时代程序员应具备的技巧:

public class TestDemeo1 {
    public static void main(String[] args) {
        User user1 = new User(1,24,"a");
        User user2 = new User(2,19,"b");
        User user3 = new User(3,22,"c");
        User user4 = new User(4,22,"d");
        User user5 = new User(5,32,"e");
        List<User> list = Arrays.asList(user1,user2,user3,user4,user5);   //转换成ArrayList 流(stream)

        //  这行代码运用到了Lamda表达式,函数式接口,链式编程,Stream流式计算
        list.stream().filter(data->{return data.getId()%2 == 1;})  // filter()过滤
                .filter(data->{return data.getAge()>=23;})
                .map(data->{return data.getName().toUpperCase();})  // 映射   返回一个流,包括将给定函数应用到该流元素的结果。
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})    //返回一个包含该流的元素流,根据提供的 Comparator排序。 
                .limit(1)    // 分页,一页显示几个元素
                .forEach(System.out::println);  //  让里面的元素执行一个操作
    }
}
class User{
    int id;
    int age;
    String name;

    public User() { }

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

    public int getId() {
        return id;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

9.ForkJoin

大化小,小任务执行后在汇总

ForkJoin的特点:工作窃取

B线程自己的任务执行完后窃取别的线程的子任务,不让线程等待从而提高效率

这里面的是双端队列,两端都可以操作

ForkJoin自己的理解和使用:ForkJoin需要通过ForkJoinPool.execute() 类似于添加线程来执行 RecursiveTask(计算任务)抽象类的任务,抽象类继承并实现里面的抽象方法

计算的类需要继承RecursiveTask实现其中的抽象方法(计算任务),任务的思想,把任务划分,递归调用,把拆分的任务压入线程队列(.fork()),返回及汇总拆分的任务的结果.join()获取子任务的结果。

计算1到10亿的加法计算

经过测试三种计算方法: 严重推翻了ForkJoin优于暴力算法的效果;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new Test().test3();
    }
    public void test2(){       //时间:322
        long num = 0L;
        long time1 = System.currentTimeMillis();
        for (int i = 0; i <= 10_0000_0000; i++) {
              num += i;
        }
        long time2 = System.currentTimeMillis();
        System.out.println("值:"+num+"  时间 :"+(time2-time1));

    }
    public void test1() throws ExecutionException, InterruptedException { //时间4807
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        long start = System.currentTimeMillis();
        ForkJoinTask<Long> forkJoinTest = new ForkJoinTest(0L,10_0000_0000L);
        //forkJoinPool.execute(); // execute执行任务是没有返回值的,无法得到计算的结果
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);//  提交任务,有返回值
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("计算的值:"+sum+"   执行的时间:"+(end - start));
    }
    public void test3(){
        long time1 = System.currentTimeMillis();      
        // Stream并行流                   //时间 206
        long reduce = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long time2 = System.currentTimeMillis();
        System.out.println("计算的值:"+reduce+"   执行的时间:"+(time2 - time1));
    }
}

forkoin:

public class ForkJoinTest extends RecursiveTask<Long> {
    private Long start;
    private Long end;

    public ForkJoinTest() { }

    public ForkJoinTest(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if((end - start)  <  1_0000){
            Long num = 0L;
            for (Long i = start; i <= end; i++) {
                 num+= i;
            }
            return num;
        }else {
            Long middle = (start+end)/2;
            ForkJoinTest task1 = new ForkJoinTest(start, middle); //拆分
            task1.fork();           // 把任务亚茹线程队列中
            ForkJoinTest task2 = new ForkJoinTest(middle+1, end);
            task2.fork();
            return task1.join() + task2.join();     //  汇总子任务的结果
        }
    }
}

10.异步回调Future

似于ajax

在程序中发起一个异步请求,不会影响到当前程序向下执行,可以利用这个实现类似于多线程的操作,并且可以具有返回值,

Future的实现类CompletableFuture:

  1. runAsync();不具有返回值
  2. supplyAsync() ;有返回值 里面是,供给型函数式接口
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{});  // 发起一个请求
future.get();           //程序处于阻塞状态,等待异步请求执行出结果

可能出现的问题,在异步请求执行中,主线程和其他线程已经执行完毕,程序就会退出,不会等待异步请求执行完。可以在主程序中加入.get() //程序处于阻塞状态,等待异步请求执行出结果

// 具有返回值的时候
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    int i = 10/0;
    return 1024;
});
future
        .whenComplete((t,u)->{    
    System.out.println("t=>"+ t);   //  t代表正常的返回结果
    System.out.println("u=>"+ u); })   // u代表错误的信息
.exceptionally(e->{        //编译失败的时候执行  
    System.out.println(e.getMessage());
    return 404; });

11. JMM

JMM:java内存模型,约定;

JMM的理解:

  1. 保证了可见性(主内存的改变能让其他线程的及时的知道)
  2. 不保证原子性
  3. 禁止指令重排
关于JMM的一些同步的约定: 线程就只在工作内存和主内存之间的约定
  1. 线程解锁前,必须把共享变量立刻刷回主存。
  2. 线程加锁前,必须读取主存中的最新值到工作内存中!
  3. 加锁和解锁是同一把锁

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

出现了线程A把主内存的值拷贝到自己的工作内存中使用,线程B修改了主内存的值,然而线程A不知道值被修改任然继续使用自己的工作内存的值。解决:Volatile

private volatile static int num = 0; // volatile关键字,保证可见性

内存交互的8种操作:

12.Volatile

1.volatile 可以保证可见性;

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

2.不保证原子性

多个线程对同一个资源做加法操作时由于加法操作不是原子性操作,会出现问题。Volatile不保证原子性

解决方案:

  1. lock锁,,synchronized同步锁

  2. java里面有原子类java.util.concurrent.atomic

    private static AtomicInteger num= new AtomicInteger(0);

    这些类的底层都直接和操作系统挂钩!在内存中修改值! Unsafe类是一个很特殊的存在 !

public class JMMDemo2 {
    private static AtomicInteger num= new AtomicInteger(0);
    public static void add(){
        // num++;  //  不是原子性操作
        num.getAndIncrement(); //底层利用CAS
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2){     // 判断存活的线程是否大于2,main线程G(c)线程
            Thread.yield();       //  线程礼让
        }
        System.out.println(num);
    }
}

3.禁止指令重排

指令重排:处理器编译器之类的会对源代码进行优化,对代码的执行顺序可能有所改变提高效率,对具有依赖关系的代码行没影响,但是对于对没有依赖关系的代码行执行顺序可能不同。

由于内存屏障,可以保证避免指令重排的现象产生!

Volatile会禁止Volatile上下的代码行有交换。

Volatile的禁止指令重排在单例模式(DCL懒汉式)运用得较多

单例模式

  1. 懒汉式单例

    • 需要用的时候再去加载进入内存(解决饿汉式的一开始就加载的弊端)

    • 具有双重检测锁下的懒汉式单例模式 简称: DCL懒汉式单例

    • DCL懒汉式单例在极端情况下(指令重排)是不安全的,因为在single = new SingletonPattern(); 的时候由于这不是原子型操作,可能发生指令重排,我们期望的执行顺序是(开辟空间,构造初始化对象,把这个对象指向这个空间),当出现对象未初始化就指向空间后,其他线程检测到对象不为null就会返回这个没有初始化的对象引起异常。

    • 解决方案:利用关键字volatile修饰对象,禁止指令重排

    // 懒汉式单例
    public class SingletonPattern {
        private volatile static SingletonPattern single;
    
        private SingletonPattern(){ }      // 私有的构造函数,外部不能够创建类的对象
    
    
        //  双重检测锁下的懒汉式单例模式  简称: DCL懒汉式单例
        public  static SingletonPattern  newSingle(){
            if(single == null){
                synchronized (SingletonPattern.class){
                   if (single == null)
                       single = new SingletonPattern();     // 不是原子型操作,可能发生指令重排
                }
            }
            return single;
        }
    
        public static void main(String[] args) {
    
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    newSingle();
                }).start();
            }
        }
    }
    
  2. 饿汉式单例: 类一开始就加载了一个对象private final static SingletonDemo1 SINGLE = new SingletonDemo1(); 不需要在创建对象

    • 弊端:当该类占用的内存很大,一开始就加载进来了,而这些空间又没有使用,就会造成空间的浪费
    //饿汉式单例模式
    public class SingletonDemo1 {
        private SingletonDemo1(){}
        
        private final static SingletonDemo1 SINGLE = new SingletonDemo1();
    
        public static SingletonDemo1 getSingle(){
            return  SINGLE;
        }
    }
    
  3. 反射:由于反射的存在,任何代码都不安全,任何的私有都可以访问到

    可以

    //  反射破坏懒汉式单例
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //正常的创建
        SingletonPattern single1 = single.newSingle();
        // 利用反射
        //反射获取类的构造器
        Constructor<SingletonPattern> s = SingletonPattern.class.getDeclaredConstructor(null);
        s.setAccessible(true);    //关闭处理器的安全检测,即可以访问私有属性
        SingletonPattern single2 = s.newInstance();       //  反射创建
        System.out.println(single1.hashCode());      // hashCode:1956725890
        System.out.println(single2.hashCode());      // hashCode:356573597 
    }
    

    被反射破解的单例模式:

    利用反射先破坏标志位,在创建对象

    // 懒汉式单例
    public class SingletonPattern {
        private volatile static SingletonPattern single;
        private static boolean lang = false ;
    
        private SingletonPattern(){  // 私有的构造函数,外部不能够创建类的对象
            // 防止反射破解,辣鸡,还是能破解   破解你设置的标志位
            if (lang == false){
                lang =true;
            }else {
                throw new RuntimeException("不要试图用反射破坏,我很牛×的");
    
            }
        }
    
    
        //  双重检测锁下的懒汉式单例模式  简称: DCL懒汉式单例
        public  static SingletonPattern  newSingle(){
            if(single == null){
                synchronized (SingletonPattern.class){
                   if (single == null)
                       single = new SingletonPattern();     // 不是原子型操作,可能发生指令重排
                }
            }
            return single;
        }
    
        //  反射破坏懒汉式单例
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
            //正常的创建
            SingletonPattern single1 = single.newSingle();
            // 利用反射
            //反射获取类的构造器
            Constructor<SingletonPattern> s = SingletonPattern.class.getDeclaredConstructor(null);
            //破解标志位
            Field lang = SingletonPattern.class.getDeclaredField("lang");
            lang.setAccessible(true); //关闭检测
    
            lang.set(lang,false);       // 破解 ,修改标志位
            System.out.println(lang.get(lang)); //  返回这个对象上指定字段的值
    
            s.setAccessible(true);    //关闭处理器的安全检测,即可以访问私有属性
            SingletonPattern single2 = s.newInstance();
    
            System.out.println(single1.hashCode());      // hashCode:1956725890
            System.out.println(single2.hashCode());      // hashCode:356573597
        }
    }
    

     反射不能破解枚举类:idea中编译生成的目标文件以及把编译生成的.class用javap反编译成java文件都会显示枚举类所具有的是午餐构造函数,但是在使用反射获取构造函数时会抛出这个枚举类不具有无参构造,利用jad工具把class反编译回去,得到的是一个具有有参的构造函数,而当反射获取有参构造函数创建对象的时候,会抛出不能使用反射创建对象的异常(预期的结果)。

  4. 防止单例模式被破坏的解决方案:枚举类型的单例

13.深入理解CAS

原子类的操作(AtomicInteger等),底层运用CAS

CAS: 全称:compareAndSet(int expect, int update),当对象的值等于我所期望的值(expect)时,就修改为指定的值(update)。

缺点:

  1. 底层是自旋锁,循环耗时

  2. 由于是cpu的操作,一次性只能保证一个共享变量的原子性

  3. 会出现ABA问题:两个线程操作同一个资源,都把同一个资源拷到自己的工作区域,都是自己所期望的值,而其中一个线程修改了这个值又把修改的值还原回去,另外的线程不知道这个值是被修改过的

14.原子引用(AtomicStampedReference)

AtomicStampedReference:具有时间,版本号 //AtomicStampedReference使用注意:如果引用的是泛型,需要对象的引用问题

  • 引入AtomicReference记录每次的变化,就可以解决ABA问题,对应的思想是乐观锁

    AtomicReference:有版本号

: 每次对资源有所操作时都会有记录

  • 也具有:compareAndSet(),参数:期望值,修改为多少,期望什么版本号,版本号变为多少,原子类的升级版,对每次值得变化都有记录

//   CAS 在多线程的时候会出现ABA问题,数据A被线程修改为B在修改为A
// 利用原子引用AtomicReference,记录
public class CASTest {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> i = new AtomicStampedReference<Integer>(1,1); // 参数:值 和 开始的版本号
        int stamp = i.getStamp();  //获取最初版本号
        new Thread(()->{
            //  ABA问题
            System.out.println(i.compareAndSet(1, 2, i.getStamp(), i.getStamp() + 1));
            //  还原数据
            System.out.println(i.compareAndSet(2, 1, i.getStamp(), i.getStamp() + 1));

        }).start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 用最初的版本号尝试修改
            System.out.println(i.compareAndSet(1, 3, stamp, i.getStamp() + 1));
            //
            System.out.println(i);

        }).start();
    }
}

15.各种锁

使用lock锁的时候,可以使用多把锁,但是每个锁都需要配对,

1.公平锁、非公平锁
  • 公平锁:非常公平,不允许插队,必须先来后到

  • 非公平锁:非常不公平,允许插队,(默认非公平锁)

  • Lock lock = (Lock) new ReentrantLock(true); //有参构造true,为公平锁
    
2.可重入锁
  • 拿到了外面的锁之后,就可以拿到里面的锁,自动获得
3.自旋锁

// 自定义自旋锁 利用AtomicReference原子引用CAS实现 // 不具有版本号

//CAS底层不断循环,直到占用锁的线程释放后才能退出并获取锁

//  自定义自旋锁 利用AtomicReference原子引用CAS实现  // 不具有版本号
public class LockDemo {
    AtomicReference<Thread> lock =new AtomicReference<>();   // 引用是线程默认为空
    public void myLock(){
        //获取此线程
        Thread thread = Thread.currentThread();
        //加锁
        while (!lock.compareAndSet(null,thread)){  // 如果不为空就一直循环

        }
        System.out.println(Thread.currentThread().getName()+"获取");
    }
    public void  unMyLock(){
        Thread thread = Thread.currentThread();
        lock.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"释放");
    }
    public static void main(String[] args) {
        LockDemo lock = new LockDemo();
        new Thread(()->{
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unMyLock();
            }
        },"A").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
                lock.myLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unMyLock();
            }
        },"B").start();
    }
}
4.死锁

16.死锁排查

jps-1 //查看当前进程号,运行状态下终端输入

jstack 进程号 //查询到死锁的信息,堆栈中描述死锁进程之间获取的锁和等待的锁信息

posted @ 2021-08-22 21:41  恸的池塘  阅读(61)  评论(0)    收藏  举报