进程与线程的区别就不过多去说了吧,进程和线程都是由操作系统进行管理与调度的,不同的是,进程与进程之间数据并不共享,各自拥有自己独立的数据存储空间。然而同一个进程下面的线程之间是可以共享进程的内存空间的。本来将从 Java 中最简单的多线程程序一点一点来讲解 Java 中多线程编程的一些方法。
0x01 实现多线程类的两种方式
继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
实现 Runnable 接口
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
相比继承 Thread 类,实现 RUnnable 接口的好处
- 避免了 Java 单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好体现了面向对象的设计思想
创建并启动线程
创建类的对象方式
MyThread thread1 = new MyThread(); thread1.start();
创建 Thread 类的对象方式
Thread thread1 = new Thread(new MyThread()); thread1.start();
注意⚠️: 线程在启动应该调用其 start()
方法,而不是 run()
方法,如果调用的是 run()
方法,则程序执行是阻塞式的。只有在调用 start()
方法后,才是通过线程的方式进行执行。
0x02 设置并获取线程名称
Thread 类提供了两个方法类设置和获取线程的名称:
void setName(String name)
设置线程的名称String getName()
获取线程名称
注: 通过 Thread.getCurrentThread()
可以获取当前正在执行的线程对象的引用。
0x03 线程调度
线程调度的两种模型:
- 分时调度模型: 所有线程轮流获得 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java 中使用的是抢占式调度模型。
Thread 类提供了用来设置线程优先级和获取线程优先级的两个方法:
public final int getPriority()
获取线程优先级public final void setPriority(int newPriority)
设置线程优先级
Java 中线程优先级三个常量即可说明:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
注意: 线程优先级高仅仅表示线程获取的 CPU 时间片的机率高。
0x04 线程控制
static void sleep(long millis)
使当前正在执行的线程暂停执行指定的毫秒数void join()
等待这个线程死亡void setDaemon(boolean on)
将此线程标记为守护线程,当运行的线程都是守护线程时,Java 虚拟机将退出。
0x05 线程生命周期
0x06 线程同步
这里来写一个模拟售票员卖票的过程。我们会有一个票务中心(TicketCenter),以及多个售票员(TicketSeller),那么多个售票员售票就是多线程同时运行的过程,当然,每个售票员售票都是从票务中心拿的票。
首先,我们来实现一个简单的。
TicketCenter.java
import java.util.ArrayList;
import java.util.List;
/**
* 票务中心
*/
public class TicketCenter {
private final List<String> tickets;
private int nextIndex;
public TicketCenter() {
tickets = new ArrayList<>();
nextIndex = 0;
}
public void pushTicket(String ticket) {
tickets.add(ticket);
}
public String getNextAvailable() {
if (nextIndex >= tickets.size()) {
return null;
}
return tickets.get(nextIndex++);
}
}
TicketSeller.java
/**
* 售票员
*/
public class TicketSeller implements Runnable {
private final String sellerName;
private TicketCenter ticketCenter;
public void setTicketCenter(TicketCenter ticketCenter) {
this.ticketCenter = ticketCenter;
}
public TicketSeller(String sellerName, TicketCenter ticketCenter) {
this.sellerName = sellerName;
setTicketCenter(ticketCenter);
}
@Override
public void run() {
while (true) {
String ticket = ticketCenter.getNextAvailable();
if (ticket == null) {
break;
}
System.out.println(sellerName + " -> " + ticket);
}
}
}
Main.java
import java.util.UUID;
public class Main {
public static void main(String[] args) throws InterruptedException {
int ticketCnt = 10000;
TicketCenter ticketCenter = new TicketCenter();
for (int i = 0; i < ticketCnt; i++) {
ticketCenter.pushTicket(UUID.randomUUID().toString());
}
Thread seller1 = new Thread(new TicketSeller("刘备", ticketCenter));
Thread seller2 = new Thread(new TicketSeller("关羽", ticketCenter));
Thread seller3 = new Thread(new TicketSeller("张飞", ticketCenter));
Thread seller4 = new Thread(new TicketSeller("赵云", ticketCenter));
seller1.start();
seller2.start();
seller3.start();
seller4.start();
}
}
这份代码,运行结果中就有可能会出现下图所示的重复取票问题。刘备卖了一张 d9d3b45a-c131-4fa5-b9a4-7305d14677e4
的票,赵云也卖了一张 d9d3b45a-c131-4fa5-b9a4-7305d14677e4
的票。
造成以上问题的原因就是在取票的时候,没有进行加锁操作。
那我们接下来对取票的操作进行加锁操作。则能够保证不出现重复卖票的情况。既然多个售票员取票都是通过 getNextAvailable()
函数进行取票,那我们可以对此函数进行 synchronized
修饰,则可以达到加锁的目的:
synchronized public String getNextAvailable() {
if (nextIndex >= tickets.size()) {
return null;
}
return tickets.get(nextIndex++);
}
当然,还可以通过 synchronized
同步代码块,如:
synchronized (obj) {
}
当然,这里的 obj
需要各个线程使用的是同一个对象实体。
通过上面的例子我们知道,线程同步有两种方式:
- 同步代码块
- 同步方法(同步方法的锁对象是
this
)
总结:无论是同步代码块还是同步方法,都需要明确使用的锁对象是啥,不然加锁是无效的。
注:如果是静态方法设置 synchronized
同步方法,则其锁对象是 ClassName.class
的静态文件对象。
0x07 线程安全的类
StringBuffer
当不需要线程安全的环境时,则建议使用StringBuilder
,速度更快。Vector
当不需要线程安全的环境时,则建议使用ArrayList
,速度更快。HashTable
当不需要线程安全的环境时候,则建议使用HashMap
,速度更快。
用途 | 同步类 | 非同步类(速度更快) |
---|---|---|
字符串构造器 | StringBuffer |
StringBuilder |
列表 | Vector |
ArrayList |
键值存储 | HashTable |
HashMap |
通过 Collections.synchronizedList()
方法,可以将一个线程不安全的列表对象,转化成一个线程安全的列表对象。
List<String> list = Collections.synchronizedList(new ArrayList<String>());
0x08 Lock 锁
Lock
实现提供比使用 synchronized
方法和语句可以获得更广泛的锁定操作
Lock 中提供了获得锁和释放锁的方法
void lock()
获得锁void unlock()
释放锁
Lock
是接口不能直接实例化,这里采用它的实现类 ReentrantLock
来实例化
ReentrantLock
的构造方法
ReentrantLock
创建一个ReentrantLock
的实例
我们将 TicketCenter.java
改成如下即可:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 票务中心
*/
public class TicketCenter {
private final List<String> tickets;
private int nextIndex;
private final Lock lock = new ReentrantLock();
public TicketCenter() {
tickets = new ArrayList<>();
nextIndex = 0;
}
public void pushTicket(String ticket) {
tickets.add(ticket);
}
public String getNextAvailable() {
if (nextIndex >= tickets.size()) {
return null;
}
lock.lock();
String ticket = tickets.get(nextIndex++);
lock.unlock();
return ticket;
}
}
注意:以上所使用锁进行 lock()
操作,有一点不够严谨,那就是,如果 String ticket = tickets.get(nextIndex++);
出现异常的时候,则可能导致无法执行 lock.unlock()
操作,也就是无法释放锁,就会出现死锁的现象。那么我们可以这样写
try {
lock.lock();
String ticket = tickets.get(nextIndex++);
} finally {
lock.unlock();
}
0x09 生产者消费者模式
生产者消费者模式是一个十分经典的多线程协作模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。所谓生产者消费者问题,实际上主要是包含两类线程:
- 一类是生产者线程用于生产数据
- 一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
- 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。
- 消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为。
为了体现生产和消费过程中的等待和唤醒,Java 就提供了几个方法供我们使用,这几个方法在 Object 类中。
Object 类的等待和唤醒方法:
方法名 | 说明 |
---|---|
void wait() |
导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法 |
void notify() |
唤醒正在等待对象监视器的单个线程 |
void notifyAll() |
唤醒正在等待对象监视器的所有线程 |
生产者消费者案例
生产者消费者案例中包含的类:
- 奶箱类(
Box
):定义一个成员变量,表示第x
瓶奶,提供存储牛奶和获取牛奶的操作。 - 生产者类(
Producer
):实现Runnable
接口,重写run()
方法,调用存储牛奶的操作。 - 消费者类(
Consumer
):实现Runnable
接口,重写run()
方法,调用获取牛奶的操作。 - 测试类(
BoxDemo
):里面有main()
方法,main()
方法中的步骤如下:- 创建奶箱对象,这是共享数据区域
- 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作。
- 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作。
- 创建两个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递。
- 启动线程
Box.java
public class Box {
public static final boolean BOX_EMPTY = false;
public static final boolean BOX_FULL = true;
// 定义一个成员变量,表示第x瓶奶
private int milk;
// 定义一个成员变量,表示奶箱的状态
private boolean state = BOX_EMPTY;
// 提供存储奶箱和获取奶箱的操作
public synchronized void put(int milk) {
// 如果有牛奶,等待消费
if (state == BOX_FULL) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.printf("送奶工将第%d瓶牛奶放入奶箱。\n", milk);
// 生产完毕后,修改奶箱状态
state = BOX_FULL;
// 唤醒其他等待线程
notifyAll();
}
public synchronized void get() {
// 如果没有牛奶,等待生产
if (state == BOX_EMPTY) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有牛奶,就消费牛奶
System.out.printf("用户拿到第%d瓶牛奶。\n", this.milk);
// 消费完毕后,修改奶箱状态
state = BOX_EMPTY;
// 唤醒其他等待线程
notifyAll();
}
}
Producer.java
public class Producer implements Runnable {
private final Box box;
public Producer(Box box) {
this.box = box;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
this.box.put(i);
}
}
}
Consumer.java
public class Consumer implements Runnable {
private final Box box;
public Consumer(Box box) {
this.box = box;
}
@Override
public void run() {
while (true) {
this.box.get();
}
}
}
BoxDemo.java
public class BoxDemo {
public static void main(String[] args) {
// 1. 创建奶箱对象,这是共享数据区域
Box box = new Box();
// 2. 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作。
Producer p = new Producer(box);
// 3. 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作。
Consumer c = new Consumer(box);
// 4. 创建两个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递。
Thread thread1 = new Thread(p);
Thread thread2 = new Thread(c);
// 5. 启动线程
thread1.start();
thread2.start();
}
}