10.1 程序、进程、线程
程序(program)是为完成特定任务、用某种语言编写的一段静态代码。
进程(process)是程序的一次执行。进程是资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread)是进程的进一步细化。若一个进程可以同时并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,拥有独立的运行栈和程序计数器(pc),线程切换的开销更小。一个进程中的多个线程共享相同的内存单元,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享资源可能带来安全隐患。
并行:多个CPU同时执行多个任务。
并发:一个CPU以时间片为单位轮流执行多个任务,宏观上看是同时执行的。
何时需要多线程:
就绪状态:线程对象调用start()之后,此时它已具备了运行的条件,只是没分配到CPU。
运行状态:就绪状态的线程获得 CPU 资源并执行 run()。
阻塞状态:线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源。在睡眠时间已到或获得资源后可以重新进入就绪状态。
死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
10.3 线程创建和使用
每个线程都通过run()方法来完成操作的,run()由JVM自动调用,不可确定时间。通过对象的start()来启动,start()仅可调用一次。
// Thread类的部分构造器
Thread()
Thread(String threadname)
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable的run方法
方法一:继承Thread类
- 创建子类继承Thread;
- 重写Thread类的run()方法,写明线程执行逻辑
- 创建Thread类对象并调用start方法启动线程
方法二:实现Runnable接口
- 定义声明Runnable接口的类
- 重写run方法
- 将声明Runnable接口的子类对象作为实际参数传递给Thread类的构造器
- 调用Thread类的start方法启动线程
两种创建方式的对比:
Runnable天然就可实现多个线程共享同一个接口实现类的对象,非常适合多个相同线程处理共享资源,继承实现就必须加static。开发当中优先选择Runnable方式。
10.4 线程调度
Java的调度方法:同优先级线程组成先进先出队列,使用时间片策略。对高优先级,使用优先调度的抢占策略。
Java 线程优先级取值范围是整数 1(Thread.MIN_PRIORITY )到10 (Thread.MAX_PRIORITY )。默认优先级为 NORM_PRIORITY(5),线程在创建时继承父线程的优先级。线程优先级不能保证线程执行顺序,非常依赖于平台。
10.5 线程的有关方法
void start():启动线程,并执行对象的run()方法
run():线程被调度时的执行逻辑
String getName()
void setName(String name)
static Thread currentThread():返回当前线程。
static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
join() :调用join()方法的线程执行之前,其他线程都将被阻塞,等同于将并发改为串行
static void sleep(long millis): 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
stop():强制线程生命期结束,不推荐使用
boolean isAlive()
getPriority()
setPriority()
10.6 线程的同步
当多个线程在操作同一共享数据时,一个线程的语句只执行了一部分,另一个线程就开始执行。这有可能会导致共享数据发生错误,产生线程不安全问题。
解决办法:对操作共享数据的多条语句,要求一个线程在执行完之前其他线程不可参与执行。Java提供了同步机制。
// 同步代码块
synchronized(同步监视器){操作共享数据的代码}
// 同步方法
public synchronized void show (){...}
同步监视器,俗称“锁”。任何一个类对象都可以作为锁,同步的多个线程必须使用同一把“锁”。在声明Runnable接口的类中,直接在run方法的同步监视器中传入this即可保证多个此线程并发时的线程安全。使用继承Thread的方法需要将同步方法声明为static,同步监视器是对象本身。使用synchronized包含的代码块既不能多也不能少,多了导致性能下降,少了则不能保证同步。
只有在线程的同步代码执行结束或在同步代码中执行了wait()方法才会释放锁;线程执行同步代码时调用sleep()、yield()暂停当前线程或其他线程调用了该线程的suspend()方法将该线程挂起均不会释放锁。
为了防止死锁问题的出现,应尽量减少同步资源的定义、避免使用嵌套同步。
Lock(锁)
通过显示定义同步锁来实现同步,同步锁使用Lock对象充当。ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,可以显式加锁和释放锁。
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
// 注意:一定要使用unlock方法释放锁,否则线程同步将不会停止。如果同步代码有异常,要将unlock()写入finally语句块
- synchronized与Lock的比较:
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且提供了更多子类。
10.7 线程通信
例:使用两个线程打印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 (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
一开始两个线程均处于就绪状态,当线程A抢到资源时,其执行notify唤醒线程B,由于锁的原因,B只能等待,当A执行wait阻塞并释放了锁,此时没有和B抢资源的线程,B必定获得资源,因此B进入又唤醒了A。
- wait():令当前线程挂起并放弃CPU、锁并等待,直到被另一线程对象唤醒为止。
- notify:唤醒正在排队等待的同步资源的线程中优先级最高的线程
- notifyAll:唤醒正在排队等待资源的所有线程
这三个方法只有在线程同步方法和代码块中才能使用,而且由同步监视器进行调用。因为这三个方法必有锁对象调用,而任意对象都可以作为同步锁,因此这三个方法在Object类中声明。
- sleep和wait方法的异同
- 相同点:一旦执行方法,都可使得当前线程进入阻塞状态
- 不同点:两个方法声明的位置不同,Thread类中声明sleep,object类中声明wait
- sleep可在任何需要的场景下调用,wait只能在同步代码块或同步方法中调用
- 如果两个方法都使用在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁 ```java 生产者/消费者问题 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果满了,店员会叫停生产者,如果有空位了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。
分析: 共享数据:产品(线程同步) 共享操作:生产产品(生产者线程)与消费产品(消费者线程) 涉及到满或空时,唤醒或停止线程(线程通信) 涉及对象Productor(线程)、Customer(线程)、Clerk(由生产者和消费者公用,存放共享数据与同步代码块)
```java
class Clerk { // 售货员
private int product = 0; // 共享数据
// 下面的两个方法进行同步,是共享的操作,同步监视器是Clerk
// 生产产品
public synchronized void addProduct() {
if (product >= 20) { // 超出则停止生产,等待唤醒
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了第" + product + "个产品");
notifyAll(); // 唤醒对面的消费者
}
}
// 消费产品
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait(); // 不足等待唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" + product + "个产品");
product--;
notifyAll(); // 取走则唤醒对面的生产者
}
}
}
class Productor implements Runnable { // 生产者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable { // 消费者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
}
}
10.8 JDK5.0新增用于创建线程方式
新增方式一:声明Callable接口
该方法相比run方法可以有返回值;可以抛出异常;支持泛型;需要借助FutureTask类。
Future接口可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等,FutureTask同时实现了Runnable和Future接口,它既可以作为Runnable被线程执行,也可以作为Future得到的Callable的返回值
class NumThread implements Callable{
//实现callable接口并实现其call方法
public Object call() throws Exception{
int sum = 0;
return sum; // 返回对象
}
}
public class ThreadNew{
public static void main(String[] args){
// 创建实现Callable接口的对象
NumThread numThread = new NumThread;
// 创建FutureTask对象
FutureTask futureTask = new FutureTask(numThread);
// 放入Runnable接口后启动线程
new Thread(futureTask).start();
try{
// 获取线程执行返回值
Object sum = futureTask.get();
}
catch(Exception e){}
}
}
新增方式二:使用线程池
经常创建和销毁线程将会快速的消耗系统资源,对性能影响大。提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,实现重复利用,从而提高响应速度、降低资源消耗、便于线程管理。
ExecutorService:线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行一个指定线程任务,没有返回值
<T> Future<T> submit(Callable<T> task):执行任务,有返回值
void shutdown() :关闭连接池
Executors:工具类,用于创建并返回不同类型的线程池,返回ExecutorService
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建在给定延迟后运行命令或者定期执行的线程池。
可以将ExecutorService强制转化为ThreadPoolExecutor,这样可以在执行前调用一些set方法进行属性设置,对线程进行管理。如:corePoolSize(核心池的大小),maximunPoolSize(最大线程数),keepAliveTime(线程没有任务时最多保持多长时间后停止)等。