多线程概览

发布时间:2022-06-21 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了多线程概览脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

线程是程序执行的最小单元,多线程是指程序同一时间可以有多个执行单元运行(这个与你的CPU核心有关)。

在java中开启一个新线程非常简单,创建一个Thread对象,然后调用它的start方法,一个新线程就开启了。

那么执行代码放在哪里呢?有两种方式:

  • ① 创建Thread对象时,复写它的run方法,把执行代码放在run方法里。
  • ② 创建Thread对象时,给它传递一个Runnable对象,把执行代码放在Runnable对象的run方法里。

如果多线程操作的是不同资源,线程之间不会相互影响,不会产生任何问题。但是如果多线程操作相同资源(共享变量),就会产生多线程冲突,要知道这些冲突产生的原因,就要先了解java内存模型(简称JMM)。

java内存模型(JMM)

JMM介绍

java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来说:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

  1. 存在两种内存:主内存和线程本地内存,线程开始时,会复制一份共享变量的副本放在本地内存中。
  2. 线程对共享变量操作其实都是操作线程本地内存中的副本变量,当副本变量发生改变时,线程会将它刷新到主内存中(并不一定立即刷新,何时刷新由线程自己控制)。
  3. 当主内存中变量发生改变,就会通知发出信号通知其他线程将该变量的缓存行置为无效状态,因此当其他线程从本地内存读取这个变量时,发现这个变量已经无效了,那么它就会从内存重新读取。

可见性

从上面的介绍中,我们看出多线程操作共享变量,会产生一个问题,那就是可见性问题: 即一个线程对共享变量修改,对另一个线程来说并不是立即可见的。

class Data {
    int a = 0;
    int b = 0;
    int x = 0;
    int y = 0;


    // a线程执行
    public void threadA() {
        a = 1;
        x = b;
    }

    // b线程执行
    public void threadB() {
        b = 2; 
        y = a; 
    }
}

如果有两个线程同时分别执行了threadA和threadB方法。可能会出现x==y==0这个情况(当然这个情况比较少的出现)。因为a和b被赋值后,还没有刷新到主内存中,就执行x = b和y = a的语句,这个时候线程并不知道a和b已经被修改了,依然是原来的值0。

有序性

为了提高程序执行性能,Java内存模型允许编译器和处理器对指令进行重排序。重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

class Reorder {
    int x = 0;
    boolean flag = false;

    public void writer() {
        x = 1;
        flag = true;
    }

    public void reader() {
        if (flag) {
            int a = x * x;
            ...
        }

    }
}

例如上例中,我们使用flag变量,标识x变量已经被赋值了。但是这两个语句之间没有数据依赖,所以它们可能会被重排序,即flag = true语句会在x = 1语句之前,那么这么更改会不会产生问题呢?

  • 在单线程模式下,不会有任何问题,因为writer方法是一个整体,只有等writer方法执行完毕,其他方法才能执行,所以flag = true语句和x = 1语句顺序改变没有任何影响。
  • 在多线程模式下,就可能会产生问题,因为writer方法还没有执行完毕,reader方法就被另一线程调用了,这个时候如果flag = true语句和x = 1语句顺序改变,就有可能产生flag为true,但是x还没有赋值情况,与程序意图产生不一样,就会产生意想不到的问题。

原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

x = 1;  // 原子性
y = x; // 不是原子性
x = x + 1; // 不是原子性
x++; // 不是原子性
System.out.println(x); // 原子性

公式2:有两个原子性操作,读取x的值,赋值给y。

公式3:也是三个原子性操作,读取x的值,加1,赋值给x。

公式4:和公式3一样。

所以对于原子性操作就两种:1. 将基本数据类型常量赋值给变量。2. 读取基本数据类型的变量值。任何计算操作都不是原子的。

小结

多线程操作共享变量,会产生上面三个问题,可见性、有序性和原子性。

  • 可见性:一个线程改变共享变量,可能并没有立即刷新到主内存,这个时候另一个线程读取共享变量,就是改变之前的值。所以这个共享变量的改变对其他线程并不是可见的。
  • 有序性:编译器和处理器会对指令进行重排序,语句的顺序发生改变,这样在多线程的情况下,可能出现奇怪的异常。
  • 原子性:只有对基本数据类型的变量的读取和赋值操作是原子性操作。

要解决这三个问题有两种方式:

  • volatile关键字:它只能解决两个问题可见性和有序性问题,但是如果volatile修饰基本数据类型变量,而且这个变量只做读取和赋值操作,那么也没有原子性问题了。比如说用它来修饰boolean的变量。
  • 加锁:可以保证同一时间只有同一线程操作共享变量,当前线程操作共享变量时,共享变量不会被别的线程修改,所以可见性、有序性和原子性问题都得到解决。分为synchronized同步锁和JUC框架下的Lock锁。

volatile关键字

看过volatile关键字底层实现就知道,我们使用volatile关键字修饰变量,就相当于给这个变量添加加了内存屏障。那么内存屏障的作用是什么呢?

  1. 它会让本地内存共享变量副本无效,即修改了这个共享变量,它会被强制刷新到主内存。读取这个共享变量,会强制从主内存中读取最新值。因此解决了可见性问题。
  2. 禁止指令重排序,即在程序中在volatile变量进行操作时,在其之前的操作肯定已经全部执行了,而且结果已经对后面的操作可见,在其之后的操作肯定还没有执行。因此解决了有序性问题。
class VolatileFeaturesExample {

    //使用volatile声明一个基本数据类型变量vl
    volatile long vl = 0L;

    //对于单个volatile基本数据类型变量赋值
    public void set(long l) {
        vl = l;
    }

    //对于单个volatile基本数据类型变量的复合操作
    public void getAndIncrement () {
        vl++;
    }

    //对于单个volatile基本数据类型变量读取
    public long get() {
        return vl;
    }

}

class VolatileFeaturesExample {
    //声明一个基本数据类型变量vl
    long vl = 0L;

    // 相当于加了同步锁
    public synchronized void set(long l) {
      vl = l;
    }

    // 普通方法
    public void getAndIncrement () {
        long temp = get();
        temp += 1L;
        set(temp);
    }

    // 相当于加了同步锁
    public synchronized long get() {
        return vl;
    }

}

如果volatile修饰基本数据类型变量,而且只对这个变量做读取和赋值操作,那么就相当于加了同步锁。

synchronized同步锁

synchronized同步锁作用是访问被锁住的资源时,只有获取锁的线程才能操作被锁住的资源,其他线程必须阻塞等待。所以对于一个线程来说,可以阻塞等待,可以运行,那么线程到底有哪些状态呢?

线程状态

在Thread类中,有一个枚举对象State标志着所有的线程状态。 

// 标志线程状态的枚举对象
public enum State {
    /**
     * 新建状态。当创建一个线程Thread对象,但是还没有调用它的start方法,就是这个状态。
      */
    NEW,

    /**
     * 运行状态。当前线程正在运行中
      */
    RUNNABLE,

    /**
     * 阻塞状态。
     * 一般是锁资源被另一线程持有,当前线程处于阻塞等待获取锁的状态,
     * 当线程获取了锁,并获取CPU执行权,就会从BLOCKED状态转成RUNNABLE状态。
     *
      */
    BLOCKED,

    /**
     * 等待状态。调用三个方法当前线程会进人这个状态:
     * 1. Object#wait() 方法
     * 2. #join() 方法 (这个方法在Thread对象中,本质上也是调用wait()方法)
     * 3. LockSupport#park() 方法
     * 这三个方法调用时都没有传递时间参数,所以没有超时限制。
     * WAITING状态的线程是处于线程等待池中,只有调用对应的唤醒方法,才能将当前线程从线程等待池中唤醒,
     * 否则线程一直等待。除非发生中断请求,也会将线程唤醒。
     * 唤醒线程的方法有:
     * 1. Object#notify() notifyAll()
     * 2. LockSupport#unpark()
     * 注意join()是线程对象的wait()方法实现的,当线程执行完毕时,会调用自己的notifyAll()方法,
     * 唤醒等待池中所有的线程。
     *
     * 还有要注意的是Object#wait() 方法只能在synchronized代码块中调用,
     * 所以当线程被唤醒时,它并不是处于可运行状态,而是处于BLOCKED状态,
     * 因为只有获取锁的线程,才能执行synchronized代码块中的代码,所以被唤醒的线程要等待锁。
     *
     * 而LockSupport#park()没有这个方面的限制
     *
     */
    WAITING,


    /**
     * 等待超时状态,调用下面五个方法当前线程会进人这个状态:
     * 1. Object#wait(long)
     * 2. #join(long) Thread.join,就是使用wait方法实现的。
     * 3. LockSupport#parkNanos
     * 4. LockSupport#parkUntil
     * 5. Thread#sleep
     *
     * 与WAITING状态相比较,当线程处于线程等待池中,如果没有调用对应的唤醒方法,
     * 但是超出规定时间,那么线程自动会被唤醒。所以就是多出了一种唤醒方式。
     * 注意Thread#sleep 没有对应的唤醒方法。
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    // 终止状态,当线程运行完毕时,就处于这个状态,而且该状态不能再转换成其他状态。
    TERMINATED;
}

线程一共有六种状态:

  • NEW:新建状态。当创建一个线程Thread对象,但是还没有调用它的start方法,就是这个状态。
  • RUNNABLE:运行状态。当前线程正在运行中。
  • BLOCKED:阻塞状态。当前线程正在等待锁资源。
  • WAITING:等待状态。当前线程处于线程等待池中,需要被唤醒。
  • TIMED_WAITING:等待超时状态。与WAITING状态相比,多了一种超时会被自动唤醒的方法。
  • TERMINATED:终止状态,当线程运行完毕时,就处于这个状态,而且该状态不能再转换成其他状态。

注意处于等待状态的线程只有两种方式被唤醒:

  • 调用对应的唤醒方法。
  • 调用该线程变量的interrupt()方法,会唤醒该线程,并抛出InterruptedException异常。

 

脚本宝典总结

以上是脚本宝典为你收集整理的多线程概览全部内容,希望文章能够帮你解决多线程概览所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签:并发