当前位置: 首页 > news >正文

[Java EE] 多线程 -- 初阶(2)

4 .线程的状态

public class demo13 {public static void main(String[] args) {for (Thread.State s:Thread.State.values()) {System.out.println(s);}}
}

  • NEW : 安排了工作 , 还没开始行动 ; new 了 thread 对象 , 还没 start() , 不具备运行条件
  • RUNNABLE : 可工作的 , 又分为 ① 正在运行 , 线程在 cpu 上运行 和 ② 就绪状态 , 线程随时可以去 cpu 上执行 ; 调用 start()线程就进入了就绪状态
  • BLOCKED : 线程因竞争对象锁失败(进入synchronized代码块时被其他线程持有锁) , 暂时停止运行 , 进入阻塞状态 ; 当锁被释放后 , 线程会重新进入就绪状态( RUNNABLE )等待调度
  • WAITING : 线程通过调用无超时的等待方法 (Object.wait() , Thread.join() , LockSupport.park() ) 进入此状态 ; 线程不会主动唤醒 , 需要等待其他线程显示唤醒 ( 如 Object.notify() ) , 否则一直等待
  • TIMED_WAITING : 线程通过调用带有超时的等待方法(Object.wait(long) , Thread.sleep(long) , Thread.join(long) )进入此状态 ; 与 WAITING的区别是 : TIMED_WAITING 超时会自动唤醒 , 重新进入就绪状态
  • TERMINATED : 执行完毕或者 ( run()方法结束 ) 或因异常退出 , 进入终止状态 ; 此时线程生命周期结束 , 无法再被启动 ( 再次调用 start() 会抛异常 )

状态转换关系:

  • 新建(New)→ 就绪(Runnable):调用start()方法
  • 就绪(Runnable)→ 阻塞(Blocked):竞争锁失败
  • 阻塞(Blocked)→ 就绪(Runnable):获得锁
  • 就绪(Runnable)→ 等待(Waiting):调用无超时等待方法
  • 等待(Waiting)→ 就绪(Runnable):被其他线程唤醒或中断
  • 就绪(Runnable)→ 超时等待(Timed Waiting):调用带超时等待方法
  • 超时等待(Timed Waiting)→ 就绪(Runnable):超时时间到或被唤醒 / 中断
  • 就绪(Runnable)→ 终止(Terminated):线程执行完毕或异常终止

NEW

public class demo14 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());t.start();}
}

RUNNABLE

public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {}});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//RUNNABLE}
}

WAITING

public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true){System.out.println("helllo thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join();}
}

此时 main 线程处于 waiting

TIMED_WAITING

public class demo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true){System.out.println("helllo thread");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();Thread.sleep(1000);System.out.println(t.getState());}
}

5.线程安全

5.1 线程安全的概念

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

线程安全问题是指 : 当多个现场同时访问共享资源(如共享变量,文件,数据库连接等),由于线程回字形顺序的不确定性(CPU 调度的随机性),导致程序出现数据不一致,逻辑错误或异常的情况

5.2 观察线程不安全

public class demo15 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//如果此处没有这两个 join ,count为0,原因是main线程先执行打印了//join的作用是: 让主线程等待子线程执行完毕t1.join();t2.join();//预期结果应该是10wSystem.out.println(count);}
}

发现多次执行不但结果不一样 , 还不符合预期

对于 :

t1.join();
t2.join();

这俩线程谁先 join();无所谓

情况 1 : t1 先结束 , t2 后结束

  1. main 先在 t1.join()阻塞等待
  2. t1 结束
  3. main 再在 t2.join()阻塞等待
  4. t2 结束
  5. main 继续执行后续打印

情况 2 : t2 先结束 , t1 后结束

  1. main 先在 t1.join()阻塞等待
  2. t2 结束 , t1.join()继续阻塞
  3. t1 结束
  4. main 执行到 t2.join() ; 但由于 t2 已经结束 , 此处不会阻塞
  5. main 继续执行后续打印

5.3 线程不安全的原因

1️⃣线程随机调度,抢占式执行(根本)

2️⃣多个线程同时修改同一个变量(修改共享数据)

3️⃣修改操作不是原子的(原子性缺失)(底层)

        原子操作 : 不可分割的操作(如读取一个 int 变量),执行过程中不会被其他线程干扰

        非原子操作 :由多个步骤组成的操作(如 count++,实际就包括读取-更新-写回),若执行到一半就被其他线程抢占 CPU ,就可能导致数据错误

针对 cout++分两个线程同时执行 5w 次这个操作 , 有以下观点:

一条 JAVA 语句不一定是原子的,也不一定只是一条指令

每一次 count++操作都是由三步操作组成:① 从内存把数据读取到 CPU (load)② 进行数据更新(add)③ 把数据写回 CPU(save)

由于两个线程他们有各自不同的上下文;此时只有当一个线程的 save 完成后,在进行下一个线程的 load 才能线程安全

4️⃣内存可见性(底层)

        可见性指 :一个线程对共享变量值的修改,能够及时的被其他线程看到

        换句话说 线程修改共享资源后,主内存的数据未能及时同步到其他线程的工作内存,导致其他线程读取到旧值

5️⃣指令重排序(底层)

CPU 指令重排序可能打乱代码执行顺序,多线程环境下引发逻辑错误

5.4 解决线程不安全问题(下文详细讲解)

1️⃣改为串行执行(解决多线程并发性执行的问题)

public class demo15 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join();     //预期结果应该是10wSystem.out.println(count);}
}

2️⃣互斥锁(synchronized 关键字)

3️⃣使用线程安全的数据结构

4️⃣减少资源共享

5️⃣volatile 关键字

6.synchronized 关键字-监视器锁 monitor

6.1 synchronized 的特性

① 互斥

synchronized 会起到互斥的效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待

阻塞等待

针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占有时,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直到之前线程解锁后,有操作系统唤醒一个新的线程,再来获取到这个锁

  • 阻塞等到时不占用 CPU 资源,避免空耗
  • 需要依赖系统或其他线程唤醒,否则一直阻塞

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

  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

② 可重入

是锁的核心特性 , 指同一线程可以多次获取同一把锁 , 不会因为自身已持有该锁而陷入死锁 , 简单来说 "线程自己不会锁住自己"

底层实现 :

锁内部维护一个 线程持有计数器 和 当前持有线程引用:

线程首次获取锁 : 计数器设为 1 , 记录持有线程

同一线程再次获取锁 : 计数器+1 , 直接放行

线程释放锁 : 计数器-1 , 当计数器为 0 时 , 才释放锁给其他线程

6.2 synchronized 使用示例

synchronized 本质上要修改指定对象的对象头 , 从使用角度来看 , synchronized 也势必要搭配一个具体对象来使用

注意 :

  • 两个线程 针对同一个对象枷锁 , 才会产生互斥效果
  • 如果是不同的锁对象 , 此时不会产生互斥效果 , 线程也不安全

① 修饰代码块 : 明确指定锁哪个对象

锁任意对象
public class demo17 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 3; i++) {synchronized (locker){System.out.println("test t1");}}});t1.start();t1.join();System.out.println("test main");}
}

可以用任意对象来作为锁 ; 这个锁本身的类型并不重要 , 重要的是 : 是否有其他线程尝试 竞争这个锁 ;

实际上 把一个对象作为锁对象 , 并不影响对象本身的使用 ; 但是一般 一个对象只有一个作用

锁当前对象(容易出现问题)
import static java.lang.Thread.sleep;public class demo17 {private static int count = 0;public void method() throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (this) {demo17.count++;}}});t1.start();//return t1;}public static void main(String[] args) throws InterruptedException {demo17 d3 = new demo17();d3.method();
//      d3.method();        demo17 d4 = new demo17();d4.method();//        demo17 d1 = new demo17();
//
//        Thread T1 = d1.method();
//        T1.start();
//        T1.join();//        Thread T2 = d1.method();
//        T2.start();
//        T2.join();//        demo17 d2 = new demo17();
//        Thread T3 = d2.method();
//        T3.start();
//        T3.join();sleep(2000);System.out.println(demo17.count);}
}

也可以使用 Thread.currentThread()来替代 cur ; 但是这样写会让其他线程尝试竞争这个锁时 获取不到相同的锁对象

  • 若多个线程通过同一个实例对象调用 method() , 则会竞争 同一把锁 (this) , 此处 count++是安全的

  • 若多个线程通过不同实例对象调用 method() , 则每个线程的锁对象是不同的实例(this 不同) , 此时 锁不互斥 , count++会出现线程安全问题(因为 count 是静态共享的)

② 直接修饰普通方法 (和上面效果差不多)


import static java.lang.Thread.sleep;public class demo17 {private static int count = 0;public synchronized void method() throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {demo17.count++;}});t1.start();//return t1;}public static void main(String[] args) throws InterruptedException {demo17 d3 = new demo17();d3.method();//d3.method();demo17 d4 = new demo17();d4.method();sleep(2000);System.out.println(demo17.count);//        demo17 d1 = new demo17();////        Thread T1 = d1.method();//        T1.start();//        T1.join();//        Thread T2 = d1.method();//        T2.start();//        T2.join();//        demo17 d2 = new demo17();//        Thread T3 = d2.method();//        T3.start();//        T3.join();}
}

③ 修饰静态方法

public class StaticSyncDemo {// 静态共享变量(类级资源)private static int staticCount = 0;// 静态同步方法:锁对象是 StaticSyncDemo.classpublic synchronized static void increment() {staticCount++; // 安全修改静态变量}public static void main(String[] args) throws InterruptedException {// 两个线程调用静态同步方法Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("staticCount = " + staticCount); // 一定是100000}
}

普通方法的锁和静态方法的锁的区别 :

普通方法的锁 : 仅对同一个实例对象的多线程生效 ; 若多个线程操作不同实例 ,锁不互斥 , 无法保证线程安全

静态方法的锁 : 对所有实例和线程生效 (因为类对象全局唯一) , 无论多少个实例 , 多线程调用静态同步方法时都会竞争同一把锁

6.3 Java 标准库中的线程安全类

Java 标准库中 很多都是线程不安全的 , 这写了可能会涉及到多线程修改共享数据 , 又没有任何加锁措施

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

还有一些线程不安全 , 使用一些机制锁来控制

  • Vector
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有没有使用锁 ,但是不涉及修改操作 , 仍然线程安全

  • String
http://www.dtcms.com/a/598118.html

相关文章:

  • 蓝牙钥匙 第89次 蓝牙钥匙未来发展趋势篇:与汽车电子架构演进深度融合
  • 网站建设解析2345网址导航开创中国
  • 营销型网站模版vi设计的目的
  • U++工程提取二进制工程
  • Zabbix告警配置全攻略:邮件+钉钉双保险
  • qq钓鱼网站制作微盟集团
  • 中国人做跨电商有什么网站自建冷库费用
  • 03|Langgraph | 从入门到实战 | 进阶篇 | 持久化
  • 如何将网站挂载域名网站建设的定位是什么意思
  • 知识管理工具sward上手指南:安装、配置与入门
  • 在Vivado中添加HLS IP后只显示路径无法显示可例化的IP核解决方式
  • 二分查找专题(十):“Z字形”的降维!当二分查找“失效”时
  • 响应式网站源码.net互联网行业的开发网站
  • Windows10 使用 dynv6 + ddns-go 实现 IPv6 固定域名远程访问指南
  • 如何自己做彩票网站免费自动回收的传奇手游
  • 教育培训网站建站网络营销是以什么为中心
  • 专家编程 | 提升编程技能的有效策略
  • 达州科创网站建设公司山东省建设备案网站审批表
  • ui设计的推荐网站及网址本公司经营网站建设
  • 项目分享|SD-Trainer:Stable Diffusion 训练集成工具
  • 专业的无锡网站建设网络贷款公司哪个好
  • Unity使用的编程语言 | 如何选择合适的语言进行高效开发
  • seo整站优化外包哪家好加盟推广公司
  • 《Ionic 滑动框:深度解析与实战指南》
  • 如何编程游戏 | 初学者快速上手游戏开发的技巧与方法
  • 德州建设信息网站wordpress修改默认id号
  • 【大语言模型】-- Prompt Engineering 提示工程
  • 清远市建设工程交易中心网站教育培训网站源码 模板 php培训机构网站源码培训学校网站源码
  • 机器学习实践项目(二)- 房价预测增强篇 - 额外知识
  • 商城建站站长工具seo综合查询引流