目录

  • 多线程定义:
  • 多线程编程
    • 创建线程方法
    • Thread 类及常见方法
      • 启动问题(start() 与 run())
        • 区别
      • 中断线程
      • 等待一个线程-join()
    • 线程的状态
    • 线程安全
      • 线程安全定义
      • 线程不安全的原因
        • jvm内存
        • 线程不安全原因
  • 解决线程不安全方法:
    • synchronized 关键字-监视器锁
    • volatile 关键字
  • wait 和 notify
    • wait()方法
    • notify()方法
    • wait 和 sleep 的对比

多线程定义:

  1. 线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  2. 进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
  3. 线程与进程区别:
  • 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
  • 切换:线程上下文切换比进程上下文切换要快得多。
  • 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
  • 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。

多线程编程

首先废话不多说,我们先来运行如下一段代码感受一下多线程:

package demo1;import java.util.Random;
public class ThreadDemo {private static class MyThread extends Thread {@Overridepublic void run() {Random random = new Random();while (true) {// 打印线程名称System.out.println(Thread.currentThread().getName());try {// 随机停止运行 0-9 秒Thread.sleep(random.nextInt(10));}catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();Random random = new Random();while (true) {// 打印线程名称System.out.println(Thread.currentThread().getName());try {Thread.sleep(random.nextInt(10));} catch (InterruptedException e) {// 随机停止运行 0-9 秒e.printStackTrace();}}}
}

感受到了吧,我猜你只发现了他会一直不停的运行。。。。。
所以接下来我们就正式来看看这到底是怎么个东西。

创建线程方法

  1. 继承 Thread 类
  1. 创建一个线程类继承Thread方法
  2. 用线程类实例化对象
  3. 启动线程
public class ThreadDemo {private static class MyThread extends Thread {//新建类继承Thread,使这个类变成线程类@Overridepublic void run() {System.out.println("这里是线程运行的代码");}}public static void main(String[] args) {//用线程类实例化对象MyThread thr1 = new MyThread();//启动线程thr1.start();}
}
  1. 实现 Runnable 接口
  1. 实现 Runnable 接口
  2. 创建 Thread 类实例, 调用Thread的构造方法时将Runnable对象作为target参数
  3. 调用start方法启动线程
public class demo2 {static class MyRunnable implements Runnable{//新建线程类实现Runnable接口@Overridepublic void run() {System.out.println("这里是线程运行的代码");}}public static void main(String[] args) {Thread thr1 = new Thread(new MyRunnable());thr1.start();}
}
  1. 当然还有他们的几种变形
  • 匿名内部类创建 Thread 子类对象
public class demo3 {public static void main(String[] args) {// 使用匿名类创建Thread 子类对象Thread t1 = new Thread() {@Overridepublic void run() {System.out.println("使用匿名类创建 Thread 子类对象");}};t1.start();}
}
  • 匿名内部类创建 Runnable 子类对象
public class 匿名内部类创建Runnable子类对象 {public static void main(String[] args) {// 使用匿名类创建 Runnable 子类对象Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使用匿名类创建 Runnable 子类对象");}});t2.start();}
}
  • lambda 表达式创建
    Java8引入的东西,可以重新了解一下,有机会的话,会出相关内容,重新贴链接
public class lambda表达式创建线程 extends Thread{public static void main(String[] args) {new Thread(()->{System.out.println("lambda方式创建线程");}).start();}
}

氮气加速——多线程:
故名思义,使用多线程的好处就是加速,在某些场合下是可以提高程序的整体运行效率的。

首先我们来看一段代码及结果感受一下

public class ThreadAdvantage {// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的private static final long count = 10000000;public static void main(String[] args) throws InterruptedException {// 使用并发方式concurrency();
// 使用串行方式serial();}private static void concurrency() throws InterruptedException {long begin = System.nanoTime();// 利用一个线程计算 a 的值Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i = 0; i < count; i++) {a--;}}});thread.start();
// 主线程内计算 b 的值int b = 0;for (long i = 0; i < count; i++) {b--;}
// 等待 thread 线程运行结束thread.join();// 统计耗时long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("并发: %f 毫秒%n", ms);}private static void serial() {// 全部在主线程内计算 a、b 的值long begin = System.nanoTime();int a = 0;for (long i = 0; i < count; i++) {a--;}int b = 0;for (long i = 0; i < count; i++) {b--;}long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("串行: %f 毫秒%n", ms);}
}

运行结果如下:


Thread 类及常见方法

前面一直介绍Thread类,我们也听了很多遍了,下面来正式了解一下这个类和其常见方法

  1. 构造方法
  2. 常见属性

    JVM会在一个进程的所有非后台线程结束后,才会结束运行。

启动问题(start() 与 run())

记住线程的启动是start()

区别
  1. start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
  2. run: run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
  3. 总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

看完那个解释是不是更晕了,嗯。。。,知道会晕所以我们来看一图就明白了

调用 start 方法, 才真的在操作系统的底层创建出一个线程哦~

start()也只是加入就绪队列,真正的开始执行由cpu决定

中断线程

等待一个线程-join()

这个用法没啥讲的,可以看下面这段代码运行感受一下

public static void main(String[] args) throws InterruptedException {Runnable target = () -> {for (int i = 0; i < 10; i++) {try {System.out.println(Thread.currentThread().getName() + ": 我还在工作!");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 我结束了!");};Thread thread1 = new Thread(target, "李四");Thread thread2 = new Thread(target, "王五");System.out.println("先让李四开始工作");thread1.start();thread1.join();System.out.println("李四工作结束了,让王五开始工作");thread2.start();thread2.join();System.out.println("王五工作结束了");}

当然还有休眠sleep()【上面代码其实已经看到了】;currentThread()获取当前线程引用等方法,我们不一一介绍了,不然太划水了。


线程的状态


线程安全

前面的方法做了解会用知道咋回事就好了,但是这个可是重点哟~
我们先来看一段代码:

 static class Counter {public int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}

很明显结果应该是100000,但是下面是我们的运行结果:


那么为什么会这样?
接下来我们正式了解线程安全:

线程安全定义

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全的原因
jvm内存



通过这两张图,我们很明显能看出来哪些东西线程之间共享,哪些是有。

  1. 内存区域:
    共享:堆、方法区、运行时常量池;
    私有:pc、栈
  2. 表现在代码中的话:
    共享:对象、类对象、静态属性;私有:局部变量
线程不安全原因
  1. 开发者角度:
  1. 共享数据:多个线程之间操作同一块数据
  2. 并且这两个线程至少有一个线程在修改这块共享数据
  1. 系统角度:
    注:

    1. 高级语言的一条语句,可能对应多条指令;
    2. 线程调度可能发生在任一时刻(不会切割指令);
    3. cpu为了提高数据获取速度会设置缓存(Cache):因为指令的执行速度远远大于内存读写速度
    4. jvm对内存进行了模拟,内存:jvm主内存;缓存:jvm:工作内存
    5. 我们写的代码在编译阶段会进行代码重排序(jvm要求无论如何优化,当线程角度结果不应该变化,所以多线程环境可能出问题)
  1. 原子性被破坏
  2. 内存可见性问题,导致某些线程读取到脏数据
  3. 代码重排序,导致线程之间数据配合出现问题

解决线程不安全方法:

回顾三个阶段我们知道破环线程安全主要是如下三个条件:

  1. 原子性被破坏
  2. 内存可见性问题,导致某些线程读取到脏数据
  3. 代码重排序,导致线程之间数据配合出现问题

那么嘿嘿嘿。。。解决方案不就来了嘛:

synchronized 关键字-监视器锁

保证原子性,当然他也能保证内存可见性


注意:

  1. 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  2. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

当然关于锁的理解这一点肯定是不够的,我们下一篇将详细介绍这个东西

volatile 关键字

保证内存可见性,无法保证原子性

上面是jvm的内存工作,下面是加入volatile关键字时的操作;

  1. 代码在写入 volatile 修饰的变量的时候:
    改变线程工作内存中volatile变量副本的值
    将改变后的副本的值从工作内存刷新到主内存
  2. 代码在读取 volatile 修饰的变量的时候:
    从主内存中读取volatile变量的最新值到线程的工作内存中
    从工作内存中读取volatile变量的副本

那么是什么意思?还记得我们之前说过:直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况,而加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
理论是不是很烦我们来看一段代码理解一下:

static class Counter {public int flag = 0;}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();}

很明显当我们输入0是上面的代码应该停止运行,然而并不会那样,因为有缓存,所以我们给flag加上volatile关键字,这样,就会从内存读写,咳咳咳。。解决了。

static class Counter {public volatile int flag = 0;}

如下图所示:

当然这个只是基本知识,其他的后续将出,当然推荐大家自己动手实验一下;

wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

wait()方法

  1. wait 做的事情:
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.
  • wait 要搭配 synchronized 来使用, 脱离 synchronized 使用 wait 会直接抛出异常.
  1. wait 结束等待的条件:
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

notify()方法

  1. notify 方法是唤醒等待的线程.
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

我们来举个例子看一下:

static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}}static class NotifyTask implements Runnable {private Object locker; public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(5000);t2.start();}


结果很明显了吧,嗯。。。就是这样等5秒后会notify介入,会让等待的东西继续进入,当然,这个一次只能唤醒一个线程,所以notifyAll()方法故名思义,唤醒所有的等待线程。不过当然如上代码,因为如果有其他线程竞争同一个资源,一起唤醒(notify()),其唤醒过程也是有顺序的。如下图所示:

wait 和 sleep 的对比

  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.
  3. sleep方法属于Thread类中的静态方法,wait属于Object的成员方法。
  4. sleep()是线程类(Thread)的方法,不涉及线程通信,调用时会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,用于线程间的通信,调用时会放弃对象锁,进入等待队列,待调用notify()或者notifyAll()唤醒指定的线程或者所有线程,才进入对象锁定池准备获得对象锁进入运行状态。
  5. sleep()方法必须捕获异常InterruptedException,而wait()\notify()以及notifyAll()不需要捕获异常。

这俩本来关系基本接近于0,就像final和finally一样,一个用法像,一个长得像,但是本质没关系,就像现在很多奇怪的的社会现象,孩子是你在养、却和你一点关系都没有、扎心了吧老铁。。。

下一篇中,就该考虑一下定时器、线程池、模式等的一些简单用法了,敬请关注

Java多线程上——基本概念及操作相关推荐

  1. Java多线程-线程的概念和创建

    前言 声明:该文章中所有测试都是在JDK1.8的环境下. 该文章是我在学习Java中的多线程这方面知识时,做的一些总结和记录. 如果有不正确的地方请大家多多包涵并作出指点,谢谢! 一.基础概念 我们知 ...

  2. java 多线程的基本概念_java基本教程之多线程基本概念 java多线程教程

    多线程是Java中不可避免的一个重要主体.下面我们将展开对多线程的学习.接下来的内容,是对"JDK中新增JUC包"之前的Java多线程内容的讲解,涉及到的内容包括,Object类中 ...

  3. Java多线程-锁的概念

    1.结婚戒指的意义 根据文献记载,最早使用戒指人就是希腊的悲剧英雄--被缚的普罗米修斯.宙斯为惩罚普罗米修斯盗火给人类,将他绑缚在考卡苏斯山上,每天都有一只老鹰飞到山上,将他的内脏啄出,到了夜晚,他所 ...

  4. java多线程上传文件_Java大文件分片上传/多线程上传

    这里只写后端的代码,基本的思想就是,前端将文件分片,然后每次访问上传接口的时候,向后端传入参数:当前为第几块文件,和分片总数 下面直接贴代码吧,一些难懂的我大部分都加上注释了: 上传文件实体类: 看得 ...

  5. java多线程编程01---------基本概念

    一. java多线程编程基本概念--------基本概念 java多线程可以说是java基础中相对较难的部分,尤其是对于小白,次一系列文章的将会对多线程编程及其原理进行介绍,希望对正在多线程中碰壁的小 ...

  6. Java多线程变量共享与隔离

    文章目录 线程相关 线程的相关API 线程的调度 线程的优先级 方法和变量的线程安全问题 静态方法 非静态方法 静态变量 实例变量 局部变量 变量共享 共享变量线程安全问题 可见性 可见性举例 共享变 ...

  7. java多线程应用场景

    java多线程应用场景 本教程操作环境:windows7系统.java10版,DELL G3电脑. 1.应用场景 (1)普通浏览器和网络服务(现在写的网络是帮你完成线程控制的中间部件),网络处理请求, ...

  8. Java多线程笔记(零):进程、线程与通用概念

    前言 不积跬步,无以至千里:不积小流,无以成江海.在学习Java多线程相关的知识前,我们首先需要去了解一点操作系统的进程.线程以及相关的基础概念. 进程 通常,我们把一个程序的执行称为一个进程.反过来 ...

  9. java多线程基础概念

    大纲:java多线程知识体系` 程序`:为了完成某一功能, (用某种语言编写的一组指令的集合),是一段静态的代码块 进程:程序的一次执行过程,是正在运行的程序,有着完整的生命周期,是资源分配的基本单位 ...

最新文章

  1. Android 动画的插值器 (Interpolator属性)
  2. Matlab与线性代数 -- 稀疏矩阵的图形显示
  3. 高并发场景下缓存的常见问题
  4. C++_static,类模板、函数模板、namespace
  5. linux环境下查看项目内存情况
  6. CentOS7 安装 MySQL 和简单优化
  7. linux gst qt,【ARM-Linux开发】Gstreamer+QT+摄像头 编程总结
  8. 企业到底需要什么样的飞鸽传书
  9. leetcode127. Word Ladder
  10. 计算机专业课如何阅卷,全国计算机等级考试评卷老师是如何阅卷的?
  11. android指定分享到qq,Android使用系统分享文件给微信,QQ指定的用户
  12. 【WiFi】WiFi 5G信道和频宽的对应关系
  13. 风云java_风云烈传-执掌风云
  14. U盘数据恢复软件推荐
  15. 好的提高代码质量的方法有哪些?
  16. 把数字翻译成中文的计算机,数字翻译成中文,把数字翻译成中文
  17. java put方法_java 实现Put request
  18. Springboot配置多个数据源
  19. 第三代酷睿i3处理器_英特尔最新10代处理器发布:游戏世界最快
  20. PKI 公钥基础设施原理与应用

热门文章

  1. ismobile什么意思_mobile是什么意思_mobile的翻译_音标_读音_用法_例句_爱词霸在线词典...
  2. pixel什么意思_pixel是什么意思_pixel的翻译_音标_读音_用法_例句_爱词霸在线词典...
  3. 风控决策矩阵的开发与场景应用
  4. 2021年中国反光材料现状与格局分析,城镇化推进,交通里程持续增长带来需求增长「图」
  5. 微信浏览器打不开推广链接怎么办,设置微信内自动跳转手机浏览器打开网页
  6. 简单理解在线性函数的估计中bias(偏差)与variance(方差)的影响
  7. css如何实现多个动画顺序播放
  8. git--“HEAD detached from”的解决方法
  9. ACM期刊/会议中的CCS CONCEPTS
  10. 文献精读-PSEP-生物质和PE塑料的共热解MD以及DFT模拟