多线程的概念

程序、进程 and 线程

  • 程序:为了完成特定任务的一段静态代码,静态对象
  • 进程:程序的一次执行过程,或者运行中的应用程序
  • 线程:由进程进一步细分为线程,即程序内部执行的一条路径

一个进程同一时间若并行执行多个线程,就是支持多线程的

线程调度

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

多线程程序的优点

  1. 提高应用程序的响应速度,增加图形化界面,用户体验
  2. 提高计算机系统的 cpu 利用率
  3. 改善程序结构,将复杂的进程分成多线程,独立运行

并发和并行

  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻, 有多条指令在多个 CPU 上同时执行。比如:多个人同时做不同的事
  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间 内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果

多线程的创建方式

继承 Thread 类

步骤

  1. 定义 Thread 的子类,重写 run 方法,run 方法体就是该线程要完成的任务
  2. 创建 Thread 子类的实例,即创建线程对象
  3. 调用线程对象的 start 方法启动

例如

public class MyThread extends Thread {
  // 构造方法,指定线程名字
  public MyThread(String name){
    super(name);
  }
  // 重写 run 方法
  @Override
  public void run(){
    // 方法体
  }
}

// 测试
public class testMyThread {
  public static void main(String[] args){
    // 创建 线程对象
    MyThread mt1 = new MyThread("子线程");
    // 开启子线程
    mt1.start();
  }
}

注意点

  1. 如果手动调用 run 方法 ,即就是普通的方法,没有开启多线程
  2. run 方法由 jvm 调用,时机取决于操作系统的 cpu调度
  3. 启动多线程 start 方法
  4. 一个线程对象只能调用一次 start 方法启动,如果重复调用了,则将抛出 以上的异常 IllegalThreadStateException

Runnable 接口

步骤

  1. 定义 Runnable 接口实现类,重写 run 方法
  2. 创建 Runnable 接口实现类的实例,此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正的线程对象
  3. 调用线程对象的 start()方法,启动线程

例如

public class MyRunnable implements Runnable{
  // 重写 run 方法
  @Override
  public void run(){
    // 方法体
  }
}
// 测试
public class testMyRunnable {
  public static void main(String[] args){
    // 创建自定义类对象 线程任务对象
    MyRunnable mr1 = new MyRunnable();
    // 创建线程对象
    Thread t = new Thread(mr1,"子线程"):
    // 开启子线程
    t.start();
  }
}

**说明:**Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含 的 run()方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是该Thread 线程负责执行其 target 的 run()方法

匿名内部类实现多线程

new Thread("新的线程"){
  @Override
  public void run(){
    // 方法体
  }
}.start();

new Thread(new Runnable(){
  @Override
  public void run(){
    // 方法体
  }
}).start();

实现和继承创建多线程的区别

  • 继承 Thread:线程代码存放 Thread 子类 run 方法中

  • 实现 Runnable:线程代码存在接口的子类的 run 方法

Runnable 的优势

  • 避免了单继承的局限性

  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源

  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立


JDK5.0 新增线程创建方式

实现 Callable 接口

步骤

  1. 创建一个 实现 Callable 的实现类
  2. 实现 call 方法,该线程要执行的操作,声明在 call 方法中
  3. 创建 Callable 接口实现类的对象
  4. 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中, 创建 FutureTask 的对象
  5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start 方法
  6. 获取 Callable 中 call 方法的返回值

举例

public class NumThread implements Callable {
  @Override
  public Object call() throws Exception {
    int sum = 0;
    for(int i = 0; i <= 10; i++){
      sum+=i;
    }
    return sum;
  }
}

public class CallableTest {
  public static void main(String[] args) {
    NumThread n1 = new NumThread();
    FutureTask f1 = new FutureTask(n1);
    new Tread(f1).start();
    
    try{
      // 	get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值
      Object sum = futureTask.get();
      System.out.println("总和为:" + sum);
      
    }catch(InterruptedException e ){
      e.printStackTrace();
    }catch(ExecutionException e){
      e.printStackTrace();
    }
    
  }
}

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

  • 相比 run 方法 具有返回值,支持泛型
  • 方法可以抛异常

缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低

线程池

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

实现步骤:

  1. 提供指定线程数量的线程池
  2. 执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接 口实现类的对象
  3. 关闭连接

例如:

//  Runnable 接口或 Callable 接口实现类的对象

class NumberThread implements Runnable{
   @Override
   public void run() {
     for(int i = 0;i <= 100;i++){
       if(i % 2 == 0){
         System.out.println(Thread.currentThread().getName() +
        ": " + i);
       }
     }
   }
}
class NumberThread1 implements Runnable{
	@Override
  public void run() {
    for(int i = 0;i <= 100;i++){
      if(i % 2 != 0){
        System.out.println(Thread.currentThread().getName() +
				": " + i);
      }
    }
  }
}
class NumberThread2 implements Callable {
   @Override
   public Object call() throws Exception {
     int evenSum = 0;//记录偶数的和
     for(int i = 0;i <= 100;i++){
       if(i % 2 == 0){
       evenSum += i;
     }
		}
     return evenSum;
   }
}

// 线程池
public class ThreadPoolTest {
 public static void main(String[] args) {
   //1. 提供指定线程数量的线程池
   ExecutorService service = Executors.newFixedThreadPool(10);
   ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
   //设置线程池中线程数的上限
   service1.setMaximumPoolSize(50);
   // 执行指定的线程的操作。
   service.execute(new NumberThread());//适合适用于 Runnable
   service.execute(new NumberThread1());//适合适用于 Runnable
   try {
     //适合使用于 Callable
 		Future future = service.submit(new NumberThread2());
 		System.out.println("总和为:" + future.get());
   } catch (Exception e) {
     e.printStackTrace();
   }
   // 关闭线程池
   service.shutdown();
 }
}

好处

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关 API

  • ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
    1. void execute(Runnable command) :执行任务/命令,没有返回值, 一般用来执行 Runnable
    2. Future submit(Callable task):执行任务,有返回 值,一般又来执行 Callable
    3. void shutdown() :关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线 程池对象
    1. Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    2. Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
    3. Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    4. Executors.newScheduledThreadPool(int corePoolSize):创建 一个线程池,它可安排在给定延迟后运行命令或者定期地执行

Thread 类常用的方法

构造器

  • public Thread():分配一个新线程对象
  • public Thread(String name):指定名字分配新线程对象
  • public Thread(Runnable target):指定创建线程目标对象,实现 Runnable 接口的 run 方法
  • public Thread(Runnable target, String name):指定创建线程目标对象,并且指定名字

常用方法

  • public void run() :此线程要执行的任务在此处定义代码

  • public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法

  • public String getName() :获取当前线程名称

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

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类

  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时 停止执行)

  • public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新 调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这 个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调 度器又将其调度出来重新执行

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未 终止,则为活动状态

  • void join() :等待该线程终止

    • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间到将不再等待
    • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
  • public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入 死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据 库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处 理,出现数据不一致的问题

  • void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成 对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁 资源,导致其它线程都无法访问被它占用的锁,直到调用 resume()。已过时,不建议使用

优先级

每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服 务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行 机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级

Thread 三个优先级常量

  • MAX_PRIORITY(10):最高优先级
  • MIN_PRIORITY(1):最低优先级
  • NORM_PRIORITY(5):普通优先级,默认情况下 main 线程具有普通优先级

常用方法:

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

多线程的生命周期

JDK1.5前:五种状态

  1. 新建(new)
  2. 就绪(Runnable)
  3. 运行(Running)
  4. 阻塞(Blocked)
  5. 死亡(Dead)

CPU 需要在多条线程之间切 换,于是线程状态会多次在运行、阻塞、就绪之间切换

JDK1.5 及之后:6 种状态

  1. New(新建):线程刚被创建,但是并未启动。还没调用 start 方法
  2. Runnable(可运行):这里没有区分就绪和运行状态
  3. Teminated(被终止):表明此线程已经结束生命周期,终止运行
  4. 阻塞状态分为三种:
    1. BLOCKED(锁阻塞):一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会
    2. TIMED_WAITING(计时等待):一个正在限时等待 另一个线程执行一个(唤醒)动作的线程处于这一状态
    3. WAITING(无限等待):一个正在无限期等待另一个线 程执行一个特别的(唤醒)动作的线程处于这一状态

说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态

线程安全问题

当我们用多线程去访问同一资源(文件,变量,记录等),如果都有读写操作就会容易出现线程安全问题

常见案例:火车站买票,有重复票或负数票问题

同步机制解决线程安全问题

多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java 中提供了同步机制 (synchronized)来解决

为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程 只能在外等着(BLOCKED)

原理

给某段代码加上锁,线程执行这段代码首先获得锁,我们称它为同步锁,线程获得了同步锁对象之后,同步锁对象就会记录这个线程的 ID,其他线程就只能等待了,除非这个线程释放了锁对象,其他线程才能重新 获得/占用同步锁对象

同步代码块

synchronized 关键字,用于某个区块前面,表示只对这个区块 的资源实行互斥访问

格式:

synchronized(同步锁对象){
  // 同步的代码
}

注:同步锁对象可以是任意类型,但是必须保证竞争同一个共享资源的多个线程必须使用同一个同步锁对象,常指定为 this 或类名.class

public class Thread1 implements Runnable {
  public void run() {
    synchronized(this) {
      for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + ” synchronized loop ” + i);
      }
    }
  }
  public static void main(String[] args) {
    Thread1 t1 = new Thread1();
    Thread ta = new Thread(t1, “A”);
    Thread tb = new Thread(t1, “B”);
    ta.start();
    tb.start();
  }
}

/**结果:
A synchronized loop 0
A synchronized loop 1
A synchronized loop 2
A synchronized loop 3
A synchronized loop 4
B synchronized loop 0
B synchronized loop 1
B synchronized loop 2
B synchronized loop 3
B synchronized loop 4
**/
public class Thread2 {
  public void m4t1() {
    synchronized(this) {
      int i = 5;
      while( i– > 0) {
      	System.out.println(Thread.currentThread().getName() + ” : ” + i);
      try {
      	Thread.sleep(500);
      } catch (InterruptedException ie) {
        	e.printStackTrace();
      }
     }
    }
  }
  public void m4t2() {
    int i = 5;
    while( i– > 0) {
    System.out.println(Thread.currentThread().getName() + ” : ” + i);
    try {
    	Thread.sleep(500);
    } catch (InterruptedException ie) {
      	e.printStackTrace();	
    }
    }
  }
  public static void main(String[] args) {
    final Thread2 myt2 = new Thread2();
    Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, “t1” );
    Thread t2 = new Thread( new Runnable() { public void run() { myt2.m4t2(); } }, “t2” );
    t1.start();
    t2.start();
  }
}

/**
结果:
t2 5
t2 4
t2 3
t2 2
t2 1
t1 5
t1 4
t1 3
t1 2
t1 1
**/

同步方法

synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着

 public synchronized void method(){ 可能会产生线程安全问题的代码 }

解决懒汉式线程安全问题

public class LazyOne {
  private static LazyOne instance;
  private LazyOne(){}
  public static synchronized LazyOne getInstance(){
 		if(instance == null){
 			instance = new LazyOne();
 		}
 	return instance;
 	}
  
  // 方式2
	public static LazyOne getInstance2(){
    if(instance == null){
      synchronized (LazyOne.class) {
        try {
          Thread.sleep(10);//加这个代码,暴露问题
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        if(instance == null){
          instance = new LazyOne();
        }
      }
    }
   	return instance;
	}
}

 /*
 注意:上述方式 2中,有指令重排问题
 mem = allocate(); 为单例对象分配内存空间
 instance = mem; instance 引用现在非空,但还未初始化
 ctorSingleton(instance); 为单例对象通过 instance 调用构造器
 从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,
 然后复制到主存储区。但是需要 
 volatile 关键字,避免指令重排。
 */

JDK5.0 新特性:Lock(锁)

Lock 锁也称同步锁,加锁与释放锁方法如下:

  • public void lock() :加同步锁
  • public void unlock() :释放同步锁
class A{
  //1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
  private final ReentrantLock lock = new ReenTrantLock();
  public void m(){
    //2. 调动 lock(),实现需共享的代码的锁定
    lock.lock();
    try{
      //保证线程安全的代码;
    }
    finally{
      //3. 调用 unlock(),释放共享代码的锁定
      lock.unlock(); 
    }
  }
}

synchronized 与 Lock 的对比

  • Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了 作用域、遇到异常等自动解锁
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多的子类),更体现面向对象
  • Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
  • Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁, synchronized 不可以
  • 开发中使用顺序:Lock ----> 同步代码块 ----> 同步方法

线程通信

当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据

比如:线程 A 用来生产包子的,线程 B 用来吃包子的,包子可以理解为同一资 源,线程 A 与线程 B 处理的动作,一个是生产,一个是消费,此时 B 线程必须 等到 A 线程完成后才能执行,那么线程 A 与线程 B 之间就需要线程通信,即— — 等待唤醒机制。

等待唤醒机制

在一个线程满足,就进入等待状态(wait() / wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()

或可以指定 wait 的时间,等时间到了自动唤醒

在有多个线程进行等待时,如果需要,使用 notifyAll()来唤醒所有的等待线程

``wait/notify` 就是线程间的一种协作机制

  1. wait:线程不再活动,不再参与调度,线程状态是 WAITING 或 TIMED_WAITING,要等着别的线程执行 通知(notify),或者等待时间到,在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列 (ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程

例如:使用两个线程打印 1-100。线程 1, 线程 2 交替打印

class Communication implements Runnable {
  int i = 1;
  public void run(){
    while(true){
      synchronized(this){
        notify();
        if(i<=100){
          System.out.println(Thread.currentThread().getName() + ":" + i++);
         }else{
           break;
         }
        try{
          wait();
        }catch(e){
          e.printStackTrace();
        }
                             
      }
    }
  }
}

注意:

  1. wait 和 notify 必须是同一个锁对象调用
  2. wait 和 notify 都是属于 Object 类的方法
  3. wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用

参考资料

尚硅谷Java基础