4.多线程学习
创始人
2025-05-31 04:10:44
0

作者:爱塔居

专栏:JavaEE

作者简介:大三学生,希望和大家一起进步。

文章目录

目录

文章目录

前言回顾

一、如何解决线程安全问题?

二、如何进行加锁

三、内存可见性引起的线程不安全

四、指令重排序


前言回顾

一、线程和进程的基本概念。

  1. 首先说明进程与线程之间的关系 : 进程中包含一条或多条线程;
  2. 第二从系统管理与分配资源和CPU调度的角度来分析:进程是系统分配资源的最小单位,线程是CPU调度的最小单位;
  3. 第三从资源使用角度来分析:进程之间不能共享资源,进程中的线程之间共享进程的所有资源;
  4. 单独介绍一下线程的特点:线程的创建、销毁、调度效率比进程更高,并且有自己独立的执行任务。

二、请说明Thread类中run和start的区别

作用功能不同:

  1. run方法的作用是描述线程具体要执行的任务;
  2. start方法的作用是真正的去申请系统线程

运行结果不同:

  1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
  2. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

三、Thread类用法,启动线程,终止线程,等待线程,获取线程引用,休眠线程。

四、线程的状态

NEW RUNNABLE TERMINATED  TIMED_WAITING  WAITING  BLOCKED

争对这些基本的线程状态,我们要有一个基本的认识。这些状态对我们未来调试多线程的问题是有很大的帮助的。

五、线程安全(重点、难点)

一个代码在多线程环境下,执行会出问题,这就叫线程不安全。

为什么会出现线程安全问题?

本质原因:线程在系统中的调度是无序的/随机的。(抢占式执行)

1.抢占式执行(罪魁祸首)。

2.多个线程修改同一个变量。

一个线程修改同一个变量=>安全

多个线程读取同一个变量=>安全

多个线程修改不同的变量=>安全

3.修改操作,不是原子的。(原子性:不可分割的最小单位)

修改操作,可以拆分成三个操作——load、add、save

某个操作,对应单个cpu指令,就是原子的。如果这个操作对应多个cpu指令,大概率就不是原子的。

4.内存可见性,引起的线程不安全

5.指令重排序,引起的线程不安全

(4、5和当前的count++的例子无关)

一、如何解决线程安全问题?

join不能防止线程抢占执行吗?

这个思想是一个办法,不过如果这么搞,就不需要多线程了,直接一个线程串行执行。

多线程的初心:进行并发编程,更好地利用多核CPU

能否让count++操作变成原子的呢?这是一个核心的思路。

加锁。举生活中的例子,上厕所。

 上厕所,打开门进去,把门锁了。上完厕所,解锁,打开门离开。

锁,就能起到保证“原子性”的操作。

锁的核心操作有两个

1.加锁

2.解锁

 一旦某个线程加锁了之后,其他线程也想加锁,就不能直接加上了,就需要阻塞等待,一直等到拿到锁的线程释放锁了为止。

记得,线程调度,是抢占式执行的

当1号释放锁之后,等待的2和3,谁能抢先一步拿到锁,那是不确定的了。

 1号上完厕所离开,2号和3号谁能进去,是不一定的。

此处的“抢占式执行”导致了线程之间的调度是“随机”的。

二、如何进行加锁

synchronized

synchronnized是java中的关键字,直接使用这个关键字来实现加锁效果。

 ()中的锁对象,可以是写作任意一个Object对象(内置类型不行),此处写了this,相当于

package threading;
class Counter{private int count=0;public void add(){synchronized (this){//加锁count++;}}public int get(){return count;}
}
public class ThreadDemo10 {public static void main(String[] args) throws InterruptedException{Counter counter=new Counter();//搞两个线程,两个线程分别对这个counter自增5w次Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {counter.add();}});t1.start();Thread t2=new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});t2.start();t1.join();t2.join();System.out.println(counter.get());}
}

输出结果: 

 加锁的本质是把并发的变成了串行的。

join只是让两个线程完整地进行串行。加锁,两个线程的某个小部分串行了,大部分都是并发的。

在上述代码中,一个线程做的工作大概是这些:

1.创建i

2.判定i<50000

3.调用add

4.count++

5.add返回

6.i++

其中只有count++是串行的,剩下的12356两个线程仍然是并发的。

在保证线程安全的前提下,同时还能让代码跑的更快一些,更好地利用下多核cpu。

无论如何,加锁都可能导致阻塞。代码阻塞,对于程序的效率肯定还是会有影响的。此处虽然是加了锁,比不加锁要慢些,肯定是比串行快,比不加锁算的准。

package threading;
class Counter{private int count=0;
//    public void add(){
//        synchronized (this){//加锁
//            count++;
//        }
//
//    }synchronized public void add(){count++;}public int get(){return count;}
}
public class ThreadDemo10 {public static void main(String[] args) throws InterruptedException{Counter counter=new Counter();//搞两个线程,两个线程分别对这个counter自增5w次Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {counter.add();}});t1.start();Thread t2=new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.add();}});t2.start();t1.join();t2.join();System.out.println(counter.get());}
}

这样写,也是一样的。直接给方法使用synchronized修饰,此时就相当于以this为锁对象。

geng如果synchronized修饰静态方法,此时就不是给this加锁,而是给类对象加锁了。

synchronized public static void test(){
}
public static void test2(){
synchronized(Counter.class){
}
}

 更常见的是手动指定一个锁对象。

private Object locker=new Object();
public void add(){synchronized (locker){count++;}

如果多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争。针对不同对象加锁,就不会有锁竞争。 

1.static 修饰的方法是什么意思?

方法不属于对象。方法分为:实例方法,类方法(静态方法)

2.类对象是什么吗

Counter.class

.java源代码文件。javac=>.class(二进制字节码文件)JVM就可以执行.class了。JVM要想执行这个.class就要先把文件内容读取到内存中(类加载)

类对象,就可以来表示这个.class文件的内容。

描述了类的方方面面的详细信息,包括不限于:

1.类的名字

2.类有哪些属性,属性的名字,类型,权限

3.类有哪些方法,方法的名字,参数,类型,权限

4.类继承自哪个类

5.类实现了哪些接口

……

此处类对象,就相当于“对象的图纸”

三、内存可见性引起的线程不安全

package threading;import java.util.Scanner;public class ThreadDemo11 {public static int flag=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (flag==0){}System.out.println("循环结束!t1结束!");});Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入一个整数");flag=scanner.nextInt();});t1.start();t2.start();}
}

我们预期效果是,当我们输入非0的整数时,输出循环结束!t1结束! 退出循环

 输入非0数之后,还在运行,并没有退出循环。

while循环,flag==0

load 从内存读取数据到cpu寄存器

cmp 比较寄存器里的值是否是0

此时,load的时间开销远远的高于cmp。读取内存虽然比读硬盘来的快,但是读寄存器,比读内存又要快。

此时,编译器就做了一个非常大胆的操作,把load就给优化掉了。只有第一次执行load才真正的执行了。后续循环都只cmp,不load(相当于是复用之前寄存器中的load过的值)

这是编译器优化的手段,是一个非常普遍的事情,能智能地调整你的代码执行逻辑,保证程序结果不变地前提下,语句变化,通过一些列操作,让整个程序执行的效率大大提升。

编译器对于“程序结果不变”单线程下判定是非常准确的。但是多线程不一定,可能导致调整后,效率提高,结果变了。

所谓的内存可见性就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了我们代码的bug。

此时,我们的处理方式,就是让编译器针对这个场景暂停优化。

如果加了sleep,循环执行速度就非常慢了。当循环次数下降了,此时load操作,不再是负担,编译器也没有必要优化了:

package threading;import java.util.Scanner;public class ThreadDemo11 {public static int flag=0;public static void main(String[] args)  {Thread t1=new Thread(()->{while (flag==0){try {Thread.sleep(1000);
}catch (InterruptedException e){e.printStackTrace();
}}System.out.println("循环结束!t1结束!");});Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入一个整数");flag=scanner.nextInt();});t1.start();t2.start();}
}

 输出结果:

volatile关键字

被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存重新读取数据。

package threading;import java.util.Scanner;public class ThreadDemo11 {volatile public static int flag=0;public static void main(String[] args)  {Thread t1=new Thread(()->{while (flag==0) {}System.out.println("循环结束!t1结束!");});Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入一个整数");flag=scanner.nextInt();});t1.start();t2.start();}
}

 

 volatile不保证原子性。volatile适用的场景,是一个线程读,一个线程写的情况。而synchronized是多个线程写的情况。

四、指令重排序

volatile还有一个效果,禁止指令重排序。

指令重排序,也是编译器优化的策略,调整了代码执行的顺序,让程序更高效。前提也是保证整体逻辑不变。

谈到优化,都要保证调整之后的结果和之前是不变得。单线程下容易保证,多线程就不好说了。

例子:

Student s

t1:

s=new Student();

t2:

if(s!=null)

s.learn();

t1中的语句大体可以分为三个操作:

1.申请内存空间——交钱

2.调用构造方法(初始化内存的数据)——装修

3.把对象的引用赋值给s(内存地址的赋值)——拿到钥匙

如果是单线程环境,此处就可以指令重排序:

1肯定先执行,2和3谁先执行,谁后执行,都可以。

如果t1按照 1 3 2的顺序执行,当t1执行完1 3 之后,即将执行2的时候,t2开始执行。由于t1的3已经执行过了,这个引用已经非空了。t2开始调用s.learn()。但是由于t1还没有初始化,learn的结果是什么,不知道了,就出现了bug。

这就是指令重排序的问题。

这个代码难以演示,因为大部分情况是正确的。

相关内容

热门资讯

nginx-动静分离-防盗链-... 动静分离 为了加快网站的解析速度,可以把动态页面和静态页面有不同的服务器来解析...
爱和自由读书心得 爱和自由读书心得  《爱和自由》之所以成为教育类畅销图书,主要是破解了以往教育的许多误区,让人们明白...
寝室安全心得体会 寝室安全心得体会(通用9篇)  当我们备受启迪时,马上将其记录下来,这样有利于我们不断提升自我。你想...
有效备课培训心得体会 有效备课培训心得体会范文(精选10篇)  当我们经过反思,有了新的启发时,不如来好好地做个总结,写一...
《昆虫记》读书心得 《昆虫记》读书心得600字(精选12篇)  当细细地品读完一本名著后,相信大家都积累了属于自己的读书...
vue2控制台关闭不了开发模式... 学习vue2后,一刚开始就听说使用Vue.config.productionTip&#...
Oracle11g Sessi... 修改参数前,备份参数文件! 修改参数前,备份参数文件&#x...
蓝桥杯每日一真题—— [蓝桥杯... 文章目录[蓝桥杯 2021 省 AB2] 完全平方数题目描述输入格式输出格式样例 #1样例输入 #1...
医德医风心得体会 2022医德医风心得体会(精选12篇)  某些事情让我们心里有了一些心得后,将其记录在心得体会里,让...
朝花夕拾读书的心得体会200... 朝花夕拾读书的心得体会200字(通用15篇)  从某件事情上得到收获以后,可以寻思将其写进心得体会中...
AndroidMvvMFram... 文档下面会对框架中所使用的一些核心技术进行阐述。该框架作为技术积累的产物,会一直更新维...
Go_反射的使用 反射可以在运行时动态地检查变量和类型,并且可以调用变量和类型的方法、获取和修改变量的值...
小学生品德教育的心得体会 小学生品德教育的心得体会(通用14篇)  当在某些事情上我们有很深的体会时,马上将其记录下来,它可以...
读书的心得体会 关于读书的心得体会范文(精选25篇)  当我们备受启迪时,往往会写一篇心得体会,这样可以记录我们的思...
谁动了我的奶酪读书心得 谁动了我的奶酪读书心得(精选10篇)  认真读完一本著作后,相信大家都增长了不少见闻,何不写一篇读书...
数据结构——查找 查找概论         查找就是根据给定的某个值,在查找表中确定一个其关键字等于给...
初一军训心得 初一军训心得  8月20号至8月24号是我们xx附中在xx教育基地军训的日子,初一军训心得。在这短短...
Linux之进程信号 目录 一、生活中的信号 背景知识 生活中有没有信号的场景呢? 是不是只有这些场景真正的...
【计算机视觉】经典的图卷积网络... 【计算机视觉】经典的图卷积网络框架(LeNet、AlexNet、VGGNet、Ince...
质量的心得体会 质量的心得体会(通用15篇)  我们有一些启发后,马上将其记录下来,这样可以记录我们的思想活动。那么...