目录
一.什么是线程
多线程可以让程序同时做多件事情,提高效率。
进程:进程是程序的基本执行实体。
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。
比如:进程是一个应用程序,线程是应用进程中相互独立,可以同时运行的功能。
二.线程中的两个概念(并发、并行)
- 并发:同一时刻,有多个指令在CPU上交替执行
- 并行:同一时刻,有多个指令在CPU上同时执行
每个蓝线表示一个线程,红线会和蓝线随机连接,连接红线的线程之间是并行,连接红线的进程与未连接红线的进程之间是并发。
三.多线程的实现方式
1.三种方式的实现
(1)继承Thread类的方式进行实现
类Thread表示Java中的一个线程,Java虚拟机允许应用程序并发的运行多个线程。
将类声明为Thread的子类,该子类应重写Thread类的run方法。接下来可以分配并启动该子类的实例:
public class PrineThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
}
}
}
然后下列代码会创建并启动两个线程:
public class test7 {
public static void main(String[] args) {
PrineThread printThread1 = new PrineThread();
printThread1.setName("线程1");
printThread1.start();
PrineThread printThread2=new PrineThread();
printThread2.setName("线程2");
printThread2.start();
}
}
(2)实现Runnable接口的方式进行实现
声明实现Runnable接口的类,该类然后实现run方法
public class PrineRun implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程名:"+Thread.currentThread().getName()+"正在运行第"+i+"次");
}
}
}
创建自己类的对象,创建一个Thread类的对象,并开启线程
public class PrineRunTest {
public static void main(String[] args) {
PrineRun prineRun = new PrineRun();
Thread thread1 = new Thread(prineRun);
Thread thread2 = new Thread(prineRun);
thread1.setName("线程1");
thread2.setName("线程3");
thread1.start();
thread2.start();
}
}
(3)利用Callable接口和Future接口方式实现
创建一个类MyCallable实现Callable接口,重写call方法(有返回值,表示多线程运行的结果)
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
创建MyCallable的对象(表示多线程要执行的任务),创建FutureTesk的对象(管理多线程运行的结果),创建Thread类的对象,并启动
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
System.out.println("Result: " + result);
}
}
2.多线程三种实现方式的对比
前面两种方法无法获取多线程运行的结果,第三种可以
四.常见的成员方法
守护线程的形象解释:聊天框为非守护线程,发送文件为守护线程,当聊天框关闭时,发送文件也会停止。
注意事项:
- 线程的优先级指的是抢占CPU的概率
- 线程的优先级默认是5
- 当所有非守护线程结束时,即使守护线程还在运行,JVM也会自动退出。
五.线程生命周期
六.线程的安全问题
多线程安全问题是并发编程中的核心挑战,多个线程同时访问共享资源时可能导致的数据不一致或程序异常行为。
1.同步代码块
利用同步代码块,把操作共享数据的代码锁起来,使同步代码块里面的代码轮流执行。
public class MyThread extends Thread {
//表示这个类的所有对象,都共享这个变量
static int ticket=0;
//锁对象,一定是唯一的
static Object lock=new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket<100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println("Thread " + Thread.currentThread().getName() + " has bought a ticket, ticket number is " + ticket);
} else {
break;
}
}
}
}
}
2.同步方法
特点:
- 同步方法是锁住方法里面所有的代码。
- 锁对象不能自己指定(非静态:this,静态:当前类的字节码文件对象)。
public class PrineRun implements Runnable {
int ticket=0;
@Override
public void run() {
while(true){
if (extracted()) break;
}
}
private synchronized boolean extracted() {
if (ticket==100) {
return true;
}else {
try{
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName()+":"+ticket);
}
return false;
}
}
3.Lock锁
同步代码块和同步方法不能让我们看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock实现提供比使用synchronized方法和语句可以获得更广发的锁定操作。
Lock中提供了获得锁(void lock())和释放锁(void unlock())的方法。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
ReentrantLock的构造方法。
ReeentrantLock():创建一个ReentrantLock的实例。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread {
//表示这个类的所有对象,都共享这个变量
static int ticket=0;
static Lock lock=new ReentrantLock();
public void run(){
while(true){
lock.lock();
try {
if(ticket< 100){
Thread.sleep( 10);
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖第"+ ticket +"张票!!!");
}else{
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
七.死锁
1.概念:
死锁:是指多个进程(或线程)在执行过程中,由于竞争资源或互相通信不当,导致彼此无限等待对方释放资源,从而使程序无法继续执行的状态。
2.死锁产生的四个必要条件(Coffman条件)
1.互斥条件(Mutual Exclusion)
-
资源一次只能被一个进程占用,其他进程必须等待该资源释放。
-
例如:打印机、数据库锁等独占资源。
2.占有并等待(Hold and Wait)
-
进程已经持有至少一个资源,同时又在等待其他被占用的资源。
3.非抢占条件(No Preemption)
- 进程已获得的资源不能被强行剥夺,必须由进程自行释放。
4.循环等待(Circular Wait)
-
存在一个进程等待的循环链,例如:
-
进程A 持有资源1,等待资源2;
-
进程B 持有资源2,等待资源1;
-
这样A和B互相等待,形成死锁。
-
这四个条件全部满足才会发生死锁。
3.死锁的经典例子(哲学家就餐问题)
5个哲学家围坐在圆桌旁,每人左右各有一把叉子。哲学家必须拿到左右两把叉子才能吃饭。如果所有哲学家同时拿起左边的叉子,并等待右边的叉子,就会导致所有人互相等待,形成死锁。
4.如何解决死锁?
1. 预防死锁(破坏四个必要条件之一)
-
破坏互斥条件:让某些资源可共享(如只读文件)。
-
破坏占有并等待:进程必须一次性申请所有资源(可能降低资源利用率)。
-
破坏非抢占条件:允许系统强制回收资源(如数据库事务回滚)。
-
破坏循环等待:规定资源申请顺序(如所有进程必须按固定顺序申请锁)。
2. 避免死锁(动态检测)
-
银行家算法:系统在分配资源前,先模拟计算是否会导致死锁,再决定是否分配。
3. 检测与恢复
-
死锁检测:定期检查资源分配图是否存在环路。
-
恢复方法:
-
终止进程:强制结束部分进程(如优先级最低的)。
-
资源抢占:回滚部分进程并释放资源。
-
4. 忽略死锁(鸵鸟策略)
-
某些系统(如Windows、Linux)允许少量死锁,依赖人工干预或重启解决。
八.生产者和消费者(等待唤醒机制)
1.概念
等待唤醒机制 是多线程编程中用于线程间协调通信的一种方式,它允许线程在某些条件不满足时主动释放锁并进入等待状态(wait
),直到其他线程修改条件后唤醒它(notify
/notifyAll
)。
代码示例(Foodie吃货类,Cook厨师类,Desk桌子类 )
Desk类
public class Desk {
//桌子上是否有面条
public static int foodFlag=0;
//总个数
public static int count=10;
//锁对象
public static Object lock=new Object();
}
Foodie类
public class Foodie extends Thread{
@Override
public void run(){
while (true) {
synchronized (Desk.lock) {
//吃货最多吃十碗
if (Desk.count == 0) {
break;
} else {
//判断桌子上是否有面条
if (Desk.foodFlag == 0) {
//没有面条,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//有面条,就吃
Desk.count--;
System.out.println("很吃,还能吃"+Desk.count+"碗");
//吃完,唤醒厨师做面条
Desk.lock.notifyAll();
//修改桌子上面条的状态(有没有面条)
Desk.foodFlag=0;
}
}
}
}
}
}
Cook类
public class Cook extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
//吃货最多吃10次,判断是否还能吃
if (Desk.count==0) {
break;
}else {
//判断桌子上是否有食物
if (Desk.foodFlag==1) {
//有食物,厨师就等待吃货吃完
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//没有食物,厨师就做面条
System.out.println("做了一碗面条");
//做完面条,修改桌子上面条的状态
Desk.foodFlag = 1;
//通知等待的吃货
Desk.lock.notifyAll();
}
}
}
}
}
}
测试类
public class test {
public static void main(String[] args) {
Foodie f = new Foodie();
Cook c = new Cook();
f.setName("厨师");
c.setName("食客");
c.start();
f.start();
}
}
九.阻塞队列
阻塞队列(Blocking Queue) 是一种线程安全的队列,提供了一种线程间安全通信的机制。当队列空时,消费者线程会被阻塞,直到队列有数据;当队列满时,生产者线程会被阻塞,直到队列有空位。
1. 阻塞队列的核心特点
特性 | 说明 |
---|---|
线程安全 | 内部使用锁(ReentrantLock )或 synchronized 保证线程安全 |
阻塞操作 | 提供 put() (阻塞插入)和 take() (阻塞取出)方法 |
容量可选 | 可以是有界队列(固定容量)或无界队列(无限容量) |
适用场景 | 生产者-消费者模型、线程池任务调度、异步任务处理 |
2.阻塞队列的核心方法
方法 | 说明 | 行为 |
---|---|---|
put(E e) | 插入元素 | 如果队列满,则阻塞,直到队列有空位 |
take() | 取出元素 | 如果队列空,则阻塞,直到队列有数据 |
offer(E e, long timeout, TimeUnit unit) | 尝试插入 | 队列满时等待指定时间,超时返回 false |
poll(long timeout, TimeUnit unit) | 尝试取出 | 队列空时等待指定时间,超时返回 null |
offer(E e) | 非阻塞插入 | 队列满时直接返回 false |
poll() | 非阻塞取出 | 队列空时直接返回 null |
peek() | 查看队首元素 | 不删除元素,队列空时返回 null |
3. Java 中的阻塞队列实现
Java 的 java.util.concurrent
包提供了多种阻塞队列实现:
实现类 | 特点 | 适用场景 |
---|---|---|
ArrayBlockingQueue | 基于数组的有界队列,FIFO | 固定大小的任务队列 |
LinkedBlockingQueue | 基于链表的可选有界队列,FIFO | 高吞吐量任务队列(默认无界) |
PriorityBlockingQueue | 支持优先级的无界队列 | 任务优先级调度 |
DelayQueue | 元素按延迟时间排序 | 定时任务、缓存过期 |
SynchronousQueue | 不存储元素,直接传递 | 线程间直接交换数据 |
LinkedTransferQueue | 改进的 LinkedBlockingQueue ,支持 transfer() | 更高性能的生产者-消费者 |
4.经典应用:生产者-消费者模型
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run(){
while(true) {
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
queue.put("面条");
System.out.println("烹饪面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class test {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
Cook cook = new Cook(queue);
Foodie foodie = new Foodie(queue);
cook.start();
foodie.start();
}
}
5. 阻塞队列 vs 普通队列
对比项 | 阻塞队列 (BlockingQueue ) | 普通队列 (Queue ) |
---|---|---|
线程安全 | ✅ 是 | ❌ 不是(除非用 ConcurrentLinkedQueue ) |
阻塞操作 | ✅ 支持 put() /take() | ❌ 不支持 |
适用场景 | 多线程协作(生产者-消费者) | 单线程或手动加锁 |