简单认识一下Java多线程

几个基本概念

程序、进程、线程

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。

如:运行中的QQ,运行中的MP3播放器。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

打开任务管理器就可以查看我们电脑上的进程。

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

举个例子:360安全卫士可以同时进行电脑体检、木马查杀、电脑清理等功能。

单核cpu和多核cpu

单核CPU,其实是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务。有多个通道,但是同一时间内能够执行任务的只有一条。

多核CPU,即是可以同时执行多个线程的任务。有多个通道,在同一时间内能够执行任务的有多条通道。能更好的发挥多线程的效率。

一个Java应用程序java.exe,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

一个简单的Java程序都包含多线程,所以多线程的应用是很有必要的。

并发和并行

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

并发:一个CPU(采用时间片)“同时”执行多个任务。比如:秒杀、多个人做同一件事。

它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并

发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行。

使用多线程的优点

背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短(线程之间的切换需要消耗时间),为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

何时需要多线程

使用多线程有很多优点,那么什么时候要用到多线程呢?

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

线程的创建和使用

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。

Thread类的特性:

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

Thread类的一些常用方法:

void start(): 启动线程,并执行对象的run()方法

void run(): 线程在被调度时执行的操作

String getName(): 返回线程的名称

void setName(String name):设置该线程名称

static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类

static void yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。但是当前线程也有可能再次争夺到CPU并继续执行下去。

void join():当某个线程执行过程中调用其他线程的join() 方法时,该线程将被阻塞,直到执行join方法的线程执行完毕;低优先级的线程也可以获得执行

static void sleep(long millis):(指定时间:毫秒)令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。抛出InterruptedException异常

void stop(): 强制线程生命期结束,不推荐使用,目前已经弃用(可能造成死锁)

boolean isAlive():返回boolean,判断线程是否还活着

创建线程方式一:继承Thread类

步骤:

  1. 定义子类继承Thread类
  2. 子类中重写Thread类中的run方法
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

代码示例:

 //1.创建一个Thread类的子类
    class MyThread extends Thread{
    
        //2.重写run方法
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println("子线程myThread: " + i);
            }
        }
    }
    
    public class ThreadTest01 {
    
        public static void main(String[] args) {
            //3.创建Thread类的子类的对象
            MyThread myThread = new MyThread();
            //4.调用start()
            myThread.start();
            for (int i = 21; i < 40; i++) {
                System.out.println("主线程main: " + i);
            }
    
        }
    
    }

运行结果如下:主线程main和子线程myThread交替执行。


创建线程方式二:实现Runnable接口

步骤:

  1. 定义子类,实现Runnable接口
  2. 子类中重写Runnable接口中的run方法
  3. 创建实现Runnable接口的子类对象
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器(Thread(Runnable target))中
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法

代码示例:

//1.创建一个子类实现Runnable接口
  class MyRunThread implements Runnable{
  
      //2.在子类中重写Runnable接口中的run方法
      @Override
      public void run() {
          for (int i = 0; i < 30; i++) {
              System.out.println(i);
          }
      }
  }
  
  public class ThreadTeat03 {
  
      public static void main(String[] args) {
          //3.创建子类对象
          MyRunThread myRunThread = new MyRunThread();
          //4.将子类对象作为参数传入Thread类的构造器中,创建Thread类对象
          Thread thread = new Thread(myRunThread);
          //5.调用Thread类对象的start方法启动线程
          thread.start();
      }
  
  }

继承Thread类方式与实现Runnable接口方式的联系和区别

区别:

  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。

联系:

实际都是重写的Runnable接口中的run方法

 public class Thread implements Runnable

那么实际开发中如何去选择呢?那还是选择实现Runnable接口的方式吧。

使用继承Runnable接口的方式有这些好处:

  1. 避免了单继承的局限性。如果一个类已经是别的类的子类那么无法再使用继承Thread类的方式
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。如果使用继承Thread类的方式,每个线程对象都会有一份自己的资源。

创建线程方式三:实现Callable接口(jdk5新增)

与使用Runnable相比,Callable功能更强大些。

  • 相比run()方法,call()方法可以有返回值
  • call()方法可以抛出异常
  • 支持泛型的返回值

使用Callable接口创建线程需要使用到FutureTask这个类。

先看下帮助文档:
在这里插入图片描述
在这里插入图片描述
创建Thread时,Runnable可以作为参数。

FutureTask是Runnable的实现类,所以可以作为Thread的参数,即FurueTask与Thread有了关联

而创建FutureTask时,Callable可以作为参数。

这时Callable就可以通过FutureTask和Thread建立关系。

使用步骤:

  1. 定义子类,实现Callable接口
  2. 子类中重写Callable接口中的call()方法
  3. 创建实现Callable接口的子类对象
  4. 创建FutureTask类的对象,将Callable接口的子类对象作为实际参数传递给FutureTask类的构造器(FutureTask(Callable callable))中
  5. 创建Thread类的对象,将FutureTask类的对象作为参数传入Thread类的构造器(Thread(Runnable target))中
  6. 调用Thread类的start方法:开启线程,调用Callable接口子类的call方法

代码示例:

//1.定义子类,实现Callable接口
    class MyCallThread implements Callable{
    
        //2.子类中重写Callable接口中的call()方法
        @Override
        public Object call() throws Exception {
            int sum = 0;
            for (int i = 1; i < 100; i++) {
                if(i % 2 == 0){
                    System.out.println(i);
                    sum += 1;
                }
            }
            return sum;
        }
    }
    
    public class CallableTest {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //3.创建实现Callable接口的子类对象
            MyCallThread myCallThread = new MyCallThread();
            //4.创建FutureTask类的对象,将Callable接口的子类对象作为实际参数传递给FutureTask类的构造器(FutureTask(Callable callable))中
            FutureTask<Integer> futureTask = new FutureTask(myCallThread);
            //5.创建Thread类的对象,将FutureTask类的对象作为参数传入Thread类的构造器(Thread(Runnable target))中
            Thread myThread = new Thread(futureTask);
            //6.调用Thread类的start方法:开启线程,调用Callable接口子类的call方法
            myThread.start();
            //获取call()方法的返回值
            Integer sum =  futureTask.get();
            System.out.println(sum);
        }
    
    }

创建线程方式四:使用线程池(jdk5新增)

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

线程池相关的API:

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
  • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
  • void shutdown() :关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
  • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

简单演示一下:

class MyRunnable implements Runnable{
    
        private int sum = 0;
    
        @Override
        public void run() {
            for (int i = 1; i < 100; i++) {
                if(i % 2 == 0){
                    System.out.println(Thread.currentThread().getName() + " : " +i);
                    sum += 1;
                }
            }
            System.out.println("sum = " + sum);
        }
    }
    
    public class ThreadPoolTest {
    
        public static void main(String[] args) {
            //1.创建一个具有固定数量线程的线程池
            ExecutorService service = Executors.newFixedThreadPool(5);
            //2.执行线程的操作
            service.execute(new MyRunnable());
            //3.关闭线程池
            service.shutdown();
    
        }
    
    }

关于线程的调度(优先级)

线程的调度策略主要有两种:

  1. 时间片策略:CPU按时间片去执行线程
  2. 抢占式策略:高优先级的线程抢占CPU

Java中线程的调度方法:

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

Java中线程的优先级分为1~10,数字越大优先级越高

其中有三个优先级常量:

   //最小优先级 
   public final static int MIN_PRIORITY = 1;
   //缺省优先级
    public final static int NORM_PRIORITY = 5;
   //最大优先级
    public final static int MAX_PRIORITY = 10;

相关的方法:

getPriority() :返回线程优先级

setPriority(int newPriority) :改变线程的优先级

注意:线程创建时继承父线程的优先级;低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的生命周期

先看源码,源码中将线程的生命周期分为六个状态:

 public enum State {
            //尚未启动的线程的线程状态
            NEW,
    		//可运行线程的线程状态。
            RUNNABLE,
            //等待监视器锁而阻塞的线程的线程状态
            BLOCKED,
         	//等待线程的线程状态。
        	WAITING,
            //具有指定等待时间的等待线程的线程状态。
            TIMED_WAITING,
    		//终止线程的线程状态。
            TERMINATED;
        
        }

我们通常总结成五种状态:

  1. 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
  5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程的生命周期状态转换图如下:

线程的同步

多线程产生的安全问题:当多条语句在操作线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行(会操作共享数据),导致共享数据的错误。

解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。(涉及操作共享数据的工作让一个线程完成)

Java对于多线程的安全问题提供了专业的解决方式:同步机制

synchronized的使用方法

方式一:同步代码块

  synchronized (同步监视器){
        // 需要被同步的代码;
    }

说明:

  1. 操作共享数据的代码即为需要被同步代码。
  2. 共享数据:多个线程共同操作的变量。
  3. 同步监视器,也称锁。任何一个类的对象都可以充当锁,很多时候也是指定为this或类名.class。如果多个线程需要同步,则它们必须共用一把锁。

方式二:同步方法

  public synchronized void show (String name){}

说明:

  1. 同步方法仍然涉及同步监视器,只不过不需要我们显式声明。
  2. 一个线程类中的所有静态方法共用同一把锁(类名.class,类锁),所有非静态方法共用同一把锁(this,对象锁)

Lock锁的简单使用

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

示例:

  class SafeDoor03 implements Runnable{
  
      private int people = 100;
      //创建Lock对象
      private Lock lock = new ReentrantLock();
  
      @Override
      public void run() {
          while (true){
              try {
                  //锁住
                  lock.lock();
                  if (people > 0) {
                      System.out.println(Thread.currentThread().getName() + "通过安检" + people);
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      people--;
                  } else {
                      break;
                  }
              }catch (Exception e){
                  e.printStackTrace();
              }finally {
                  //释放锁
                  lock.unlock();
              }
  
          }
      }
  }

推荐的使用顺序:Lock -> 同步代码块(已经进入了方法体,分配了相应资源)-> 同步方法(在方法体之外)

死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

产生死锁的必要条件:

  1. 互斥条件:线程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一线程所占用。
  2. 请求和保持条件:当线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个线程—资源的环形链。

演示一个简单的死锁:

public class DeadLock {

    public static void main(String[] args) {

        Object o1 = new Object();
        Object o2 = new Object();

        Thread t1 = new Thread(new T1(o1,o2));
        Thread t2 = new Thread(new T2(o1,o2));

        t1.start();
        t2.start();

    }

}

class T1 implements Runnable{
    Object o1;
    Object o2;

    T1 (Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }

    public void run(){
        //拿到同步监视器o1
        synchronized (o1){
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){}
            //想要获取同步监视器o2
            synchronized (o2){

            }
        }
    }
}

class T2 implements Runnable{
    Object o1;
    Object o2;

    T2 (Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }

    public void run(){
        //拿到同步监视器o2
        synchronized (o2){
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){}
            //想要获取同步监视器o1
            synchronized (o1){

            }
        }
    }
}

这里假设先执行到的是T1的run方法,此时t1线程拿到了同步监视器o1并进入睡眠;然后执行到T2的run方法,此时t2线程拿到了同步监视器o2并进入睡眠;此时t1线程结束睡眠继续往下执行,它想要获取同步监视器o2,但是o2被t2线程拿走了,所以t1线程无法继续执行;同理,t2线程想要获取同步监视器o1,但是o1被t1线程拿走了,所以t2线程无法继续执行。就这样,t1和t2谁也不让谁,都想抢夺对方的同步监视器,陷入了僵局,也就是死锁。

预防死锁:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个线程分配其他的资源:(破坏请求保持条件)
  • 可剥夺资源:即当某线程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个线程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

线程的通信

涉及的方法:

wait():当前线程就进入阻塞状态,并释放同步监视器。等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

notify():唤醒正在排队等待同步资源的其中一个线程

notifyAll():唤醒正在排队等待资源的所有线程

说明:

  1. 这三个方法只有在同步代码块或同步方法中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
  2. 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

举个简单的例子:两个线程交替打印输出数字

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
            	//唤醒其他线程
                obj.notify();
                if(number <= 50){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //调用wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }

    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

面试题:sleep()和 wait()有什么异同?

相同点:都可以使当前线程进入阻塞状态

不同点:

  1. wait()来自Object类成员方法,sleep()来自Thread类静态方法
  2. 如果二者在同步代码块或者同步方法中使用,wait()会释放锁,sleep()是不会释放锁的
  3. wait()必须在同步代码块中使用; sleep()可以在任何地方使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值