早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源。在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费。

操作系统出现后,计算机可以运行多个程序,不同的程序在单独的进程中运行。操作系统负责为各个独立的进程分配各种资源。并且不同的进程间可以通过一些通信机制来交换数据,比如:套接字、信号处理器、共享内存、信号量等。

一、了解多线程

1.1 进程与线程

想必大家都听说过这两个名词,它们之间有什么联系与不同呢?

记得当时上操作系统课时,书上有这么一句话:进程是独立拥有 cpu 资源的最 小单位,线程是接受 cpu 调度的最小单位。网上看了那么多的解释,还是觉得书上说的最简单明了。

进程中可以同时存在多个程序控制流程的线程,线程会共享进程范围内的资源,因此一个进程可以有多个线程。打个比方,进程就像一个大工厂,而线程就是工厂的工人,多个工人负责协同完成工厂的任务。

1.2 多线程的优势

  • 发挥多处理器的强大能力
  • 简化建模过程
  • 简化异步事件处理机制
  • 响应更灵敏的用户界面

1.3 多线程安全性问题

多线程是一把双刃剑,使用多线程时意味着对开发人员有一定的技术要求。可见学会了多线程技术,就能写出更优雅的代码了,哈哈~

多个线程同时访问意味着“共享”变量,这些“共享”变量往往在其生命周期内是可变的,这样以来就极易出现多线程安全性问题。

什么是线程安全,这里给一个比较准确的定义:当多个线程访问某各类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

二、Java 内存模型

为什么要在多线程专题里涉及到 Java 内存模型呢?我觉得它可以帮助我们更好的理解多线程的工作机制。

比如很多人都听说过或了解过 volatile 关键字,都知道它能保证内存可见性问题,但是理解起来总是太过抽象。如果你了解了 Java 内存模型,它可以很好的帮助你理解这些问题。

Java 内存模型规定了所有的变量都存储在主内存中(Main Memory)中(可以类比为物理硬件的主内存)。每条线程都有自己的工作内存(Working Memory),线程的工作内存中保存了主内存中的变量的拷贝,线程对变量的所有操作(读取、赋值)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。

不同的线程之间无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

如果你对 Java 内存区域了解的话,很容易就会想工作内存、主内存与 Java 内存区域中的堆、栈、方法区是否有一定关系呢?

如果在实例变量的角度来看,主内存对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次来看,主内存直接对应于物理硬件的内存。

三、解决线程安全性问题

使用 Java 创建线程的几种方式这里就不再赘述了,我们来了解几种处理多线程安全性问题的方法。

3.1 synchronized 关键字

Java 提供了一种内置的锁机制(synchronized 关键字)来保证原子性与内存可见性。关于原子性与内存可见性问题会在讲述 volatile 关键字时详细的介绍,感兴趣的可以关注后面的文章。

Java 的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。当线程 A 和线程 B 同时访问临界资源,如果线程 A 获取锁,线程 B 就必须等待或阻塞,A 不释放 B 就只能等待下去。

由于每次只有一个线程执行内置锁内的代码,因此被 synchronized 关键字保护的临界资源会以原子(一组不可分割的单元)的方式执行,多个线程间执行时不会受到干扰,原子性与数据库事务有相同的含义。

synchronized 关键字可以用来修饰方法和代码块,如果修饰非静态方法和同步代码块,使用的锁是当前对象,如果修饰静态方法和静态代码块,使用的是当前类的 Class 对象作为锁。

使用方式如下

public class SyncTest {private Integer num = 1;public int numAutoIncrement() {synchronized (this) {return num ++;}}public static void main(String[] args) {SyncTest syncTest = new SyncTest();// 开启 10 个线程for (int i = 0; i < 10; i++) {new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + syncTest.numAutoIncrement())).start();}}
}

3.2 使用原子类

jdk1.5 之后 java.util.concurrent.atomic 包下引入了诸如 AtomicIntegerAtomicLong 等特殊的原子性变量类。

这些变量类底层使用 CAS 算法与 volatile 关键字确保对所有的计数器操作都能保证原子性与可见性,也就解决了多线程安全问题。

使用方式如下

public class SyncTest {private AtomicInteger atomicInteger = new AtomicInteger(1);public int numAutoIncrement() {return atomicInteger.getAndIncrement();}public static void main(String[] args) {SyncTest syncTest = new SyncTest();for (int i = 0; i < 10; i++) {new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + syncTest.numAutoIncrement())).start();}}
}

3.3 使用显示 Lock 锁

jdk1.5 之前,解决多线程共享对象访问的机制只有 synchronizedvolatile。jdk1.5 新增了一种新的机制:ReentrantLockReentrantLock 并不是为替代内置锁而生的,当内置锁机制不适用时,可以考虑使用这种更灵活的加锁机制。

使用方式如下

public class SyncTest {private Lock lock = new ReentrantLock();private Integer num = 1;public int numAutoIncrement() {lock.lock();try {return num++;} finally {lock.unlock();}}public static void main(String[] args) {SyncTest syncTest = new SyncTest();for (int i = 0; i < 10; i++) {new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + syncTest.numAutoIncrement())).start();}}
}

从上面的代码可以看出来,使用显示锁机制相对内置锁要麻烦一些,使用时要注意必须在 finally 语句块中释放锁,否则当出现异常时,获取到的锁永远不会被释放,在代码中存在这种情况无疑是一种定时炸弹。

显示锁相较于内置锁还提供了等待可中断、公平与非公平等功能,当然还有一个优点是显示锁更加灵活。

PS:
这篇博文中很多知识点拿出来都可以单独写一篇文章,这里只是总结了一部分重要的知识点,先让大家对多线程有一个感性的认识。后面会陆续的补充并发编程系列的文章。如果大家觉得写得还行的话,请持续关注后面的文章。

参考资料

《Java 并发编程实战》
《深入理解 Java 虚拟机》

Java 并发编程系列之带你了解多线程相关推荐

  1. Java并发编程系列

    Java并发编程系列 2018-03-08 Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) ...

  2. java并发实战编程pdf_「原创」Java并发编程系列25 | 交换器Exchanger

    2020年Java面试题库连载中 [000期]Java最全面试题库思维导图 [001期]JavaSE面试题(一):面向对象 [002期]JavaSE面试题(二):基本数据类型与访问修饰符 [003期] ...

  3. reentrantlock非公平锁不会随机挂起线程?_【原创】Java并发编程系列16 | 公平锁与非公平锁...

    本文为何适原创并发编程系列第 16 篇,文末有本系列文章汇总. 上一篇提到重入锁 ReentrantLock 支持两种锁,公平锁与非公平锁.那么这篇文章就来介绍一下公平锁与非公平锁. 为什么需要公平锁 ...

  4. 『死磕Java并发编程系列』并发编程工具类之CountDownLatch

    <死磕 Java 并发编程>系列连载中,大家可以关注一波:

  5. Java 并发编程系列之闭锁(CountDownLatch)

    在讲闭锁之前,我们先来思考一个问题:在多线程环境下,主线程打印一句话,如何保证这句话最后(其他线程全部执行完毕)打印? 博主目前可以想到的实现方式有两种.一种是通过 join() 方法实现,另一种就是 ...

  6. 『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

    目录 线程安全问题 活跃性问题 性能问题 有态度的总结 头发很多的程序员:『师父,这个批量处理接口太慢了,有什么办法可以优化?』架构师:『试试使用多线程优化』第二天头发很多的程序员:『师父,我已经使用 ...

  7. java并发编程系列-内存模型基础

    java线程之间的通信对程序开发人员是完全透明的,内存的可见性问题很容易困扰很多开发人员.本篇博文将揭开java内存模型的神秘面纱,来看看内存模型到底是怎样的. 并发编程中需要处理的两个关键问题: · ...

  8. Java并发编程系列之CyclicBarrier详解

    简介 jdk原文 A synchronization aid that allows a set of threads to all wait for each other to reach a co ...

  9. Java并发编程系列之CountDownLatch用法及详解

    背景 前几天一个同事问我,对这个CountDownLatch有没有了解想问一些问题,当时我一脸懵逼,不知道如何回答.今天赶紧抽空好好补补.不得不说Doug Lea大师真的很牛,设计出如此好的类. 1. ...

最新文章

  1. 【Tomcat】Tomcat 系统架构与设计模式,第 1 部分: 工作原理
  2. linux介绍及目录结构(一)
  3. INTERSPEECH 2021 AutoSpeech挑战赛开启报名
  4. MULE ESB功能介绍
  5. 对团队建设与管理的几点看法
  6. php having,having方法
  7. caffe common 程序分析 类中定义类
  8. 零基础自学java_零基础学Java——小白的Java之路(4)
  9. 输油管的布置数学建模matlab,输油管的布置-数学建模.docx
  10. 设计一个求立方体体积的父类,包含一个显示底面各个形状信息的统一方法,信息显示方式 “类别+周长+面积”;一个计算和显示立方体体积的统一方法 设计三个子类(利用继承关系):圆柱、长方体、三棱柱
  11. ArcGIS Server 10.8.1安装
  12. Movavi Video Suite 使用教程|如何刻录DVD ?使用Movavi Video Suite!
  13. 100047. 【NOIP2017提高A组模拟7.14】基因变异
  14. graphql 嵌套查询_了解GraphQL中的查询
  15. 数据集转换成LMDB格式
  16. 《离散时间信号处理学习笔记》—z变换(一)
  17. 安装gnome-screenshot截图工具
  18. IF 19.865代谢组学高分文章,非靶标代谢流助力揭示18SrRNA中m6A控制肝癌机制!
  19. OCTEON startup analysis
  20. 2021年全球干细胞收入大约2832.7百万美元,预计2028年达到5673.4百万美元,2022至2028期间,年复合增长率CAGR为11.3%

热门文章

  1. springboot开发的项目上传图片到服务器后不能访问
  2. proto的介绍和基础使用
  3. 【过程记录】springcloud配置使用Eureka作服务发现组件并进行微服务注册
  4. SpringSide 4 QuickStart运行Demo
  5. Spring MVC配置多个视图解析器(FreeMarker,JSP)
  6. i节点详解以及软连接和硬链接的区别
  7. ZBar 自定义界面
  8. 关于 java.toString() ,(String),String.valueOf的区别
  9. char与byte的区别
  10. Java基础篇:对象拷贝:clone方法 以及 序列化