Java - 线程 Thread

参考文章1

参考文章2

run() 重写 run() 编写我们要执行的代码逻辑。
start() 调用 start() 启动一个新线程来执行 run() 中逻辑。

二、线程安全

原子性 一个或多个指令(操作)在CPU执行过程中不会中断不会分割(注意是CPU指令层面而不是语言层面),由于不恰当的执行时序而出现不正确结果的情况称为竞态条件(Race Condition)
可见性 一个线程对内存(共享变量)的修改,其它线程可以立刻看到。
有序性 程序按照代码的先后顺序执行,指令没有被编译器重排序。在没有同步的情况下,编译器、处理器以及运行时等都有可能对操作的执行顺序进行一些意想不到的调整。

2.1 可见性

  • 对于单核CPU,只有一个CPU缓存,所有线程都在这唯一一颗CPU上执行,一个线程对内存(共享变量)的修改,其它线程可以立刻看到,这就是可见性。
  • 对于多核CPU,每颗CPU都有自己的缓存,这时CPU和内存就不好掰扯了。内存中有一个变量 x=100元,线程A消费80元(操作CPU-1 缓存)然后同步到内存中 x=20元,线程B消费60元(操作CPU-2 缓存)然后同步到内存中 x=30元,钱越花越多,这里的问题是线程A对共享变量的操作对于线程B来说是不可见的。

2.2 原子性

2.2.1 读取→修改→写入

递减操作 num -- 是一种紧凑语法使其看上去像是一个操作(语言层面),但是它并非原子的(CPU指令层),不会作为一个不可分割的操作来执行,实际上包含了三个独立操作:读取num值、将值+1、将计算结果赋值给num。

下方代码中,直觉上打印0,实际是0-2000之间的随机数。num 初始值为2000,在某些情况下两个线程读到的值都为2000,接着执行累加操作,并且都将 num 赋值为1999。

var num: Int= 2000
private fun reduceNum() {
    for (i in 1..1000) { num-- }
}
fun main() {
    val thread1 = thread { reduceNum() }
    val thread2 = thread { reduceNum() }
    thread1.join()
    thread2.join()
    println("num= $num")
}

2.2.2 先检查后执行

通过观测一个可能失败的结果来决定下一步动作,代码形式为 if(condition) { ... }。

下方代码中,假定线程 A 和线程 B 同时执行 getInstance。线程 A 看到 instance 为空,因而创建一个新的 LazyInitRace 实例。线程 B 同样需要判断 instance 是否为空。此时的 instance 是否为空,要取决于不可预测的时序,包括线程的调度方式,以及线程 A 需要花多长时间来初始化 LazyInitRace 并设置 instance。如果当线程 B 检查时,instance 为空,那么在两次调用 getInstance 时可能会得到不同的结果,即使 getInstance 通常被认为是返回相同的实例。

public class LazyInitRace {
    private LazyInitRace instance = null;
    public LazyInitRace getInstance() {
        // 先检查instace是否已经被初始化,如果已经初始化则返回现有的实例
        // 否则将创建一个新的实例,返回一个实例引用
        if (instance == null) {
            instance = new LazyInitRace();
        }
        return instance;
    }
    private LazyInitRace() { }
}

2.3 有序性

下方代码中,number 很可能输出 0,因为子线程可能看到了写入 ready 的值,但却没有看到之后写入 number 的值,这种现象被称为 “重排序 (Reordering)”。

public class VisibilityTest {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println("number = " + number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        ready = true;
        number = 45;
    }
}

三、线程同步

  • 在多线程并发控制,当多个线程同时操作一个可共享的资源时,如果没有采取同步机制,将会导致数据不准确,因此需要加入同步锁,确保在该线程没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。
  • JDK提供锁分为两种:synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。
synchronized 互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。
volatile 解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。
lock 是一个接口,lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
ReentrantLock 可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。

3.1 synchronized 修饰同步代码块或方法

  • 由于java的每个对象都有一个内置锁,用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需获得内置锁,否则就处于阴塞状态。
  • wait()、notify()、notifyAll() 只能在同步代码中使用,因为要持有锁的线程来操作,而同步代码才有锁,否则报错IllegalMonitorStateException。这些方法被定义在 Object 中而不是 Thread 是因为锁可以是任意对象,而能被任意对象调用的方法定义在Object中。
对象锁

可以 new 出很多对象,每个对象都是独立的实体,所以对对象加锁在不同对象之间不会相互影响。

  • 同步代码块锁住类中的非静态变量
  • 同步代码块锁住this
  • 关键字锁住非静态方法
类锁

类信息和静态变量/方法都存在于方法区中,在 JVM 中只有一份,方法区又是线程共享的,所以对类加锁在不同线程之间共享。

  • 同步代码块锁住类中的静态变量
  • 同步代码块锁住XXX.class
  • 关键字锁住静态方法
sleep( ) wait( ) join()
相同 等待
区别 仍然持有CPU资源。 释放自己的锁(让其它线程可以进到被同一把锁锁住的代码中),也就释放了CPU资源。 在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。会释放锁。
notify( ) notifyAll( )
相同 仅仅是通知在 wait 的地方的线程要释放锁了,只有同步代码块或同步方法中的代码全执行完才会真正释放锁,所以在同步代码中的先后顺序无差别。
区别 唤醒此obj锁上第一个被等待的线程。 唤醒所有等待的线程。

3.2 volatile 修饰变量

volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。

3.3 ReentrantLock 重入锁

ReentrantLock() 创建一个ReentrantLock实例
lock() 获得锁
unlock() 释放锁

3.4 Atomic 原子类

JDK中提供了很多atomic类,如AtomicInteger\AtomicBoolean\AtomicLong,它们是通过CAS完成原子性。

AtomicInteger(int value) 创建个有给定初始值的AtomicInteger整数。
addAndGet(int data) 以原子方式将给定值与当前值相加。

3.5 LinkedBlockingQueue< E> 阻塞队列

3.6  ThreadLocal 局部变量

  • 每个线程都会保存一份该变量的副本,副本之间相互独立(数据在同线程中被多个地方共享,但多线程中不共享),这样每个线程都可以随意修改自己的副本,而不影响其他线程。
  • Thread类中有一个成员变量 threadLocals 它是一个空的 ThreadLocalMap。ThreadLocalMap 是 ThreadLocal 的静态内部类,Entry 继承于 WeakReference,key 是 ThreadLocal 类型,value是 Object 类型。
  • 创建 ThreadLocal 对象用来存取值,操作的是 CurrentThread 里的 ThreadLocalMap,由于每个 Thread 中都有自己的 ThreadLocalMap,当不同 Thread 访问代码时,ThreadLocal 操作的是它们各自持有的数据。

synchornized ThreadLocal
相同 都是用于解决多线程并发访问安全。
区别 synchornized用于线程间数据共享。 ThreadLocal用于线程间数据隔离。
利用锁的机制,使变量或代码块在同一时刻只能被一个线程访问。 为每个线程提供自己的变量,使不同线程访问时,代码操作的是它们各自持有的数据。

在一个线程中可以创建多个 ThreadLocal 对象(作用是当作key存入value),不管是调用 ThreadLocal 对象的 get() 还是 set() 内部都会先获取当前线程Thread,然后获取 Thread 所持有的 ThreadLocalMap(即Thread的成员变量threadLocals)判断是否为 null:

  • 为 null 就创建一个 ThreadLocalMap 并赋值,set() 是接着以该 ThreadLocal 为 key 存入目标值、get() 是接着以该 ThreadLocal 为 key 存入初始化值(默认null,在get()之前调用过 setInitialValue() 或创建 ThreadLocal 时重写过 initialValue() 的话就是自定义的值)。
  • 不为 null 的话,get() 就获取该 ThreadLocal 对象(key)对应的 value(在Android中是Looper),set() 就以该ThreadLocal 对象为 key 存入 value(在Android中是Looper)。 

 

四、线程阻塞

  1. 线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行。
  2. 线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行。
  3. 线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作。
  4. 线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。
  5. 线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。
  6. 线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。
  7. 线程执行suspend()使线程进入阻塞状态,必须resume()方法被调用,才能使线程重新进入可执行状态。

五、线程中断 interrupt( )

  • 使用 interrupt() 中断,但调用 interrupt() 方法只是传递中断请求消息,并不代表要立马停止目标线程,然后通过抛出InterruptedException来唤醒它。
  • 为什么不能提供 stop 停止线程
    • 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。
    • 释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

public void interrupt()

中断当前线程。

public boolen isInterrupt()

判断当前线程是否被中断。

public static boolen interrupted()

清除当前线程的中断状态,并返回之前的值。

六、线程池

 线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗。当一个任务提交到线程池时:

  1. 首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步。
  2. 判断工作队列是否已满,没有满则加入工作队列,否则执行下一步。
  3. 判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。

七、 线程种类

FixedThreadPool 可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队。
SingleThreadExecutor 单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量。
CachedThreadPool 按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务。
ScheduledThreadPoolExecutor 继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE。

猜你喜欢

转载自blog.csdn.net/HugMua/article/details/130536757