美文网首页
Java基础(12)——多线程

Java基础(12)——多线程

作者: 让时间走12138 | 来源:发表于2020-04-25 19:05 被阅读0次

    本节内容

    • 1.线程&进程&主线程&子线程概念

    • 2.使用继承Thread开启线程

    • 3.使用Runnable接口开启线程

    • 4.线程的生命周期

    • 5.线程的终止

    • 6.线程礼让yield()

    • 7.多线程导致的问题

    • 8.使用lock和synchronized

    • 9.线程间通信

    一、线程&进程&主线程&子线程概念

    1.进程:正在运行/执行的一个程序,进程是用于管理所有的资源的,不进行实际任务
    2.线程:完成具体任务(一个进程里面可以有多个线程)。例如QQ运行起来,可以和一个人聊天,又可以和另一个人视频,同时还可以刷空间,这些任务都可以同时进行,这都是由线程来操作的。
    3.程序:静态的代码,如果不运行起来就没有意义。而一个程序运行起来就是一个进程,其中又包含多种线程。
    4.主线程:在Java中,main方法里面的代码就在主线程中跑。在Android和IOS中,主线程就是启动程序,开到的UI界面,也叫UI主线程
    5.子线程:除了主线程之外的都是子线程
    6.为什么使用多线程:在主线程里面,任务的执行顺序是从上至下的。如果其中一个任务需要花费大量时间(下载数据等),那么这个任务后面的任务就会被阻塞,必须等这个任务结束才能被执行。这样用户的体验效果不好,这个时候就需要将耗时的任务放在另外一个子线程里面执行。这个就叫多线程。
    7.注:不管是主线程还是子线程,都有自己独立的内存空间(执行路径/生命周期)。子线程也要从上至下执行。一个线程只完成一个任务,但是一个进程里面有多个线程。

    二、使用继承Thread开启线程

    1.获取当前线程信息,调用Thread.currentThread()即可
    System.out.println(Thread.currentThread());
    
    2.如何开启线程:①写一个类继承于Thread ②写一个类实现Runnable接口
    3.使用继承Thread开启线程的具体过程:首先创建类继承于Thread,具体执行的任务放在run方法里面
    class TestThread extends Thread {
      public TestThread(String s) {
            super(s);
        }
        @Override
        public void run() {
            //线程需要执行的任务
            for (int i = 0; i < 100; i++) {
                    System.out.println((i+1)+ " ");
            }
        }
    }
    
    接着在main函数里面创建具体对象,然后就可以通过start()启动线程了。不能调用run方法,不然它还在主线程里面。调用start方法是因为,系统会自动将这个任务放到队列中,等待调度
         TestThread t =new TestThread("子线程");
          t.start();
    
    注:可以给我们自己创建的类添加一个构造方法,然后创建对象的时候可以自己给这个线程命名。
    4.可以创建多个对象,然后调用start()方法。但是没有一定的先后顺序。线程是通过抢占时间片来获取运行机会的,谁抢到时间片,谁就可以运行。时间片是由操作系统来分配的。所以每一次执行结果大部分是不一致的。
    三、使用runnabld接口开启线程
    1.创建一个类,实现runnable接口(这个类只是一个任务,并不能创建线程)
    class TestRunnable implements Runnable{
        @Override
        public void run() {
       System.out.println(Thread.currentThread().getName());
          for (int i=1;i<=100;i++){
              System.out.println(i);
          }
        }
    }
    
    2.创建一个自定义类的一个具体对象,然后创建一个Thread对象(它可以创建线程),让这个线程去执行testRunnable的任务,线程名字为子线程。然后启动线程。
         TestRunnable t= new TestRunnable();
          Thread t1= new Thread(t,"子线程1");
          t1.start();
    
    注:只创建一个TestRunnable类的对象无法开启子线程,必须依赖于Thread类
    3.如果想要两个线程,那么可以再定义一个Thread类
          TestRunnable t= new TestRunnable();
          Thread t1= new Thread(t,"子线程1");
          Thread t2 =new Thread(t,"子线程2");
          t1.start();
          t2.start();
    
    注:当调用start方法时,这个线程会自动扔到操作系统的任务队列(线程池)中,至于这个任务什么时候被执行,我们无法确定,由操作系统来决定

    两种启动线程的方式对比

    方式①:使用继承Thread ,方式②:使用runnabld接口
    1.创建对象的过程方式①要简便一点,方式②要复杂一点
    2.方式①不能再继承于其他的类了,不能实现其他的功能,因为Java是单继承。但是方式②可以继承多个接口,还能实现更多的功能。
    3.综上:方式②灵活一点,更易于扩展。所以最好用接口的方式来开启线程

    四、线程的生命周期

    1.线程的生命周期:从创建到结束
    2.线程的五个状态:创建状态—>就绪状态—>运行状态—>死亡状态,运行状态受阻时可能出现阻塞状态。
    比如在运行状态调用sleep()或join()就会进入其他阻塞(等休眠时间到或者join()线程执行完毕,IO流阻塞结束,然后就会返回就绪状态),调用wait()就会进入等待阻塞(当使用notifiy()唤醒的时候就会回到就绪状态),调用synchronized()就会进入同步阻塞(当这个锁解开了,就会进入就绪状态)
    注:就绪状态得到时间片就变成运行状态,运行状态失去时间片就变回就绪状态。运行状态之后,如果run方法结束,那么就进入死亡状态。
    ①创建状态(NEW):new Thread()
    ②就绪状态:1.新的线程调用start(),2.阻塞条件结束,3.正在运行的线程时间片被其他线程抢夺
    ③运行状态(RUNNABLE):从就绪状态到运行状态是由操作系统实现的,外部无法干预
    ④死亡状态:1.run方法结束 2.手动让线程暂停 (比如调用stop),但是不建议使用stop,而建议通过其他方式让线程暂停。
    ⑤阻塞状态(BLOCKED):运行状态受阻时进入阻塞状态
    注意:可以调用getState()方法来查看线程当前的运行状态,在创建对象前后调用显示的结果不一样
     System.out.println(t1.getState());
    
    如果想要进入阻塞状态,让它显示TIMED_WAITING,那么可以在自己定义的类里面添加一个sleep()方法,再调用getState()方法就可以显示这个,在网络延时的时候可能会使用sleep()
    try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
    

    五、线程的终止

    1.如何让一个线程结束:①不要直接调用stop()方法来结束一个线程 ②自己写一个变量/标识符,用来标识线程结束的临界点
    首先在自己写的类里面定义一个变量
    private boolean shouldStop = true;
    
    然后再创建一个方法,里面就是对shouldStop做一个赋值。并且在run方法里使用while循环,让它做参数
    public void terminated(){
          shouldStop=false;
        }
     public void run() {
        while (shouldStop){
            System.out.println("子线程");
        }
    
    然后在主类里面调用一下这个方法,就可以自己暂停。因为主类会不断的抢占时间片,所以有可能一直输出不了子线程
      MyThread t= new MyThread("测试线程");
         t.start();
        for(int i=0;i<100;i++){
            System.out.println("主线程"+(i));
            if(i==2){
                t.terminated();
            }
        }
    

    六、线程礼让yield()

    1.调用yield()函数可以让其他线程先执行,礼让的线程就会直接进入就绪状态,如果这个线程再次获得时间片就会执行,可能礼让失败
      TestRunnable t= new TestRunnable();
           Thread t1= new Thread(t,"小美");
           t1.start();
           for (int i=0;i<20;i++){
               System.out.println("主线程"+i);
               if(i==5){
                   Thread.yield();
               }
           }
    
    以上就是当i=5的时候,for循环就会先让t1这个线程执行,礼让给它之后,它进入就绪状态,然后和t1线程一起争夺时间片。因为它就在主线程里面,所以如果时间片争夺成功,就又会进入运行状态。这样就可能导致礼让不成功。
    2.也可以调用join()方法,让其他线程插队,那么当前线程就会进入阻塞状态。只有当插队的线程执行完毕,当前线程才会进入运行状态。
      TestRunnable t= new TestRunnable();
           Thread t1= new Thread(t,"小美");
           t1.start();
           for (int i=0;i<20;i++){
               if(i==5){
                   try {
                       t1.join();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               System.out.println("主线程"+i);
           }
    

    七、多线程导致的问题

    1.多线程的优点:执行效率高,不会阻塞主线程(因为耗时的任务都在子线程里面)
    2.多线程的缺点:如果多个线程操作同一个资源,有可能出现不安全
    public class MyClass {
        public static void main(String[] args) {
            TicketWindow t1 =new TicketWindow("重庆");
            TicketWindow t2= new TicketWindow("成都");
            t1.start();
            t2.start();
            }
    }
    class TicketWindow extends Thread {
        public TicketWindow(String s) {
            super(s);
        }
        private static int total = 100;
        @Override
        public void run() {
         for (int i=0;i<20;i++){
            if (total > 0) {
                System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
                total--;
            }
        }
    }
    
    例如实现以上代码,结果显示为以下,他们第一张买到了一样的票
    当前出票口为:重庆 票号为:20
    当前出票口为:成都 票号为:20
    当前出票口为:成都 票号为:18
    当前出票口为:成都 票号为:17
    当前出票口为:成都 票号为:16
    当前出票口为:成都 票号为:15
    ···
    

    八、使用lock和synchronized

    两种方式保证线程安全:①锁 Lock ②线程同步
    1.锁:必须使用的是同一个锁,先创建一把锁
    private static Lock lock= new ReentrantLock();
    
    然后调用lock上锁,再调用unlock解锁,把它们放在实现功能的代码前面与末尾即可,以上面买票举例
    
      lock.lock();
            if (total > 0) {
            System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
                total--;
            }
                //解锁
                lock.unlock();
    
    这样就不会买到一样的票了
    2.线程同步synchronized,必须保证锁的是同一个对象,每一个对象都维护一把锁,可以修饰代码块,也可以修饰方法
    (1)修饰代码块:使用时可以先创建一个Object类对象,然后将这个对象作为synchronized方法的参数,然后将要锁的代码用synchronized包裹起来。以下代码前后并不连续
    private static Object object= new Object();
    
    synchronized (object) {
               if (total > 0) {
            System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
                   total--;
               }
           }
    
    (2)修饰方法:在定义函数时添加一个synchronized,随时都可以调用
     private synchronized void BuyTickets(){
            if (total > 0) {
            System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
                total--;
            }
        }
    
    3.synchronized的好处:当第一个线程进这个锁的时候,会执行相应的代码,当第二个线程过来的时候不能立刻执行这段代码,因为它被锁住了。只有等第一个线程解锁以后,第二个线程才会开始
    注:不管是锁代码块还是锁方法,尽量让锁的范围小一点。范围太大,其他线程等的时间就越长,这样效率反而会下降。

    九、线程间的通信

    1.线程间的通信也就是交互,有三种方法可以实现线程间的交互:wait():让某个线程等待, notify():唤醒某个线程, notifyAll():唤醒多个线程
    2.注意:这三个方法必须由同步监视器来调用,也就是大家都在抢同一个资源(或者说用同一个锁锁定的资源对象)
    3.例如:我们要让两个线程分别交替输出,第一个线程输出以后,调用wait方法,等它输出结束后再调用notify()方法将其它在这同一个锁里等待的线程唤醒
    4.如果某个创建后只会使用这一次,那么可以使用匿名内部类的方法,甚至连task这个对象都不用创建,直接调用start方法
    public class MyClass {
        public static void main(String[] args) {
        new Thread(new Runnable() {
          @Override
          public void run() {
              for (int i = 0; i < 20; i++) {
                  System.out.println(i+1);
              }
          }
      }).start();
        }
    }
    
    5.锁接收一样的参数,可以保证是同一个锁,线程在争夺同一个资源
    6.让两个线程交替输出的实战:比如让数字和英文字母交替输出
    public class MyClass {
        static int state=1;
        static Object obj=new Object();
        public static void main(String[] args) {
        new Thread(new Runnable() {
            int num=1;
          @Override
          public void run() {
              for (int i = 0; i < 20; i++) {
                     while (true){
                      synchronized (obj){
                          //判断当前是不是在输出字母
                          if(state!=1){
                              //当前这个线程需要等待一下
                              try {
                                  obj.wait();
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                          }
                          //输出数字
                          System.out.println(num);
                          num++;
                          if(num>26){
                              break;
                          }
                          //唤醒当前obj锁上的其他等待的线程
                          state=2;
                          obj.notify();
                      }
                     }
              }
          }
      }).start();
            new Thread(new Runnable() {
               char alpha='a';
                @Override
                public void run() {
                   while (true){
                        synchronized (obj){
                            //判断当前是不是在输出数字
                            if(state!=2){
                                //当前这个线程需要等待一下
                                try {
                                    obj.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            System.out.println(Character.toChars(alpha));
                            alpha++;
                            if(alpha>'z'){
                                break;
                            }
                            state=1;
                            obj.notify();
                        }
                   }
                }
            }).start();
        }
    }
    
    7.让数字和英文字母交替输出还有另一种方法,我们可以创建一个类,然后在这个类里面添加两种方法,分别是输出数字和输出字母,并且给这两种方法加锁。然后创建一个新类的对象,通过这个对象调用相应的方法
    public class MyClass {
        static Data d= new Data();
        public static void main(String[] args) {
         new Thread(new Runnable() {
             @Override
             public void run() {
                d.printNum();
             }
         }).start();
    
         new Thread(new Runnable() {
             @Override
             public void run() {
                 d.printAlpha();
             }
         }).start();
    
        }
        public static void test(){
    
        }
    }
    
    class Data{
        int num =1;
        int alpha='a';
        int state=1;
        public  synchronized void printNum()  {
            while (true){
                if (state!=1){
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(num);
                num++;
                if(num>26){
                    break;
                }
                state=2;
                this.notify();
            }
        }
        public synchronized void printAlpha(){
              while (true){
                  if(state!=2){
                      try {
                          this.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                  System.out.println(Character.toChars(alpha));
                  alpha++;
                  if(alpha>'z'){
                      break;
                  }
                  state=1;
                  this.notify();
              }
        }
    }
    
    OK,以上就是今天的全部内容,再见!

    相关文章

      网友评论

          本文标题:Java基础(12)——多线程

          本文链接:https://www.haomeiwen.com/subject/kkoyihtx.html