当前位置:网站首页 > Java基础 > 正文

java并发编程多线程基础



引言

在现代软件开发中,多线程并发编程是提升系统性能和效率的关键技术之一。Java语言自1.5版本开始引入了强大的并发工具类库,并提供了丰富的API来支持多线程编程。本文将深入探讨Java中的多线程并发编程概念、原理以及实战技巧。

一、线程创建

在Java中,创建线程主要有两种方式:


1. 继承Thread类
示例代码:

 

详细解释:

  • 我们通过创建一个继承自java.lang.Thread的子类MyThread来实现一个新的线程类型。
  • 在MyThread类中重写了run()方法,这个方法包含了线程要执行的任务逻辑。
  • main方法中创建了两个MyThread对象,并调用它们的start()方法启动新线程。当调用start()方法时,Java虚拟机会为每个线程创建新的执行路径,并调用该线程对应的run()方法。

2. 实现Runnable接口
示例代码:

 

详细解释:

  • 这次我们创建了一个实现了java.lang.Runnable接口的类RunnableTask,并在其中实现了run()方法。
  • Runnable接口仅包含一个run()方法,这意味着它允许我们将任务与线程的具体实现(即Thread类)解耦。
  • 在main方法中,我们创建了RunnableTask实例并将其传递给Thread构造函数,然后启动这两个线程。即使不直接扩展Thread类,由于Thread类可以接受Runnable类型的参数,因此仍然能够执行并发任务。

二、线程同步

Java多线程并发编程中,线程同步是一种控制多个线程对共享资源的访问,以确保线程安全的方法。下面通过一个示例代码来详细解释线程同步的使用。

 

在上述示例代码中,我们通过在deposit()和withdraw()方法上添加synchronized关键字来实现线程同步。这意味着在同一时刻,只有一个线程可以执行这两个方法中的任意一个。

 

在这个测试类中,我们创建了一个BankAccount对象,并启动了三个线程,分别执行存款、取款操作。由于我们在线程同步中使用了synchronized关键字,因此这些操作将会按顺序执行,确保了线程安全。

三、等待/通知机制

Java中的等待/通知机制是线程间通信的一种方式,通过调用Object类的wait()、notify()和notifyAll()方法来实现。下面是一个使用这些方法进行线程同步的示例代码,并对其进行详细解释

 

详细解释:
在这个BoundedBuffer类中,我们模拟了一个容量有限的缓冲区,它可以由多个生产者线程向其中添加元素,同时也可以由多个消费者线程从中移除元素。

  • put()方法用于将元素放入缓冲区,当缓冲区已满时,生产者线程会调用lock.wait(),使自己进入等待状态,释放锁。
  • 当消费者从缓冲区取走一个元素后,在take()方法中会调用lock.notifyAll()唤醒所有等待在该锁上的生产者线程,使其重新尝试执行操作。

四、并发工具类

Java并发工具类库java.util.concurrent(JUC)提供了一系列高效的线程同步和并发控制工具,下面列举几个常用的并发工具类并给出示例代码及详细解释:


1. CountDownLatch
作用:允许一个或多个线程等待其他线程完成操作。
示例代码:

 

详细解释:

  • CountDownLatch的构造函数接收一个整数值作为参数,表示需要计数到零的次数。
  • 当通过countDown()方法减少计数到0时,那些在await()上等待的线程将被释放,可以继续执行。

2. Semaphore
作用:限制同时访问特定资源的线程数量。
示例代码:

 

详细解释:

  • Semaphore对象用于维护一组许可证,通过acquire()获取许可证,如果没有可用的许可证则会阻塞当前线程。
  • 当线程完成对共享资源的访问时,通过release()方法归还许可证,这样其他等待的线程就可以继续执行。

3. CyclicBarrier
作用:多个线程到达某个屏障点时会被阻塞,直到最后一个线程到达,然后所有线程才会一起继续执行。
示例代码:

 

详细解释:

  • CyclicBarrier的构造函数接收一个整数参数,表示需要等待的线程数。
  • 当每个线程执行完自己的任务后,调用await()方法,这时线程将会阻塞,直到所有线程都调用了await()方法。当所有线程都到达这个障碍点时,它们将被同时释放,可以继续后续的操作

五、原子类


1.AtomicInteger

 

详细解释:

  • AtomicInteger提供了一种线程安全的方式来增加或减少整数值。
  • 在上述示例中,我们创建了一个AtomicInteger对象作为计数器,并启动了两个线程分别对其增加1000次。
  • 即使两个线程同时尝试修改这个共享的计数器,由于incrementAndGet()方法内部实现了CAS(Compare and Swap)算法,确保了即使在多线程环境下,计数器的值也能准确无误地增加2000,不会出现线程安全问题。

2.AtomicReference

 

详细解释:

  • AtomicReference用于存储并原子地更新引用类型(如对象引用)的值。
  • 在这个例子中,我们创建了一个AtomicReference来保存字符串值,并通过set()方法在多线程环境下安全地更新它的值。

六、线程局部变量

Java中的线程局部变量是通过java.lang.ThreadLocal类来实现的。它为每个线程创建一个单独的变量副本,使得在并发执行时,每个线程可以拥有独立的变量值,从而避免了线程间共享资源带来的同步问题。


示例代码:

 

详细解释:

  • ThreadLocal是一个泛型类,其声明和使用方式如上所示。
  • 在main方法中,我们创建了一个固定大小的线程池,并提交了两个任务。
  • 每个任务内部调用THREAD_LOCAL.set()方法设置线程局部变量的值,这里将线程ID作为变量值的一部分。
  • 当我们在任务内部获取这个变量值时,即使两个线程同时运行,由于每个线程都有自己的ThreadLocal副本,所以它们不会互相影响,输出各自的线程ID相关的值。
  • 使用完ThreadLocal后,可以通过调用remove()方法删除当前线程关联的变量,以防止内存泄漏。如果线程结束后不再需要该变量,这是个好习惯。

这样,每个线程都有自己独立的ThreadLocal存储空间,当在一个线程中修改ThreadLocal的值时,其他线程的ThreadLocal值不受影响,这在处理每个线程特有的状态信息、或者在多线程环境中需要隔离数据的情况下非常有用。例如,在Web应用中,ThreadLocal经常用于存储与请求相关的会话信息,确保不同请求之间的数据互不干扰。

七、线程状态管理

Java线程有五种基本状态,分别是:新建(New)、运行(Runnable)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。下面通过示例代码来说明如何管理线程状态以及状态之间的转换。


1.线程状态转换 - Runnable到Blocked

 

详细解释:

  • 在这个例子中,我们创建了一个新的线程t。当线程开始执行时,它调用了LockSupport.park()方法,这使得线程进入了Blocked状态,即线程在等待某个条件变为真或获java并发编程多线程基础得某种资源之前无法继续执行。然后主线程通过LockSupport.unpark(t)唤醒了这个被挂起的线程,使其重新回到Runnable状态,并最终完成执行。


2.线程状态转换 - Runnable到Waiting

 

详细解释:

  • 在本例中,我们创建了一个线程t,它在其run方法内调用lock.wait()进入Waiting状态。这意味着线程会释放锁并暂停执行,直到其他线程在同一把锁上调用notifyAll()或者notify()方法。在主线程中,我们在一定延迟后调用notifyAll()方法,将等待中的线程唤醒,使其从Waiting状态转换回Runnable状态。

八、线程调度策略

在Java中,线程调度主要由Java虚拟机(JVM)和底层操作系统共同完成。Java提供了两种不同的优先级设置来影响线程调度,但请注意,尽管设置了线程优先级,具体的调度策略仍然很大程度上取决于操作系统的实现。


示例代码:线程优先级

 

详细解释: 在这个示例中,我们创建了两个线程thread1和thread2。其中thread1的优先级被设置为最大值(Thread.MAX_PRIORITY),这意味着在所有条件相同的情况下,操作系统将更倾向于调度优先级高的线程执行。
然而,实际的调度结果可能会受到很多因素的影响,包括但不限于:

  1. 时间片轮转:操作系统通常使用时间片轮转的方式进行线程调度,即使高优先级线程也可能需要等待其时间片。
  2. 抢占式调度:某些情况下,高优先级线程可以在低优先级线程正在运行时被抢占并获得CPU资源。
  3. 线程状态:线程必须处于可运行状态(Runnable)才能被调度,阻塞或等待状态的线程不会参与调度。
  4. 操作系统的调度策略:Java的线程优先级映射到操作系统的优先级可能并不直接对应,且不同操作系统的调度机制各异。

因此,虽然可以设置线程优先级,但在编写多线程程序时,应尽量避免依赖于优先级来进行同步控制,而是更多地依靠并发工具类如synchronized、volatile关键字、java.util.concurrent包下的工具类等来进行正确的并发控制和通信。

九、死锁检测与避免

死锁是指两个或多个线程相互等待对方持有的资源而造成的僵局。在Java多线程并发编程中,死锁是一个需要特别注意的问题。下面通过一个示例来演示死锁的发生以及如何检测和避免。


示例代码:死锁的演示

 

详细解释:

在这个示例中,我们定义了两个线程thread1和thread2,它们分别试图获取两个资源resource1和resource2的锁。线程thread1首先尝试获取resource1的锁,然后获取resource2的锁;而线程thread2的顺序相反。如果线程thread1在获取resource1的锁之后,线程thread2立即获取了resource2的锁,然后线程thread1尝试获取resource2的锁时将被阻塞,线程thread2尝试获取resource1的锁时也将被阻塞,这时就形成了一个死锁。


死锁的检测
Java提供了一个工具来检测死锁,即jstack命令。通过运行jstack命令并指定进程ID,我们可以获得线程的堆栈信息,从而查看是否有线程处于死锁状态。

死锁的避免
以下是一些常见的策略来避免死锁的发生:

  1. 资源的有序获取:确保所有线程按照相同的顺序获取资源锁,可以避免死锁的发生。
  2. 设置超时:在尝试获取锁时设置一个超时时间,如果在指定时间内无法获取锁,则线程放弃并重试或执行其他操作。
  3. 锁的粒度控制:尽量使用细粒度的锁,而不是大粒度的锁,以减少锁竞争和死锁的可能性。
  4. 避免嵌套锁:尽量避免使用嵌套锁,因为这可能导致锁的获取顺序变得复杂,从而增加死锁的风险。
  5. 检测锁的依赖关系:在设计多线程程序时,通过分析锁的依赖关系图,可以提前发现潜在的死锁风险

十、非阻塞同步机制

在Java中,非阻塞同步机制主要包括原子变量(Atomic Variables)和循环CAS(Compare and Swap)等技术。其中,java.util.concurrent.atomic包下的原子类就是实现非阻塞同步的一种重要手段。


示例代码:使用AtomicInteger进行非阻塞同步

 

详细解释:

  • 在上述示例中,我们使用了AtomicInteger来代替传统的整型变量,并通过其提供的compareAndSet方法实现了线程安全的递增操作。
  • compareAndSet方法是基于CAS算法的,它会比较当前值是否与预期的旧值相等,如果相等则将值设置为新的值。这个过程是一个原子操作,意味着在多线程环境下不会出现数据竞争的问题。
  • 当多个线程同时调用increment方法时,每个线程都会尝试去更新计数器的值。由于compareAndSet方法的特性,只有一个线程能成功地更新计数器,其他线程在发现当前值已改变后会继续下一轮循环尝试。
  • 这种非阻塞同步机制的优点在于避免了传统锁机制带来的上下文切换和线程阻塞,从而提高了并发性能。

总结

版权声明


相关文章:

  • 大专零基础转行java2024-11-03 09:42:03
  • java语言基础0032024-11-03 09:42:03
  • 小黑java爬虫零基础教程2024-11-03 09:42:03
  • java基础步骤2024-11-03 09:42:03
  • 广东java基础班2024-11-03 09:42:03
  • java代码层面的基础优化2024-11-03 09:42:03
  • java基础授课2024-11-03 09:42:03
  • java语言编程基础 中国工信2024-11-03 09:42:03
  • Java基础末尾知识2024-11-03 09:42:03
  • 大数据基础java多线程入门2024-11-03 09:42:03