JavaEE多线程-线程的状态和安全问题

目录

  • 一、线程中的基本状态
  • 二、线程安全问题
  • 三、线程安全的标准类
  • 四、synchronized 关键字-监视器锁monitor lock
    • synchronized 的特性
  • 五、volatile 关键字

一、线程中的基本状态

NEW: 安排了工作, 还未开始行动, 就是创建了Thread对象, 但还没有执行start方法(内核里面还没有创建对应PCB), 这个状态是java内部的状态, 与操作系统中线程的状态没有关联.

RUNNABLE: 可工作的, 又可以分成正在工作中和即将开始工作(即正在CPU上执行的任务或者在就绪队列里随时可以去CPU上执行的).

BLOCKED: 线程正在等待锁释放而引起的阻塞状态(synchronized加锁).

WAITING: 线程正在等待等待唤醒而引起的阻塞状态(waitf方法使线程等待唤醒).

TIMED_WAITING: 在一段时间内处于阻塞状态, 通常是使用sleep或者join(带参数)方法引起.

TERMINATED:Thread对象还存在, 但是关联的线程已经工作完成了, 这个状态也是java内部的状态, 与操作系统中线程的状态没有关联.

线程的状态其实是一个枚举类型:Thread.State

二、线程安全问题

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。

举一个存在线程安全问题的例子:

class Counter {
    public int count = 0;

    public void add() {
        count++;
    }
}

public class Demo {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 搞两个线程, 两个线程分别针对 counter 来 调用 5w 次的 add 方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        // 启动线程
        t1.start();
        t2.start();

        // 等待两个线程结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的 count 值
        System.out.println("count = " + counter.count);
    }
}

在这里插入图片描述
为什么会出现这种情况呢?
原因还是线程的抢占式执行, 线程调度的顺序是随机的, 就造成线程间自增的指令集交叉, 导致运行时出现两次或者多次自增但值只会自增一次的情况, 导致得到的结果会偏小.

一次的自增操作本质上可以分成三步:
把内存中变量的值读取到CPU的寄存器中(load).
在寄存器中执行自增操作(add)
将寄存器的值保存至内存中(save)

如果是两个线程并发的执行count++, 此时就相当于两组 load, add, save进行执行, 此时不同的线程调度顺序就可能会产生一些结果上的差异.

线程加锁

为了解决由于 “抢占式执行” 所导致的线程安全问题, 我们可以针对当前所操作的对象进行加锁, 当一个线程拿到该对象的锁后, 就会将该对象锁起来, 其他线程如果需要执行该对象所限制任务时, 需要等待该线程执行完该对象这里的任务后才可以.

在Java中最常用的加锁操作就是使用synchronized关键字进行加锁.

方式一:使用synchronized关键字修饰普通方法, 这样会给方法所对在的对象加上一把锁.

class Counter {
    public int count = 0;

    synchronized public void add() {
        count++;
    }
}

方法二:使用synchronized关键字对代码段进行加锁, 需要显式指定加锁的对象.

class Counter {
    public int count = 0;

    public void add() {
        synchronized (this) {
            count++;
        }
    }
}

方法三:使用synchronized关键字修饰静态方法, 相当于对当前类的类对象进行加锁.

class Counter {
    public static int count = 0;

    synchronized public static void add() {
        count++;
    }
}

加锁本质上就是把并发变成了串行执行, 这样的话这里的自增操作其实和单线程是差不多的, 甚至上由于add方法, 要做的事情多了加锁和解锁的开销, 多线程完成自增可能比单线程的开销还要大, 那么多线程是不是就没用了呢? 其实不然, 对方法加锁后, 线程运行该方法才会加锁, 执行完该方法的操作后就会解锁, 此方法外的代码并没有受到限制, 这部分程序还是可以多线程并发执行的, 这样整体上多线程的执行效率还是要比单线程要高许多的.

产生线程安全问题的原因
1.线程是抢占式执行的,线程间的调度充满随机性。(线程不安全的根本原因)
2.多个线程对同一个变量进行修改操作。
3.原子性.
4.指令重排序和内存可见性问题

三、线程安全的标准类

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

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder
  • 但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer
  • 还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的

  • String
  • 四、synchronized 关键字-监视器锁monitor lock

    synchronized 的特性

    1.互斥

    synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
    同一个对象 synchronized 就会阻塞等待.
    进入 synchronized 修饰的代码块, 相当于 加锁
    退出 synchronized 修饰的代码块, 相当于 解锁
    JavaEE多线程-线程的状态和安全问题_第1张图片
    synchronized用的锁是存在Java对象头里的。
    理解 “阻塞等待”.

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

    注意:

    上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这
    也就是操作系统线程调度的一部分工作.

    假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
    和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
    获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

    synchronized的底层是使用操作系统的mutex lock实现的.

    2.刷新内存
    synchronized 的工作过程:

    1. 获得互斥锁
    2. 从主内存拷贝变量的最新副本到工作的内存
    3. 执行代码
    4. 将更改后的共享变量的值刷新到主内存
    5. 释放互斥锁
      所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分

    3.可重入
    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
    在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
    如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
    到锁, 并让计数器自增.
    解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

    五、volatile 关键字

    volatile 能保证内存可见性
    JavaEE多线程-线程的状态和安全问题_第2张图片
    代码在写入 volatile 修饰的变量的时候,

    改变线程工作内存中volatile变量副本的值
    将改变后的副本的值从工作内存刷新到主内存
    代码在读取 volatile 修饰的变量的时候,

    从主内存中读取volatile变量的最新值到线程的工作内存中
    从工作内存中读取volatile变量的副本

    volatile 不保证原子性
    volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见
    性.

你可能感兴趣的:(JavaEE,java-ee,java,jvm)