多线程の小入门

那一天,CPU们终于想起来被压榨的恐惧

前言

哈哈,上述只是玩笑话,这篇blog只是为了帮助大家(主要也能帮助自己)快速入门多线程相关的知识

当然,这只是快速入门,大概会有缺缺少少的地方,如有错误,还请大家海涵并指出错误!

线程的创建

你要想玩多线程,你肯定要创建线程吧!

1、继承Thread类

  1. 创建一个类并继承Thread类
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("我是MyThread类所有线程的一个模板!")
    }
}
  1. 在你的主线程中new一个MyThread线程
public class Main{
    public static void main(String args[]){
        //主线程要执行的代码
        System.out.println("我是主线程");
        
        //利用MyThread类创建出了一个线程(用这个模板刻出来了一个)
        MyThread myThread01 = new MyThread();

        //利用MyThread类又创建出了一个线程
        MyThread myThread02 = new MyThread();

        //分别开启两个线程
        myThread01.start();
        myThread02.start();
    }
}

上面这就是创建多线程的一种方法

那么咱来分析一个问题:你说,这一串代码怎么执行捏

你一听:嗨,你介不把我当傻berber吗,我打第一天记事儿起就听说啦啊,代码肯定是从上往下执行的啊


解释:

是啊,上面说的确实没有问题,代码是从上往下执行的,可是这有个前提:只有一个前台线程。也就是说,这个结论对于多线程,不!适!用!因为嘛呢,你如果只有一个main方法(线程),那么CPU还考虑嘛考虑,就你小子了,从上往下执行吧。但是呢,你现在又看热闹不嫌事大引入了两个线程myThread01和myThread02,介下CPU就得好好琢磨琢磨,先来哪个好呢,所以这个时候,你的三个线程执行的控制大权全在CPU手里,它看哪个耐人儿就先来哪个,你也控制不了哪个先执行

这时候,聪明绝顶的小亮就又发问了:这么说,只要CPU决定好了先执行MyThread02,再执行main,最后执行MyThread01,代码就按照这个顺序执行了吗


嘿,还得是小亮聪明,但是这时候练习JAVA时长30个月的肖黑紫吐掉了嘴里的荔枝,说道:

你以为CPU一心一意啊,CPU就好像是那古代的皇上,前一秒还搂着2号妃子,还没搂完呢,直接把2号推到一遍去,开始抱着1号妃子,抱了一小会,还是没抱完,这时候又开始搂着2号妃子。CPU也是如此,执行一会a线程,可能还没完成,就停下a线程去执行b线程…

实现Runnable接口

  1. 定义一个类实现Runnable接口并重写run方法
public class MyThread implements Runnable{

    @Override
    public void run(){
        System.out.println("我是通过Runnable接口创建的线程内核!")
    }
}
  1. 在你的主线程中new一个MyThread线程
~~~java
public class Main{
    public static void main(String args[]){
        //主线程要执行的代码
        System.out.println("我是主线程");
        
        //利用MyThread类创建出了一个线程内核
        Runnable runnable = new MyThread();

        //利用Thread类创建出来一个线程执行容器,然后把线程内核扔进去
        Thread myThread01 = new Thread(runnable);
        
        // //利用Thread类再创建出来一个线程执行容器,然后把线程内核扔进去
        Thread myThread02 = new Thread(runnable);

        //分别开启两个线程
        myThread01.start();
        myThread02.start();
    }
}

理解:

这个创建线程方式和第一种为何有所不同呢,为了便于理解,我把这个现象解释成这样:Thread类所实例化出来的对象都是可以执行线程的容器,容器是需要把线程内核放进去才能执行线程的。而继承了Thread的类恰恰获得了可以执行线程的容器,而自己重写的run方法恰好使这个容器获得了线程内核,结果就是继承了Thread的类所实例化出来的对象直接是一个可执行的线程

而第二种实现Runnable接口的类,因其本身没有继承Thread类,故无法获得执行线程的容器,只能去编写线程内核,然后在自己去new一个Thread对象(线程容器),再把内核放进去,这样才能执行。


既然提到了线程容器和线程内核这两个我抽象理解出来的概念,那我们就顺势可以引出一个更方便的创建线程的方式

使用Lamda表达式编写线程内核

这种方法十分简便,我们直接先上代码

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

        //创造一个线程容器,然后直接用Lamda为它赋予线程内核:()->{执行的代码;}
        Thread myThread = new Thread(()->{
            System.out.println("我是一个线程内核,待myThread线程start的时候,我将会被CPU考虑执行");
        })
    }
}

“()->{执行的代码;}”,这就是线程内核

实现Callable接口

不常用,读者如感兴趣请见谅并自行百度,我不打扰,我走了哈

线程的状态


个人理解:

  • 新建状态:Thread t = new Thread(线程内核)是新建状态,这个不难理解
  • 就绪状态:t.start()不是代表它立刻就开始执行,而是把这个t线程同主线程一起交给CPU,如同我们上面说的,让CPU去随意调度它俩,但是如果你想让最大概率的去执行某个线程,那么你就调用getPriority()先看看每个线程的权重,然后再用setPriority(int n)设置每个线程的权重,这样CPU有很大概率先执行(非100%)
  • 运行状态:当CPU决定执行t线程的时候,t就进入运行状态。当CPU还想继续执行t线程的代码的时候却执行到了t.yield(),CPU会尝试暂停执行t,让t回到就绪状态,转而去执行主线程,但是CPU可能很任性,虽然t.yield(),但是它偏要继续执行,那就没办法了
  • 阻塞状态(指别的线程):在存在锁的情况下(可以先跳过线程的状态,后面会写锁),如果处于运行状态的t线程使用了sleep(1000),那么CPU在此时就应该停下来等1s再运行,在这1s其它线程也没法执行,这便会形成阻塞状态。
    当t线程使用了join()方法,CPU就会一条道走到黑,偏要执行t,执行完t再去执行别的线程,此时也会形成阻塞状态(阻塞其它线程的运行)
  • 阻塞等待状态(指自己):当处于执行状态的t线程执行wait()方法的时候,t线程会立刻释放掉所占有的CPU的资源并丢掉自己的锁,如果调用的是没有参数的wait()方法,那么就会一直处于等待状态,不去抢锁,不去抢占CPU,直接摆烂,如果是有参数的wait(3000),那么就是沉睡3s后再去争夺CPU资源和抢锁
  • 阻塞锁定状态:这种情况下哥们不是开摆了,而是爷们要战斗,这个状态下的t线程虽然被阻塞了,但是它要去勇于争夺CPU资源和锁,比如,引入了锁机制后,a线程在运行状态中也就是a拿到了锁,那么t线程就时时刻刻准备好把这个锁给它抢过来,这就是阻塞锁定状态。还有一种情况,就是正在运行中的a线程调用了t.notify()方法使得开摆了的处于阻塞等待状态的t线程变成了爷们要战斗状态,这也是阻塞锁定状态
  • 结束状态:顾名思义,需要注意一点的是,如果主线程比其它线程结束的早,那么程序会直接结束,除非t线程中调用了join方法,这种情况下就会让CPU执行完t线程和主线程再结束
    。这里我有一些补充,这个线程中有一种无私的线程叫守护线程,JVM虚拟机只需要确保用户线程执行完毕,而不需要等待守护线程执行完,所以如果你可以调用t线程的setDaemon()使其变为守护线程。

解决线程并发的问题

不知道大家有没有遇到这样一种情况啊,就是利用并发写一个买票小demo的时候,诶,出现了少票的情况。没遇到也没关系啊,咱来给大伙说说

public class Main{
    public static void main(String[] args){
        int i = 100;

        Thread myThread01 = new Thread(()->{
            i--;
            System.out.println("苏珊拿到了第"+i+"张票");
        });
        
        Thread myThread02 = new Thread(()->{
            i--;
            System.out.println("知音拿到了第"+i+"张票");
        });

        myThread01.start();

    }
}

大伙说,这里会输出什么?

买票齁币多,多少黄牛说:我觉得会输出:

[“苏珊拿到了第99张票”
“知音拿到了第98张票”]
或者是:
[“知音拿到了第99张票”
“苏珊拿到了第98张票”]

你看这黄牛,考虑的就是周到,连CPU可能会先调哪个线程全都考虑周到了,可是呢,你就一定确保调到myThread01线程的时候一定会全部执行完再去执行myThread02线程吗?如果执行的顺序是如下呢:

myThread01:i--;//99
myThread02:i--;//98
myThread01:System.out.println("苏珊拿到了第"+i+"张票");//苏珊拿到了第98张票
myThread02:System.out.println("知音拿到了第"+i+"张票");//知音拿到了第98张票

what???怎么会出现这种情况,老黄牛惊掉了下巴:那…总不能两个人都拿第98张票吧,我的钱可,不对不对,这怎么才能像我那样"保障"客户的利益呢!

诶,要出现老黄牛那样的情况,其实并不难,无非是执行完一个线程再去执行另一个线程,可我们该如果实现这种情况呢?

这种情况也好实现:引入锁机制!

用🔒实现同步

使用synchronized修饰方法

public class MyThread implements Runnable{
    @Override
    public void run(){
        lock();
    }

    public synchronized void lock(){
        System.out.println("在该线程执行此方法的时候,该线程获得一把锁,只有获得这个锁的线程,才可处于线程运行状态,直到该线程运行完或者wait()才会释放锁,拥有同一个线程内核的多个线程只有一个线程可以获得锁,没获得锁的线程只能靠边等待");
    }
}

使用synchronized修饰代码块(常用)

synchronized(锁对象){
    System.out.println("当该线程执行到代码块时,要去看锁对象手中是否有锁,如果没有锁,该线程就要去拿一把锁来送给锁对象后,才可执行代码块中的代码,待执行完代码块中的代码,锁对象就扔掉手里的锁,同一个锁对象只能拥有一把锁,待锁对象手里没有锁后其它线程才可送给锁对象自己拿到的锁然后去执行代码块中的代码");
}

使用ReentrantLock类实现Lock(常用)

怎么说呢,其实我比较喜欢这样的方式实现锁机制,可能是看起来比较清晰

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

        //定义一个Lock
        ReentrantLock myLock = new ReentrantLock();


        Thread myThread = new Thread(()->{
            myLock.lock();
            try{
                System.out.println("我已经拿到了myLock锁,其它要拿myLock锁的线程无法再运行!");
            }
            finally{
                System.out.println("解锁!");
                myLock.unlock();
            }
        })
    }
}

要实现锁机制,上面三种都可以!

但是我这里要写出一个东西:死锁!

死锁是怎么来的呢,它又是怎么实现的呢(!!!不是说要实现,是要避免!!!)

synchronized代码块形成死锁

//定义两个拿锁的人
Object lockMan01 = new Object();
Object lockMan02 = new Object();

//定义第一个线程(包括线程容器和线程内核)

Thread myThread01 = new Thread(()->{
    synchronized(lockMan01){
        System.out.println("lockMan01已经拿到🔒!");
        synchronized(lockMan02){
            System.out.println("lockMan02已经拿到🔒!");
        }
    }
});
//第二个线程
Thread myThread02 = new Thread(()->{
    synchronized(lockMan02){
        System.out.println("lockMan02已经拿到🔒!");
        synchronized(lockMan01){
            System.out.println("lockMan01已经拿到🔒!");
        }
    }
})

你说说怎么执行会出现死锁呢?对!就是如同你说的那样:

1.执行myThread01线程,执行代码块使lockMan01拿到锁,然后执行输出语句,这个时候先不去执行剩下的一个代码块
2.执行myThread02线程,执行代码块使lockMan02拿到锁,然后执行输出语句,然后接着去执行剩下的代码块synchronized(lockMan01){
            System.out.println("lockMan01已经拿到🔒!");
        }
可是,问题来了,这个代码块能顺利执行吗?不能啊,我myThread02线程虽然此时拿到一把锁,可是呢,你lockMan01手里有一把锁啊,我myThread02手里的这把锁没法给lockMan01,所以我就没法执行代码块里的代码啊
3.尝试去执行myThread01线程,可惜,同理无法执行。

这就是死锁产生的原因,ReentrantLock同样也会产生死锁现象,我们来康康

//定义两把锁
ReentrantLock lock01 = new ReentrantLock();
ReentrantLock lock02 = new ReentrantLock();

//创建线程一
Thread myThread01 = new Thread(()->{
    lock01.lock();
    try{
        System.out.println("我已经拿到了lock01锁")
    }
    finally{
        lock02.lock();
        try{
        System.out.println("我已经拿到了lock02锁");
        lock01.unlock();}
        finally{
            lock02.unlock();
        }
        
    }
});
//创建线程二
Thread myThread02 = new Thread(()->{
    lock02.lock();
    try{
        System.out.println("我已经拿到了lock02锁")
    }
    finally{
        lock01.lock();
        try{
        System.out.println("我已经拿到了lock01锁");
        lock02.unlock();}
        finally{
            lock01.unlock();
        }
        
    }
});

相信聪明的大伙也早都猜到了,当两个线程一启动时,极有可能产生死锁,当然了,死锁产生的方式五花八门,这里只是说了最简单的一种

线程协作

PVZ系列!这是我最爱啊,我们就用介来说说,什么是线程协作

线程协作中有两个身份:生产者,消费者

对应着植物大战僵尸中:向日葵,疯狂戴夫

向日葵生产阳光是一个线程,戴夫种植物又是一个线程。你说,种植物需要阳光啊,所以说,这两个线程就要进行协作。协作的方法有两个:

管程法(消费者和生产者中有缓冲区)

对应到植物大战僵尸中,向日葵(生产者)产生阳光后,不是直接给戴夫(消费者)吧(别说这玩意也烫手),而是放在阳光仓库里,等到阳光仓库里的阳光到100了再通知戴夫(消费者)去种豌豆射手(消费),化成代码也就是:

向日葵可以一直生产,生产出来的东西放到阳光仓库中,如果还没到100就让戴夫种植物线程wait(),如果到了100就让戴夫notify()

这就是管程法,也可叫做缓冲法

信号灯法

大家是不是都打过僵王博士,诶,咱就用这个僵王博士举例子。假设一共六个线程吧:boss线程,1线程,2线程,3线程,4线程,5线程。这六个线程分别对应boss,一二三四五路出僵尸,并且咱在这六个线程外单独设立一个int flag = 0;代表着boss线程在运行(僵王博士还活着),一二三四五线程也会一直运行(出僵尸)。一旦僵王博士死了,那么flag = 1;此时一二三四五线程都会一直停止。你想想,是不是,僵王博士活着,12345路一直出僵尸,僵王博士死了,12345路僵尸也没了。这便是信号灯法!

具体使用哪种方法我觉得还是需要看应用场景

使用线程池

ExecutorService service  = Executors.newFixedThreadPool(10);//创建一个能放10个线程的线程池

service.excute(new MyThread());//向线程池种加入线程

service.shutDown();//关闭线程池
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值