多线程

多线程知识点

--多线程基础—(线程小案例)

创建线程-Thread

1.创建线程TestThread1 观察主线程和run线程体的运行结果 (顺序)

//创建线程方式1:继承Thread类,重写run()方法,调用start开启线程
//总结:注意,线程开启不一定立即执行,由CPU调度执行

public class TestThread1 extends Thread {
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("我在图书馆---"+i);
        }
    }

    public static void main(String[] args) {
        //main线程  -主线程
        //创建一个线程对象
        TestThread1 testThread1=new TestThread1();
        //调用start()方法 开启线程
        //此时这里是start方法 使主线程main和run方法线程体同时进行
        //如果把start换成run方法 则会先执行完run()再执行main主方法
        testThread1.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("我在学习多线程--"+i);
        }
        }
    }

2.创建线程TestThread2 观察T1 T2 T3 三个顺序运行但不顺序执行

(按照方法 理应先执行t1 然后是t2 t3) 但执行结果每次都不一样 所以线程的执行顺序不一样的)

此案例需要导入lib jar包 下载地址:

http://commons.apache.org/proper/commons-io/download_io.cgi

package com.gao;


import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
//练习Thread,实现多线程同步下载多个图片
public class TetsThread2 extends Thread {
    private String url;//图片地址
    private String name;//图片名称

    public TetsThread2(String url,String name){
        this.url=url;
        this.name=name;
    }
        //下载图片线程的执行体
    public void run(){
        webDownloader webDownloader=new webDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为"+name);
    }

    public static void main(String[] args) {
        TetsThread2 t1=new TetsThread2("https://uploadfiles.nowcoder.com/images/20190422/392538858_1555913364515_1DFB6E348B5F86DDF063259779AECF89","1.jpg");
        TetsThread2 t2=new TetsThread2("https://uploadfiles.nowcoder.com/images/20190422/392538858_1555913378487_493AF0131422EB9655B98CF0AB9D5244","2.jpg");
        TetsThread2 t3=new TetsThread2("https://uploadfiles.nowcoder.com/files/20200326/548380650_1585215809223_php.png","3.jpg");

        t1.start();
        t2.start();
        t3.start();
    }
//下载器
    class webDownloader{
        public  void downloader(String url,String name){
            try {
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO异常,downloader方法出错");
            }
        }
}

}

3.模拟线上抢票问题

package com.gao;
//模拟线上抢票
//发现问题:多个线程操作同一资源的情况下,线程不安全,数据紊乱
public class TestThread4 implements Runnable {
    //总票数
private int ticknumber =10;
    public void run(){
        while (true){
    if (ticknumber<=0){
        break;
    }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->拿到了"+ticknumber--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket=new TestThread4();

        new Thread(ticket,"小明").start();
        new Thread(ticket,"小王").start();
        new Thread(ticket,"小红").start();
    }
}

4.模拟龟兔赛跑案例

package com.gao;
//模拟龟兔赛跑

public class Race implements Runnable{
    //胜利者
    private static String winner;
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            //模拟兔子睡觉
            if (Thread.currentThread().getName().equals("兔子")&&i%10==0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


            //判断比赛是否结束
            boolean flag=gameover(i);
            if (flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
        }
    }
    //判断是否完成比赛
    private boolean gameover(int steps){
        //判断是否有胜利者
        if(winner!=null){
            return true;
        }{
            if (steps==100){
                winner=Thread.currentThread().getName();
                System.out.println("winner is "+winner);
                return true;
            }
        }

        return false;
    }

    public static void main(String[] args) {
        Race race=new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}


静态模式代理(结婚案例)

package com.gao.demo2;

//静态代理模式总结:
//真实对象和代理对象都要实现同一个接口
//代理对象需要代理真实角色
//好处:代理对象可以做很多真实对象做不了的事情
        //真实对象专注做自己的事情
public class StacticProxy {
    public static void main(String[] args) {
        new Thread(()-> System.out.println("I love You ")).start();

        new weddingCompany(new you()).HappyMarry();

        you you=new you();//你要结婚
        weddingCompany weddingCompany=new weddingCompany(new you());
        weddingCompany.HappyMarry();

    }
}

interface Marry{

    void HappyMarry();
}

//真实角色
class you implements Marry{

    @Override
    public void HappyMarry() {
        System.out.println("Get married so Happy");
    }
}
//代理角色 帮你结婚
class  weddingCompany implements Marry{
    //代理-->真实目标角色
    private Marry target;
    public weddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();//这就是真实对象
        after();
    }

    private void after() {
        System.out.println("结婚之后,收尾款");
    }

    private void before() {
        System.out.println("结婚之前 布置现场");
    }
}

lamda 表达式

1.推导lamda表达式(从静态内部类 局部内部类 匿名内部类 简化到lamda表达式)

package com.gao.lamda;
/*
推导lamda表达式
 */
public class Testlamda1 {
    //3.静态内部类
    static  class Like2 implements Ilike{

        @Override
        public void lambda() {
            System.out.println("I like lamda2");
        }
    }

    public static void main(String[] args) {
        Ilike like=new like();
        like.lambda();

        like=new Like2();
        like.lambda();

        //4.局部内部类
        class Like3 implements Ilike{

            @Override
            public void lambda() {
                System.out.println(" i like lamda3");
            }
        }
        like=new Like3();
        like.lambda();

    //5.匿名内部类,没有类的名称,必须借助接口或父类
        like=new Ilike() {
            @Override
            public void lambda() {
                System.out.println("i like lamda4");
            }
        };
        like.lambda();

        //6.用lamda简化
        like=()->{
            System.out.println("i like lamda 5");
        };
        like.lambda();

    }

}

//1.定义一个函数接口
interface  Ilike{
    void  lambda();
}

//2.定义实现类
class like implements  Ilike{

    @Override
    public void lambda() {
        System.out.println("I like lamda");
    }
}
  1. lamda表达式的从复杂到最简的简化过程

    总结:/总结 //lamda表达式 只能有一行代码的情况才能简化成一行, //如果多行只能用代码块包裹 //前提还得接口必须是函数式接口(接口内只有一个方法)(此案例 接口Ilove内只有一个方法love)

    package com.gao.lamda;
    
    public class Testlamda2 {
    
        public static void main(String[] args) {
    
                //1.lamda 表示简化
               Ilove love=(int a) ->{
                   System.out.println(" i love you " +a);
               };
    
               //简化1.去掉了参数类型
            love=(a)-> {
                System.out.println(" i love you " + a);
            };
    
            //简化2.简化括号
            love=a-> {
                System.out.println(" i love you " + a);
            };
    
            //简化3.简化花括号
            love=a -> System.out.println("i love you -"+a);
    
    
                //总结
                    //lamda表达式 只能有一行代码的情况才能简化成一行,
                    //如果多行只能用代码块包裹
                    //前提还得接口必须是函数式接口(接口内只有一个方法)(此案例 接口Ilove内只有一个方法love)
    
    
    
               love.love(520);
           }
        }
    
    interface Ilove{
        void love(int a);
    }
    
    

--多线程和高并发高级

第一节 线程的基本概念

三种线程方法
  1. sleep ,意思就是睡眠 ,当前线程暂停一段时间让给别的线程去运行,sleep是由睡眠时间而定,等规定的时间到了自动复活
  2. Yield 就是当前线程正在执行的时候停止下来进入等待队列 ,回到等待队列里在系统的调度算法里呢 还是依然可能把你刚回去的这个线程拿回来继续执行,意思就是我让出来一下CPU,后面调度能不能抢到就不管了。
  3. join 意思就是在自己当前线程加入你调用的线程(),本线程等待。等调用的线程执行完 自己再去执行。例如:ti和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2上去运行 ,t1等t2运行完后,t1和t2并成为一条线再去运行(自己 jion 自己 没意义)
线程状态

常见的线程状态有六种:

当我们new一个线程时,还没调用start()该线程 处于 新建状态

线程调用start()方法时 它会被线程调度器执行,也就是交给操作系统执行,这整个状态就叫Runnable 内部有(1)Ready就绪状态/(2)Running运行状态

如果线程顺利执行完了就会进入(3)Teminated结束状态,

在Runnable 这个人状态里还有一些状态的变迁 (4)TimedWaiting等待、(5)Waiting等待、(6)Blocked阻塞。

synchronized锁
  • synchronized锁的是对象 并不是锁的代码 这一点一定要搞清楚🔺

  • synchronized(Object)

    • -不能用String常量 Integer Long这些数据类型
  • 在一个方法上不加任何参数的话 锁的是this

  • 锁定方法和非锁定方法 同时进行

为什么要加🔒?

比如:我们对一个数字递增,两个线程一块对它递增,如果两个线程同时访问的时候,第一个线程一读它是0,然后把它+1,在自己的线程内部内存里面算 还没写回去的时候此时第二个线程读到了它还是0,再加1写回去,本来是加两次但结果还是1.那我们就可以给这个递增的过程上把🔒,就是说第一个线程对这个线程访问的时候它是独占的,不允许别的线程访问,必须等我对他+1完成返回之后才能读。

synchronized 可重入性 (是必须了解的一个概念)

模拟一个父类子类的概念,父类P 里面有个方法m是synchronized 子类C 里面用super.m调用 如果是不可重入那父子间的这个继承就死锁了

synchronized的底层实现:
  • 早期,jdk早期的时候 这个synchronized的底层是重量级的,重到这个synchronized要去操作系统去申请锁的底部,这就会造成synchronized的效率非常低。

  • 改进 ,后俩有了锁升级这个概念 马哥 的文章《我就是厕所所长》

    1. https://www.jianshu.com/p/b43b7bf5e052

    2. https://www.jianshu.com/p/16c8b3707436

      马哥微信公众号小说https://mp.weixin.qq.com/s/Fep24OWHeck5O-sgILi39Q

  • 执行时间短,线程数少,用自旋

  • 执行时间长,线程数多 用系统锁

第二节 volatile与CAS

volatile

作用:(面试常考 必须记住)

1.保障线程可见性

线程运行过程如上图所示 先访问 然后吧这个值copy一份到自己的工作空间,然后再去对这个值进行改变,但检查这个方法有无新值不容易控制,加入了volatile在之后保证了线程的可见性 当main主线程对m方法值进行更改之后 t1就会马上看到

2.禁止指令重排序

指令重排序是和cpu有关系的 每次写都会被线程读到,加了volatile之后,cpu原来执行一体哦啊指令的时候是一步步执行,但是现在cpu为了提高效率,它会把指令并发的来执行,第一个指令执行得到一半的时候第二个就可能已经开始执行了,这叫流水线式执行。

这是new一个方法的过程图

如果没有volatile cpu可能就会对1 2 3 步重排序 可能在杠申请内存给a赋初始值0 的时候就把a调走了

所以给它加了volatile之后就不会出现指令重排序的可能。

CAS(无锁优化 自旋)

假如我原先想改变某一个值0 ,我想把它变成1,但是其中我想做到线程安全,就只能加锁 synchronized,不然线程就不安全。我们现在可以用另一个操作来代替这把锁,就是cas操作,可以把它想象成一个方法,这个方法有三个参数,cas(V,Expected,New Value)。

V第一个参数就是要改的那个值;Expected第二个参数是期望当前的这个值会是及;NewValue是要设定的新值。比如原来这个值变成3了,我这个线程想改这个值的时候我一定期望你现在是3,是3我才改的,如果你在我改的过程变成了4了,那你跟我的期望值就对不上了,说明有另一个线程改了这个值,那我这个CAS就重新再试一下,再试的时候我希望这个值是4,在修改的时候期望值就是4,没有其他的线程修改这个值,那好,我概念改成5,这就是CAS操作。

ABA问题(面试)

假如是你有一个值,我拿到这个值是1,想把它变成2 ,我拿到1用cas操作,期望值是1,准备变成2,这个对象Object。早这个过程种,没有一个线程改过的话我肯定是可以更改的,但是如果有一个线程把这个1变成2后来又变成了1.中间值发生过更改,它不会影响我这个cas下面的操作,这就是ABA问题。

怎么解决:

如果是int类型的,最终值是你所期望值,没有关系,这种没有关系可以不去管这个问题;如果确实想管这个问题 可以加版本号,做任何一个值的修改,修改完之后加1,后面检查的时候连带版本号一块都检查。

总结:如果是基础类型,无所谓,不会影响结果值;

​ 如果是引用类型:就像你的女朋友和你分手后又复合,中间经历了其他的男人;(通俗易懂)

第三节 Atomic类和线程同步新机制

CountDownLatch

latch意思是门栓 ,这个锁的意思就是给线程加了给门栓 (倒数的一个门栓5,4,3,2,1,计数完了,门栓打开程序继续往下执行。

CycicBarrier

CycicBarrier是一个同步工具 意思是循环栅栏,假如两个线程 一个定义了100个参数线程 在主线程调用的时候我给它加上这么一个栅栏 满20人 栅栏就倒了 到了就把数据放出去 然后栅栏再立起来 以此类推 知道100个参数全出去。就像坐车一样 100个人坐车 但车只有20座位 上来20 线程一看满20了 好 发车 然后再来 再满20 好 发车,这样运行五次就100个人全走了。

Phaser

Phaser就是一个分阶段的栅栏 更像是结合了CountDownLatch和CycicBarrier

ReadWriteLock(重点)

意思很明显 字面意思 读 写 就是叫读写锁

读写锁的概念其实就是共享锁和排他锁,读锁就是共享锁,写锁就是排他锁。

就好比学校的组织结构 有多个人同时访问学校网址这个组织结构 有读的 有写的 加入A同学想往里面写个名字 叫王八摔 但是刚写到王八 数据就被B同学读走了 所以为了避免这种情况我们就要加锁

这种锁new ReentranReadWriteLocl()是ReadWriteLock的一种实现,在这给实现里我们又分出两把锁来 一个是readLock,另一个是WriteLock,通过它的方法readWriteLock.readLock()来拿到readLok对象,读锁我们就拿到了。通过readWriteLock.writeLock()拿到write对象。这两把锁在我读的时候扔进去,因此,读线程是可以一起读的,也就是说所有的读线程可以一秒完成。

总结:加了这个读写锁意思就是允许其他读线程一起读 ,写线程来了我不给它写,你先别写,等我读完了你再写。读线程读的时候我们一起读 ,因为读是不改变线程原来的内容,写线程上来就把整个线程全锁定,你先不要读,等我写完你在读。

Semaphore

简单含义就是限流

Semaphore s = new Semaphore() 是来控制几个线程同时执行的 售票处 你不管再多的人来买票 我售票窗口就三个 只能允许三个线程同时运行。

acquire();这个方法是阻塞方法 acquire的意思就是得到 如果Semaphore s = new Semaphore(5)写的是5 那我acquire一下之后它就得减一变成4 后面的线程此时如果同时执行 访问到的就是四 以此类推 直到变成0 就是五个线程同时运行了 别的线程就等着等到下一批五个 五个 。。。

线程结束的时候必须要加个 release();方法 因为我刚acquire完 减一 如果线程完成后不加回来 那么后面线程就没办法运行了 ,所以必须要加release 还原化。

Semaphore s = new Semaphore(1,true)第二个值穿true是设置公平锁,但这基本用不到 因为用到这个就是能控制线程的先来后到 这个不容易控制 所以说这个公平锁意义不大。

Exchanger

这个就是扩展知识面用的 简单说就是一个交换器 就是把两个线程里面的数据交换一下

这个使用场景比较多的应该就是游戏里面双方互换装备 。

第四节 LockSupport和淘宝面试题与源码阅读方法论

LockSupport
package com.lock;

import java.util.concurrent.TimeUnit;
public class LockSupport {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            for (int i = 0; i <10 ; i++) {

                System.out.println(i);
                //到这很简单的一个线程
                //循环十次  每次秒输出一个数 一直到9
                //接下来给这个线程加上一个LockSupport锁
               if (i==5){
                   //使用park()方法阻塞当前线程t 此时运行结果到五就停了
                   java.util.concurrent.locks.LockSupport.park();
               }
               //实验 unpark可以两次吗? 答案是可以
                if (i==7){
                    //使用park()方法阻塞当前线程t 此时运行结果到7就停了
                    java.util.concurrent.locks.LockSupport.park();
                }

                try {
                    //加上一个停顿  每一秒停顿
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //执行线程
        t.start();
        //执行unpark  此时这个unpark和start是在主线程同时执行的
        java.util.concurrent.locks.LockSupport.unpark(t);
        java.util.concurrent.locks.LockSupport.unpark(t);


        //这里做了个小拓展  让这个阻塞停止八秒后再唤醒
        try {
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after 8 senconds");
        java.util.concurrent.locks.LockSupport.unpark(t);
    }
}

这是个LockSupport的小案例 LockSupport是通过park()方法在某个节点上阻塞 和使用unpark再在主线程上唤醒,这个唤醒方法是和start方法在主线程同时执行的。

总结:

1.LockSupport不需要synchronized加锁就可以实现线程内阻塞和唤醒

2.LockSupport.unpark();可以先与LockSupport.park();执行,并且线程不会阻塞。

3.如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒。

LockSupport中park()和unpark()方法的实现原理:

这两个方法的实现是由Unsefa类提供的,而Unsefa类是由C和C++语言完成的,原理是:他主要通过一个变量作为一个标识,变量在0和1之间来个i切换,当这个变量大于0的时候线程就获得了“令牌”,从这一点我们不难知道,其实park()和unpark()就是在改变这个变量的值,来达到线程的阻塞和唤醒的。

小案例(消费者生产者)
package com.mianshi;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class  MyContainer1<T>{
    final private LinkedList<T> lists=new LinkedList<T>();
    final private int max=10;
    private int count=0;
    //生产者
    public synchronized void put(T t){
        //
        while (lists.size()==max){
                try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        lists.add(t);
        ++count;
        this.notifyAll();//通知消费者线程进行消费
    }
        //消费者
        public synchronized T get(){
        T t=null;
        while (lists.size()==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        t=lists.removeFirst();
        count--;
        this.notifyAll();//通知生产者进行生产
            return t;
        }

    public static void main(String[] args) {
        MyContainer1<String> c=new MyContainer1<>();
        //启动消费者线程
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                for (int j=0;j<5;j++) System.out.println(c.get());
            },"c"+i).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //启动生产者线程
        for (int i = 0; i <2 ; i++) {
            new Thread(()->{
                for (int j=0;j<25;j++)c.put(Thread.currentThread().getName()+" "+j);
            },"p"+i).start();
        }
    }

}

源码阅读技巧和AQS源码结构解析

第五节 AQS源码阅读与强软弱虚及ThreadLocal原理

第六节 并发容器

From Vector To Queue
1.原始的ArrayList
/**
 * 有N张火车票  每张票都有一个编号
 * 同时有10个窗口对外售票
 *
 */
package com.bingfarongqi;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

public class TicketSeller1 {
        //定义一个容器 tickets 总票数
            static List<String> tickets=new ArrayList<>();
            static {
                //总共1000张票 定义每张票的编号
                for (int i = 0; i <1000 ; i++) tickets.add("票编号"+i);
            }

    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
            while (tickets.size()>0){
                try {
                    TimeUnit.MICROSECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //把容器内第0个位置的票remove
                System.out.println("销售了--"+tickets.remove(0));
            }
            }).start();
        }
    }
}


这种最原始的集合容器 一眼可以看出 没有加锁 问题就在这 10个线程同时运行没有加锁肯定会出问题

size大于0 的时候没有问题 因为只要还有剩余的票我就往外卖,取一张往外remove

但是当剩下最后一张票的时候问题就出现了 好几个线程执行到这都发现size大于0 所有线程都往外卖一张票,那么就会发生只有一个线程拿到了票,其余线程就会输出空null,所以没有加锁 线程不安全。

2.Vector
/**
 * 有N张火车票  每张票都有一个编号
 * 同时有10个窗口对外售票
 *
 */
package com.bingfarongqi;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

public class TicketSeller2 {
    //定义一个容器 tickets 总票数
    static Vector<String> tickets=new Vector<>();
    static {
        //总共1000张票 定义每张票的编号
        for (int i = 0; i <1000 ; i++) tickets.add("票编号"+i);
    }

    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                while (tickets.size()>0){
                    try {
                        TimeUnit.MICROSECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //把容器内第0个位置的票remove
                    System.out.println("销售了--"+tickets.remove(0));
                }
            }).start();
        }
    }
}


第二种是容器Vetor 是自带内部锁

读它的内部方法时会看到synchronized

虽然Vetor上来先加了锁保证了线程的安全性 但是当我们读这个程序的时候发现还是不对,锁为了线程的安全,就是当我们调用size方法的时候他加锁了,调用了remove的时候也加锁了,可不幸的是你这两个中间没加 那好多个线程依然断定这个size是大于0 的 又出现了超卖的现象。

3.Linkenlist

/**
 * 有N张火车票  每张票都有一个编号
 * 同时有10个窗口对外售票
 *
 */
package com.bingfarongqi;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

public class TicketSeller3 {
    //定义一个容器 tickets 总票数
    static List<String> tickets=new LinkedList<>();
    static {
        //总共1000张票 定义每张票的编号
        for (int i = 0; i <1000 ; i++) tickets.add("票编号"+i);
    }

    public static void main(String[] args) {
        for(int i = 0; i <10 ; i++){
            new Thread(()->{
                while (true){
                    //要在外层单独加一把锁 不然还是出问题
                    synchronized (tickets){
                        if (tickets.size()<=0) break;

                    try {
                        TimeUnit.MICROSECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //把容器内第0个位置的票remove
                    System.out.println("销售了--"+tickets.remove(0));
                }
                }
            }).start();
        }
    }
}


这个容器虽然是用了加锁 但由于子啊调用并发容器的时候你是调用了两个原子方法,所以你在外层还是得再加上一把锁 这个不会出现什么问题 它会踏踏实实的执行 但是效率不是最高的。

4.Queue
/**
 * 有N张火车票  每张票都有一个编号
 * 同时有10个窗口对外售票
 *
 */
package com.bingfarongqi;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

public class TicketSeller4 {
    //定义一个容器 tickets 总票数
    static Queue<String> tickets=new ConcurrentLinkedQueue<>();
    static {
        //总共1000张票 定义每张票的编号
        for (int i = 0; i <1000 ; i++) tickets.add("票编号"+i);
    }

    public static void main(String[] args) {
        for(int i = 0; i <10 ; i++){
            new Thread(()->{
                while (true){
                    String s=tickets.poll();
                    if (s==null) break;
                        //把容器内第0个位置的票remove
                        else System.out.println("销售了--"+s);
                    }
            }).start();
        }
    }
}


这个容器是最新的一个接口 主要是为了多线程用的 所以以后多线程这种单个元素的时候多考虑Queue 少用List呵呵Set

这里调用了个poll方法 poll的意思就是我从ticket去取值,这个值什么时候空了说明里面的值已经没了,所以while(true)不断往外销售,一直等到票没了

它的源码 :

意思是说从queue的头部取值 等取完了 也就是值为null的时候我就返回null值。

ConcurrentMAP

这里会出现一个跳表结构

CopyOnWrite

CopyOnWrite的意思就是写时复制

主要是在写等待时候很少 读的时候很多的情况 这个时候就可以考虑用CopyOnWrite方式来提高效率了。

Vector是在写的时候加锁 读的时候也加锁,但是CopyOnWriteList用的时候读的时候不加锁 写的时候会在原来的基础上拷贝一个,拷贝的时候扩展出来一个新元素来,然后把你添加的这个扔到最后的位置,与此同时把指向老的容器的一个引用指向新的,这就叫写时复制。

BlockingQueue

BlockingQueue的概念重点在Blocking上 Blocking阻塞 Queue队列,是阻塞队列。

这个Queue提供了一些可以给多线程比较友好的接口 :

1.offer 对应的就是原来的add 与add的区别就在于 add如果加不进去是会抛出异常 ,所以我们用的最多的是Queue里面的offer 它会概念一个返回值。

2.poll 用来取数据 取出数据并且remove

3.peek 拿出来数据 取出数据而不remove

package com.bingfarongqi.f_03;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class BlockingQueue {
    public static void main(String[] args) {
        Queue<String> strs=new ConcurrentLinkedQueue<>();
        for (int i = 0; i <10 ; i++) {
            strs.offer("a"+i);//add
        }
        //10个数据
        System.out.println(strs);
        //size是10
        System.out.println(strs.size());
            //取出a0 并且remove掉 所以size还剩9
        System.out.println(strs.poll());
        System.out.println(strs.size());
        //因为刚才poll取出a0并且remove 所以peek取出的只能是a1 并且size还是9
        //因为peek不remove 所以size 9 不变
        System.out.println(strs.peek());
        System.out.println(strs.size());
    }
}


LinkedBlockingQueue

LinkedBlockingQueue是在Queue的基础上又添加了两个方法 一个叫put 一个叫take

这两个方法真正的实现了阻塞,put往里面装 如果满了的话就会让这个线程阻塞住,take往外取,如果空了的话就会阻塞住。 所以这是真正的实现了 生产者和消费者的容器。

ArrayBlockingQueue(扩展-面试常问问题)

ArrayBlockingQueue是有界的 可以界定容器的大小。

当往容器里面仍数据的时候 一旦满了 ,这个put方法就会阻塞住,offer用返回值来判断到底加没加成功。

扩展- 面试常问问题:Queue和list的区别到底在哪里?

主要是在这,添加里一些对线程友好的API :offer peek poll put take

package com.bingfarongqi.f_03;

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

public class ArrayBlockingQueue_test {
    static BlockingQueue<String> strs=new ArrayBlockingQueue<>(10);
    static Random r=new Random();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i <10 ; i++) {
            strs.put("a"+i);
        }
        //strs.put("aaa");//满了就会等待,程序阻塞(无返回值)
        //strs.add("aaa");//满了就会报出异常
        strs.offer("aaa");//满了就会返回值判定成功
        strs.offer("aaa",1, TimeUnit.SECONDS);

        System.out.println(strs);
    }
}


DelayQueue(扩展PriorityQueue)

DelayQueue可以实现时间上的排序,主要是按照在里面等待的时间进行排序。

就看站着线程 正常情况下按照顺序 t1 >2>3>4>5

但是这里看的是后面指定的时间 t1是1秒 t2是2秒 t3是1.5 t4是2.5 t5是0.5

所以根据这个时间 运行顺序就是t5>1>3>2>4

这个主要是用在调度时间的案例 ,比如 谁谁谁一个小时后干什么 三小时后干什么.....

扩展--:

DelayQueue的本质上用的是一个PriorityQueue,PriorityQueue是从AbstractQueue继承的。

PriorityQueue的特点是它内部你往里装的时候并不是按顺序,而是内部进行了一个排序,按照优先级,最小的优先。它内部的结构其实是一个二叉树 .

package com.bingfarongqi.f_03;

import java.util.PriorityQueue;

public class PriorityQueue_test {
    public static void main(String[] args) {
        PriorityQueue<String> q=new PriorityQueue<>();

        q.add("c");
        q.add("e");
        q.add("a");
        q.add("d");
        q.add("z");
        for (int i = 0; i <5 ; i++) {
            System.out.println(q.poll());
            //输出结果是 a c d e z
        }
    }
}


这个例子就很明显的展现胡了二叉树 优先级的问题 最小的在上面 。

SynchronusQueue

这个容器的容量是0 它不是用来装东西的,而是专门用来两个线程传内容的,第三节的最好有一个容器叫Exchanger 就是那个两个人交换装备的,他们两个本质上概念是一样的。

package com.bingfarongqi.f_03;

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

public class SynchronusQueue_test {
    //容量为0
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> strs=new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
            strs.put("aaa");//阻塞等待消费者
            //strs.put("bbb");
            //strs.add("aaa");

        System.out.println(strs.size());
    }
}


SynchronusQueue在线程池的作用特别大 ,很多线程取任务,互相之间进行任务调度的时候都用它。

TransferQueue

TransferQueue添加了一个新方法 叫transfer 这个方法和put相似 但是和put唯一的不同是put是把数据放进来就走 然后接着放 知道满了 然后等待,transfer是撞进来一个数据就在这等着 ,直到消费者把这个数据拿走了我才回去再做我的事。

package com.bingfarongqi.f_03;

import java.util.concurrent.LinkedTransferQueue;

public class TransferQueue_test {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs=new LinkedTransferQueue<>();

        new Thread(()->{
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        strs.transfer("aaa");
//        strs.put("aaa");
//        new Thread(()->{
//            try {
//                System.out.println(strs.take());
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }).start();
    }
}


一般使用场景:我付了钱,这个订单我付款完成了,但是我一直要等这个付款的结果完成才可以给客户反馈。

第七节 线程池

华为面试题

先看一道华为面试题 要求第一个线程从1-26,第二个线程从a-z 然后两个线程同时执行,交替输出。

我们先用LockSupport ,启了两个线程 t1 t2 先让t1输出一个(1) 然后叫醒t2 此时t2第一句就是让自己阻塞 防止和t1第一句打印混乱,t1叫醒t2之后 t2紧接着打印 这里也就是A 最后叫醒t1 就这样来来回回阻塞叫醒 26次 1-26就打印完了。

package com.mianshi;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

public class LockSupport_A_z1_26test1 {
    static Thread t1=null,t2=null;
    public static void main(String[] args) throws Exception {
//        char[] aI="1234567".toCharArray();
//        char[] aC="ABCDEFG.toCharArray();
        List aI= Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9,10);
        List aC = Arrays.asList('A', 'B', 'C', 'D', 'E', 'F', 'G','H','I','J');

        t1=new Thread(()->{
            for (Object c:aI){
                System.out.println(c);
                LockSupport.unpark(t2);//叫醒t2
                LockSupport.park();//t1 自己阻塞
            }
        },"t1");
        t2=new Thread(()->{
            for (Object c:aC){
                LockSupport.park();//t2上来先自己阻塞
                System.out.println(c);
                LockSupport.unpark(t1);//叫醒T1
            }
        },"t2");

        t1.start();
        t2.start();
    }
}


高并发理论-线程池
Callable
package com.xianchengchi;

import java.sql.CallableStatement;
import java.util.concurrent.*;

public class Callable_test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> c= new Callable() {
            @Override
            public Object call() throws Exception {
                return "Hello Callable";
            }
        };
        ExecutorService service= Executors.newCachedThreadPool();
        Future<String> future=service.submit(c);//异步

        System.out.println(future.get());
        service.shutdown();
    }
}


这里我们扩展两个概念 同步 和异步

同步:把任务交给线程池之后等待线程池执行完之后返回值主线程才能继续

异步:把任务交给线程池之后主线程该干啥干啥 不受影响

线程池的七个参数

1.corePoolSize核心线程数:最开始的时候有这个线程里面是有一定的核心线程数的。

2.maxximumPoolSize最大线程数:线程数不够了,能扩展到啊最大线程数是多少。

3.KeepAliveTime生存时间:意思是这个线程有很长时间没干活了请你把它归还给操作系统。

4.TimeUnit.SECONDS生存时间的单位:毫秒还是纳秒还是秒自己去定义。

5.任务队列:就是上节课讲的BlockingQueue,各种各样的BlockingQueue你读可以往里面扔,我们这用的是ArrayBlockingQueue,参数最多可以装四个任务。

6.线程工厂:defaultThreadFactory

7.拒绝策略:指的是线程池忙,而且任务队列满的时候我们要执行各种拒绝策略,jdk默认提供了4种拒绝策略,也可以自己定义。

  • Abort:抛异常
  • Discard:扔到;不抛出异常
  • DiscardOldest:扔掉排队时间最长的
  • CallerRuns:调用者处理结果

--p121下的代码未写🔺

第八节 线程池与源码阅读

xxxxx

第九节 JMH与Disruptor

JMH

官网:http://openjdk.java.net/projects/code-tools/jmh

创建JMH测试

1.注入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <properties>
        <project.build.sourceUEncoding>UTF-8</project.build.sourceUEncoding>
        <encoding>UTF-8</encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <groupId>org.example</groupId>
    <artifactId>Test_JMH</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.21</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.21</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

2.IDEA安装插件 JMH plugin

file>Settings>Plugins>JMH plugin

3。注解配置

compiler->Annotation Procesors->Enable Annotation Procession

4.定义测试类PS(ParallelStream)

写了一个类,并行处理流的程序,定义了一个list集合,往这个集合里仍了1000个数。第一个方法是用foreach来判断我们这1000个线程里到底有谁是质数,第二个是使用并行处理流的方法来把集合的数分成若干份 同时判断的。

package com.gaozhenhua;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class PS {
    static List<Integer> nums=new ArrayList<>();
    static {
        Random r=new Random();
        //往集合里面仍了1000个数
        for (int i = 0; i <10000 ; i++) nums.add(1000000+r.nextInt(1000000));
    }
        static void foreach(){
        //使用foreach判断是否是质数  (这里调用的是prime方法)
        nums.forEach(v->isPrime(v));
        }

        static void paraller(){
        //使用并行处理流 来判断质数
            //把这个集合的数分成若干份 同时处理
        nums.parallelStream().forEach(PS::isPrime);
        }

        //写了一个方法 判断是否是质数
        static boolean isPrime(int num){
            for (int i = 2; i <num/2 ; i++) {
                if (num%i==0) return false;
            }
            return true;
        }
}


5。写单元测试(PSTest)

注意:这个测试类一定要再test package下面

我们对这个方法进行测试testforeach,很简单我就调用PS这个类的foreach就行了,对他的厕所最关键的是我加了这个注解@Benchmark,这个JMH的注解,是要被JMH解析处理的

package com.gaozhenhua;

import org.openjdk.jmh.annotations.*;

public class PSTest {
    @Benchmark
        @Warmup(iterations=1,time = 3)//在专业测试里面首先要进行预热,预热次数和预热时间
    @Fork(5)//意思是用多少个线程去执行我们的程序
    @BenchmarkMode(Mode.Throughput)//是对基准测试的一个模式,这个模式用的最多的是Throughput吞吐量
    @Measurement(iterations = 1,time = 3)//这个是整个测试要测试多少遍,调用这个方法要调用多少次

    public void testForEach(){
        PS.foreach();
    }
}


6.运行测试类

运行之后会遇到下面的错误

这个错误是因为JMH运行需要访问系统的TMP目录 解决方法:

7.最后 阅读测试报告

总结 :JMH中的基本概念

  • Warmup:预热,由于JVM中对于特定代码会存在优化(本地化),预热对于测试结果很重要
  • Mesurement:总共执行多少次测试
  • Timeout
  • Threads:线程数,有fork指定
  • Benchmark mode:最基准的模式
  • Benchmark :测试哪一段代码
Disruptor

关于Disruptor介绍的资料:

马士兵gethub主页:https://github.com/bjmashibing/TestJMH

源码:https://github.com/LMAX-Exchange/disruptor

Getting-Started:https://github.com/LMAX-Exchange/disruptor/wiki/Getting-Started

maven:https://mvnrepository.com/artifact/com.lmax/disruptor

Disruptor叫无锁、高并发、环形Buffer,直接覆盖旧的数据(不用清除),降低GC频率,用于生产者和消费者模式。

Disruptor的核心和特点

Disruptor也是一个队列,和其他队列不同的是它是一个环形队列。

posted @ 2020-10-07 10:44  北斋  阅读(112)  评论(0)    收藏  举报