多线程基础2

文章详细阐述了线程安全的概念及不安全的原因,包括原子性、可见性和代码顺序性问题,并提出了synchronized关键字解决原子性,volatile保证内存可见性,以及wait和notify解决抢占执行问题的策略。此外,还讨论了单例模式的实现、阻塞队列的应用和线程池的使用方法。
摘要由CSDN通过智能技术生成
  1. 线程安全(重难点)

1.1概念

线程安全就是说在多线程各种随机的调度顺序下,代码不存在bug,能够符合预期的方式进行执行。

1.2 线程不安全原因

  • 修改共享数据;

多线程针对一个变量进行修改。

  • 原子性;

一条Java语句不一定是原子的,也不一定只是一条指令。不保证原子性,线程可能发生抢占式执行,打断一个线程对变量的操作,可能导致结果错误。

  • 可见性;

由于线程对变量的修改存在不能同时同步的情况,当两个线程对同一个变量进行操作时就会出现一定的问题。

  • 代码顺序性;

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,指令重排序后可能会导致线程不安全问题。

1.3 解决方案

1.3.1 synchronized 加锁解决原子性问题

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

synchronized在JVM中也叫“监视器锁”。

  • synchronized写法:

  1. 修饰普通方法,锁对象相当于this;

  1. 修饰代码块,锁对象在()中指定;

  1. 修饰静态方法,锁对象相当于类对象(不是锁整个类)。

  • synchronized的可重入性:

针对一个线程连续针对一把锁加锁两次,第二次加锁被占用,阻塞等待,等地一把锁被解锁,第二把所才能加锁成功,形成死锁问题,这样的锁称为不可重入锁。

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

synchronized引入一个计数器,每次加锁计数器++,每次解锁计数器--,如果计数器为0,加锁操作才真加锁,同样计数器为0,解锁操作才真解锁。

无论是正常执行代码块还是异常执行代码块,都会触发解锁操作,不用担心发生死锁。

1.3.2 volatile关键字保证内存可见性

  • volatile强制读取内存,减慢速度但是保证数据准确性;

  • volatile修饰变量,不能保证内存原子性;

  • volatile与synchronized同时使用保证线程安全;

  • JMM:Java Memory Model(Java内存模型):工作内存和主内存(CPU寄存器和内存);

  • 编译器优化可能会导致每次真的读取主内存,而是直接读取工作内存中的缓存,导致内存可见性问题,volatile禁止编译器优化,解决此问题,保证每次读取内存都是从主内存重新读取;

  • volatile还可以禁止指令重排序。

1.3.3 wait和notify解决抢占执行问题

  • wait和notify

  1. wait是Object的方法,可以使用任意的类的实例,都能调用wait方法;

  1. 线程执行到wait就会发生阻塞,直到另一个线程调用notify才能将其唤醒;

  1. 释放当前锁->进行等待通知->满足条件时(调用notify)被唤醒,尝试重新获取锁;

  1. 写代码时需要保证加锁的对象和调用wait的对象是同一个对象,调用wait的对象和调用notify的对象也是同一个对象;

  1. 没有线程wait,notify不会有副作用。

  • notifyAll

多个线程都在wait,notify只唤醒一个,notifyAll全部唤醒。

  • wait、notify、notifyAll都是Object方法类。

  • wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常。

1.3.4 wait和sleep对比

  1. 都是让线程进入阻塞等待状态;

  1. sleep通过时间控制何时唤醒;

  1. wait是由其他线程通过notify唤醒;

  1. wait 需要搭配 synchronized 使用. sleep 不需要;

  1. wait 是 Object 的方法 sleep 是 Thread 的静态方法。

  1. 单例模式

2.1 两种写法

  • 饿汉模式:类加载阶段创建实例;

  • 懒汉模式:需要用时创建实例。(效率高)

2.2 线程安全的单例模式

  • 饿汉模式不存在线程安全问题;

  • 懒汉模式需要判断实例是否为空:

  1. 加锁;

  1. 双重if(该加锁时再加,避免花费不必要的成本);

  1. 使用volatile,避免指令重排序,保证指令是有效的,避免得到不完全的对象,只有内存没有数据。

  1. 阻塞队列

3.1 非”先进先出“队列

  • 优先级队列(PriorityQueue)

  • 消息队列:出队列时会指定某个类型的元素先出

3.2 阻塞队列基本特点

  • 线程安全;

  • 带有阻塞功能:

  1. 如果队列满,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成;

  1. 如果队列空,继续出队列,出队列操作也会阻塞,直到队列不空,出队列才能完成。

3.3 阻塞队列应用场景

  • 生产者消费模型:描述的是多线程协同工作的一种方式。

  • 使用阻塞队列,有利于代码解耦合;

  • 削峰填谷:当流量剧增时,没有承受压力的服务器按照原来的节奏消费数据,不受影响。

3.4 使用方法

  • 标准库的阻塞队列(没有提供"带有阻塞"取队首元素功能);

  • 自己实现阻塞队列:

  1. 先实现一个普通队列;(基于链表/数组)

  1. 加上线程安全;

  1. 加上阻塞的实现。

  1. 定时器

4.1 定义

在服务器开发中,客户端请求服务器需要等待服务器的响应,在这里,为了避免等待时间过长,就会设置一个定时器来设置一个时间,达到一个设定时间后就会执行某个指定好的代码。

4.2 使用方法

  • 标准库中的定时器(可以安排多个任务);

  • 自己实现定时器:

  1. 描述任务:任务信息、执行时间;

  1. 如果让定时器管理多个任务,一个timer可以安排多个任务;

  1. 任务安排到优先级阻塞队列中,从队列中取元素,创建一个单独扫描线程,让这个线程不停检查队首元素,如果时间到了则执行该任务

  1. 线程池

5.1使用方法

  • 标准库中的线程池;

此处创建线程池,没有显示的new,而是通过另外Executors类的静态方法 newCachedThreadPool来完成,此方法叫做工厂方法;构造方法存在一定的局限性,为了避免局限引入工厂模式。

核心操作为submit将任务加入到线程池中。

  • 自己实现线程池:

  1. 创建阻塞队列;

  1. 将任务放入阻塞队列;

  1. 创建N个线程取队列元素;

  1. 线程空,队列自然阻塞等待;线程非空,线程取任务执行任务直到任务执行完毕队列阻塞等待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值