[Java成神之路]之多线程详解

进程

在操作系统中运行的程序就是进程,例如腾讯视频,B站 ,当你在看视频的时候,屏幕同时传递给你的信息分别是(图像,声音,弹幕),因为这是在播放视频的时候同时进行的,所以这个在程序中称为一个进程中运行了三个线程,反观我们的大脑也在同时开三个线程来接收并分析视频中传递的信息

程序和进程和线程之间的关系

  • 程序本身是静态的,没有任何运行的含义
  • 当程序跑起来的时候,操作系统会分配资源并创建一个关于程序的进程,进程则是执行程序的一次过程,所以进程本身是动态的。
  • 一个进程中可以包含若干个线程,这些线程都是在主线程有并行需求的时候从Main线程中开辟出来的子线程
  • 程序跑起来变成进程,进程中按代码的规定开辟线程,真正执行任务的是线程

线程创建的方式

Thread

使用继承的方式继承Thread类,并重写父类的run()方法,最后创建Thread子类的对象调用start()方法开辟线程。

public class ThreadSon extends Thread{
    @Override
    public void run() {
        System.out.println("Hello!");
    }

    public static void main(String[] args) {
        //继承Thread子类对象
        ThreadSon ts = new ThreadSon();
        ts.start();
    }
}

Runnable

使用接口实现类的方式创建Runnable接口实现类,并重写Run方法,在运行该线程的时候将接口实现类对象传入Thread()构造器,直接调用Thread的start()方法间接运行该线程。

public class RunnableSon implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello");
    }

    public static void main(String[] args) {
        //接口实现类对象
        RunnableSon rs = new RunnableSon();
        //使用静态代理的方式创建匿名Thread对象,并调用start()方法,间接运行该线程
        new Thread(rs).start();
    }
}

Callable

实现Callable接口的线程具有一个特点,那就是开辟带返回值的线程,前面两种(Thread,Runnable)是不带返回值的开辟线程。还值得一提的就是Callable内置了线程池技术,现在可能一听有点迷糊,但是也不是什么大不了的东东,一句话你就能理解!

别的方法创建线程,当线程执行完之后就被销毁了,而根据Callable创建的线程,当一条线程执行完之后,就会被存放在线程池中待命,当有新的任务的时候,就直接复用线程池中待命的线程,减少了系统不断开辟资源的次数;

下面我们就来看一下如何使用Callable方式创建线程!

public class CallableSon implements Callable<String> {

    private String SayHello;

    //构造器赋初值
    public CallableSon(String sayHello) {
        this.SayHello = sayHello;
    }

    @Override
    public String call() throws Exception {
        return SayHello;
    }

    public static void main(String[] args) throws Exception{
        CallableSon s1 = new CallableSon("Hello");
        //线程池
        ExecutorService ser_pool = Executors.newFixedThreadPool(3);
		//提交任务并等待分配资源执行(会查看线程池中是否有待命线程,没有则等待状态)
        Future<String> value = ser_pool.submit(s1);
        //打印线程运行返回值
        System.out.println(value.get());
        //清空线程池
        ser_pool.shutdownNow();
    }
}

总结:

了解了三种创建线程的方法,细心的小伙伴会发现这三种方法中最简单的代码最少的也就是第一种Thread方法了,有想法的小伙伴就会挑选其中一种比较简单的方式来应用了。

但是我想说的是这三种创建线程方法各有各的优点,在应用的时候应该根据实际需求应用。下面我们就来谈一下这三种方式的优点,这样就便于你在实际应用的时候在最短的时间内做出最正确的决策。

  • 首先是Thread方式,学过JavaSE的同学应该都知道Java是单继承,多接口。
    单继承具有局限性,这个局限性的一方面就体现在线程中,使用Thread单继承的情况下是无法实现多个线程同时访问被Thread继承的类的,当一条线程处理非常大的数据量的时候,多余的系统资源还不能合理的利用的时候这个弊端就体现了出来。
  • 其次是Runnable,我们补上第一点挖的坑啊,使用Runnable接口定义的实现类,可以完美解决资源合理分配和多线程访问同一对象的问题,但是这样也有弊端,那就造成了线程不安全的问题,线程不安全通俗来说就是多线程访问同一对象数据不统一的问题,后续会提到线程同步的联系,所以这个问题也被有效的解决了,当然出问题了也要第一时间反应过来,还是有必要了解的。
  • Callable方式创建线程的优点就是有效的节省了系统资源不必要的浪费,在一些任务数大且单体任务量小的情况下,应用Callable可以说是再好不过了,毕竟节省了不断创建线程和销毁线程的动作。

线程的状态

线程的方法

  • sleep() 表示 使一个线程进入休眠状态,注意啊!是休眠状态,休眠状态中不会释放当前操作对象的锁,相当于是抱着锁睡觉的,指定时间之后,会自动醒来。

    Thread.sleep(1000); //以毫秒为单位,1 秒 = 1000 毫秒
    
  • wait() 也表示是一个线程进入类似于休眠的状态,注意啊!这个状态是沉睡的状态,沉睡过程中会将当前操作对象的锁释放,相当于释放当前的操作资源。但是这个沉睡buff一旦附上,那么就需要配合**notify()notifyAll()**来将其唤醒,并不会自己醒来。

    对象.wait();		//进入沉睡
    对象.notify();	//唤醒
    
  • yield() 该方法表示线程的礼让,表示当一个线程进入CPU执行的时候,理论上是在执行的过程中出现了该代码则CPU暂时停止执行该线程,去执行其他线程,执行完其他线程之后再回到当前线程继续执行。但是有一句话不得不说,你线程礼让可以,但是是否礼让成功那就要看CPU的心情了。如果是处于并发的情况下,礼让线程没有多大作用,还是会继续执行。

    class Yield implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始执行");
            Thread.yield();   //线程礼让
            System.out.println(Thread.currentThread().getName()+"执行结束");
        }
    
        public static void main(String[] args) {
            Yield yd = new Yield();
    
            new Thread(yd,"one").start();
            new Thread(yd,"two").start();
        }
    }
    
    

    执行结果 : 显然这次礼让成功

    one开始执行
    two开始执行
    one执行结束
    two执行结束
    
    
  • join() 该方法意为强制执行被调用的线程并停滞当前线程的任务,将所有资源归我所用。通俗来讲就是插队…咳咳…

    class VIP extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "开始执行");
            System.out.println(Thread.currentThread().getName()+"执行结束");
        }
    
        public static void main(String[] args) throws Exception{
            VIP vip = new VIP();
            //启动vip线程
            vip.start();
    
            for (int i = 0; i < 6; i++) {
                System.out.println(Thread.currentThread().getName() + i);
                if (i==2){ //表示当i为2的时候 vip 线程会插队进来
                    vip.join(); //VIP线程插队
                }
            }
        }
    }
    
    
    

    运行结果:

    main0
    main1
    main2
    Thread-0开始执行
    Thread-0执行结束
    main3
    main4
    main5
    
    
  • getState() 该方法为获取线程状态

    thread.getState()
    
    

    线程的状态分为:

    • NEW 线程尚未启动状态
    • RUNNABLE 线程处于正在执行状态
    • BLOCKED 线程处于阻塞状态
    • WAITING 线程处于等待状f态
    • TERMINATED 线程处于死亡状态…
  • setPriority(int x) 该方法意为设置线程的优先级,当然java中已经预定的一些常量可供使用,也可自己设置,在调用时的实参中应当写整型的1~10,不可以为负数

    thread.setPriority(10); 	//设置线程优先级为10,顺便提一下,线程默认优先级为5
    thread.getPriority();		//获得当前线程的优先级
    
    thread.start();				//注意:一定要先设置优先级,在启动线程
    
    

线程同步

​ 线程不安全这个词想必大家都听过,是因为多个线程访问同一对象造成的数据之间不同步时暴露出来的问题,当然解决这个问题也相当简单,下面就带大家了解一下线程锁机制吧。

​ 官方的解释是队列+锁才构成线程同步的条件,其实我觉得只要应用了线程锁的机制后,队列就是应用之后的效果。这就相当于线程锁是病因,而队列只是症状,大家好好理解一下,我举个栗子。

​ 在食堂打饭的时候,阿姨给学生打饭,学生对于阿姨是独占的状态,也就相当于一把锁,如果产生了这种独占的状态,那么后面的学生就无法使用正在打饭的阿姨打饭,只能当前面的学生打完饭,释放完阿姨给当前学生打饭的工作后,阿姨才可以给下一位学生打饭。所以为了大家都能打上饭,不管是排好队去打饭也好,还是前面的学生打完饭了然后一堆人去抢阿姨的独占状态也罢,就结果而言自然而然的就形成了队列。

线程不安全举例

/**
 *      该类意为暴露 当处于多个线程访问同一对象的时候线程是不安全的问题
 *
 *      值的提一个知识点的是:网络延迟是为了放大问题的发生性
 */
public class UnsafeBuyTicket implements Runnable{

    //票数
    private int TicketNumber = 10;

    @Override
    public void run() {
        while(true){

            if(TicketNumber <= 0){
                System.out.println("票数以空");
                break;
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+TicketNumber--+"票");
        }
    }

    /**
     *  就依据结果而言,不同的对象拿到了相同的票号,这就是线程不安全导致的
     * @param args
     */
    public static void main(String[] args) {
        UnsafeBuyTicket us = new UnsafeBuyTicket();

        new Thread(us,"小明").start();
        new Thread(us,"老师").start();
        new Thread(us,"黄牛").start();

    }
}

运行结果如下,可以看出,有不同对象拿到了相同的票号

小明拿到了第10票
黄牛拿到了第8票
老师拿到了第9票
小明拿到了第7票
老师拿到了第7票
黄牛拿到了第6票
老师拿到了第5票
黄牛拿到了第4票
小明拿到了第5票
小明拿到了第3票
黄牛拿到了第2票
票数以空
老师拿到了第1票
票数以空
票数以空

下面给大家介绍一下线程锁的应用方式,一共分为两类,分别是synchronized方式和Lock显式声明方式。

synchronized同步线程的应用方法有两种:

  • synchronized关键字

    这个关键字只能写在方法中,跟public和static一样

    public static synchronized void Darling(){};
    
  • synchronized代码块

    这个代码块需要传一个形参,这个形参就是多线程访问的同一个对象,默认锁住当前应用的对象

    回顾一下上面这个例子,应用上线程锁的方式是,以下代码为节选。

    synchronized(this){ //默认锁住当前操作对象,this意为当前对象
                while(true){
                    System.out.println(TicketNumber);
                    if(TicketNumber <= 0){
                        System.out.println("票数以空");
                        break;
                    }
    
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"拿到了第"+TicketNumber--+"票");
                }
            }
    

下面给大家介绍lock锁,这种锁是一个显式声明的锁,操作方法跟我们的日常生活一样,如果想锁住大门保证房屋的安全性,那么首先你需要去购买一把锁,然后手动上锁,手动解锁,跟程序抽象出来的概念是一个道理。

ReentrantLock lock = new ReentrantLock();	//创建一个lock对象,在实际生活中相当于购买一把锁
lock.lock();	//手动上锁
lock.unlock();	//手动解锁

下面我们还是借助上面那个例子,使用lock锁的方式实现线程的同步功能。

public class UnsafeBuyTicket implements Runnable{

    //票数
    private static int TicketNumber = 10;

    //创建lock对象,顺便提一下lock类在java的JUC并发包中
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        //手动上锁
        lock.lock();
            while(true){
                if(TicketNumber <= 0){
                    System.out.println("票数以空");
                    break;
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"拿到了第"+TicketNumber--+"票");
            }

        //手动解锁
        lock.unlock();
    }

    /**
     *  就依据结果而言,不同的对象拿到了相同的票号,这就是线程不安全导致的
     * @param args
     */
    public static void main(String[] args) {
        UnsafeBuyTicket us = new UnsafeBuyTicket();

        new Thread(us,"小明").start();
        new Thread(us,"老师").start();
        new Thread(us,"黄牛").start();

    }

}

死锁

​ 官方来说就是在多个线程中,当前线程长久等待已被其他线程占有的资源而陷入阻塞的一种状态,通俗来讲就是A线程需要运行,那需要B线程的资源,B线程要运行,那需要A线程的资源两者处于闭环且相互等待的进程中。下面我举个例子,用代码来实现就更好理解了。

package Threading.Lock;

/**
 *  书写一个例子来概述一下什么是死锁,还有死锁是怎样产生的
 *      有一项园丁的任务是种花,种花分为两步,第一步把花种上,第二部浇水
 */
public class Dead_lock {

    public static void main(String[] args) {
        Grow_Flowers g1 = new Grow_Flowers(0,"张园丁");
        Grow_Flowers g2 = new Grow_Flowers(1,"李园丁");

        new Thread(g1).start();
        new Thread(g2).start();
    }
}

//花
class Flower{}
//水壶
class Water{}

//钉钉子
class Grow_Flowers implements Runnable{

    //花朵
    private static Flower flower = new Flower();
    //水壶
    private static Water water = new Water();

    private int choice;
    private String name;

    public Grow_Flowers(int choice,String name){
        //选择的东西
        this.choice = choice;
        //使用东西的人的名字
        this.name = name;
    }

    @Override
    public void run(){
        try {
            grow();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //种花
    public void grow() throws Exception{
        //拿起花种花
        if(choice == 0){
            synchronized (flower){ //获得花之后并上锁独占
                System.out.println(this.name+"拿起了花");
                Thread.sleep(2000); //2秒之后想获得水壶
                synchronized (water){
                    System.out.println(this.name+"拿起了水壶");
                }
            }
        }else{
            synchronized (water){
                System.out.println(this.name+"拿起了水壶");
                Thread.sleep(1000); //1秒后想获得花
                synchronized(flower){
                    System.out.println(this.name+"获得了花");
                }
            }
        }
    }
}

运行结果:
在这里插入图片描述

显而易见程序并没有停止,而是两条线程处于互相等待对方的状态。

​ 当然解决方法也很简单,以上程序只是让大家理解什么是死锁和死锁是怎么产生的,解决方法就是使用完某个对象之后就释放,这样就可以避免死锁了。

节选部分更改代码

//种花
    public void grow() throws Exception{
        //拿起花种花
        if(choice == 0){
            synchronized (flower){ //获得花之后并上锁独占
                System.out.println(this.name+"拿起了花");
                Thread.sleep(2000); //2秒之后想获得水壶
            }
            synchronized (water){
                System.out.println(this.name+"拿起了水壶");
            }
        }else{
            synchronized (water){
                System.out.println(this.name+"拿起了水壶");
                Thread.sleep(1000); //1秒后想获得花
            }
            synchronized(flower){
                System.out.println(this.name+"获得了花");
            }
        }
    }

结语:

​ 到这里为止多线程的各种知识点已经涉及到了,当然是以我理解的方式呈现给大家的,肯定有不足的地方,也欢迎各路小伙伴补充,这也是鄙人初次开通博客,想以此记录自己学习的点点滴滴,毕竟学了就忘在博主的身上体现的淋漓尽致呀,哈哈哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡特霖娜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值