基于进程和线程的多任务,其最小的调度单位分别是进程和线程。

在基于线程的环境中,单个进程可以同时处理不同的任务,每个线程共享地址空间。

基于线程的多任务和基于进程的相比,开销小。相互间的通信和上下文切换开销不同。

Java 的线程模型

Java 的运行时系统使用多线程,当某一个线程阻塞时,不影响其他正在执行的线程。如果是单核处理器,线程轮流使用 CPU 时间片;如果是多核处理器,多个线程可以同时处理。

线程有 running、ready、run、suspended、resumed、blocked、terminated 这些状态。

Java 中线程优先级由一个整数指定,某个线程的优先级是相对于其他线程而言的。优先级在上下文切换时使用。上下文切换的情形:

  • 一个线程自愿放弃 CPU 的使用
  • 高优先级的线程抢占低优先级线程的 CPU 时间。

如果至少有两个相同优先级线程竞争 CPU 时间片,此时根据 OS 的不同有不同的行为。对于部分 OS,相同优先级的线程以 CPU 时间片轮转的方式调度线程;对于另一部分 OS,当某个线程不主动放弃 CPU 的使用,其他同优先级的线程无法运行。

monitor 是一种控制机制,可以将其看作一个小盒子,任一时刻仅允许一个线程位于其中。当一个线程在里面时,所有其他线程必须等待,直到这个线程退出盒子。用来保证共享资源在任何时刻都由一个线程使用。

Java 中,每一个对象隐含有 monitor 机制。某个线程调用一个对象的同步方法时,该对象的其他所有同步方法都不能被其他线程调用。

Java 提供了一种清晰、低成本的方式用于线程间通信。消息系统允许一个线程进入一个对象的同步方法,等待直到另一个线程通知它退出。

Java 的多线程有 Thread 类和 Runnable 接口,Thread 封装了执行的线程。为创建一个线程,要么继承 Thread 类要么实现 Runnable 接口。

主线程

Java 程序启动时,有一个线程立即执行,这个线程称为主线程。所有其他线程都由主线程产生;主线程是最后一个结束执行的线程,因为它需要执行各种终止操作。

public static Thread currentThread() 方法返回当前线程的引用,可以获得主线程的引用。

打印输出线程时,依次打印线程名、优先级和线程组名。线程组是控制作为一个整体的多个线程状态的数据结构。

创建线程

实现 Runnable 接口来新建线程的方式为:首先定义一个实现 Runnable 接口的类,该类必须实现 Runnable 接口中的 public void run() 方法,在 run() 方法里面,定义的是新线程的执行逻辑,可以看作是另一个程序的执行逻辑。在这个方法里面,允许定义类、变量和调用方法等操作。在这个类里面,需要实例化一个 Thread 类对象,构造器之一为 Thread(Runnable threadObj, String threadName)。新线程创建后,并不会执行,需要调用 Thread 类中的 start() 方法才会开始执行。start() 方法本质上调用了 run() 方法。

// 定义实现了 Runnable 接口的类 A,实现该接口的 run 方法
// run 方法中的内容即为子线程的执行内容
// 在 A 中实例化 Thread 类
// 以上创建完成子线程

// 使用子线程
// 实例化类 A
// 调用类 A 中 Thread 类实例的 start 方法

class A implements Runnable {
  Thread t;
  
  A() {
    t = new Thread(this, "ThreadName");
  }
  
  public void run() {
    try {
	  for (int i = 5; i >= 1; i--) {
	    System.out.println("child: " + i);
		Thread.sleep(500);
	  }
	} catch (InterruptedException e) {
	  System.out.println("child interrupted");
	}
	System.out.println("exit child thread");
  }
}

public class T {
  public static void main(String[] args) {
    A a = new A();
	a.t.start();
	
	try {
	  for (int i = 5; i >= 1; i--) {
	    System.out.println("main: " + i);
	    Thread.sleep(1000);
	  }
	} catch (InterruptedException e) {
	  System.out.println("main interrupted");
	}
	System.out.println("exit main thread");
  }
}

创建线程的另一种方法是定义 Thread 类的子类,需要实现 Thread 中的 run() 方法。调用该类的 start() 方法就可以启动新线程。

class CustomThread extends Thread {
  CustomThread() {
    super("ThreadName");
  }
  
  public void run() {
    try {
	  for (int i = 5; i >= 1; i--) {
	    System.out.println("child: " + i);
	    Thread.sleep(500);
	  }
	} catch (InterruptedException e) {
	  System.out.println("child interrupted");
	}
	System.out.println("exit child thread");
  }
}

class B {
  public static void main(String[] args) {
    CustomThread c = new CustomThread();
    c.start();

    try {
      for (int i = 5; i >= 1; i--) {
        System.out.println("main: " + i);
        Thread.sleep(1000);
      }
    } catch (InterruptedException e) {
      System.out.println("main interrupted");
    }
    System.out.println("exit main thread");
  }
}

具体使用上述两种方法中的哪一种取决于实际情况。实现 Runnable 接口则可以继承其他类;如果不重写 Thread 类中的其他方法,继承 Thread 的方式更简单。

创建多线程

和上一部分一样,只不过调用时创建了一个类的多个实例,分别调用每个实例的 start() 方法。

使用 isAlive() 和 join()

调用 Thread 类的 final boolean isAlive() 方法的线程如果正在执行则返回 true,否则返回 false。

如果在线程 A 中,在线程 B 上调用了 final void join() 方法,则 A 线程会等待,直到线程 B 执行完成后,A 线程恢复执行。

线程优先级

当线程调度器决定运行哪个线程时,会选择优先级高的线程运行。
final void setPriority(int level) 方法设置线程的优先级,可选值范围从 MIN_PRIORITY(1) 到 MAX_PRIORITY(10),默认的优先级为 NORM_PRIORITY(5)。
final int getPriority() 方法获得线程的优先级。

同步

当一个资源被多个线程同时访问时,需保证在任何时候,只有一个线程在使用该资源,这种机制称为同步。monitor 是作为互斥锁的对象,当某个线程在某一时刻获得了锁,,则称该线程进入了 monitor。此时,其他尝试进入 monitor 的线程在已经进入 monitor 的线程退出之前状态变为 suspended,也称等待 monitor。进入 monitor 的线程可以再次进入 monitor。

在 Java 中,使用同步比较简单。每个对象都有与之关联的隐式 monitor。当一个对象的 synchronized 方法被调用时,当前线程进入 monitor,任何调用该对象 synchronized 方法的其他线程必须等待,直到当前线程从 synchronized 方法返回。

class Nchr {
  synchronized void call(String msg) {
    System.out.print("a-" + msg);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("-end");
  }
}

class Ca implements Runnable {
  Nchr obj;
  String msg;
  Thread t;
  
  public Ca(Nchr n, String m) {
    obj = n;
    msg = m;
    t = new Thread(this);
  }
  
  public void run() {
    obj.call(msg);
  }
}

public class Out {
  public static void main(String[] args) {
    Nchr n = new Nchr();
    Ca one = new Ca(n, "one");
    Ca two = new Ca(n, "two");
    Ca three = new Ca(n, "three");

    one.t.start();
    two.t.start();
    three.t.start();
    /* 输出
            a-one-end
            a-two-end
            a-three-end
    */
    try {
      one.t.join();
      two.t.join();
      three.t.join();
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
  }
}

如果想要调用的方法是非 synchronzied 方法,但又想使用同步,可以使用同步块。如下:

// 同步块括号中的引用表示需要同步的对象
// 上一个代码段中 Ca 类的 run 方法改写的等价形式
public void run() {
  synchronized(t) {
    t.call(msg)
  }
}

线程间通信

在 Java 中,Object 类中的 wait()notify()notifyAll() 方法用于线程间通信。这三个方法必须在 synchronized 上下文中使用。

  • final void wait():让调用该方法的线程放弃 monitor 进入休眠状态,等待直到另一个线程进入这个 monitor 并调用 notify()notifyAll() 方法。
  • final void notify():唤醒在同一个对象上调用了 wait() 方法的一个线程。
  • final void notifyAll():唤醒所有在同一个对象上调用了 wait() 方法的线程,给予其中某个线程访问权限。

使用 wait() 方法的线程有可能不是通过调用 notify()notifyAll() 方法唤醒的。wait() 方法应该在检查条件的循环中使用。

class Q {
	int value;
	boolean isSet = false;
	
    // synchronized 方法必须执行完,其他方法才能执行
	synchronized void put(int value) throws InterruptedException {
        // 值已经设置了,则挂起
		while (isSet) {
			wait();
		}
		this.value = value;
		System.out.println("put: " + value);
		isSet = true;
		notify();
	}
	
	synchronized void get() throws InterruptedException {
		while (!isSet) {
			wait();
		}
		int value = this.value;
		System.out.println("get: " + value);
		isSet = false;
		notify();
	}
}

class Producer1 implements Runnable {

	Thread thread;
	private Q q;
	
	Producer1(Q q) {
		thread = new Thread(this);
		this.q = q;
	}
	
	@Override
	public void run() {
		int i = 0;
		while (i <= 20) {
			try {
				q.put(i++);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
}

class Consumer1 implements Runnable {
	Thread thread;
	private Q q;
	
	Consumer1(Q q) {
		thread = new Thread(this);
		this.q = q;
	}

	@Override
	public void run() {
		while (true) {
			try {
				q.get();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

public class PCFixed {
	public static void main(String[] args) {
		Q q = new Q();
		Producer1 p = new Producer1(q);
		Consumer1 c = new Consumer1(q);
		p.thread.start();
		c.thread.start();
	}
}

一个线程 a 进入对象 X 的 monitor 中,同时另一个线程 b 进入对象 Y 的 monitor 中。然后, a 调用 Y 的 synchronized 方法, b 调用 X 的 synchronized 方法。此时,两个线程都需无限等待,发生死锁。

挂起、恢复和停止

线程执行状态的改变通过设置一个状态变量决定。在 run() 方法中根据状态变量使用 wait() 方法挂起,在其他方法中设置状态变量的值,有时需要调用 notify() 方法唤醒。

获得线程状态

Thread.State getState() 方法返回调用该方法时线程所处的状态。由于该方法返回结果之后,线程的状态可能已发生变化,所以不能用于线程同步。Thread.State 的取值有:

说明
BLOCKED 因等待锁挂起
NEW 没有开始执行
RUNNABLE 正在执行或者正等待CPU时间将执行
TERMINATED 完成执行
TIMED_WAITING 挂起一段时间,如 sleep
WAITING 因等待事件发生挂起

使用工厂方法创建和启动线程

如果需要使用一条语句创建并启动线程,而不是分两步进行,可以使用工厂方法。工厂方法是一个静态方法,在这个方法里面,创建类的实例并启动线程,然后返回对实例的引用。

class NewThread {
  public static NewThread createAndStart() {
    var t = new NewThread();
    t.start();
    return t;
  }
}

var n = NewThread.createAndStart();

参考

[1] Herbert Schildt, Java The Complete Reference 11th, 2019.

 posted on 2024-04-18 15:27  x-yun  阅读(9)  评论(0编辑  收藏  举报