【JavaEE】多线程(Part3线程安全)

目录

  • 前言 + 回顾
  • 一、线程安全
  • 二、 synchronized关键字
    • 1. synchronized相关
    • 2. synchronized的特性:
    • 3. Java 标准库中的线程安全类(了解)
  • 三、代码参考
  • THINK


前言 + 回顾

今天不学习, 明天变垃圾!

  1. 本文主要介绍【线程安全】相关问题,以及【synchronized】关键字。
  2. 【回顾】
    1) 多线程 解决并发编程,能够更充分地利用多核CPU资源; 但是进程的创建需要分配资源,进程销毁需要释放资源,开销较大; 所以:引入了多线程,一个进程可以包含多个线程,创建第一个线程的时候就把资源分配好,后续再在同一个进程中创建线程的时候就直接共享之前的线程资源,而销毁的时候只用释放最后一个线程的资源,这样就节省了分配和释放资源的开销。
    2) 创建的Thread实例和操作系统内核中的线程是一一对应的关系。
    3) 创建线程的五种方法
    4) run描述了线程要做的工作,真正线程开始运行靠的是start方法
    5) 中断线程方法:①自己定义标志位 ②使用isInterrupted方法判定,使用interrupt方法类来触发中断(如果线程是Runnable状态就会设置标志位,但是如果是阻塞状态(如sleep、wait等)就会抛出异常,清除标志位)
    6) 线程等待join: main线程等待t线程结束, join也会陷入阻塞
    7) 线程休眠sleep
    8) 获取线程对象引用Thread.cureentThread();
    9) 线程状态:NEW / TERMINATER / RUNNABLE / WAITING / TIMED_WAITING / BLOCKED

一、线程安全

  1. 线程安全的意思就是:在多线程各种随机的调度顺序下,代码没有bug,都能够符合预期的方式来执行; 而如果在多线程随机调度下代码出现bug,此时就认为线程是不安全的。
    (代码有无bug看是否满足需求文档)

  2. 有些代码在多线程环境下执行会出现bug,这样的问题就称为“线程不安全”(也就是与预期顺序不一致)

  3. 线程不安全的原因】:
    ① 抢占式执行:多个线程调度执行过程可以视为是“全随机”的(也不能理解成纯随机的,但是确实在应用层程序上是没有规律的)(所以:在写代码的时候,就需要考虑到在任意一种调度的情况下都是能够运行出正确结果的)
    (内核实现的,我们无能为力)
    多个线程修改****同一个变量:
    【String是不可变对象(也就是不能修改String的内容,这并不是说用final修饰,而是把set系列方法给藏起来了),设计成不可变的好处之一就是“线程安全”】
    (有时候可以通过调整代码来规避线程安全问题,但是普适性不高)
    ③ 修改操作不是原子的:
    CPU执行指令都是以“ 一个指令”为单位进行执行,一个指令就相当于CPU上的“最小单位”了,不会说该条指令还没执行完就把线程调度走了。
    (eg. count++就是三条指令
    而像是有的修改操作如int的赋值就是单个CPU指令,安全一些)

  • 注:解决线程安全问题最常见的方法就是从这里入手:把多个操作通过特殊手段打包成一个原子操作
    (一个线程是否安全的判定是复杂的)
    ④ 内存可见性问题:JVM的代码优化(逻辑等价条件下提高效率)引入的bug
    ⑤ 指令重排序
    (以上并不是线程不安全的全部原因)
  1. 如何针对线程不安全问题进行反制?/ 如何做才能让线程安全?
    加锁 变为 原子操作! 使用完后解锁
    (以上述count++为例:++之前先加锁,++完了之后解锁,在加锁和解锁之间进行修改,此时就算别的进程想要修改也是无法修改的,这就保证了原子性!)
    (其他的线程只能阻塞等待,阻塞等待的线程状态就是BLOCKED状态)
    加锁之后 就是互斥的

  2. 原子性其实就是同步互斥,表示操作是互相排斥的。


二、 synchronized关键字

1. synchronized相关

  1. Java代码中进行加锁使用的是 synchronized!!
    锁具有独占性:如果当前没人来加锁。则此时加锁成功;但是如果当前锁已经被加上了,那么加锁操作就会阻塞等待。

  2. 加锁不是说CPU一鼓作气就执行完该线程,中间也是有可能会有调度切换的;但是即使该线程被切换走了,另外的线程依旧是BLOCKED的状态,则此时另外线程也是无法在CPU上运行的。

  3. 加锁后的多线程不一定比串行执行快,具体看代码如何实现的

  • 锁的代码越多,就叫做“锁的粒度越大/越粗”,并发程度降低;而锁的代码越少,就叫做“锁的粒度越小/越细”,并发程度提高
  1. 就以count++为例:一个线程加锁、一个线程不加锁,此时能否保证线程的安全呢?
    1)线程安全,不是加了锁就一定安全的;而是通过加锁让并发修改同一个变量变为串行修改同一个变量,此时才是安全的。 而不正确的加锁方式并不一定能够解决线程安全问题。
    2) 所以:是不能保证线程安全的。一个线程加锁并不会涉及锁竞争,也就不会阻塞等待,也就不会由并发修改同一变量变为串行修改同一变量,故是不安全的。

  2. 要加锁的代码如果不是在一个方法里,怎么办呢?
    1) synchronized不仅可以修饰方法,还可以修饰代码块。所以可以将要加锁的代码放到一个代码块中。
    2) 在synchronized修饰代码块时,()括号中的内容是我们所要针对的加锁对象,成为“锁对象”。
    3) 在使用锁的时候,一定要明确 当前是针对哪个对象加锁。这很重要,会直接影响后面锁操作是否会触发阻塞。我们关心的是 是否存在(同一个)锁对象,是否存在锁竞争

  3. 在Java中任意的对象都可以作为锁对象(这一点和其他语言差别很大,如C++、Python、Go等语言中,正常的对象都不能作为锁对象,只有特定的对象可以用于加锁)

  4. 一个synchronized只能锁一个对象。

Synchronized(this) 针对当前对象加锁;

Public/private Object locker = new Object; … synchronized(locker)

  1. 如果synchronized直接修饰方法,相当于锁对象就是this
    大部分情况下,直接写this作为锁对象是可以的。

  2. 只要是锁同一个对象就有锁竞争/互斥,不关乎是否是同一个方法。

  3. 小结synchronized的几种写法:
    ① 修饰普通方法,锁对象相当于this
    ② 修饰代码块,锁对象在()中指定
    ③ 修饰静态方法,锁对象相当于类对象(也就是.class)【只有一份!但是不是锁整个类】

2. synchronized的特性:

synchronized的特性:

  1. 互斥:也就是 加锁/解锁。
    (上一个线程解锁之后, 下一个线程并不是立即就能获取到锁.)
  2. 刷新内存(存疑)
  3. 可重入【不好理解】:
    1) 一个线程针对同一把锁连续加锁两次就可能造成死锁。
    而在加锁两次之后不会产生死锁的就叫做**“可重入锁”,会产生死锁的叫“不可重入锁”。
    2)可重入锁的底层实现其实很简单:只要 让锁里面记录好是哪个线程持有的这把锁。
    如:t线程尝试对this对象来加锁,this这个锁里面就记录了是t线程持有了它;第二次进行加锁的时候如果该this锁发现是原来的t线程,则
    直接通过**,没有任何负面影响,不会阻塞等待。
    3) 那么在何时进行解锁又是一个问题:引入计数器。
    每次加锁,计数器++, 每次解锁,计数器–。 只有在计数器为0的时候才能够真正加锁和解锁
  • 可重入锁的实现要点:

①让锁里持有线程对象,记录是谁加了锁;
②维护一个计数器,用来衡量啥时候真加锁,啥时候真解锁,啥时候又是直接放行。

4) 在加锁代码中出现异常,如果没人catch就会脱离之前的代码块,脱离一层代码块计数器就-1,然后经过多次脱离 则计数器会最终减到0;所以是不会在加锁代码中出现异常时死锁的,无论如何解锁代码都是可以执行到的

5) 可重入已经在synchronized中处理好了,也就不会发生死锁了。

3. Java 标准库中的线程安全类(了解)

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

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

  1. 但是还有一些是线程安全的,使用了一些锁机制来控制。线程安全:

Vector (不推荐使用) :相当于线程安全的ArrayList
HashTable (不推荐使用):相当于线程安全的HashMap
ConcurrentHashMap
StringBuffer:StringBuffer 的核心方法都带有synchronized

  1. 还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的:String

  2. 【补充: final修饰一个变量,禁止修改; final修饰类,禁止继承; final修饰方法,禁止重写。】
    如: private final char value[]; final修饰数组是为了不让这个引用的指向发生改变;而内容不改变是因为private以及没有public的setter方法。


三、代码参考

Demo8-9


THINK

  1. 线程不安全原因
  2. 线程安全处理方式
  3. synchronized关键字(主要原子性)
  4. String、StringBuff线程安全
  5. 代码代码!!

你可能感兴趣的:(Note-JavaEE,java-ee,java,jvm,多线程,synch)