一篇文章搞懂多线程和高并发

一篇文章搞懂多线程和高并发

基于之前对多线程的理解不全面且在实际应用缺乏相关的经验问题,导致在项目开发中应用到多线高并发场景的时候找不到方向,经过了一个游戏工会并发的修改和再次对多线程高并发方案的学习,加深了其理解。如今对之前的学习进行一个总结,加深理解,同时也留作记录!

Java内内存模型

要想理解为什么会发生并发问题,首先我们必须要先学习Java内存模型。只有深刻理解了Java内存模型才能更好的理解后面的内容,因为Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的指,以及在必须时如何同步的访问共享变量,而且学习Java内存模型还有有助于我们无须执行代码的情况下对多线程代码的执行效果进行判断。。那开始呗!

Java内存模型

线程栈Thread Strack:

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。

一个线程仅能访问自己的线程栈。

本地创建时时存放在栈区的,因此一个线程创建的本地变量对其它线程不可见,仅自己可见。
因此,每个线程拥有每个本地变量的独有版本。

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。
一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

线程堆Teap:

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。
如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,那么这个对象仍然是存放在堆上,但是它的引用确实在栈上的。

线程堆栈的总结点:

存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。

一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。

一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。

一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。

一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。

静态成员变量跟随着类定义一起也存放在堆上。

上图中methodOne()声明了一个原始类型的本地变量和一个引用类型的本地变量。

每个线程执行methodOne()都会在它们对应的线程栈上创建localVariable1和localVariable2的私有拷贝。localVariable1变量彼此完全独立,仅“生活”在每个线程的线程栈上。一个线程看不到另一个线程对它的localVariable1私有拷贝做出的修改。每个线程执行methodOne()时也将会创建它们各自的localVariable2拷贝。

然而,两个localVariable2的不同拷贝都指向堆上的同一个对象。代码中通过一个静态变量设置localVariable2指向一个对象引用。仅存在一个静态变量的一份拷贝,这份拷贝存放在堆上。因此,localVariable2的两份拷贝都指向静态变量的同一个实例,如果不加处理的话多线程访问下就会出现并发问题。

理解了上面的Java内存模型还是不够,归根到底,还是要回归到计算机硬件中,那么我们来理解一下Java内存模型和硬件内存架构之间的联系!

cpu内存架构

CPU:

一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行

寄存器CPU Registers:

每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

高级缓存:

由于CPU访问寄存器的速度远大于主存,因此每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存。

主存

一个计算机还包含一个主存。**所有的CPU都可以访问主存。**主存通常比CPU中的缓存大得多。

总结:

通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

由于读取的时主内存中的数据,但是操作数据确实在寄存器中,因此者就会导致数据在多个线程操作下会发生数据的不一致问题,从而产生并发问题。

阐述了Java内存模型和CPU内存架构问题,那么他们之间时如何联系起来的,下面继续!

Java内存模型和CPU内存架构关系

硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如上图所示!

共享对象可见性

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不接见的。

想象一下,共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中。然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

解决这个问题你可以使用Java中的volatile关键字。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。
下面会对这个Volatile关键字进行详细的说明。

Race Conditions

如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions。

想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增在了两个,每个CPU缓存中一次。

如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。

然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次。

解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。
同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。



原子性

多线程引起的线程安全问题究其本质,是因为多条线程操作同一数据的过程中,破坏了数据的原子性。所谓原子性,就是不可再分性。

在某一个时间点,系统中只会有一条线程去执行任务,下一时间点有可能又会切换为其他线程去执行任务,我们无法预测某一时刻究竟是哪条线程被执行,这是由CPU来统一调度的。

int i = 1;
int temp; 

while(i < 10){
	temp = i; //读取i的值
	i = temp + 1; //对i进行+1操作后再重新赋给i
};

上面的代码能很好的说明原子性问题!

那如何解决上面的问题?

Lock:软件层面上实现原子操作
CAS:硬件层面上实现原子操作

我们知道一条线程被CPU调度执行任务时,最少要执行一行代码,所以解决办法很简单,只要将读和写合并到一起就可以了,下面的代码是通过JUC中的原子操作来完成自增的操作即可。下面会详细的讲解JUC的类。

private static AtomicInteger count = new AtomicInteger(0);
 
 while(count < 10){
    count.incrementAndGet(); 
 };

注意点:

只存在读数据的时候,不会产生线程安全问题。
在java中,只有同时操作成员(全局)变量的时候才会产生线程安全问题,局部变量不会。

下面讲解锁的一些知识点

lock锁分:

内部锁Synchronized
显示锁:lock,Reentrantlock

上面两种锁都时可重入锁,可重入的指一个线程拥有一个锁的同时还能再次申请该锁进行操作。

synchronized锁

synchronized(this){~} 当this时句柄或者时变量的是时候常用private final 来修饰,因为此者可以保证多线程访问过来的都时同一个内部锁。

###synchronized的保证线程安全的原理

对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。

在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

不过有几点需要注意:

1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

反编译synchronized的字节码如下

public class InsertData {
    private Object object = new Object();
     
    public void insert(Thread thread){
        synchronized (object) {
        }
    }
     
    public synchronized void insert1(Thread thread){
         
    }
     
    public void insert2(Thread thread){
         
    }
}

从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

synchronized的具体使用方式可以参考之前写的一篇文章,地址如下:
https://blog.csdn.net/huanghailiang_ws/article/details/97116356


Lock

显式使用锁需要结合的使用lock()和unlock()方法,而通常unlock方法需要放到finally中防止代码因为错误而导致释放不了锁。

线程同步机制的底层是因为:内存屏障

线程获得和释放锁的两个动作:

1.刷新处理器缓存
2.冲刷处理器缓存

而内存屏障就是在获得锁的时候刷信处理器缓存,而释放锁的时候将结果数据冲刷到处理器缓存中。

volatile关键字

使得读写操作都是在主内存中进行读写

volatile只保证对变量的读写操作时原子性,并不能保证对变量的赋值操作也具有原子性,如下

int count2 = count1 + 1;

若count1时局部变量,那么具有原子性,
若count2是全局变量,那么则不具有原子性。

volatile使用的场景一般是:状态变量


读写锁:

源代码如下,使用的场景时只读>写操作,读取线程持有所有锁的时间比较长。

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

未完待续~~~

猜你喜欢

转载自blog.csdn.net/huanghailiang_ws/article/details/97541704