文章也发布在我的个人博客上:https://blog.ysboke.cn/archives/129.html

概述

每个Thread类的示例都代表一个线程,而进程是操作系统级别的多任务,JVM就是运行在一个进程当中的。所以在Java中更多的应该考虑线程。进程的内存是可以被多个线程共享使用的。

使用线程根本上是为了更充分的利用cpu资源。

线程的状态

查看Java源码可知,线程的状态一共有6种,分别是新建、运行、阻塞、等待、超时等待、终止。即new、runnable、blocked、waiting、timed-waiting、terminated。

  • New:线程刚被创建时,未调用start方法,还未被纳入线程调度,此时为新建状态。

  • Runnable:Java中runnable与running统称runnable,此时调用了start方法(runnable),获取到cpu时间片后即可运行(running)。线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

  • blocked:阻塞状态,阻塞于锁。是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  • waiting:此状态的线程需要其他线程的操作,例如通知或中断。处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  • timed-waiting:相比于waiting,该状态可以自定义时间后自行返回。无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  • terminated:表示线程已经执行完毕。当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

创建线程

两种方法,一种是继承Thread类,但是如果类本身已经继承了其他类,那就得实现runnabl接口。

Thread本身是对Runnable接口的一个实现。

都要实现run方法,即线程逻辑。

什么是锁

简单来说,锁就是用来控制多线程情况下的访问行为,可以理解为一种许可,获得许可才允许执行。

数据在并发访问下容易出现读写不一致的问题,例如写线程还未结束写变量,读线程就来访问了,导致访问的数据不正确。所以给读写线程加速,未完成任务前不释放锁,此时其他线程就没法来读写变量,保证了原子性。

什么是重入锁

ReentrantLock是实现Lock接口的一个类,支持重入性。线程在被两次lock加锁后会被阻塞,在复杂的调用场景中为了避免这种情况,于是就有了可重入锁。只要lock和unlock次数相同即可。

重入锁和synchronized区别

性能上和synchronized几乎没区别,但是ReentrantLock功能更丰富,支持公平锁和非公平锁,更适合并发场景

使用上synchronized更简单,不用手动加锁解锁,都是隐式完成的。而ReentrantLock需要手动加锁解锁,解锁操作应该尽量放在finally代码块里。两种操作不平衡容易死锁。添加参数true后实现公平锁,不加为非公平锁。

    public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

重入锁的核心功能委托给内部类sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。

重入性实现原理

在AbstractQueuedSynchronizer对象里有个状态变量state,state为0表示锁空闲,大于0表示被占用,数值表示当前线程重复占用的次数。

private volatile int state;

那么实现一个简单的lock如下:

 final void lock() {
// compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待
acquire(1);
}

其中的acquire():

 public final void acquire(int arg) {
//tryAcquire() 再次尝试获取锁,
//如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
//同时宣布获得所成功,这正是重入的关键所在
if (!tryAcquire(arg) &&
// 如果获取失败,那么就在这里入队等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果在等待过程中 被中断了,那么重新把中断标志位设置上
selfInterrupt();
}

公平锁与非公平锁

默认情况下,重入锁是不公平的,多个线程竞争锁时不按照顺序来,而是随机获取。非公平锁如果第一次竞争失败,则会和公平锁一样进入等待队列。而公平锁则是按照先到先得的顺序获取锁,但是有性能损失。

也可以这么理解:公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。

从代码上看:

//非公平锁
final void lock() {
//上来不管三七二十一,直接抢了再说
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//抢不到,就进队列慢慢等着
acquire(1);
} //公平锁
final void lock() {
//直接进队列等着
acquire(1);
}

公平锁能避免饥饿争抢问题,线程不会重复获取锁。如果饥饿问题,那么可能有线程长时间获取不到锁。

就选择而言,大部分情况下使用非公平锁。

获取锁时限时等待

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了获取锁限时等待的方法trylock(),可以传入时间参数,无参表示立即返回锁申请的结果。相比lock()来说,避免了无限等待的情况。

构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
} static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}

线程通过调用tryLock()方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。

使用condition

condition是重入锁的伴生对象。它提供了在重入锁的基础上,进行等待和通知的机制。可以使用 newCondition()方法生成一个Condition对象:

 private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制,而ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

如何使用condition?

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException { lock.lock();
new Thread(new SignalThread()).start();
System.out.println("主线程等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("主线程恢复运行");
}
static class SignalThread implements Runnable { @Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("子线程通知");
} finally {
lock.unlock();
}
}
}
}

运行结果:

主线程等待通知
子线程通知
主线程恢复运行

实现一个阻塞队列

使用condition实现,

阻塞队列是一种特殊的先进先出队列,它有以下几个特点:

1、入队和出队线程安全

2、当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

阻塞队列:

public class MyBlockingQueue<E> {

    int size;//阻塞队列最大容量

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//队列底层实现

    Condition notFull = lock.newCondition();//队列满时的等待条件
Condition notEmpty = lock.newCondition();//队列空时的等待条件 public MyBlockingQueue(int size) {
this.size = size;
} public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() ==size)//队列已满,在notFull条件上等待
notFull.await();
list.add(e);//入队:加入链表末尾
System.out.println("入队:" +e);
notEmpty.signal(); //通知在notEmpty条件上等待的线程
} finally {
lock.unlock();
}
} public E dequeue() throws InterruptedException {
E e;
lock.lock();
try {
while (list.size() == 0)//队列为空,在notEmpty条件上等待
notEmpty.await();
e = list.removeFirst();//出队:移除链表首元素
System.out.println("出队:"+e);
notFull.signal();//通知在notFull条件上等待的线程
return e;
} finally {
lock.unlock();
}
}
}

测试代码:

public static void main(String[] args) throws InterruptedException {

    MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
for (int i = 0; i < 10; i++) {
int data = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(data);
} catch (InterruptedException e) { }
}
}).start(); }
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer data = queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
} }

为了让大家更好的理解重入锁的使用方法。现在我们使用重入锁,实现一个简单的计数器。这个计数器可以保证在多线程环境中,统计数据的精确性,请看下面示例代码:

public class Counter {
//重入锁
private final Lock lock = new ReentrantLock();
private int count;
public void incr() {
// 访问count时,需要加锁
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
} public int getCount() {
//读取数据也需要加锁,才能保证数据的可见性
lock.lock();
try {
return count;
}finally {
lock.unlock();
}
}
}

总结

ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。

  1. 对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致。
  2. 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
  3. 重入锁的内部实现是基于CAS操作的。
  4. 重入锁的伴生对象Condition提供了await()和singal()的功能,可以用于线程间消息通信。

编写参考:

https://www.cnblogs.com/takumicx/p/9338983.html

https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

https://www.cnblogs.com/heqiyoujing/p/10409953.html

Java并发基础之多线程的更多相关文章

  1. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  2. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  3. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  4. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

  5. java 并发性和多线程 -- 读感 (一 线程的基本概念部分)

    1.目录略览      线程的基本概念:介绍线程的优点,代价,并发编程的模型.如何创建运行java 线程.      线程间通讯的机制:竞态条件与临界区,线程安全和共享资源与不可变性.java内存模型 ...

  6. Java 并发和多线程(一) Java并发性和多线程介绍[转]

    作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...

  7. Java并发性和多线程

    Java并发性和多线程介绍   java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...

  8. Java并发性和多线程介绍

    java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...

  9. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  10. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

随机推荐

  1. mybatis - [12] 日志工厂

    题记部分 001 || 日志工厂 如果一个数据库操作出现了异常,需要通过日志定位问题. 002 || Log4j Log4j是Apache的一个开源项目,通过使用Log4j,可以控制日志信息输送的目的 ...

  2. 大模型本地部署搭建【ollama + deepseek + dify】

    大模型本地部署搭建[在线] 一.ollama的下载.安装.配置 ollama是管理和运行所有开源大模型的平台 下载地址:https://ollama.com/download 或github下载:ht ...

  3. springboot2.1.6整合activiti6.0(一)

    一.pom <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3. ...

  4. manim边学边做--场景Scene简介

    在 Manim 社区版本中,Scene(场景)是构建动画的核心概念之一,它为我们提供了一个结构化的方式来组织和呈现动画内容. 本文将介绍什么是Scene,它在Manim动画中的作用,以及不同类型的Sc ...

  5. FormCreate设计器v5.6发布—AI加持的低代码表单设计器正式上线!

    近期DeepSeek可谓是刷遍全网,当然,在DeepSeek等AI技术的推动下,人工智能正以惊人的速度改变着各行各业.AI不仅是一种技术趋势,更是未来生产力的核心驱动力. 如今,FormCreate设 ...

  6. 【EX6-1】带孔平板拉伸的弹塑性分析

    带孔平板拉伸的弹塑性分析 来源:<ABAQUS有限元分析实例详解>石亦平等 1. 建模 塑性数据: abaqus输入塑性数据见ABAQUS弹塑性分析 网格划分: 2. 分析设置 单元类型设 ...

  7. wordpress无法显示gitee图床的图片

    wordpress无法显示gitee图床的图片 Question:如题 Solution:是防盗链的问题,gitee官网给出了防盗链的方法,而github貌似没有. Reference:你已经是个成熟 ...

  8. cypress route 拦截 zepto ajax 请求时候 response 返回 null 问题原因

    原文链接:https://blog.jijian.link/2020-08-03/cypress-zepto-ajax-response-null/ 如果你项目有如下几个巧合,大概率会发现一个 aja ...

  9. 编写你的第一个 Django 应用程序,第3部分

    本教程从教程 2 停止的地方开始.我们是 继续网络投票应用程序,并将专注于创建公众界面 – "视图". 在我们的投票应用程序中,我们将有以下四个视图: 问题"索引&quo ...

  10. CoreOS 手动升级篇

    说到升级...通常肯定会以下2个步骤: 检查是否有新版本. 下载和安装新版本. 在 CoreOS 中也一样,我们先来看下在 CoreOS 中对应的命令: # 检查是否有新版本 update_engin ...