java线程的前世今生

多线程

1. 什么是并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程也可以有多个线程的,这个应用程序也可以称之为多线程程序。

3. 线程调度:

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

4. 创建线程类

  • Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
    创建线程的方式总共有两种:
  • 一是通过继承Thread类来创建并启动多线程的步骤如下:
    1). 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    2). 创建Thread子类的实例,即创建了线程对象
    3). 调用线程对象的start()方法来启动该线程
  • 代码演示:
//测试类
public class Demo01 {
     public static void main(String[] args) {    
        //创建自定义线程对象        
        MyThread mt = new MyThread("新的线程!");        
       //开启新线程        
       mt.start();        
        //在主方法中执行for循环        
      for (int i = 0; i < 10; i++) {        
             System.out.println("main线程!"+i);            
         }        
    }    
}
//自定义线程类:
public class MyThread extends Thread {
        //定义指定线程名称的构造方法    
        public MyThread(String name) {    
        //调用父类的String参数的构造方法,指定线程的名称        
       super(name);        
   } 
      //重写run方法,完成该线程执行的逻辑 
  @Override    
  public void run() {    
       for (int i = 0; i < 10; i++) {        
      System.out.println(getName()+":正在执行!"+i);            
     }        
  }    
}
  • 二是实现Runnable接口方式,步骤如下:
    1). 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2). 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    3). 调用线程对象的start()方法来启动线程。
    代码演示:
public class MyRunnable implements Runnable{
  @Override    
  public void run() {    
      for (int i = 0; i < 20; i++) {        
         System.out.println(Thread.currentThread().getName()+" "+i);            
      }        
   }    
}

public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}
  • 通过实现 Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现Runnable接口的类。

  • 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

  • Thread 和Runnable的区别
    如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
    实现Runnable接口比继承Thread类所具有的优势:
    1). 适合多个相同的程序代码的线程去共享同一个资源。
    2). 可以避免java中的单继承的局限性。
    3). 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    4). 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

5. 线程安全

  • 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  • 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

6. 线程同步
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
有三种方式完成同步操作:同步代码块,同步方法,锁机制。

同步代码块
格式:

synchronized(同步锁){
     需要同步操作的代码
}
  • 同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
  • 锁对象 可以是任意类型。有多个线程对象 要使用同一把锁。
  • 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。
  • 代码演示:
public class Ticket implements Runnable{
     private int ticket = 100;    
   //执行卖票操作    
   @Override    
   public void run() {    
   //每个窗口卖票的操作         
    //窗口 永远开启         
   while(true){        
      sellTicket();            
     }        
  }    
   
// 锁对象 是 谁调用这个方法 就是谁     
// 隐含 锁对象 就是  this      
public synchronized void sellTicket(){    
        if(ticket>0){//有票 可以卖  
            //出票操作
            //使用sleep模拟一下出票时间
            try {
               Thread.sleep(100);  
            } catch (InterruptedException e) {
               // TODO Auto‐generated catch block  
               e.printStackTrace();
                           }
            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
  }    
}

Lock 锁
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。
public class Ticket implements Runnable{
      private int ticket = 100;    
     Lock lock = new ReentrantLock();    
   //执行卖票操作    
    @Override    
    public void run() {    
   //每个窗口卖票的操作         
  //窗口 永远开启         
  while(true){        
   lock.lock();            
    if(ticket>0){//有票 可以卖            
    //出票操作                 
    //使用sleep模拟一下出票时间                 
   try {                
        Thread.sleep(50);                    
   } catch (InterruptedException e) {                
    // TODO Auto‐generated catch block                    
            e.printStackTrace();                    
   }                
      //获取当前线程对象的名字                 
         String name = Thread.currentThread().getName();                
         System.out.println(name+"正在卖:"+ticket‐‐);                
               }            
   lock.unlock();            
             }        
      }    
}

7. 线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析
在这里插入图片描述
Timed Waiting

  • Timed Waiting:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待),那么我们通过一个案例加深对该状态的一个理解。
  • 案例:实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。
  public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
                }
            System.out.print(i);
            try {
                Thread.sleep(1000);
               System.out.print("    线程睡眠1秒!\n");  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
    }
}

通过案例可以发现, sleep方法的使用还是很简单的。我们需要记住下面几点:

  • 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  • 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
  • sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
    Timed Waiting 线程状态图:
    在这里插入图片描述
    BLOCKED (锁阻塞)
  • Blocked:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
  • Blocked 线程状态图:
    在这里插入图片描述
    Waiting (无限等待)
  • Wating状态:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
  • waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
  • Waiting 线程状态图:
    在这里插入图片描述
    8.线程间通信
  • 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。比如:线程A用来生成方便面的,线程B用来方便面的,方便面可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
  • 为什么要处理线程间通信: 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
  • 如何保证线程间通信有效利用资源:多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制
  • 等待唤醒机制:就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。
  • 等待唤醒中的方法:
    1). wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 wating。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中。
    2).notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
    3). notifyAll:则释放所通知对象的 wait set 上的全部线程。
  • 调用wait和notify方法需要注意的细节:
    1). wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
    2). wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
    3). wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

关于线程各个状态之间的关系图
在这里插入图片描述

下一篇我们将讲线程池

发布了57 篇原创文章 · 获赞 108 · 访问量 8567

猜你喜欢

转载自blog.csdn.net/weixin_42369886/article/details/104657872
今日推荐