zoukankan      html  css  js  c++  java
  • java-多线程的入门_进阶总结

    多线程

    概述图

    1.概述

    进程:正在执行中的程序,其实时应用程序在内存中运行的那片空间。

    线程:进程中的一个执行单元,负责进程中的程序的运行,一个进程中至少要有一个线程。

    (进程可以理解为是一个QQ程序,QQ运行本身就是一个线程(main),你可以在QQ上做好多事情,每个事情就相当于一个线程)

    一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序

    程序启动了多线程,有什么应用呢?
    可以实现多部分程序同时执行,专业术语称之为 并发

    1.1 并发与并行的区别?

    你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行
    你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发
    你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行

    并发的关键是你有处理多个任务的能力,不一定要同时。
    并行的关键是你有同时处理多个任务的能力。

            所以我认为它们最关键的点就是:是否是『同时』

    1.2 同步和异步的区别?

    在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,
    直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。
    当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率

    1.3 多线程的使用可以合理使用cpu的资源

    如果线程过多会导致降低性能。

    CPU处理程序时是通过快速切换完成的,在我们看来好像随机一样。

    举例:金山卫士进行电脑体检的同时,还可以清理垃圾,木马查杀等,这就是多线程

    总结:什么时候使用多线程技术呢?
    当多部分代码需要同时执行时,就需要使用多线程技术。

    1.4 线程的存在解决什么问题?

        多部分代码同时执行的问题。
        传统的单个主线程从头执行到尾的运行安排,当遇到较多的操作次数时,效率偏低
    多线程的弊端:
         开启过多会降低效率,多线程的原理其实是多个执行单元执行一个程序,CPU高速在多个线程之间进行切换,
         当线程个数过多时,CPU切换一个循环的时间过长,相应的降低了程序运行的效率
    多线程的特性

          随机性,因为CPU的快速切换造成的。其他总结:线程有并发性

     

    2.创建线程

    2.1 thread

                1,通过原来的主线程的执行和需求做对比。
                2,需求:在主线程中执行到部分代码多次无法继续执行,如何实现让下面的代码和主线程同时执行。
                3,创建方式,通过查api。Thread类。
                    继承Thread类。
                    3.1 定义一个类继承Thread。
                    3.2 重写run方法。
                    3.3 创建子类对象,就是创建线程对象。
                    3.4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
                    目的让主线程和自定义线程同时执行。
                   

     class ThreadDemo
                    {
                        public static void main(String[] args)
                        {
                            Ticket t = new Ticket(100);
                            Ticket t1 = new Ticket(10);
    
                            t.start;
                            t1.run();
                        }
                    }
                    class Ticket extends Thread
                    {
                        private int tickets;
                        Ticket( int tickets ){
                            this.tickets = tickets;
                        }
                        public void run(){
                            if ( ticket>0 )
                            {
                                System.out.println(Thread.currentThread().getName()+tickets--);
                            }
                        }
                    }

    为什么要这么做?
    继承Thread类:因为Thread类描述线程事物,具备线程应该有功能。
    那为什么不只讲创建Thread类的对象呢?
    Thread t1 = new Thread();
    t1.start();//这么做没有错,但是该start调用的时Thread类中的run方法,
    而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。

    创建线程的目的是什么?

    是为了建立单独的执行路径,让多部分代码实现同时执行。
    也就是说线程创建并执行需要给定的代码(线程的任务)。
    对于之前所讲的主线程,它的任务定义在main函数中。
    自定义线程需要执行的任务都定义在run方法中。
    Thread类中的run方法内部的任务并不是我们所需要,只要重写这个run方法,
    既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。
    所以进行了重写run方法动作。

    多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。
    进行方法的压栈和弹栈。

    当执行线程的任务结束了,线程自动在栈内存中释放了。
    但是当所有的执行线程都结束了,那么进程就结束了。

    2.2 体会调用run和调用start的区别?

           调用run方法仅仅是主线程执行run方法中的代码;调用start方法是开启线程,让新建的线程与主线程同时执行

    其他总结:注意在这个程序中,run()方法是中没有循环的;没有创建线程就不能执行;一个线程只有调用了start()方法才能开始线程,

    3.创建线程thread的原理

                   1,为什么要继承Thread,

                    因为Thread类描述了线程的任务存在的位置:run方法。
                2,为什么要覆盖run方法。
                    为了定义线程需要运行的任务代码。
                3,为什么调用start方法。
                    为了开启线程,并运行子类的run方法的任务代码。
                4,创建线程的目的是什么?
                    为了执行线程任务,而是任务都定义在run方法中。
                    run方法结束了,线程任务就是结束了,线程也就是结束了。
                5,线程任务:每一个线程都有自己执行的代码,主线程的代码都定义在主函数中,自定义线程的代码都定义在run方法中。

    3.1 多线程的运行原理

    1,疑问,如果多部分代码同时执行,那么在栈内存方法执行是怎么完成的呢?
    其实是,每一个线程在栈内存中都有自己的栈内存空间,在自己的栈空间中
    进行压栈和弹栈。
    2,主线程结束,程序就结束吗?
    主线程结束,如果其他线程还在执行,进程是不会结束了,当所有的线程都结束了,进程才会结束。
    3,画出多线程的运行内存图。

     

     3.2 异常在多线程中的特点

    throw异常是可以结束功能。
    如果功能是线程的任务,那么功能结束就代表着线程结束。
    而且异常信息中会体现出该异常在哪个线程上发生。


                Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 4
                    at Demo.run(ThreadDemo.java:68)

    4,创建线程实现Runnable接口

                 1,通过查阅api得到第二种方式         

                2,不明确原理的情况下,先应用。

                3,方式二步骤:
                    3.1,定义类实现Runnable接口:
                    3.2,覆盖接口中的run方法。
                    3.3,创建Thread类的对象:
                    3.4,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
                    3.5,调用Thread类的start方法开启线程。
                    到这里,你就应该可以使用方式二创建线程了。动手自己通过方式二完成线程的创建并运行。★★★★★
                4,方式二的原理:★★★
                    4.1,定义类实现Runnable接口:避免了继承Thread类的单继承局限性。
                    4.2,覆盖接口中的run方法。将线程任务代码定义到run方法中。
                    4.3,创建Thread类的对象:只有创建Thread类的对象才可以创建线程。
                    4.4,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
                        因为线程已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,
                        所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
                    4.5,调用Thread类的start方法开启线程。
                5,方式二和方式一的区别:【面试题】★★★★★
                    5.1 第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。
                    5.2 实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
                        继承Thread类:线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务
                        实现runnable接口:将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。
                        Runnable接口对线程对象和线程任务进行解耦。
              

               查询到的源码解释

    class Thread{
    
        private Runnable target;
    
        Thread(Runnable target)
        {
            this.target = target;
        }
        public void run() {
            if (target != null) {
                target.run();
            }
        }
        public void start()
        {
            run();
        }
    }
    
    Runnable d = new Demo();
    Thread t = new Thread(d);
    t.start();

    5,多线程的售票案例

    案例:售票的例子。

    售票的动作需要同时执行,所以使用多线程技术。

    class Tickets implements Runnable{
        private int tickets = 100;
    
        public void run() {
            while(tickets > 0)
            System.out.println(Thread.currentThread().getName()+"  "+tickets--);
            
        } 
        
        public static void main(String[] args) {
            Tickets t = new Tickets();
            Thread t1 = new Thread(t,"t1");
            Thread t2 = new Thread(t,"t2");
            Thread t3 = new Thread(t,"t3");
            Thread t4 = new Thread(t,"t4");
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            
        }

    6,线程的状态

    1,线程运行是有多种状态的

     1.1 创建  new Thread类或者其子类对象。

      1.2 运行 start().  具备者CPU的执行资格和CPU的执行权。
      1.3 冻结 释放了CPU的执行资格和CPU的执行权。比如执行到sleep(time)  wait() 导致线程冻结。
      1.4 临时阻塞状态: 具备者CPU的执行资格,不具备CPU的执行权。
      1.5 消亡:线程结束。run方法结束。  

    举例:老师给几个小朋友喂饭

                    

     

    8 多线程的安全问题         

    分析上面的案例 (多个线程共享一个资源

    我下倒水呢,刚到一半(if语句判断后,不去打印了),我离开了(不去打印了),下一个(另一个线程)来了

    把我里面水喝了,(tickets--),我回来了,我这时不用在判断了,直接打印tickets--(但是ticktes 已经减了一次了)不合理的

    案例:售票的示例。

                1,通过案例让安全问题发生。
                2,安全问题产生的原因:
                    2.1 线程任务中有共享的数据。
                    2.2 线程任务中有多条操作共享数据的代码
                    当线程在操作共享数据时,其他线程参与了运算导致了数据的错误(安全问题)
                3,安全问题的解决思路:
                    保证在线程执行共享数据代码时,其他线程无法参与运算。
                4,安全问题的解决代码体现:
                    同步代码块。synchronized(obj){需要被同步的代码}  【举例:火车上的卫生间】
                5,同步的好处:
                    解决了安全问题。
                6,同步的弊端:
                    降低了程序的性能。
                7,同步的前提:
                    必须保证多个线程在同步中使用的是同一个锁
                    解决了什么问题?
                    当多线程安全问题发生时,加入了同步后,
                    问题依旧,就要通过这个同步的前提来判断同步是否写正确。

    其他总结:同步里面的对象相当于火车上卫生间的锁,火车上就你自己一个人(一个线程),你去卫生间不用锁,3号卫生间的锁和4号卫生间的锁是没有关系的.因此多个线程要用同一个锁.



    使用同步解决上面的问题

    class Ticket implements Runnable
    {
        //1,描述票的数量。
        private int tickets = 100;
        //2,售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
        //线程任务中通常都有循环结构。
        private Object obj = new Object();
        public void run()
        {
            while(true)
            {
                synchronized(obj)
                {
                    if(tickets>0)
                    {
                        //要让线程在这里稍停,模拟问题的发生。sleep  看到了0 -1 -2 错误的数据,这就是传说中的多线程安全问题。
                        try{Thread.sleep(1);}catch(InterruptedException e){/*未写处理方式,后面讲*/}
    
                        System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。
                    }
                }
            }
        }
    }
    class ThreadDemo3 
    {
        public static void main(String[] args) 
        {
            //1,创建Runnable接口的子类对象。
            Ticket t = new Ticket();
    
            //2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
            Thread t1 = new Thread(t);
            Thread t2 = new Thread(t);
            Thread t3 = new Thread(t);
            Thread t4 = new Thread(t);
    
            //3,开启四个线程。
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }

    其他总结:同步里面的对象相当于火车上卫生间的锁,火车上就你自己一个人(一个线程),你去卫生间不用锁,3号卫生间的锁和4号卫生间的锁是没有关系的.因此多个线程要用同一个锁.

    习题

    //写出下面代码执行的结果(此题需写出分析过程)
    
    class MyThread extends Thread{
        public void run(){
            try {
                Thread.currentThread().sleep(3000);
            } catch (InterruptedException e) {
            }
            System.out.println("MyThread running");
        }
    }
    
    public class ThreadTest{
        public static void main(String argv[]) {
            MyThread t = new MyThread();
            t.run();
            t.start();
            System.out.println("Thread Test");
          }
    }

    执行结果:
        MyThread running
        Thread Test
        MyThread running
    过程分析:
        1,MyThread t = new MyThread();
            创建线程对象,但并未开启。
        2,t.run();
            是主线程执行t对象的run方法。
            在run方法中,主线程执行到sleep(3000);主线程就处于冻结状态,
            3秒后,主线程恢复到运行状态,就打印了"MyThread running"。
        3,t.start();
            主线程执行该句,开启了一个新的线程Thread-0。
            这时程序中有两个线程,执行出现了两种情况:
                情况一:主线程执行完t.start()方法后,继续执行打印语句,输出"Thread Test"。
                        然后新线程Thread-0执行run方法,并sleep(3000)3秒,3秒后,执行"MyThread running"
                情况二:主线程执行完t.start()方法后,开启了一个新线程Thread-0,
                        该新线程Thread-0就开始执行,调用run方法,sleep(3000)3秒,这时主线程开始执行,
                        打印了"Thread Test",主线程结束,3秒后,新线程Thread-0打印"MyThread running"。

    //两个客户到一个银行去存钱,每个客户一次存100,存3次。
    //问题:该程序是否有安全问题,如果有,写出分析过程,并定义解决方案。

    class Bank
    {
        private int sum;
        public void add(int num)
        {
            sum = sum + num;
            System.out.println("sum="+sum);//每存一次,看到银行金额变化。
        }
    }
    class Consumer implements Runnable
    {
        private Bank b = new Bank();
        public void run()
        {
            for(int x=0 ; x<3; x++)
            {
                b.add(100);//一次存100.循环3次,
            }
        }
    }
    class Test
    {
        public static void main(String[] args)
        {
            Consumer c = new Consumer();
            Thread t1 = new Thread(c);
            Thread t2 = new Thread(c);
            t1.start();
            t2.start();
    
        }
    }

     

    分析依据:多线程安全问题产生的原因:
    1,共享数据,b对象中的sum。
    2,操作共享数据的多次运算。
    sum = sum + num;
    System.out.println("sum="+sum);


    作者:8亩田
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接.

    本文如对您有帮助,还请多帮 【推荐】 下此文。
    如果喜欢我的文章,请关注我的公众号
    如果有疑问,请下面留言

    学而不思则罔 思而不学则殆
  • 相关阅读:
    Java实现 LeetCode 242 有效的字母异位词
    Java实现 LeetCode 212 单词搜索 II
    Java实现 LeetCode 212 单词搜索 II
    Java实现 LeetCode 212 单词搜索 II
    Java实现 LeetCode 212 单词搜索 II
    Java实现 LeetCode 344 反转字符串
    Java实现 洛谷 P1208 [USACO1.3]混合牛奶 Mixing Milk
    Java实现 洛谷 P1208 [USACO1.3]混合牛奶 Mixing Milk
    Java实现 洛谷 P1208 [USACO1.3]混合牛奶 Mixing Milk
    Java实现 洛谷 P1208 [USACO1.3]混合牛奶 Mixing Milk
  • 原文地址:https://www.cnblogs.com/liu-wang/p/6067525.html
Copyright © 2011-2022 走看看