多线程学习
多线程
程序、进程、线程
-
程序是为了完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码,静态的对象。
-
进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——声明周期。
- 程序是静态的,进程是动态的。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
线程,进程可以进一步细化为线程,是一个程序内部的一条执行程序。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
- 一个进程的多个线程共享相同的内存单元/内存地址空间 -> 它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全隐患。
-
一个Java应用程序其实至少有三个线程:main() 主线程、gc() 垃圾回收线程、异常处理线程。如果发生异常,会影响主线程。
-
并行:多个CPU同时执行多个任务。并发:一个CPU(采用时间片轮转)同时执行多个任务。
-
多线程程序的优点:
- 提高应用程序的相应。尤其是图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解和修改。
-
多线程的需求场景:
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
线程的创建和使用
多线程的创建有两种方法:
- 方法一:继承于 Thread 类
- 创建一个继承于 Thread 类的子类
- 重写 Thread 类的 run() 方法 --> 将此线程执行的操作声明在 run() 中
- 创建 Thread 类的子类的对象
- 通过此对象调用 start() 方法
- 方法二:实现 Runnable 接口
- 创建一个实现了 Runnable 接口的类
- 实现类去实现 Runnable 中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 通过 Thread 类的对象调用 start() 方法
两种方式的比较:开发中,优先选择实现 Runnable 接口的方式
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享资源的情况
Thread 类中的常用方法:
- start():(1) 启动当前线程;(2)调用当前线程的 run()
- run():通常需要重写Thread类中的此方法,将创建的线程要执行操作声明在此方法中
- currentThread():静态方法,返回当前执行代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前 cpu 的执行权(多个线程一起争抢)
- join():在线程a中调用线程 b 的 join() 方法,此时线程 a 进入阻塞状态,直到线程 b 完全执行完以后,线程 a 才结束阻塞状态
- sleep(long millis):让线程休眠(阻塞)指定的 millis 毫秒。在指定的时间内是阻塞状态
- isAlive():判断当前线程是否存活
线程的调度
- 调度的策略
- 时间片轮换
- 抢占式:高优先级的线程抢占CPU
- Java的调度方式
- 同优先级线程组成先进先出(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
- 线程的优先级等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 --> 默认的优先级
- 涉及方法
- getPriority():返回线程优先级
- setPriority():设置线程优先级
- 注意
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
线程的生命周期
JDK 中用 Thread.State 类定义了线程的几种状态。Java语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源
- 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程状态转换图

其中 suspend() 和 resume() 已过时,这两个方法可能造成死锁
线程的同步
问题的提出:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据
最初的两种解决方式
方式一:同步代码块
synchronized(同步监视器) {
/*
需要被同步的代码
说明:
1.操作共享数据的代码,即为需要被同步的代码
2.共享数据:多个线程共同操作的变量。
3.同步监视器,俗称:锁。任何一个类的对象都可以来充当锁。
4.锁的要求:多个线程必须要共用同一把锁
5.对于实现类可以考虑用this(当前对象)来充当锁,也可以使用"类名.class"来充当锁
*/
}
方式二:同步方法
如果操作共享数据的方法完整声明在一个方法中,可以将此方法声明成同步方法。
// 以多窗口买票为例
// 继承 Thread 类
class Test1 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while(true) {
if(!sell()) {
break;
}
}
}
//正确写法
private static synchronized boolean sell() { //同步监视器为:Test1.class
//错误写法
//private synchronized boolean sell() 同步监视器为:this
if(ticket == 0) {
return false;
}
System.out.println("票号:" + ticket);
ticket--;
return true;
}
}
//实现 Runnable 接口
class Test2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true) {
if(!sell()) {
break;
}
}
}
private synchronized boolean sell() {
if(ticket == 0) {
return false;
}
System.out.println("票号:" + ticket);
ticket--;
return true;
}
}
总结:
- 同步方法仍然需要涉及到同步监视器,只是不需要我们显示声明
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身
JDK5.0新增方法,Lock(锁)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
举例:以多窗口卖票
class Window implements Runnable {
private int ticket = 100;
// 1. 实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while(true) {
try {
// 2.调用lock()
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票,票号:" + ticket);
ticket--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
Lock与synchronized的异同
- 相同:二者都可以解决线程安全问题
- 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;Lock需要手动的启动同步(lock()),同时结束同步也需要手动释放(unlock())
- Lock是显式锁锁,synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先的使用顺序:
Lock -> 同步代码块(已经进入方法体,分配了相应资源) -> 同步方法(在方法体之外)
同步的好处与坏处
- 同步的方法,解决了线程的安全问题。 --- 好处
- 操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程。 ---坏处
线程死锁问题
什么是死锁?
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
线程间的通信
举例:使用两个线程交替打印1~100.
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while(true) {
synchronized (this) {
notify();
// notifyAll();
if(number <= 100) {
System.out.println(Thread.currentThread().getName() + " : " + number++);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
涉及到的三个方法:
- wait():一旦执行次方法,当前线程进入阻塞状态,并释放锁。
- notify():一旦执行此方法,就会唤醒被wait阻塞的一个线程。若有多个被wait的线程,则唤醒优先级高的
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
说明:
- wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中。
- 这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,否则会出现"IllegalMonitorStateException"异常。
- 这个方法是定义在java.lang.Object类中。
面试题:wait()和sleep()的异同?
- 相同点:一旦执行方法,都可以使当前线程进入阻塞状态
- 不同点:
- 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
- 调用范围不同:sleep()方法可以在任何需要的场景下使用,wait()方法必须使用在同步代码块或者同步方法中。
- 关于是否释放同步监视器(锁):如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放锁,wait()会释放锁。
经典问题:生产者/消费者问题
- 生产者(Productor)将产品交给店员(Clerk), 而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 可能会出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
// 简单实现
class Product {
private int productCount = 0;
// 生产产品
public synchronized void produceProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + "生产第" + productCount + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public synchronized void consumeProduct() {
if(productCount > 0) {
System.out.println(Thread.currentThread().getName() + "消费第" + productCount + "个产品");
productCount--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer implements Runnable {
private Product product;
public Producer(Product product) {
this.product = product;
}
@Override
public void run() {
System.out.println("开始生产");
while(true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.produceProduct();
}
}
}
class Customer1 implements Runnable {
private Product product;
public Customer1(Product product) {
this.product = product;
}
@Override
public void run() {
System.out.println("开始消费");
while(true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.consumeProduct();
}
}
}
JDK5.0新增线程创建方法
新增方法一:实现Callable接口
-
与Runnable相比,Callable功能更加强大
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTaks类,比如获取返回结果
-
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类。
- FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
-
实现方法
- 创建一个实现Callable接口的实现类
- 实现Call()方法,将此线程需要执行的操作放入Call()中
- 创建Callable接口实现类的对象
- 创建FutureTask对象,并将Callable接口实现类的对象传递给构造器
- 将FutureTask对象传递给Thread类的构造器中,并调用start()方法
- 若要获取返回值,需要调用FutureTask中的get()方法
新增方式二:使用线程池
-
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
-
好处:
- 提高效应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理,如:
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- ...
-
JDK5.0起提供了线程池相关的API:ExecurtorService和Executors
-
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
-
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n): 创建一个线程池, 它可安排在给定延迟后运行命令或者定期地执行。
-
-
实现方法
- 提供指定线程数量的线程池
- 执行指定的线程的操作。需要提供实现Runnable接口或者Callable接口的实现类
- 使用完后关闭线程池

浙公网安备 33010602011771号