目录
前言:
1.线程的寿命周期
2.线程的安全问题
3.锁
同步代码块:
同步方法:
死锁:
4.生产者和消费者模式(等待唤醒机制)
总结:
当今软件开发领域中,多线程编程已成为一项至关重要的技能。然而,要编写出高效、可靠的多线程程序并不容易。多线程编程面临着许多挑战,如线程安全性、资源共享、死锁等问题。因此,对于初学者来说,深入理解Java多线程的工作原理和机制是至关重要的。只有通过掌握多线程的核心概念、了解常见问题和解决方案,我们才能写出健壮且高性能的多线程应用。
本文将为大家逐步深入介绍Java多线程的重要概念和机制。我们将从线程的创建和启动开始,讨论如何使用线程池管理线程,并探讨线程间的通信和同步技术。我们还将介绍一些常用的多线程设计模式和最佳实践,帮助读者更好地应用多线程技术解决实际问题。
线程的生命周期描述了一个线程从创建到终止的整个过程,一般包含以下几个阶段:
新建状态(New):
可运行状态(Runnable):
运行状态(Running):
阻塞状态(Blocked):
无限期等待状态(Waiting):
限期等待状态(Timed Waiting):
终止状态(Terminated):
需要注意的是,线程的状态可以相互切换,具体的转换由Java的线程调度器和操作系统决定。线程的生命周期和状态的转换对于多线程编程非常重要,合理地管理线程的状态可以提高程序的性能和并发能力。
我们用一个案例来说明:
现在我们要开设三个窗口来买票,一共有100张票,请你利用多线程的知识完成。
class MyThread extends Thread {
static int tick=0;
public void run() {
// 定义线程要执行的任务
while(true)
{
if(tick<100)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tick++;
System.out.println(getName()+"正在卖第"+tick+"张票");
}
else
{
break;
}
}
}
}
public class test05 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
很多同学在第一时间就会写出这样一个简单的多线程,但是当我们运行之后,就会有一个明显的问题:出现了一张票卖了两次这种情况 ,也会出现了卖超了的这种现象。
我们来详细解释一下为什么
线程1和线程2和线程3都在抢夺cpu调度,假设线程1抢到之后,那么他先进入if语句,但if语句中有一个sleep,执行到这里后,线程1就会被阻塞睡眠,此时线程2和线程3重新抢夺cpu调度,线程2抢到资源之后进入if语句也会睡眠,然后就是线程3进入资源,也会睡眠。随着这三个的睡眠周期结束,就又会执行if中的代码。当tick还没来得及打印的时候,线程2醒来又会抢夺cpu资源,如果抢到了,就又会执行一次tick++,接下来又是线程3.如此这样循环,就会造成卖出两张票并且可能卖超的结果。
通过这个案例我们可以看出多线程在执行的时候,有一个重要的隐患:
线程的执行具有随机性
那么我们最简单的思路就是:
设计一种方法,这个方法使得 如果一个线程正在执行代码,那么其他的线程必须等待,只有当这个线程执行完之后,其他的线程才可以抢占CPU资源。这就是我们下面要介绍的东西
把代码块用锁锁起来
synchronized(锁)
{
操作共享数据的代码
}
特点:
因此我们尝试一下用锁来改进一下
class MyThread extends Thread {
static int tick=0;
static Object oj = new Object();
public void run() {
// 定义线程要执行的任务
while(true)
{
try {
Thread.sleep(10);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
synchronized (oj)
{
if(tick<10000)
{
tick++;
System.out.println(getName()+"正在卖第"+tick+"张票");
}
else
{
break;
}
}
}
}
}
锁的注意点:
锁的粒度:要在保证线程安全的前提下,尽量减小锁的范围。过大的锁粒度可能导致不必要的线程阻塞,影响性能。可以考虑使用细粒度锁或者使用并发集合类来提高并发性能。
锁的公平性:锁可以是公平的或非公平的。公平锁会按照线程请求锁的顺序依次获取锁,而非公平锁则不保证线程获取锁的先后顺序。在选择锁时,根据具体情况选择公平或非公平锁。
死锁情况:死锁是指两个或多个线程相互等待对方释放持有的锁,从而导致所有线程无法继续执行的情况。为避免死锁,需要谨慎设计锁的获取顺序,并尽量避免嵌套锁的情况。
锁的释放:在使用锁时,需要保证锁的正确释放,以免出现资源泄漏或线程饥饿等问题。一般可以使用try-finally块来确保在发生异常时仍能正确释放锁。
锁的性能:锁的竞争会带来一定的性能开销,过多的锁竞争可能会影响应用的并发性能。可以考虑使用读写锁、无锁数据结构或并发集合类等替代方案,来降低锁竞争带来的性能开销。
死锁检测和避免:一旦发生死锁,所有线程都将无法继续执行。为了避免死锁,可以使用工具进行死锁检测,并合理设计锁的获取和释放顺序,避免潜在的死锁情况。
把方法用 锁 锁起来
修饰符 synchronized 返回值类型 方法名 (方法参数){...}
特点:
非静态:this
静态:当前类的字节码文件
则我们可把前面的改写为:
class MyThread extends Thread {
static int tick=0;
static final Object oj = new Object();
public synchronized void run() {
// 定义线程要执行的任务
while(true)
{
if (tick < 100) {
tick++;
System.out.println(getName() + "正在卖第" + tick + "张票");
} else {
break;
}
}
}
}
死锁是指在多线程编程中,两个或多个线程互相持有对方需要的资源,导致它们都无法继续执行,称为死锁现象。
死锁的发生通常需要满足以下四个条件,也称为死锁的必要条件:
当以上四个条件都满足时,就可能出现死锁。在死锁发生时,这些线程将无法继续执行下去,需要通过一些策略进行解决,如避免死锁的产生、检测死锁、解除死锁等。
解决死锁的方法一般有以下几种:
public class DeadlockExample {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1 acquired lock on resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1 acquired lock on resource2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2 acquired lock on resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2 acquired lock on resource1");
}
}
});
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Execution completed");
}
}
在上述代码中,两个线程 thread1
和 thread2
分别尝试获取 resource1
和 resource2
的锁。但是它们获取锁的顺序是相反的,即 thread1
先获取 resource1
的锁,再获取 resource2
的锁;而 thread2
先获取 resource2
的锁,再获取 resource1
的锁。
这种情况下,如果两个线程同时启动,则 thread1
获取了 resource1
的锁并等待 resource2
的锁释放,而 thread2
获取了 resource2
的锁并等待 resource1
的锁释放。由于两个线程相互等待对方所持有的锁,它们将处于死锁状态,无法继续执行下去。
在Java中,生产者-消费者模式是一种常见的多线程协作模式,用于解决生产者和消费者之间的数据交换和同步问题。
我们在以前的多线程中,会发现每条线程是都执行都是随机的,可能会是
A A A A B B A A B A
而等待唤醒机制可以是线程的交替变得有规律,变为
A B A B A B A B A B A
生产者是生成数据的线程,而消费者是消耗数据的线程。下面是对Java中生产者和消费者的详细介绍:
生产者:
消费者:
共享缓冲区:
为了实现生产者-消费者模式,可以使用以下方法之一:
wait() 和 notify():
Condition 和 Lock:
java.util.concurrent.locks.Condition
和 java.util.concurrent.locks.Lock
接口来实现线程的等待和唤醒操作。await()
和 signal()
方法进行线程的等待和唤醒操作。生产者-消费者模式可以帮助解决多线程并发情况下的数据同步和数据交换问题,确保生产者和消费者之间的协调运行。这种模式在许多并发编程场景中都有应用,如线程池、消息队列、生产者-消费者问题等。
生产者与消费者模式的意义:
解耦生产者和消费者:
提高系统的并发性和吞吐量:
缓冲区平衡生产和消费速度:
实现线程间的通信和同步:
综上所述,生产者-消费者模式是一种重要的多线程编程模式,它能够提高系统的并发性、吞吐量和效率,实现生产者和消费者之间的解耦和协作,确保数据交换和同步的正确性和可靠性。在并发编程和异步系统中广泛应用。
import java.util.LinkedList;
class Producer implements Runnable {
private LinkedList buffer;
private int maxSize;
public Producer(LinkedList buffer, int maxSize) {
this.buffer = buffer;
this.maxSize = maxSize;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
produce(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void produce(int value) throws InterruptedException {
synchronized (buffer) {
while (buffer.size() == maxSize) {
System.out.println("缓冲区已满,生产者等待...");
buffer.wait();
}
buffer.add(value);
System.out.println("生产者生产: " + value);
buffer.notifyAll();
}
}
}
class Consumer implements Runnable {
private LinkedList buffer;
public Consumer(LinkedList buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void consume() throws InterruptedException {
synchronized (buffer) {
while (buffer.size() == 0) {
System.out.println("缓冲区为空,消费者等待...");
buffer.wait();
}
int value = buffer.removeFirst();
System.out.println("消费者消费: " + value);
buffer.notifyAll();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
LinkedList buffer = new LinkedList<>();
int maxSize = 5;
Producer producer = new Producer(buffer, maxSize);
Consumer consumer = new Consumer(buffer);
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
这个示例中,生产者线程通过 produce()
方法在 buffer
中生产数据,而消费者线程通过 consume()
方法从 buffer
中消费数据。其中,buffer
是一个共享的缓冲区,采用了等待和唤醒机制来实现线程的同步。
注意,在示例中使用了 LinkedList
作为缓冲区,但这只是一种示例使用的数据结构,实际上可以使用其他线程安全的数据结构,如 ArrayBlockingQueue
或 LinkedBlockingQueue
来实现更高效的生产者-消费者模式。
运行代码示例后,你可以观察到生产者逐个生成数据并放入缓冲区,而消费者逐个从缓冲区中取出数据消费,它们之间的执行是交替进行的。当缓冲区已满时,生产者线程会等待;当缓冲区为空时,消费者线程会等待。这样,生产者和消费者之间的数据交换和同步就实现了。
今天我们学习了多线程中必要有意思的寿命周期,锁以及一个多线程的经典模式:生产者和消费者模式。多线程作为一项处理高并发和高吞吐量的重要技术,其各项知识点我们都应该拥有较好的掌握程度,这样才可以熟练的使用多线程。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!