多线程

Posted Sleepinglion

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关的知识,希望对你有一定的参考价值。

多线程

第一部分:线程

1. 并发和并行

​ 并发:两个或多个事件在同一时间间隔发生。(交替执行)

​ 并行:两个或多个事件在同一时刻发生。(同时执行)

2. 进程和线程

​ 进程:是指一个内存中运行的应用程序,每个进程都有一块独立的内存空间,一个应用程序可以运行多个进

程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是进程从创建、运行到消亡

的过程。

​ 线程:线程是进程中的一个执行单元,负责程序的运行,一个进程中至少有一个线程。一个进程中是可以有多

个线程的,这个应用程序也称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程可以包含多个线程。

3. CPU

​ cpu:又称中央处理器,包含运算器和控制器。

​ 作用:对数据进行处理,控制电脑中软件和硬件的运行。

​ 注意:电脑是可以有多个cpu的,但市场上的家用电脑一般都只有一个cpu。

4. 线程调度

​ 分时调度:所有线程轮流分配cpu的使用权,平均分配每个线程占用cpu的时间。

​ 抢占式调度:优先让优先级高的线程使用cpu,如果线程的优先级相同,会随机选择一个(线程随机性),java

使用的为抢占式调度。

5. 创建线程类

5.1 继承Thread类(java.lang.Thread)

  • 创建Thread类的子类
  • 子类重写Thread类中的run(),设置线程任务
  • 创建Thread子类对象
  • 调用Thread类中的start(),开启新的线程,执行run()
Thread类常用构造方法、方法
    常用构造方法:
        public Thread();    分配一个新的线程对象
        public Thread(String name); 分配一个指定名字的新的线程对象
        public Thread(Runnable target); 分配一个带有指定目标的新的线程对象
        public Thread(Runnable target,String name); 分配一个带有指定目标的新的线程对象并指定名字
    常用方法:
        public String getName();    获取当前线程的名称
        public void start();    导致此线程开始执行;JVM调用此线程的run()
        public void run();  此线程要执行的任务在此处定义代码(线程代码体)
        public static Thread currentThread();   返回对当前正在执行的线程对象的引用
        public void setName(String name)    设置当前线程的名称
        public static void sleep(Long millis); 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
//1.创建Thread类的子类
class MyThread extends Thread{
    private String name;
    //2.重写run()
    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println("run: " + i);
        }
    }
}

public class Demo1Thread {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread mt = new MyThread();
        //4.调用Thread类中的start(),开启新线程,执行run();
        mt.start();

        //主线程会继续执行主方法中的代码
        for (int i = 0; i < 15; i++) {
            System.out.println("main:" + i);
        }
    }
}

上述多线程代码实现原理图:

多线程原理:内存图解

5.2 实现Runnable接口(java.lang.Runnable)

  • 创建Runnable接口实现类
  • 实现类重写Runnable接口中的run(),设置线程任务
  • 创建接口实现类对象
  • 创建Thread类对象:通过有参构造器传递实现类对象
  • 调用Thread类中的start(),开启新的线程,执行run()
实现Runnable接口创建多线程的好处:
	1.避免了单继承的局限性
    	java中一个类只能直接继承一个类,类继承了Thread类就不能再继承其它类
        实现了Runnable接口,还能继承其它的类,实现其它的接口
    2.增强了程序的扩展性,降低了程序的耦合性(解耦)
        实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)
        实现类中,重写了run():设置线程任务
        创建Thread类对象,调用start(): 用来开启新线程
//1.创建Runnable接口的实现类
class RunnableImpl implements Runnable{
    //2.重写Runnable接口的run();
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

public class Demo1Runnable {
    public static void main(String[] args) {
        //3.创建实现类对象
        RunnableImpl mt = new RunnableImpl();
        //4.创建Thread类对象:构造方法中传递Runnable接口中的实现类对象
        Thread t = new Thread(mt);
        //5.调用Thread类的start()
        t.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

5.3 匿名内部类方式实现多线程的创建

/*匿名内部类实现多线程的创建:

    匿名:没有名字
    内部类:写在其它类内部的类

    匿名内部类的作用:简化代码
        把子类继承父类,重写父类的方法,创建子类对象合一步完成
        把实现类,实现类接口,重写接口中的方法,创建实现类对象合成一步完成

    匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

    格式:
        new 父类/接口(){
            重写父类/接口中的方法
        };
 */
public class Demo1InnerClassThread {
    public static void main(String[] args) {
        //线程的父类是Thread
        new Thread() {
            //重写Thread类中的run()
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "Java");
                }
            }
        }.start();

        //线程的接口是Runnable
        Runnable r = new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 15; i++) {
                    System.out.println(Thread.currentThread().getName() + "程序员");
                }
            }
        };
        new Thread(r).start();

        //简化接口的方式
        new Thread(new Runnable(){
            public void run(){
                for (int i = 0; i < 15; i++) {
                    System.out.println(Thread.currentThread().getName() + "持续努力");
                }
            }
        }).start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}

第二部分:线程安全

1. 概述

多线程程序访问共享数据,会产生线程安全问题。

2. 线程安全问题产生的原理

3. 解决线程安全问题?

线程同步机制:

  • 同步代码块
  • 同步方法
  • 锁机制

3.1 同步代码块

/*	
	模拟卖票案例:
        创建3个线程,同时开启,对共享的票进行出售。
		卖票案例出现了线程安全问题:卖出了不存在的票和重复的票。

    解决线程安全问题的第一种方式:使用同步代码块
    格式:
        synchronized(锁对象){
            需要同步操作的代码;
        }

    注意:
        1.同步代码块中的锁对象,可以是任意对象。
        2.必须保证多个线程使用的锁对象是同一个。
        3.锁对象的作用:
            把同步代码块锁住,只让一个线程在同步代码块中执行。
 */
public class Demo1Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用Thread类的start():开启新的线程,执行run()
        t0.start();
        t1.start();
        t2.start();

    }
}

class RunnableImpl implements Runnable{
    //定义一个多个线程的票源
    private int ticket = 100;
    //创建一个锁对象
    Object obj = new Object();
    //设置线程任务:卖票
    public void run(){
        //使用死循环,让卖票操作重复执行
        while(true) {
            //先判断票是否存在
            synchronized(obj){
                if (ticket > 0) {
                    //提高程序安全问题出现的概率,线程休眠10ms
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

同步技术的原理图解

3.2 同步方法

/*
    解决线程安全问题的第二种方式:使用同步方法
    实现步骤:
        1.把访问了共享数据的代码抽取出来,放到一个方法中;
        2.在方法上添加synchronized修饰符

    格式:
        修饰符 synchronized 返回值类型 方法名(参数列表){
            可能会出现线程安全问题的代码(访问了共享数据的代码)
        }
 */

public class Demo1Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:" + run); //@1b6d3586
        //创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用Thread类的start():开启新的线程,执行run()
        t0.start();
        t1.start();
        t2.start();
    }
}

class RunnableImpl implements Runnable{
    //定义一个多个线程的票源
    private int ticket = 100;
    //设置线程任务:卖票
    public void run(){
        System.out.println(this); //@1b6d3586
        //使用死循环,让卖票操作重复执行
        while(true) {
            payTicket();
        }
    }
    
    /*
        定义一个同步方法:
            同步方法也会把方法内部的代码锁住,只让一个线程执行。
            其锁对象是:实现类对象 new RunnableImpl();
            也就是this。
     */
    public synchronized void payTicket() {
            //先判断票是否存在
            if (ticket > 0) {
                //提高程序安全问题出现的概率,线程休眠10ms
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
}

3.3 Lock锁

/*	解决线程安全问题的第三种方式:使用Lock锁

    java.util.concurrent.locks.Lock接口
    Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
    Lock接口中的方法:
        void lock(); 获取锁
        void unlock(); 释放锁
    java.util.concurrent.locks.ReentrantLock implements Lock接口

    使用步骤:
        1.在成员位置创建一个ReentrantLock对象
        2.在可能会出现线程安全问题的代码前调用Lock接口中的lock()获取锁
        3.在可能会出现线程安全问题的代码后调用Lock接口中的unlock()释放锁
 */
public class Demo1Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建3个Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用Thread类的start():开启新的线程,执行run()
        t0.start();
        t1.start();
        t2.start();

    }
}

class RunnableImpl implements Runnable{
    //定义一个多个线程的票源
    private int ticket = 100;
    //创建一个ReentrantLock对象:多态
    Lock l = new ReentrantLock();
    public void run(){
        //使用死循环,让卖票操作重复执行
        while(true) {
            //在可能会出现线程安全问题的代码块前调用lock(): 获取锁
            l.lock();
            //先判断票是否存在
            if (ticket > 0) {
                //提高程序安全问题出现的概率,线程休眠10ms
                try {
                    Thread.sleep(10);
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //在可能会出现线程安全问题的代码块后调用unlock(): 释放锁
                    l.unlock(); //无论程序是否异常,都会把锁释放。
                }
            }
        }
    }
}

第三部分:线程状态

1. 线程状态图

2. 线程通信

2.1 线程间通信:

​ 多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

2.2 为什么要处理线程通信?

​ 多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我

们希望他们有规律的执行,那么线程之间需要一些协调通信,以此来帮我们达到多线程共同操作同一份数据。

2.3 等待唤醒机制

保证线程间通信有效利用资源。

/*	等待唤醒案例:线程之间的通信		
    创建一个顾客线程(消费者):告知老边要吃包子的种类和数量,调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)        
    创建一个老板线程(生产者):花了5秒钟做包子,做好包子之后,调用notify(),唤醒顾客吃包子   
 
    注意:        
      顾客和老板必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行        
      同步使用的锁对象必须保证唯一        
      只有锁对象才能调用wait(),notify()    

    Object类中的方法:        
      void wait(); 在其它线程调用此对象的notify()或notifyAll()方法前,导致当前线程等待        
      void notify(); 唤醒在此对象监视器上等待的单个线程                       
                     会继续执行wait()方法之后的代码。 
*/
public class Demo1WaitAndNotify {    
  public static void main(String[] args) {        
    //创建锁对象;        
    Object obj = new Object();        
    //创建顾客线程:        
    new Thread(){            
      @Override            
      public void run() {                
        System.out.println("告知老板要吃包子的种类和数量:");                
        synchronized(obj){                    
          try {                        
            //调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)                        
            obj.wait();                    
          } catch (InterruptedException e) {                        
            e.printStackTrace();                    
          }                    
          //继续执行wait()方法之后的代码                    
          System.out.println("饿死我了,开吃!");                
        }            
      }        
    }.start();        
    //创建老板线程:        
    new Thread(){          
      public void run(){              
        //花5秒做包子              
        try {                  
          Thread.sleep(5000);              
        } catch (InterruptedException e) {                  
          e.printStackTrace();              
        }              
        System.out.println("包子已经做好,唤醒顾客吃包子。");              
        //做好包子之后,唤醒顾客吃包子              
        synchronized(obj){                  
          obj.notify();              
        }          
      }        
    }.start();    
  }
}
/*    进入到TimeWaiting(计时等待)有两种方式:        
    1.使用sleep(long millis):在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态        
    2.使用wait(long millis):wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态    
    唤醒的方法:        
      1.void notify(); 唤醒在此对象监视器上等待的单个线程        
      2.void notifyAll(); 唤醒在此对象监视器上等待的所有线程 
*/
public class Demo2WaitAndNotify {    
    public static void main(String[] args) {        
      //创建锁对象;        
      Object obj = new Object();        
      //创建顾客线程:        
      new Thread(){            
        @Override            
        public void run() {                
          System.out.println("告知老板要吃包子的种类和数量:");                
        synchronized(obj){                    
          try {                        
            //调用wait(),放弃cpu的执行,进入到WAITING状态(无线等待)                        
            obj.wait(3000);                    
          } catch (InterruptedException e) {                        
            e.printStackTrace();                    
          }                    
          //继续执行wait()方法之后的代码                    
          System.out.println("饿死我了,开吃!");                
        }            
      }        
    }.start();    
  }
}

3. 线程池

3.1 概念

线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

好处:    
  a. 降低资源消耗。减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务。    
  b. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。    
  c. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下    
 (每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机。)

3.2 线程池的使用

import java.util.concurrent.*;
/*	线程池:JDK1.5之后提供    
    java.util.current.Executors;线程池的工厂类,用来生成线程池。    
    Executors中的静态方法:     
      static ExecutorService newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池。        
      参数:            
        int nThreads: 创建线程池中包含的线程数量        
      返回值:            
        ExecutorService接口,返回的是ExecutorService接口实现类对象,可以使用ExecutorService接口接收(面向接口编程)    

    java.util.current.ExecutorService;线程池接口        
    用来从线程池中获取线程,调用start(),执行线程任务。            
      submit(Runnable task) 提交一个Runnable任务用于执行        
    关闭/销毁线程池的方法:            
      void shutdown();    

    线程池的使用步骤:        
      1.使用线程池工厂类Executors里面提供的静态方法newFixedThreadPool生成一个指定线程数量的线程池        
      2.创建一个Runnable接口实现类,重写run(),设置线程任务        
      3.调用ExecutorService中的submit(),传递线程任务(实现类),开启线程,执行run()        
      4.调用ExecutorService中的shutdown(),销毁线程池(不建议执行)。 
*/
public class Demo1ThreadPool {    
  public static void main(String[] args) {        
    //1.创建线程池:使用Executor类中的静态方法newFixedThreadPool(int nThreads)        
    ExecutorService es = Executors.newFixedThreadPool(2);        
    //3.调用ExecutorService中的submit(),传递线程任务(实现类对象),开启线程,执行run()        
    es.submit(new RunnableImpl());        
    //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程还可以继续使用。        
    es.submit(new RunnableImpl());        
    es.submit(new RunnableImpl());        
    //4.调用ExecutorService中的shutdown(),销毁线程池(不建议执行!)       
    es.shutdown();        
    //线程池都没有了,不能再使用线程。        
    es.submit(new RunnableImpl());//抛出异常:RejectedExecutionException    
  }
}

//2.创建Runnable接口实现类,重写run()
class RunnableImpl implements Runnable{    
  public void run(){        
  System.out.println(Thread.currentThread().getName() + "创建了一个新的线程正在执行");    
  }
}

4. Lambda表达式

4.1 函数式编程思想概述

4.2 Lambda的使用前提

​ Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法

    无论是JDK内置的Runnable、Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。

  2. 使用Lambda必须具有上下文推断

    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为"函数式接口"

4.3 Lambda表达式标准格式

/*    Lambda表达式的标准格式:        
      由三部分组成:            
        a.一些参数            
        b.一个箭头            
        c.一段代码       

      格式:            
        (参数列表) -> {一段重写方法的代码}        

      解释说明格式:            
        ():接口中抽象方法的参数列表,没有参数,就空着;有就写出参数,多个参数之间使用逗号分隔。            
        ->:传递的意思,把方法传递给方法体{}            
        {}:重写接口中的抽象方法的方法体 
*/
public class Demo1Lambda {    
  public static void main(String[] args) {        
    //使用匿名内部类的方式,实现多线程        
    new Thread(new Runnable(){            
      @Override            
      public void run() {                
        System.out.println(Thread.currentThread().getName() + "新线程创建了");            
      }        
    }).start();        

    //使用Lambda表达式,实现多线程        
    new Thread(() -> {            
      System.out.println(Thread.currentThread().getName() + "新线程创建了");        
    }).start();        

    //优化省略Lambda:        
    new Thread(() -> System.out.println(Thread.currentThread().getName() + "新线程创建了")).start();    
  }
}

4.4 Lambda的省略格式

以上是关于多线程的主要内容,如果未能解决你的问题,请参考以下文章

线程学习知识点总结

多个请求是多线程吗

python小白学习记录 多线程爬取ts片段

多线程编程

多线程编程

python多线程