什么叫多线程

1.中断和线程阻塞 1-1 阻塞 首先需要说明的是,线程阻塞和中断并不是同一个概念,阻塞指的是要进入临界区的线程必须具备可以获得临界区资源的条件,才可以进入临界区,否则,需要在临界区外面阻塞等待。 如上图,对于线程2和线程3,即为阻塞。 1-2 进入阻塞的方式

1.中断和线程阻塞

1-1 阻塞

首先需要说明的是,线程阻塞和中断并不是同一个概念,阻塞指的是要进入临界区的线程必须具备可以获得临界区资源的条件,才可以进入临界区,否则,需要在临界区外面阻塞等待。

如上图,对于线程2和线程3,即为阻塞。

1-2 进入阻塞的方式

java中进入阻塞的方式有以下几种:

1.Object,wait();//可传入一个long型参数,表示线程在一个特定的时间后会被唤醒,若不传,则永久阻塞,或需要由notify()来唤醒

2.Thread.join();//可传入一个long型参数,含义同上

3.Thread.sleep(long time);//long型参数必须指明,含义同上

4.LockSupport.park();//可传入一个long型参数,含义同上

1-3 线程中断

中断其实对应操作系统的一个信号量,在线程收到信号之后,要保存上下文,将中断位设为true,并交出CPU执行权,但对于一个阻塞的线程,该过程无法完成,因此会抛出异常,这个异常就是中断异常。

在中断异常抛出后,操作系统由用户态切换为内核态来处理这个异常,待处理完成后,线程将重新加入到就绪队列中等待系统的调度。但相比于阻塞被唤醒,其会丢掉之前所有的上下文内容,重新开始处理。

举个例子,必须你在写一份文档,并且没有保存,现在文档突然卡了(类比为线程阻塞),此时,你有两种选择,一是等待文档自己恢复过来(对应线程被唤醒),二是强行关闭文档,然后重新打开,但此时文档中原先你写的内容会丢失(对应抛出中断异常,然后操作系统对异常进行处理)。

1-4 interrupt/ interrupted/ isInterupted的区别

1)interrupt

public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

interrupt()方法师用于中断当前线程,从interrupt0()的源码注释中不难看出,i中断的具体方式是将线程的中断标志位设置为true,由于interupt0()是native修饰的方法,无法看到它的源码,但是通过操作系统的知识我们可以知道,操作系统会轮询线程的标志位,如发现某个线程的标志位为true,会中断这个线程。

2)interrupted & isInterrupted

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

可以看到这个方法调用了isInterrupted()方法,但这个方法时native修饰的,没法看到源码。

private native boolean isInterrupted(boolean ClearInterrupted);

不过,通过操作系统的知识我们可以知道,参数中的这个变量的含义是是否需要将中断位复原,如果是true,则复原,false则不复原。

也就是说interrupted方法返回的是当前线程的中断状态,其中第一次调用该方法时,若当前线程被中断,则会返回true,待中断标记被清除后,再次调用,则返回false

isInterupted方法,只是简单地查询了中断位的状态,并没有复原中断位的功能。

2.synchronized关键字

synchronized是同步的意思,其可以静态与非静态的成员变量,静态与非静态的方法。其修饰不同的方法/变量时,代表着对不同的对象加了锁,具体的含义如下:

1、修饰非静态属性和方法时,拿到的是调用这个方法或者属性的对象(this)的锁。

2、synchronize()修饰代码块时,拿到的是指定对象的锁。

3、修饰类、静态方法、静态代码块时,由于没有this指针,因此拿到的是类锁,也就是该类的class对象。

synchornized的实现原理:

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }
 
    private static void method() {
    }
}

SynchronizedDemo.class

 可以参考这段代码对应的编译结果,可以看到,在进入同步代码块的时候,多了一个monitoerenter,在退出的时候,多了一个monitorexit。那么这两行具体的含义是什么呢?

其实是在调用底层的一个互斥量,我们可以将这个互斥量理解为一个对象,这个对象有一个state属性和currentThread属性,分别用来记录当前对象的持有计数(即被请求了多少次)以及持有这个对象的线程。

因为currentThread记录的是当前持有这个对象的线程的id,id不可能有多个值,因此,这也就是为什么只有一个线程能进入同步代码块的原因。

除了这两个属性之外,这个对象还会维护一个thread id list,用于记录获取这个对象,但是失败了的线程id,等待当前线程的锁释放之后,从这个list中按照一定策略,选出一个线程来作为这个对象的持有线程。

对于并发编程的三大特性,即原子性——操作不可被打断,可见性——主存的变量值对各个线程可见,有序性——涉及到线程安全的指令指令不可以进行重排序,synchronized满足这三大特性。

3.volatile关键字

相比于synchronized关键字,volatile关键字有以下几点不同:

1.不可修饰方法

2.可以保证有序性和可见性,但不保证原子性。

volatile可见性的实现是基于缓存一致性协议。设想一种场景,多个线程共享同一片内存,每个线程会在自己的缓存中缓存这个变量的值,但是一旦某个线程修改了主存中这个变量的值,那么缓存一致性协议会让所有线程缓存的变量值全部失效,那么每个线程若再想对这个变量进行操作,那么就必须回到主存中重新取值。

 具体过程可以参照这幅图。

对于有序性,先来看看指令排序可能会带来哪些问题,举个例子:

线程1执行:

X=1; a = Y;

线程2执行:

Y=1; b=X;

对a和b的结果进行读取,可能有以下三种答案:

一是a =0, b=1;

二是a =1, b=0;

三是a=1, b=1;

之所以会出现这三种结果就是因为指令存在重排序所导致的,线程1在读取Y的时候,Y=1可能因为指令重排序的问题而没有在a = Y之前执行,导致a = 0,对于b=0也是同样的道理。那么这个volatile关键字是如何保证有序性的呢?

答案是引入了一个内存屏障,CPU的内存屏障可以分为四种:

1)LoadLoad:禁止读和读重排序

2)StoreStore:禁止写和写重排序

3)LoadStore:禁止读和写重排序

4)StoreLoad:禁止写和读重排序

java中将其封装为loadFence,storeFence和fullFence

    public native void loadFence();

    public native void storeFence();

    public native void fullFence();

其对应的含义分别如下:

1)在volatile写操作之前插入内存屏障,保证volatile的写操作不会写前面的写操作重排序

2)在volatile写操作之后插入内存屏障,保证volatile之后的写操作不会后面的读操作重排

3)在volatile读操作之后插入内存屏障,保证volatile的读操作不会和之后的读操作和写操作进行重排序。

知秋君
上一篇 2024-08-13 17:48
下一篇 2024-08-13 17:12

相关推荐