其实多线程还有很多的东西要说, 我们慢慢来, 可能会有一些东西没说到, 那就没办法了, 只能说尽量吧!
1.synchronized 关键字
说到多线程肯定离不开这个关键字, 为什么呢? 因为多线程之间虽然有各自的栈和 PC 计数器, 但是也有一些区域是共享的(堆和方法区), 这些共享的区域就不可避免的造成一些问题, 比如一个线程对共享区的一个变量进行修改时, 此时另外一个线程也要对这个数据进行修改, 就会出现同步问题, 到底是以哪个线程为主呢?
最常见的可能就是银行转账了, 假如我就 100 块, 我要向朋友小明转账 100 块, 由于转账可能需要一些时间, 所以这个时候我的账户余额显示的还是 100 块! 于是我趁着这段时间立马又向小红转账 100 块, 也会成功, 于是最后我的账户余额会变成负 100 块, 这显然就是不对的.
于是我们可以用. synchronized 关键字来修饰这个方法, 让这个方法在多线程的情况下, 一次只能被一个线程操作, 在这个线程用完这个方法之前, 其他的线程不可以调用这个方法, 这就是锁;
你想一下, 你回到你自己的房间里就把们反锁了, 别人肯定就进不来了啊!
. 关于 synchronized 关键字的用法大概就几种
1.1. 同步方法
同步方法很显然是加在方法上面, 注意位置, 要放在返回类型前面:
1.2. 同步代码块
我们可以用 synchronized 将方法中的代码都包起来, 这种和上面那种是等同的;
但是有的时候我们不想整个方法都用 synchronized 包起来, 因为这样的效率很低(注意, synchronized 关键字修饰的区域越小效率越高), 我们可以把其中一些主要的逻辑给包起来:
1.3. 分析
我们说一说那个 this 代表什么意思, 也可改成其他的么? 当然可以改成其他的, 类型随意, 随便什么都行, 字符串, 对象, 或者 User.class 等等, 那到底有什么用呢?
怎么说呢, 我就说说我的理解吧! 我将 synchronized(){}这样的看作是一把锁, 而括号里面的 this 就是锁芯, 假如有两个锁芯相同的锁, 那么钥匙肯定是一样的!
根据这个道理, 我们说说多线程调用这个代码块的规则, 首先, 一个程序中有很多的同步代码块(也就是锁), 每一个锁都对应有一个锁芯, 有可能多个锁的锁芯相同; 而每个线程默认拥有全部锁的钥匙, 这个时候一个线程打开一把锁之后进去, 立刻把门反锁锁死, 而且这个反锁比较牛逼, 能把所有相同锁芯的门都反锁锁死, 其他线程即使拥有这种锁芯的钥匙也是打不开的, 但是可以打开其他类型的锁;
反正我是这样理解的, 如果你有更好的理解方式最好用自己的理解方式;
顺便看一看下面的的简单代码:
- package com.wyq.thread;
- public class Bank {
- public void toMoney(int money){
- synchronized (this) {
- System.out.println("转账金额:"+money);
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- public void save(int num){
- synchronized (this) {
- System.out.println("存钱:"+num);
- }
- }
- public static void main(String[] args) {
- Bank bank = new Bank();
- new Thread(new Runnable() {
- @Override
- public void run() {
- bank.toMoney(100);
- }
- }).start();
- new Thread(new Runnable() {
- @Override
- public void run() {
- bank.save(200);
- }
- }).start();
- }
- }
- View Code
你们觉得执行的结果怎么是什么? 答案: 由于这两个锁的锁芯是一个型号的, 所以先是转账方法执行, 执行完毕之后才是存钱方法
现在我们把存钱方法的锁芯换成 Bank.class 试试效果, 可以看到这两个互不影响;
这说明了一个线程执行一个同步代码块时, 该类中的其他锁芯相同的同步代码块就会被锁死; 但是其他锁芯的方法还是可以正常使用;
但是你们有没有想过假如把 synchronized 关键字加到静态方法上会怎么样呢? 有兴趣的可以试试, 我直接说一下结论, 下面两种是等效的:
2. 线程的生命周期
前面我们说了这么多都是说的多线程的用法, 我一直的理念就是学新知识先不要看概念什么的, 先会熟练运用, 用多了再理解概念会很深刻 1
我们就随便看看线程的生命周期吧!
我们每次都是调用 xxx.start()方法表示本线程已经准备就绪, CPU 可以随时的过来运行这个线程, 难道线程一创建就直接是准备就绪的吗? 话说线程是什么时候创建的啊? 是是在 new Thread()的时候? 还是调用 start()方法得时候? 难道是 CPU 来调用这个线程的时候再创建线程吗? emmmm, 是不是感觉比较模糊啊, 那么接下来我们就简单看看一个线程从创建到死亡到底经历了哪些阶段?
线程从出生到死亡分为五个状态, 我们根据图来看看五个状态分别是干什么的:
新建 (NEW): 注意: 我们在代码中 new Thread(xxx) 的时候只是创建一个普通的 java 对象, 还没有创建线程, 只有调用的 start()方法的时候 jvm 才会真正的开始新建线程;
准备就绪 (Runnable): 调用 start() 方法线程也创建了之后, 此线程不会马上被 CPU 调用, 会进入准备就绪状态等待 CPU 过来; 在 start()方法内部才是真正开始创建线程!!!
运行 (Running): 当 CPU 调用这个线程的时候这个线程就处于运行状态, 并且开始执行 run() 方法内部的逻辑
阻塞(Blocked): 正处于运行状态的线程由于一些原因被终止了, 线程就进入阻塞状态, 也可以我们人为的让线程阻塞! 注意: 阻塞状态下的线程无法被 CPU 在此调用, 除非想办法让阻塞状态变成准备就绪状态才有可能被 CPU 调用.
死亡 (Terminated): 线程死亡, 要么是线程 run() 方法正常执行完毕, 要么就是执行这个线程的时候出现了什么问题被迫死亡...
随意看看 start()方法的源码, 非常少, 很好理解:
- public synchronized void start() {
- // 线程新创建时 threadStatus=0, 这里是判断当前线程是不是新建的, 如果不是, 就抛出一个异常
- if (threadStatus != 0)
- throw new IllegalThreadStateException();
- // 将此线程添加到组中, 这个组的类型是 ThreadGroup, 其实在这个组里面就是维持了一个 Thread[]数组, 用于保存我们将要运行的线程
- group.add(this);
- boolean started = false;
- try {
- // 调用下面的那个本地方法, 并设置运行的标志为 true
- start0();
- started = true;
- } finally {
- try {
- if (!started) {
- // 假如线程运行的标志设置失败, 就抛出线程开始失败异常
- group.threadStartFailed(this);
- }
- } catch (Throwable ignore) {
- }
- }
- }
- // 这是一个本地方法也就是 JNI 方法, 用 C++ 实现的, 所以我们这里看不到任何实现, 但是可以猜想这里面会创建线程, 分配线程的内存空间,
- // 然后就是等待 CPU 的调度, 内部就会调用我们创建线程的 run()方法
- private native void start0();
由于本人对 C++ 不熟悉, 那个 JNI 的方法源码就不献丑了! 假如有小伙伴对那个 JNI 方法很有兴趣的话, 可以参考一个老哥的博客: https://www.jianshu.com/p/81a56497e073, 没兴趣的话就算了...
其实我感觉分析到了这一步就差不多了, 稍微提一下, 看了 start()方法的源码, 问一个很有趣的问题, 假如我们创建线程的时候多次调用 start()方法会怎么样呢? 例如下面:
答案是, 多次调用 start()方法就会报错, 看上面源码的第一个注释那里, 试想一下, 对于同一个 Thread 对象, 第一次调用的 start()方法之后线程可能就被 CPU 调用了, 就不再是新建状态了, 我们再进行 start()方法肯定就会抛异常啊! 异常信息我也截一下图看看:
但是啊, 假如要改的话怎么改呢? 要么就把 for 循环去掉, 要么 for 循环就把 Thread thread = new Thread(xxx)这一段东西也包含进去, 使得每一次都是新建一个线程去调用 start()方法, 那就不会报错了!
3.JMM 和 JVM 的区别
这两个很像, 但是对于新手来说第一次看到这两个还真是懵懵分不清楚, 首先 jvm 指的是 java 虚拟机(我们一般也叫做 jvm 的内存模型), 一般指的是 jvm 组成部分, 其实就是我们前面说的那几个部分组成, java 栈, java 堆, 方法区等等, 这里就不多说了;
那么 JMM 又是一个什么鬼呢? 我们看一句话: Java 线程之间的通信由 Java 内存模型 (简称为 JMM) 控制, JMM 决定一个线程对共享变量的写入何时对另一个线程可见! 这句话的意思就是对于多线程来说, 线程之间的通信就是由 JMM 控制, 而 JMM 是一个 java 内存模型, 其实就是线程对共享区数据读取和写入的一个模型!
我随便借了一张 java 内存模型 (简称 JMM) 示意图:
方便我们理解, 我们可以把主内存看作 jvm 中的共享区(java 堆 + 方法区), 假如我们创建的一个线程要从共享区中的数据进行修改, 看一下一个简单的例子:
想一想为什么不是按照顺序打印的啊? 你看, 还有的打印居然重复了, 有两个 0, 这真是日了狗了, 按理来说创建了 20 个线程, 每一个线程都会拿到 i 进行 + 1 错作, 并打印, 每个数字应该只会出现一次啊!
不是按照顺序打印的很好理解, 因为线程的执行顺序是 CPU 随机调度的嘛, 但是重复的数据就不能理解了, 于是这就要看看我们上面的 JMM 模型了;
首先是每个线程都有自己的私有内存空间(栈 + PC 计数器), 其实还有一个私有的空间叫做线程的工作空间, 这其实就是起到一个缓存的作用, 因为 CPU 在调度线程的时候由于 CPU 运算速度太快, 从内存中读取的话也很慢, 很浪费时间, 于是有了缓存这个作为 CPU 和内存之间的缓冲!
假如一个线程要从共享区拿到数据, 首先会将这个数据复制一份到这个线程的工作空间, 然后 CPU 调用这个线程的时候其实就是 CPU 对这个线程工作空间的数据进行运算, 将运算后的结果覆盖工作空间原来的值, 等这个线程进入死亡状态的时候, 线程工作空间的值就会回写到主存中覆盖原有的值.
是不是觉得一切都很好, 然而这里却很有问题, 假如线程 A 将共享区的变量 I=0 复制到线程 A 的工作空间, 然后 CPU 操作, 对 i=i+1, 此时 i=1 但是由于线程还有其他逻辑要处理, 还没有来得及写入到共享区覆盖原有值 0, 另外一个线程 B 也创建了, 并且也将共享区的 i=0 读到 B 线程的内存空间, 然后也对 i=i+1, 此时 i 也是等于 1, 然后线程 A,B 都将自己的值写入共享区覆盖原来的 i, 此时共享区中 i=1; 很坑, 明明进行了两个线程的错作, i 的值却只是增加了一次;
要怎么解决这个问题呢? 这里简单介绍两种方式, 第一种就是前面说的 synchronized 关键字, 我们可以看看效果:
第二种方案是用 volatile 关键字, 这个关键字修饰一个成员变量, 存放在共享区, 这个成员变量就相当于暴露在所有的线程眼中, 只要有线程对这个变量进行什么修改, 所有的线程都会知道, 然后也会相应的进行修改;
举个例子: 一个变量被 volatile 修饰, 假如有一个线程读取了这个变量的值, 并在该线程的工作空间中进行了修改, 那么马上就会回写到共享区, 其他线程如果也要用到这个共享区变量, 就要直接从从共享区中拿, 这样可以保证拿到的数据始终都是最新的, 当然这样做其实就是去掉线程工作区间, 会影响性能....
我们看看一个例子:
注意: 从这里开始可能会有点不好理解了, 友军可以跳过!!!
那么我们就要问了, 这个线程的工作空间在哪里呢? 是不是在线程私有空间的栈中还是 PC 计数器中呢?
答案是: 没有这个所谓的线程工作空间, 这是一个虚拟模型, 实际系统中并没有直接的对应; 我找了很多的资料, 我也是看的云里雾里, 我把那些资料给大家看看, 看过就好, 不要深究;
答案 1:"工作内存" 是一个虚拟模型, 实际系统中并没有直接的对应. java 官方文档中也没有 "工作空间" 这个概念, 应该是有人为了让读者理解 java 支持的非常松散的内存一致性模型才提出来的. 有点误人子弟. 这个 "工作内存" 是各种 CPU 架构支持的内存模型跟编译器的各种优化而产生的一个效果, 并没有工作内存跟主内存相互拷贝的实际动作;
答案 2:"工作缓冲区" 是一个抽象的概念, JVM 只是规范了主存和线程内存的变量访问时候的需要满足的规范(如可见性等), 并没有对这个缓冲区做实现上的限制. 也就是说, 把共享变量从主存拷贝到线程的工作内存后, 具体放在哪里, 取决于具体的虚拟机实现, 只要满足 JMM 规范, 至于具体放在哪里(寄存器, 内存 Cache 等等), 其实也没关系的;
答案 3: 对于 JMM 和 JVM 本身的内存模型, 这两者并没有关系; 如果一定要对应, 那就从变量, 主内存, 工作空间的定义来看, 主内存主要定义 java 堆中对象实例数据部分, 而工作内存则对对英语虚拟机栈中的部分区域; 从更低的层次来看, 主内存就是物理内存, 而为了获取更好的执行速度, 虚拟机 (甚至硬件系统本身的优化措施) 可能会让工作内存由于存储与寄存器和高速缓存中, 因为运行时主要访问的是工作内存
总结:
估计后面会继续说说线程, 还有好多东西啊, 比如怎么人为的去将线程由运行状态变为阻塞状态, 然后想办法再把阻塞状态变为准备就绪状态, 还有其他的各种锁, 以及一些概念性的东西, 慢慢来吧!
假如有小伙伴想看书学习多线程的话, 可以参看一本叫做《JAVA 多线程设计模式》, 电子档链接: https://pan.baidu.com/s/1ng_bAGE-ieNUZoczFHCnJA 提取码: sqz3
来源: https://www.cnblogs.com/wyq1995/p/10759582.html