多线程基础必要知识点!看了学习多线程事半功倍(转)
Posted jiangwz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础必要知识点!看了学习多线程事半功倍(转)相关的知识,希望对你有一定的参考价值。
多线程三分钟就可以入个门了!
前言
之前花了一个星期回顾了Java集合:
在写文章之前通读了一遍《Java 核心技术 卷一》的并发章节和《Java并发编程实战》前面的部分,回顾了一下以前写过的笔记。从今天开始进入多线程的知识点咯~
之前在学习Java基础的时候学多线程基础还是挺认真的,可是在后面一直没有回顾它,久而久之就把它给忘掉得差不多了..在学习JavaWeb上也一直没用到多线程的地方(我做的东西太水了…)。
由于面试这一部分是占很大比重的,并且学习多线程对我以后的提升也是很有帮助的(自以为)。
我其实也是相当于从零开始学多线程的,如果文章有错的地方还请大家多多包含,不吝在评论区下指正呢~~
一、初识多线程
1.1介绍进程
讲到线程,又不得不提进程了~
进程我们估计是很了解的了,在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:
进程的定义:
进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
-
进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源
1.2回到线程
那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢?
为使程序能并发执行,系统必须进行以下的一系列操作:
-
(1)创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB;
-
(2)撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB;
-
(3)进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。
可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。使OS具有更好的并发性
-
简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能【调度】)。
那么线程在哪呢??举个例子:
也就是说:在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
-
所以说:一个进程会有1个或多个线程的!
1.3进程与线程
于是我们可以总结出:
-
进程作为资源分配的基本单位
-
线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
线程有3个基本状态:
-
执行、就绪、阻塞
线程有5种基本操作:
-
派生、阻塞、激活、 调度、 结束
线程的属性:
-
1)轻型实体;
-
2)独立调度和分派的基本单位;
-
3)可并发执行;
-
4)共享进程资源。
线程有两个基本类型:
-
1) 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。
-
2) 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行以及撤消线程。
值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权
1.4并行与并发
并行:
-
并行性是指同一时刻内发生两个或多个事件。
-
并行是在不同实体上的多个事件
并发:
-
并发性是指同一时间间隔内发生两个或多个事件。
-
并发是在同一实体上的多个事件
由此可见:并行是针对进程的,并发是针对线程的。
1.5Java实现多线程
上面说了一大堆基础,理解完的话。我们回到Java中,看看Java是如何实现多线程的~
Java实现多线程是使用Thread这个类的,我们来看看Thread类的顶部注释:
通过上面的顶部注释我们就可以发现,创建多线程有两种方法:
-
继承Thread,重写run方法
-
实现Runnable接口,重写run方法
1.5.1继承Thread,重写run方法
创建一个类,继承Thread,重写run方法
public class MyThread extends Thread {
@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
}
我们调用一下测试看看:
public class MyThreadDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
1.5.2实现Runnable接口,重写run方法
实现Runnable接口,重写run方法
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
我们调用一下测试看看:
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
结果还是跟上面是一样的,这里我就不贴图了~~~
1.6Java实现多线程需要注意的细节
不要将run()
和start()
搞混了~
run()和start()方法区别:
-
run()
:仅仅是封装被线程执行的代码,直接调用是普通方法 -
start()
:首先启动了线程,然后再由jvm去调用该线程的run()方法。
jvm虚拟机的启动是单线程的还是多线程的?
-
是多线程的。不仅仅是启动main线程,还至少会启动垃圾回收线程的,不然谁帮你回收不用的内存~
那么,既然有两种方式实现多线程,我们使用哪一种???
一般我们使用实现Runnable接口
-
可以避免java中的单继承的限制
-
应该将并发运行任务和运行机制解耦,因此我们选择实现Runnable接口这种方式!
二、总结
这篇主要是讲解了线程是什么,理解线程的基础对我们往后的学习是有帮助的。这里主要是简单的入了个门
在阅读顶部注释的时候我们发现有”优先级“、”后台线程“这类的词,这篇是没有讲解他们是什么东西的~所以下一篇主要讲解的是Thread的API~敬请期待哦~
使用线程其实会导致我们数据不安全,甚至程序无法运行的情况的,这些问题都会再后面讲解到的~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助~
参考资料:
-
《Java 核心技术卷一》
-
《Java并发编程实战》
-
《计算机操作系统-汤小丹》
前言
昨天已经写了:
如果没看的同学建议先去阅读一遍哦~
在写文章之前通读了一遍《Java 核心技术 卷一》的并发章节和《Java并发编程实战》前面的部分,回顾了一下以前写过的笔记。从今天开始进入多线程的知识点咯~
我其实也是相当于从零开始学多线程的,如果文章有错的地方还请大家多多包含,不吝在评论区下指正呢~~
一、Thread线程类API
声明本文使用的是JDK1.8
实现多线程从本质上都是由Thread类来进行操作的~我们来看看Thread类一些重要的知识点。Thread这个类很大,不可能整个把它看下来,只能看一些常见的、重要的方法。
顶部注释的我们已经解析过了,如果不知道的同学可前往:多线程三分钟就可以入个门了!
1.1设置线程名
我们在使用多线程的时候,想要查看线程名是很简单的,调用Thread.currentThread().getName()
即可。
如果没有做什么的设置,我们会发现线程的名字是这样子的:主线程叫做main,其他线程是Thread-x
下面我就带着大家来看看它是怎么命名的:
nextThreadNum()
的方法实现是这样的:
基于这么一个变量-->线程初始化的数量
点进去看到init方法就可以确定了:
看到这里,如果我们想要为线程起个名字,那也是很简单的。Thread给我们提供了构造方法!
下面我们来测试一下:
-
实现了Runnable的方式来实现多线程:
public class MyThread implements Runnable {
@Override
public void run() {
// 打印出当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}
测试:
public class MyThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//带参构造方法给线程起名字
Thread thread1 = new Thread(myThread, "关注公众号Java3y");
Thread thread2 = new Thread(myThread, "qq群:742919422");
thread1.start();
thread2.start();
// 打印当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}
结果:
当然了,我们还可以通过setName(String name)
的方法来改掉线程的名字的。我们来看看方法实现;
检查是否有权限修改:
至于threadStatus这个状态属性,貌似没发现他会在哪里修改:
1.2守护线程
守护线程是为其他线程服务的
-
垃圾回收线程就是守护线程~
守护线程有一个特点:
-
当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。
-
也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行了
使用线程的时候要注意的地方
-
在线程启动前设置为守护线程,方法是
setDaemon(boolean on)
-
使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
-
守护线程中产生的新线程也是守护线程
测试一波:
public class MyThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//带参构造方法给线程起名字
Thread thread1 = new Thread(myThread, "关注公众号Java3y");
Thread thread2 = new Thread(myThread, "qq群:742919422");
// 设置为守护线程
thread2.setDaemon(true);
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName());
}
}
上面的代码运行多次可以出现(电脑性能足够好的同学可能测试不出来):线程1和主线程执行完了,我们的守护线程就不执行了~
原理:这也就为什么我们要在启动之前设置守护线程了。
1.3优先级线程
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素!
线程的优先级是高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了)~
可以看到的是,Java提供的优先级默认是5,最低是1,最高是10:
实现:
setPriority0
是一个本地(navite)的方法:
private native void setPriority0(int newPriority);
1.4线程生命周期
在上一篇介绍的时候其实也提过了线程的线程有3个基本状态:执行、就绪、阻塞
在Java中我们就有了这个图,Thread上很多的方法都是用来切换线程的状态的,这一部分是重点!
其实上面这个图是不够完整的,省略掉了一些东西。后面在讲解的线程状态的时候我会重新画一个~
下面就来讲解与线程生命周期相关的方法~
1.4.1sleep方法
调用sleep方法会进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态!
于是乎,我们的图就可以补充成这样:
1.4.2yield方法
调用yield方法会先让别的线程执行,但是不确保真正让出
-
意思是:我有空,可以的话,让你们先执行
于是乎,我们的图就可以补充成这样:
1.4.3join方法
调用join方法,会等待该线程执行完毕后才执行别的线程~
我们进去看看具体的实现:
wait方法是在Object上定义的,它是native本地方法,所以就看不了了:
wait方法实际上它也是计时等待(如果带时间参数)的一种!,于是我们可以补充我们的图:
1.4.3interrupt方法
线程中断在之前的版本有stop方法,但是被设置过时了。现在已经没有强制线程终止的方法了!
由于stop方法可以让一个线程A终止掉另一个线程B
-
被终止的线程B会立即释放锁,这可能会让对象处于不一致的状态。
-
线程A也不知道线程B什么时候能够被终止掉,万一线程B还处理运行计算阶段,线程A调用stop方法将线程B终止,那就很无辜了~
总而言之,Stop方法太暴力了,不安全,所以被设置过时了。
我们一般使用的是interrupt来请求终止线程~
-
要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了(明白这一点非常重要!)
-
也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了。
-
具体到底中断还是继续运行,应该由被通知的线程自己处理
Thread t1 = new Thread( new Runnable(){
public void run(){
// 若未发生中断,就正常执行任务
while(!Thread.currentThread.isInterrupted()){
// 正常任务代码……
}
// 中断的处理代码……
doSomething();
}
} ).start();
再次说明:调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!
我们来看看源码是怎么讲的吧:
再来看看刚才说抛出的异常是什么东东吧:
所以说:interrupt方法压根是不会对线程的状态造成影响的,它仅仅设置一个标志位罢了
interrupt线程中断还有另外两个方法(检查该线程是否被中断):
-
静态方法interrupted()-->会清除中断标志位
-
实例方法isInterrupted()-->不会清除中断标志位
上面还提到了,如果阻塞线程调用了interrupt()方法,那么会抛出异常,设置标志位为false,同时该线程会退出阻塞的。我们来测试一波:
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
Main main = new Main();
// 创建线程并启动
Thread t = new Thread(main.runnable);
System.out.println("This is main ");
t.start();
try {
// 在 main线程睡个3秒钟
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("In main");
e.printStackTrace();
}
// 设置中断
t.interrupt();
}
Runnable runnable = () -> {
int i = 0;
try {
while (i < 1000) {
// 睡个半秒钟我们再执行
Thread.sleep(500);
System.out.println(i++);
}
} catch (InterruptedException e) {
// 判断该阻塞线程是否还在
System.out.println(Thread.currentThread().isAlive());
// 判断该线程的中断标志位状态
System.out.println(Thread.currentThread().isInterrupted());
System.out.println("In Runnable");
e.printStackTrace();
}
};
}
结果:
接下来我们分析它的执行流程是怎么样的:
2018年4月18日20:32:15(哇,这个方法真的消耗了我非常长的时间)…..感谢@开始de痕迹的指教~
该参考资料:
-
https://www.cnblogs.com/w-wfy/p/6414801.html
-
https://www.cnblogs.com/carmanloneliness/p/3516405.html
-
https://www.zhihu.com/question/41048032/answer/89478427
-
https://www.zhihu.com/question/41048032/answer/89431513
二、总结
可以发现我们的图是还没有补全的~后续的文章讲到同步的时候会继续使用上面的图的。在Thread中重要的还是那几个可以切换线程状态的方法,还有理解中断的真正含义。
使用线程会导致我们数据不安全,甚至程序无法运行的情况的,这些问题都会再后面讲解到的~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助
参考资料:
-
《Java核心技术卷一》
-
《Java并发编程实战》
-
《计算机操作系统-汤小丹》
前言
不小心就鸽了几天没有更新了,这个星期回家咯。在学校的日子要努力一点才行!
只有光头才能变强
回顾前面:
本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基础的。要是能好好理解这些基础,那么我们往后的学习就会事半功倍。
当然了,《Java并发编程实战》可以说是非常经典的一本书。我是未能完全理解的,在这也仅仅是抛砖引玉。想要更加全面地理解我下面所说的知识点,可以去阅读一下这本书,总的来说还是不错的。
首先来预览一下《Java并发编程实战》前4章的目录究竟在讲什么吧:
第1章 简介
-
1.1 并发简史
-
1.2 线程的优势
-
1.2.1 发挥多处理器的强大能力
-
1.2.2 建模的简单性
-
1.2.3 异步事件的简化处理
-
1.2.4 响应更灵敏的用户界面
-
1.3 线程带来的风险
-
1.3.1 安全性问题
-
1.3.2 活跃性问题
-
1.3.3 性能问题
-
1.4 线程无处不在
ps:这一部分我就不讲了,主要是引出我们接下来的知识点,有兴趣的同学可翻看原书~
第2章 线程安全性
-
2.1 什么是线程安全性
-
2.2 原子性
-
2.2.1 竞态条件
-
2.2.2 示例:延迟初始化中的竞态条件
-
2.2.3 复合操作
-
2.3 加锁机制
-
2.3.1 内置锁
-
2.3.2 重入
-
2.4 用锁来保护状态
-
2.5 活跃性与性能
第3章 对象的共享
-
3.1 可见性
-
3.1.1 失效数据
-
3.1.2 非原子的64位操作
-
3.1.3 加锁与可见性
-
3.1.4 Volatile变量
-
3.2 发布与逸出
-
3.3 线程封闭
-
3.3.1 Ad-hoc线程封闭
-
3.3.2 栈封闭
-
3.3.3 ThreadLocal类
-
3.4 不变性
-
3.4.1 Final域
-
3.4.2 示例:使用Volatile类型来发布不可变对象
-
3.5 安全发布
-
3.5.1 不正确的发布:正确的对象被破坏
-
3.5.2 不可变对象与初始化安全性
-
3.5.3 安全发布的常用模式
-
3.5.4 事实不可变对象
-
3.5.5 可变对象
-
3.5.6 安全地共享对象
第4章 对象的组合
-
4.1 设计线程安全的类
-
4.1.1 收集同步需求
-
4.1.2 依赖状态的操作
-
4.1.3 状态的所有权
-
4.2 实例封闭
-
4.2.1 Java监视器模式
-
4.2.2 示例:车辆追踪
-
4.3 线程安全性的委托
-
4.3.1 示例:基于委托的车辆追踪器
-
4.3.2 独立的状态变量
-
4.3.3 当委托失效时
-
4.3.4 发布底层的状态变量
-
4.3.5 示例:发布状态的车辆追踪器
-
4.4 在现有的线程安全类中添加功能
-
4.4.1 客户端加锁机制
-
4.4.2 组合
-
4.5 将同步策略文档化
那么接下来我们就开始吧~
一、使用多线程遇到的问题
1.1线程安全问题
在前面的文章中已经讲解了线程【多线程三分钟就可以入个门了!】,多线程主要是为了提高我们应用程序的使用率。但同时,这会给我们带来很多安全问题!
如果我们在单线程中以“顺序”(串行-->独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全性问题
因为在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题
简单举个例子:
-
下面的程序在单线程中跑起来,是没有问题的。
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
但是在多线程环境下跑起来,它的count值计算就不对了!
首先,它共享了count这个变量,其次来说++count;
这是一个组合的操作(注意,它并非是原子性)
-
++count
实际上的操作是这样子的: -
读取count值
-
将值+1
-
将计算结果写入count
于是多线程执行的时候很可能就会有这样的情况:
-
当线程A读取到count的值是8的时候,同时线程B也进去这个方法上了,也是读取到count的值为8
-
它俩都对值进行加1
-
将计算结果写入到count上。但是,写入到count上的结果是9
-
也就是说:两个线程进来了,但是正确的结果是应该返回10,而它返回了9,这是不正常的!
如果说:当多个线程访问某个类的时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!
有个原则:能使用JDK提供的线程安全机制,就使用JDK的。
当然了,此部分其实是我们学习多线程最重要的环节,这里我就不详细说了。这里只是一个总览,这些知识点在后面的学习中都会遇到~~~
1.3性能问题
使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!
就比如说我们的Servlet,一个Servlet对象可以处理多个请求的,Servlet显然是一个天然支持多线程的。
又以下面的例子来说吧:
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
从上面我们已经说了,上面这个类是线程不安全的。最简单的方式:如果我们在service方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全了。
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
虽然实现了线程安全了,但是这会带来很严重的性能问题:
-
每个请求都得等待上一个请求的service方法处理了以后才可以完成对应的操作
这就导致了:我们完成一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题!
在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。
这些都是我们接下来要学习的地方:学习使用哪种同步机制来实现线程安全,并且性能是提高了而不是降低了~
二、对象的发布与逸出
书上是这样定义发布和逸出的:
发布(publish) 使对象能够在当前作用域之外的代码中使用
逸出(escape) 当某个不应该发布的对象被发布了
常见逸出的有下面几种方式:
-
静态域逸出
-
public修饰的get方法
-
方法参数传递
-
隐式的this
静态域逸出:
public修饰get方法:
方法参数传递我就不再演示了,因为把对象传递过去给另外的方法,已经是逸出了~
下面来看看该书给出this逸出的例子:
逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!理解起来是不是简单了一丢丢?
2.1安全发布对象
上面谈到了好几种逸出的情况,我们接下来来谈谈如何安全发布对象。
安全发布对象有几种常见的方式:
-
在静态域中直接初始化 :
public static Person = new Person()
; -
静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
-
对应的引用保存到volatile或者AtomicReferance引用中
-
保证了该对象的引用的可见性和原子性
-
由final修饰
-
该对象是不可变的,那么线程就一定是安全的,所以是安全发布~
-
由锁来保护
-
发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出
三、解决多线程遇到的问题
从上面我们就可以看到,使用多线程会把我们的系统搞得挺复杂的。是需要我们去处理很多事情,为了防止多线程给我们带来的安全和性能的问题~
下面就来简单总结一下我们需要哪些知识点来解决多线程遇到的问题。
3.1简述解决线程安全性的办法
使用多线程就一定要保证我们的线程是安全的,这是最重要的地方!
在Java中,我们一般会有下面这么几种办法来实现线程安全问题:
-
无状态(没有共享变量)
-
使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
-
加锁(内置锁,显示Lock锁)
-
使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
-
原子性(就比如上面的
count++
操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!) -
容器(ConcurrentHashMap等等…)
-
……
-
…等等
3.2原子性和可见性
何为原子性?何为可见性?当初我在ConcurrentHashMap基于JDK1.8源码剖析中已经简单说了一下了。不了解的同学可以进去看看。
3.2.1原子性
在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!
-
count++
,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了(因为没有中间的三部环节,一步到位【原子性】~
原子性就是执行某一个操作是不可分割的,
- 比如上面所说的count++
操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~
- JDK中有atomic包提供给我们实现原子性操作~
也有人将其做成了表格来分类,我们来看看:
图片来源:https://blog.csdn.net/eson_15/article/details/51553338
使用这些类相关的操作也可以进他的博客去看看:
-
https://blog.csdn.net/eson_15/article/details/51553338
3.2.2可见性
对于可见性,Java提供了一个关键字:volatile给我们使用~
-
我们可以简单认为:volatile是一种轻量级的同步机制
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
我们将其拆开来解释一下:
-
保证该变量对所有线程的可见性
-
在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
-
不保证原子性
-
修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的。
使用了volatile修饰的变量保证了三点:
-
一旦你完成写入,任何访问这个字段的线程将会得到最新的值
-
在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
-
volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:
-
修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
-
该变量不会纳入到不变性条件中(该变量是可变的)
-
在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)
参考资料:
-
http://www.cnblogs.com/Mainz/p/3556430.html
-
https://www.cnblogs.com/Mainz/p/3546347.html
-
http://www.dataguru.cn/java-865024-1-1.html
3.3线程封闭
在多线程的环境下,只要我们不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。
就用我们熟悉的Servlet来举例子,写了那么多的Servlet,你见过我们说要加锁吗??我们所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰!
在方法上操作,只要我们保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的
在线程封闭上还有另一种方法,就是我之前写过的:ThreadLocal就是这么简单
使用这个类的API就可以保证每个线程自己独占一个变量。(详情去读上面的文章即可)~
3.4不变性
不可变对象一定线程安全的。
上面我们共享的变量都是可变的,正由于是可变的才会出现线程安全问题。如果该状态是不可变的,那么随便多个线程访问都是没有问题的!
Java提供了final修饰符给我们使用,final的身影我们可能就见得比较多了,但值得说明的是:
-
final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的!
就好像下面这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量
所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~~~
-
因此,仅仅只能够说明hashMap是一个不可变的对象引用
final HashMap<Person> hashMap = new HashMap<>();
不可变的对象引用在使用的时候还是需要加锁的
-
或者把Person也设计成是一个线程安全的类~
-
因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的!
要想将对象设计成不可变对象,那么要满足下面三个条件:
-
对象创建后状态就不能修改
-
对象所有的域都是final修饰的
-
对象是正确创建的(没有this引用逸出)
String在我们学习的过程中我们就知道它是一个不可变对象,但是它没有遵循第二点(对象所有的域都是final修饰的),因为JVM在内部做了优化的。但是我们如果是要自己设计不可变对象,是需要满足三个条件的。
3.5线程安全性委托
很多时候我们要实现线程安全未必就需要自己加锁,自己来设计。
我们可以使用JDK给我们提供的对象来完成线程安全的设计:
非常多的"工具类"供我们使用,这些在往后的学习中都会有所介绍的~~这里就不介绍了
四、最后
正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。
无论是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,我们的程序才可以更加健壮!
可以发现的是,上面在很多的地方说到了:锁。但我没有介绍它,因为我打算留在下一篇来写,敬请期待~~~
书上前4章花了65页来讲解,而我只用了一篇文章来概括,这是远远不够的,想要继续深入的同学可以去阅读书籍~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助
以上是关于多线程基础必要知识点!看了学习多线程事半功倍(转)的主要内容,如果未能解决你的问题,请参考以下文章