精华内容
下载资源
问答
  • 个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。局部变量局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被线程共享。所以,基础类型的局部变量是...

    允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。

    局部变量

    局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

    public voidsomeMethod(){long threadSafeInt = 0;

    threadSafeInt++;

    }

    局部的对象引用

    对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:

    public voidsomeMethod(){

    LocalObject localObject= newLocalObject();

    localObject.callMethod();

    method2(localObject);

    }public voidmethod2(LocalObject localObject){

    localObject.setValue("value");

    }

    样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。

    对象成员

    对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:

    public classNotThreadSafe{

    StringBuilder builder= newStringBuilder();publicadd(String text){this.builder.append(text);

    }

    }

    如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:

    NotThreadSafe sharedInstance = newNotThreadSafe();new Thread(newMyRunnable(sharedInstance)).start();new Thread(newMyRunnable(sharedInstance)).start();public class MyRunnable implementsRunnable{

    NotThreadSafe instance= null;publicMyRunnable(NotThreadSafe instance){this.instance =instance;

    }public voidrun(){this.instance.add("some text");

    }

    }

    注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。

    当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:

    new Thread(new MyRunnable(newNotThreadSafe())).start();new Thread(new MyRunnable(new NotThreadSafe())).start();

    现在两个线程都有自己单独的NotThreadSafe对象,调用add()方法时就会互不干扰,再也不会有竞态条件问题了。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。

    线程控制逃逸规则

    线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

    如果一个资源的创建,使用,销毁都在同一个线程内完成,

    且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

    资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。

    即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:

    检查记录X是否存在,如果不存在,插入X

    如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:

    线程1检查记录X是否存在。检查结果:不存在

    线程2检查记录X是否存在。检查结果:不存在

    线程1插入记录X

    线程2插入记录X

    同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

    展开全文
  • 线程安全和共享资源

    2021-05-12 14:50:07
    多线程调用安全的代码被称为线程安全。如果代码的部分是线程安全的,则说明不含竞态条件。只有在多个线程更新共享资源时才会出现竞争条件。因此,了解Java线程在执行时共享哪些资源非常重要。 本地变量 本地变量存储...

    本文翻译自http://tutorials.jenkov.com/java-util-concurrent/atomicreferencearray.html,人工翻译,仅供学习交流。

    线程安全和共享资源

    多线程调用安全的代码被称为线程安全。如果代码的部分是线程安全的,则说明不含竞态条件。只有在多个线程更新共享资源时才会出现竞争条件。因此,了解Java线程在执行时共享哪些资源非常重要。

    本地变量

    本地变量存储在每个线程的自己栈中,意味着线程不共享本地变量。下面是一个线程安全本地变量的例子:

    public void someMethod(){
    
      long threadSafeInt = 0;
    
      threadSafeInt++;
    }
    

    本地对象引用

    对对象的局部引用略有不同,引用本身不是共享的。然而,被引用的对象并不存储在每个线程的本地堆栈中。所有对象都存储在共享堆中。如果一个在本地创建的对象永远不会转义创建它的方法,那么它就是线程安全的。实际上,您还可以将其传递给其他方法和对象,只要这些方法或对象都不能使传递的对象对其他线程可用。
    下面是一个线程安全的局部对象的例子:

    public void someMethod(){
    
      LocalObject localObject = new LocalObject();
    
      localObject.callMethod();
      method2(localObject);
    }
    
    public void method2(LocalObject localObject){
      localObject.setValue("value");
    }
    

    本例中的LocalObject实例没有从方法中返回,也不会传递给任何其他可以从someMethod()方法外部访问的对象。每个执行someMethod()方法的线程都将创建自己的LocalObject实例并将其分配给localObject引用。因此LocalObject在这里的使用是线程安全的。事实上,整个someMethod()方法是线程安全的。即使LocalObject实例作为参数传递给同一类中的其他方法,或者在其他类中,它的使用是线程安全的。当然唯一的例外是,,如果其中一个方法以LocalObject作为参数被调用,以允许从其他线程访问它的方式存储LocalObject实例。

    对象的成员变量

    对象成员变量(字段)与对象一起存储在堆上。如果两个线程调用同一个对象实例上的一个方法并且这个方法更新对象成员变量,该方法不是线程安全的。下面是一个非线程安全的方法示例:

    public class NotThreadSafe{
        StringBuilder builder = new StringBuilder();
    
        public add(String text){
            this.builder.append(text);
        }
    }
    

    如果两个线程在同一个NotThreadSafe实例上同时调用add()方法,然后它会导致竞争条件。例如:

    NotThreadSafe sharedInstance = new NotThreadSafe();
    
    new Thread(new MyRunnable(sharedInstance)).start();
    new Thread(new MyRunnable(sharedInstance)).start();
    
    public class MyRunnable implements Runnable{
      NotThreadSafe instance = null;
    
      public MyRunnable(NotThreadSafe instance){
        this.instance = instance;
      }
    
      public void run(){
        this.instance.add("some text");
      }
    }
    

    请注意两个MyRunnable实例如何共享同一个NotThreadSafe实例。因此,当它们调用NotThreadSafe实例上的add()方法时,就会导致竞争条件。但是,如果两个线程在不同的实例上同时调用add()方法,这样就不会导致竞态。以下是之前的示例,但略有修改:

    new Thread(new MyRunnable(new NotThreadSafe())).start();
    new Thread(new MyRunnable(new NotThreadSafe())).start();
    

    现在这两个线程都有自己的NotThreadSafe实例,因此它们对add方法的调用不会相互干扰。代码不再具有竞争条件。因此,即使一个对象不是线程安全的,它仍然可以以一种不会导致竞态条件的方式使用。

    线程控制转义规则

    当试图确定您的代码对某个资源的访问是否线程安全时,你可以使用线程控制转义规则:

    If a resource is created, used and disposed within
    the control of the same thread,
    and never escapes the control of this thread,
    the use of that resource is thread safe.
    

    资源可以是任何共享资源,如对象、数组、文件、数据库连接、套接字等。在Java中,你并不总是显式地释放对象,所以"dispose "的意思是失去对该对象的引用或使其无效。
    即使对象的使用是线程安全的,如果该对象指向文件或数据库等共享资源,您的应用程序作为一个整体可能不是线程安全的。例如,如果线程1和线程2都创建了自己的数据库连接,连接1和连接2,每个连接本身的使用是线程安全的。但是连接所指向的数据库的使用可能不是线程安全的。例如,如果两个线程都执行如下代码:

    check if record X exists
    if not, insert record X
    

    如果两个线程同时执行,他们正在检查的记录X碰巧是相同的记录,有一个风险是两个线程最终都插入它。这就是:

    Thread 1 checks if record X exists. Result = no
    Thread 2 checks if record X exists. Result = no
    Thread 1 inserts record X
    Thread 2 inserts record X
    

    这也可能发生在操作文件或其他共享资源的线程上。区分线程控制的对象是否是资源很重要,或者它只是引用资源(就像数据库连接那样)。

    下一节:Thread Safety and Immutability

    展开全文
  • 线程间到底共享了哪些进程资源

    千次阅读 多人点赞 2020-12-18 14:10:00
    进程和线程这两个话题是程序员绕不开的,操作系统提供的这两个抽象概念实在是太重要了。关于进程和线程有一个极其经典的问题,那就是进程和线程的区别是什么?相信很同学对答案似懂非懂。记住了不一...

    进程和线程这两个话题是程序员绕不开的,操作系统提供的这两个抽象概念实在是太重要了。

    关于进程和线程有一个极其经典的问题,那就是进程和线程的区别是什么?相信很多同学对答案似懂非懂。

    记住了不一定真懂

    关于这个问题有的同学可能已经“背得”滚瓜烂熟了:“进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源”。

    可是你真的理解了上面最后一句话吗?到底线程之间共享了哪些进程资源,共享资源意味着什么?共享资源这种机制是如何实现的?对此如果你没有答案的话,那么这意味着你几乎很难写出能正确工作的多线程程序,同时也意味着这篇文章就是为你准备的。

    逆向思考

    查理芒格经常说这样一句话:“反过来想,总是反过来想”,如果你对线程之间共享了哪些进程资源这个问题想不清楚的话那么也可以反过来思考,那就是有哪些资源是线程私有的

     

    线程私有资源

    线程运行的本质其实就是函数的执行,函数的执行总会有一个源头,这个源头就是所谓的入口函数,CPU从入口函数开始执行从而形成一个执行流,只不过我们人为的给执行流起一个名字,这个名字就叫线程。

    既然线程运行的本质就是函数的执行,那么函数执行都有哪些信息呢?

    在《函数运行时在内存中是什么样子》这篇文章中我们说过,函数运行时的信息保存在栈帧中,栈帧中保存了函数的返回值、调用其它函数的参数、该函数使用的局部变量以及该函数使用的寄存器信息,如图所示,假设函数A调用函数B:

    此外,CPU执行指令的信息保存在一个叫做程序计数器的寄存器中,通过这个寄存器我们就知道接下来要执行哪一条指令。由于操作系统随时可以暂停线程的运行,因此我们保存以及恢复程序计数器中的值就能知道线程是从哪里暂停的以及该从哪里继续运行了。

    由于线程运行的本质就是函数运行,函数运行时信息是保存在栈帧中的,因此每个线程都有自己独立的、私有的栈区。

    同时函数运行时需要额外的寄存器来保存一些信息,像部分局部变量之类,这些寄存器也是线程私有的,一个线程不可能访问到另一个线程的这类寄存器信息

    从上面的讨论中我们知道,到目前为止,所属线程的栈区、程序计数器、栈指针以及函数运行使用的寄存器是线程私有的。

    以上这些信息有一个统一的名字,就是线程上下文,thread context。

    我们也说过操作系统调度线程需要随时中断线程的运行并且需要线程被暂停后可以继续运行,操作系统之所以能实现这一点,依靠的就是线程上下文信息。

    现在你应该知道哪些是线程私有的了吧。

    除此之外,剩下的都是线程间共享资源。

    那么剩下的还有什么呢?还有图中的这些。

    这其实就是进程地址空间的样子,也就是说线程共享进程地址空间中除线程上下文信息中的所有内容,意思就是说线程可以直接读取这些内容。

    接下来我们分别来看一下这些区域。

     

    代码区

    进程地址空间中的代码区,这里保存的是什么呢?从名字中有的同学可能已经猜到了,没错,这里保存的就是我们写的代码,更准确的是编译后的可执行机器指令

    那么这些机器指令又是从哪里来的呢?答案是从可执行文件中加载到内存的,可执行程序中的代码区就是用来初始化进程地址空间中的代码区的。

    线程之间共享代码区,这就意味着程序中的任何一个函数都可以放到线程中去执行,不存在某个函数只能被特定线程执行的情况

     

     数据区

    进程地址空间中的数据区,这里存放的就是所谓的全局变量。

    什么是全局变量?所谓全局变量就是那些你定义在函数之外的变量,在C语言中就像这样:

    char c; // 全局变量
    void func() {    }
    

    其中字符c就是全局变量,存放在进程地址空间中的数据区。

    在程序员运行期间,也就是run time,数据区中的全局变量有且仅有一个实例,所有的线程都可以访问到该全局变量

    值得注意的是,在C语言中还有一类特殊的“全局变量”,那就是用static关键词修饰过的变量,就像这样:

    void func(){    static int a = 10;}
    

    注意到,虽然变量a定义在函数内部,但变量a依然具有全局变量的特性,也就是说变量a放在了进程地址空间的数据区域,即使函数执行完后该变量依然存在,而普通的局部变量随着函数调用结束和函数栈帧一起被回收掉了,但这里的变量a不会被回收,因为其被放到了数据区。

    这样的变量对每个线程来说也是可见的,也就是说每个线程都可以访问到该变量。

     

    堆区

    堆区是程序员比较熟悉的,我们在C/C++中用malloc或者new出来的数据就存放在这个区域,很显然,只要知道变量的地址,也就是指针,任何一个线程都可以访问指针指向的数据,因此堆区也是线程共享的属于进程的资源。

     

    栈区

    唉,等等!刚不是说栈区是线程私有资源吗,怎么这会儿又说起栈区了?

    确实,从线程这个抽象的概念上来说,栈区是线程私有的,然而从实际的实现上看,栈区属于线程私有这一规则并没有严格遵守,这句话是什么意思?

    通常来说,注意这里的用词是通常,通常来说栈区是线程私有,既然有通常就有不通常的时候。

    不通常是因为不像进程地址空间之间的严格隔离,线程的栈区没有严格的隔离机制来保护,因此如果一个线程能拿到来自另一个线程栈帧上的指针,那么该线程就可以改变另一个线程的栈区,也就是说这些线程可以任意修改本属于另一个线程栈区中的变量。

    这从某种程度上给了程序员极大的便利,但同时,这也会导致极其难以排查到的bug。

    试想一下你的程序运行的好好的,结果某个时刻突然出问题,定位到出问题代码行后根本就排查不到原因,你当然是排查不到问题原因的,因为你的程序本来就没有任何问题,是别人的问题导致你的函数栈帧数据被写坏从而产生bug,这样的问题通常很难排查到原因,需要对整体的项目代码非常熟悉,常用的一些debug工具这时可能已经没有多大作用了。

    说了这么多,那么同学可能会问,一个线程是怎样修改本属于其它线程的数据呢?

    接下来我们用一个代码示例讲解一下。

     

    修改线程私有数据

    不要担心,以下代码足够简单:

    void thread(void* var) {
        int* p = (int*)var;
        *p = 2;
    }
    
    
    int main() {
        int a = 1;
        pthread_t tid;
        
        pthread_create(&tid, NULL, thread, (void*)&a);
        return 0;
    }
    

    这段代码是什么意思呢?

    首先我们在主线程的栈区定义了一个局部变量,也就是 int a= 1这行代码,现在我们已经知道了,局部变量a属于主线程私有数据,但是,接下来我们创建了另外一个线程。

    在新创建的这个线程中,我们将变量a的地址以参数的形式传给了新创建的线程,然后我来看一下thread函数。

    在新创建的线程中,我们获取到了变量a的指针,然后将其修改为了2,也就是这行代码,我们在新创建的线程中修改了本属于主线程的私有数据。

    现在你应该看明白了吧,尽管栈区是线程的私有数据,但由于栈区没有添加任何保护机制,一个线程的栈区对其它线程是可以见的,也就是说我们可以修改属于任何一个线程的栈区。

    就像我们上文说得到的,这给程序员带来了极大便利的同时也带来了无尽的麻烦,试想上面这段代码,如果确实是项目需要那么这样写代码无可厚非,但如果上述新创建线程是因bug修改了属于其它线程的私有数据的话,那么产生问题就很难定位了,因为bug可能距离问题暴露的这行代码已经很远了,这样的问题通常难以排查。

     

    动态链接库

    进程地址空间中除了以上讨论的这些实际上还有其它内容,还有什么呢?

    这就要从可执行程序说起了。

    什么是可执行程序呢?在Windows中就是我们熟悉的exe文件,在Linux世界中就是ELF文件,这些可以被操作系统直接运行的程序就是我们所说的可执行程序。

    那么可执行程序是怎么来的呢?

    有的同学可能会说,废话,不就是编译器生成的吗?

    实际上这个答案只答对了一半。

    假设我们的项目比较简单只有几个源码文件,编译器是怎么把这几个源代码文件转换为最终的一个可执行程序呢?

    原来,编译器在将可执行程序翻译成机器指令后,接下来还有一个重要的步骤,这就是链接,链接完成后生成的才是可执行程序。

    完成链接这一过程的就是链接器。

    其中链接器可以有两种链接方式,这就是静态链接动态链接

    静态链接的意思是说把所有的机器指令一股脑全部打包到可执行程序中,动态链接的意思是我们不把动态链接的部分打包到可执行程序,而是在可执行程序运行起来后去内存中找动态链接的那部分代码,这就是所谓的静态链接和动态链接。

    动态链接一个显而易见的好处就是可执行程序的大小会很小,就像我们在Windows下看一个exe文件可能很小,那么该exe很可能是动态链接的方式生成的

    而动态链接的部分生成的库就是我们熟悉的动态链接库,在Windows下是以DLL结尾的文件,在Linux下是以so结尾的文件。

    说了这么多,这和线程共享资源有什么关系呢?

    原来如果一个程序是动态链接生成的,那么其地址空间中有一部分包含的就是动态链接库,否则程序就运行不起来了,这一部分的地址空间也是被所有线程所共享的。

    也就是说进程中的所有线程都可以使用动态链接库中的代码。

    以上其实是关于链接这一主题的极简介绍,关于链接这一话题的详细讨论可以参考《彻底理解链接器》系列文章。

     

    文件

    最后,如果程序在运行过程中打开了一些文件,那么进程地址空间中还保存有打开的文件信息,进程打开的文件也可以被所有的线程使用,这也属于线程间的共享资源。

     

     One More Thing:TLS

    本文就这些了吗?

    实际上关于线程私有数据还有一项没有详细讲解,因为再讲下去本篇就撑爆了,而且本篇已经讲解的部分足够用了,剩下的这一点仅仅作为补充,也就是选学部分,如果你对此不感兴趣的话完全可以跳过,没有问题

    关于线程私有数据还有一项技术,那就是线程局部存储,Thread Local Storage,TLS。

    这是什么意思呢?

    其实从名字上也可以看出,所谓线程局部存储,是指存放在该区域中的变量有两个含义:

    • 存放在该区域中的变量是全局变量,所有线程都可以访问

    • 虽然看上去所有线程访问的都是同一个变量,但该全局变量独属于一个线程,一个线程对此变量的修改对其他线程不可见。

    说了这么多还是没懂有没有?没关系,接下来看完这两段代码还不懂你来打我。

    我们先来看第一段代码,不用担心,这段代码非常非常的简单:

    int a = 1; // 全局变量
    
    
    void print_a() {
        cout<<a<<endl;
    }
    
    
    void run() {
        ++a;
        print_a();
    }
    
    
    void main() {
        thread t1(run);
        t1.join();
    
    
        thread t2(run);
        t2.join();
    }
    

    怎么样,这段代码足够简单吧,上述代码是用C++11写的,我来讲解下这段代码是什么意思。

    • 首先我们创建了一个全局变量a,初始值为1

    • 其次我们创建了两个线程,每个线程对变量a加1

    • 线程的join函数表示该线程运行完毕后才继续运行接下来的代码

    那么这段代码的运行起来会打印什么呢?

    全局变量a的初始值为1,第一个线程加1后a变为2,因此会打印2;第二个线程再次加1后a变为3,因此会打印3,让我们来看一下运行结果:

    2
    3
    

    看来我们分析的没错,全局变量在两个线程分别加1后最终变为3。

    接下来我们对变量a的定义稍作修改,其它代码不做改动:

    __thread int a = 1; // 线程局部存储
    

    我们看到全局变量a前面加了一个__thread关键词用来修饰,也就是说我们告诉编译器把变量a放在线程局部存储中,那这会对程序带来哪些改变呢?

    简单运行一下就知道了:

    2
    2
    

    和你想的一样吗?有的同学可能会大吃一惊,为什么我们明明对变量a加了两次,但第二次运行为什么还是打印2而不是3呢?

    想一想这是为什么。

    原来,这就是线程局部存储的作用所在,线程t1对变量a的修改不会影响到线程t2,线程t1在将变量a加到1后变为2,但对于线程t2来说此时变量a依然是1,因此加1后依然是2。

    因此,线程局部存储可以让你使用一个独属于线程的全局变量。也就是说,虽然该变量可以被所有线程访问,但该变量在每个线程中都有一个副本,一个线程对改变量的修改不会影响到其它线程。

     

    总结

    怎么样,没想到教科书上一句简单的“线程共享进程资源”背后竟然会有这么多的知识点吧,教科书上的知识看似容易,但,并不简单

    希望本篇能对大家理解进程、线程能有多帮助。

    展开全文
  • 多线程背景知识介绍1 多线程基础概念**进程与线程 **进程 进程程序(任务)的执行过程(动态性),持有资源(共享内存,共享文件)和线程,是资源和线程的载体。线程 进程的最小执行单元,比如将一个班级视为进程,则班里...

    多线程背景知识介绍

    1 多线程基础概念

    **进程与线程 **

    进程 进程程序(任务)的执行过程(动态性),持有资源(共享内存,共享文件)和线程,是资源和线程的载体。

    线程 进程的最小执行单元,比如将一个班级视为进程,则班里的每一个学生就是线程,所有学生共享黑板,教师等。

    线程的交互:互斥与同步,公共资源有限,需要竞争。

    2 可见性

    什么是可见性?

    可见性:一个线程对共享变量的修改,能及时的被其他线程看到。

    共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

    工作内存:java内存模型抽象出的概念。

    ###Java内存模型(JMM)

    java内存模型描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。可这么理解:

    (1) 所有的变量都存储在住内存中;

    (2)每个线程都有自己独立的工作内存,里边保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

    f74d079692e3a23fcffe27172eed8376.png

    两条规定:

    (1) 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;

    (2) 不同线程无法直接访问其他线程工作内存中的变量,线程之间传递变量值需要通过主内存完成。

    共享变量可见性实现原理

    线程1对共享变量的修改要被线程2看到,需要下边两个步骤:

    (1) 把线程1工作内存中修改后的共享变量刷新到主内存中;

    (2) 把主内存中更新过的共享变量更新到工作内存2中。

    a39f6f2b710ab706ea8874a7cee2d6ba.png

    3 synchronized实现可见性

    JMM关于synchronized的两条规定:

    (1) 线程解锁前,必须把共享变量最新值刷新到主内存中;

    (2) 线程加锁时,将清空工作内存中共享变量的值,使用共享变量时,重新从主内存中读取最新值(加锁解锁需要是同一把锁)

    线程解锁前对共享变量的修改在下次加锁时对其他线程可见

    线程执行互斥代码的过程:

    (1)获得互斥锁;

    (2) 清空工作内存;

    (3) 从主内存拷贝共享变量副本到工作内存;

    (4) 执行代码;

    (5)将修改后的共享变量刷新到主内存;

    (6) 释放互斥锁。

    补充2个知识点:重排序

    重排序

    代码的执行顺序与实际书写顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化,主要有3种:

    (1)编译器优化的重排序;(编译器优化)

    (2)指令集并行重排序(处理器优化);

    (3) 内存系统的重排序(处理器优化)。

    as-if-serial

    as-if-serial:无论如何重排序,程序执行结果应该与代码书写顺序执行结果一致(java编译器,运行时和处理器都会保证java在单线程下遵循as-if-serial语义)。

    ```

    int num1=1;

    int num2=2;

    int sum=num1+num2;

    ```

    单线程:第1,2行的顺序可以重排,但第3行不能,重排序不会给单线程带来内存可见性的问题;

    多线程中程序交错执行时,重排序可能会造成内存可见性问题。

    下面的代码演示内存可见性问题:

    ```

    public class SynchronizedDemo {

    //共享变量

    private boolean ready = false;

    private int result = 0;

    private int number = 1;

    //写操作

    public void write(){

    ready = true; //1.1

    number = 2; //1.2

    }

    //读操作

    public void read(){

    if(ready){ //2.1

    result = number*3; //2.2

    }

    System.out.println("result的值为:" + result);

    }

    //内部线程类

    private class ReadWriteThread extends Thread {

    //根据构造方法中传入的flag参数,确定线程执行读操作还是写操作

    private boolean flag;

    public ReadWriteThread(boolean flag){

    this.flag = flag;

    }

    @Override

    public void run() {

    if(flag){

    //构造方法中传入true,执行写操作

    write();

    }else{

    //构造方法中传入false,执行读操作

    read();

    }

    }

    }

    public static void main(String[] args) {

    SynchronizedDemo synDemo = new SynchronizedDemo();

    //启动线程执行写操作

    synDemo .new ReadWriteThread(true).start();

    try {

    Thread.sleep(1000);

    } catch (InterruptedException e) {

    // TODO Auto-generated catch block

    e.printStackTrace();

    }

    //启动线程执行读操作

    synDemo.new ReadWriteThread(false).start();

    }

    }

    ```

    可能的结果

    (1) 1.1->2.1->2.2->1.2 result:3;

    (2) 1.1->2.1->1.2->2.2 result:6;

    (3) 1.2->2.1->2.2->1.1 result:0.

    可见性分析

    导致共享变量在线程间不可见的原因: synchronized解决方案

    (1)线程的交叉执行; 原子性

    (2) 重排序结合线程交叉执行; 原子性

    (3) 共享变量更新后没有在工作内存与主内存间及时更新。 可见性

    安全的代码

    ```

    //写操作

    public synchronized void write(){

    ready = true; //1.1

    number = 2; //1.2

    }

    //读操作

    public synchronized void read(){

    if(ready){ //2.1

    result = number*3; //2.2

    }

    System.out.println("result的值为:" + result);

    }

    ```

    synchronized既可以保证原子性,也可以保证可见性。

    4 volatile实现可见性

    volatile如何实现内存可见性:

    深入来说,通过加入内存屏障和禁止重排序优化来实现的:

    对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,强制刷新到主内存;

    对volatile变量执行读操作时,会在读操作前加入一条load指令,强制从主内存读取。

    java中volatile相关的指令共有8个,store和load是其中两个,具体可自行百度。

    但是,volatile不能保证原子性

    以num++为例,这行代码并非原子操作,而是分为3步:

    (1) 读取num;

    (2) num+1;

    (3) 写入num。

    加入有A ,B两个线程同时对num进行num++操作:volatile int num=5;

    (1)线程A读取num;

    (2)线程B读取num;

    (3)线程B对num+1;

    (4)线程B写入最新的num值6;

    (5)线程A中的num仍然为5,执行+1操作后为6,写入主内存6

    结果为6,期望值为7.这就是volatile不能保证原子性造成的。

    解决方法:

    保证num自增操作的原子性:

    (1) 使用synchronized关键字;

    (2) 使用ReentrantLock;

    (3) 使用AtomicInteger。

    volatile适用场合

    要在多线程中安全的使用volatile变量,必须同时满足:

    (1) 对变量的操作不依赖其当前值:

    不满足:count++;count=count+1;

    满足:Boolean变量,温度值等。

    (2) 该变量没有包含在具有其他变量的不变式中:

    不满足:不变式,low

    volatile不需要加锁,不会阻塞线程,效率高于synchronized,但只能保证内存可见性,无法保证原子性。

    ### synchronized和volatile的比较

    volatile比synchronized更轻量级;

    volatile没有synchronized使用广泛。

    展开全文
  • Java多线程整理

    2021-03-01 06:23:38
    一、线程池过于频繁的创建/销毁线程浪费性能,线程并发数量过多,JVM调度是抢占式的,线程上线文切换抢占系统资源导致阻塞。1.线程池线程数:一般CPU密集型:CPU+1IO密集型:[(线程等待时间+线程CPU时间)/线程CPU...
  • 我的目标是使用多线程来加速耗时的部件.在此示例中,我给出了我应该并行化的部分的原型public static ArrayList createListOfObject2(ArrayList mylist) {ArrayList listToReturn = new ArrayList<>();Object.....
  • 一、竞态条件与临界区      在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问...多线程同时执行下面的代码可能会出错: public class Counter { protected long count = 0;
  • 进程使用独立的数据空间,而线程共享进程的数据空间。 线程状态图 多线程会带来哪些性能问题? 调度开销,一般线程数往往大于CPU核心数,这样操作系统再执行线程时就会出现上下文切换,从而产生一定性能开销; ...
  • Java 多线程编程总结-------------------------------------------------------------------------------------------------下面的内容是很早之前写的,内容不够充实,而且是基于Java1.4的内容,Java5之后,线程并发...
  • Java多线程

    2021-02-26 17:20:31
    一、sychronized介绍并发时,线程需要操作同一个资源,容易导致错误数据的产生,为了解决这个问题,当存在线程操作共享数据时,需要保证同一时刻只有一个线程在操作共享数据,其他线程必须等到该线程处理完...
  • 互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放...
  • 随便谈谈多线程

    2021-08-15 18:46:12
    多线程基础 文章目录多线程基础前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人...
  • 1. 禁止同一个 JobDetail 中的个实例并发执行Quartz 定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行,如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。禁止并发执行...
  • } } } /** * 因为涉及共享资源的操作,需要同步 * wait():此方法必须在synchronized修饰的代码块或者方法中使用 * @throws Exception */ public synchronized void thread() throws Exception { if ...
  • 1.ThreadLocal2.如何保证高并发场景下的线程安全?...锁机制1.ThreadLocalThreadLocal如何实现多线程数据隔离?ThreadLocal内存泄漏问题?ThreadLocal脏数据问题?ThreadLocal主要功能:进行对象跨层传输,使用Th...
  • 架构师:『试试使用多线程优化』 第二天 头发很多的程序员:『师父,我已经使用多线程,为什么接口还变慢了?』 架构师:『去给我买杯咖啡,我写篇文章告诉你』 ……吭哧吭哧买咖啡去了 在实际工作中,错误...
  • Java多线程(一文看懂!)

    2021-07-28 17:36:15
    一,多线程的介绍 二,多线程的实现方式 三,多线程的五大状态 四,多线程的调度 五,线程的同步(多口售票问题) 六,线程的协作(生产者-消费者模型) 一,多线程的介绍 百度中多线程的介绍(multithreading):...
  • 线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。 进程拥有这许多共性的...
  • 每个线程都保存了一份该线程使用到的共享变量的副本。 如果线程A与线程B之间要通信的话,必须经历下面2个步骤: a. 线程A将本地内存A中更新过的共享变量刷新到主内存中去。 b. 线程B到主内存中去读取线程A之前已经...
  • 在单个程序中我们经常用多线程来处理不同的工作,尤其是有的工作需要等,那么我们会新建一个线程去等然后执行某些操作,当做完事后线程退出被回收。当一个程序运行时,就会有一个进程被系统所创建,同时也会有一个...
  • 多线程

    2021-04-18 06:24:31
    在操作系统中,安装了个程序,并发指的是在一段时间内宏观上有个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分...
  • C++/C# Thread多线程总结

    2020-12-19 16:40:18
    一直都想写一篇关于多线程的文章,总结一下这方面的知识,也为自己和有需要的人提供参考,不再赘述,开始吧, 现在计算机一般都是多核的,4核和8核的比较多件,用于线上计算或专用工作的计算机更是大概率“土豪”...
  • 多线程面试题整理

    2021-01-22 10:52:55
    1.多线程有什么用? 1)发挥多核CPU 的优势 随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的 ,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上 就浪费了...
  • 多线程记住了不必定真懂有的同窗可能已经“背得”倒背如流了:“进程是操做系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源”。app但是你真的理解了上面这句话吗?到底线程之间共享了哪些进程资源...
  • Java多线程题库

    千次阅读 2021-02-12 22:03:49
    一、填空题处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入____阻塞_____状态。处于新建状态的线程被启动后,将进入线程队列排队等待CPU,...
  • 文章目录前言进程与线程setPriority()interrupt()setDaemon()线程生命周期java线程模型java原子性 可见性Java有序性保证内存访问的顺序性volatile关键字, synchronized关键字总结 前言 提示:这里可以添加本文要...
  • Java基础--多线程

    2021-02-26 14:31:54
    一、程序、进程、线程1、区别(1)程序是一段静态的代码,为应用程序执行的蓝本。(2)进程为程序的一次动态执行过程,包括代码的加载、执行以及...(2)进程拥有一个包含了某些资源的内存区域,线程共享进程的内...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 99,267
精华内容 39,706
关键字:

多线程使用共享资源的规则