精华内容
下载资源
问答
  • 2018-08-14 10:18:33

    我是小白,最近在学习实现一个简单的线程池,线程池中维护了一个存放线程的数组以及一个任务队列。在创建线程池(构造函数中)时创建若干个线程,起始时由于任务队列中没有任务,创建的每一个线程都处于阻塞状态。使用pthread_create函数创建线程时把所有的线程与同一个函数关联在一起,也就是产生如下的代码:

    pthread_t p[5]; //线程池中有5个线程

    for(int i = 0; i < 5; ++i)

    {

    pthread_create(&p[i], NULL, func, NULL);

    }

    看到这儿,我心里产生了一个疑问:可不可以将所有的线程都关联同一个回调函数func呢?

    经过网上查阅相关资料,是可以这样做的,并且即使使用同一个函数这些函数之间也是互不影响的。每个进程都会有自己的虚拟地址空间,同一个进程中的每个线程都会在这个虚拟空间中,并被分配相应的资源。如果仅仅只使用函数内部的局部变量,这些线程之间是互不相关的,各执行各自的,不用使用互斥锁。如果不同的线程访问了全局变量那么要使用互斥锁,使得某一时刻只有一个线程操作该全局变量。

    下面通过一个代码说明两个线程关联一个函数:

    
       
    1. #include <pthread.h>
    2. #include <iostream>
    3. using namespace std;
    4. void *print(void *arg)
    5. {
    6. for ( int i = 0; i < 20; ++i)
    7. {
    8. cout << i << endl;
    9. }
    10. return NULL;
    11. }
    12. int main()
    13. {
    14. pthread_t p1, p2;
    15. pthread_create(&p1, NULL, print, NULL);
    16. pthread_create(&p2, NULL, print, NULL);
    17. pthread_join(p1, NULL);
    18. pthread_join(p2, NULL);
    19. return 0;
    20. }



    从结果可以看出这两个线程都使用了print函数,它们各自执行各自的,不会因为使用了同一个函数而受到影响。

    1.两个线程调用同一个函数

     两个线程中的函数的局部变量由于是保存在不同的线程中,因此不需要进行互斥处理(除非有非栈内存在捣乱,这种情况必须要有互斥锁)

    2.两个不同进程中的两个线程调用同一个处理函数

    同样,两个线程中的函数的局部变量由于是保存在不同的线程中,因此不需要进行互斥处理



    更多相关内容
  • java 多个线程同时写一个文件

    千次阅读 2019-10-11 17:00:17
    话不说,先直接上代码: 主方法: import java.util.concurrent.CountDownLatch; /** * @ProjectName: emp_customer * @Package: PACKAGE_NAME * @ClassName: Test * @Author: Administrator * @...

    话不多说,先直接上代码:

    主方法:

    import java.util.concurrent.CountDownLatch;
    
    /**
     * @ProjectName: emp_customer
     * @Package: PACKAGE_NAME
     * @ClassName: Test
     * @Author: Administrator
     * @Description: ${description}
     * @Date: 2019/10/11 14:10
     * @Version: 1.0
     */
    public class Test {
         public static void main(String args[]){
    
             //线程数
             int threadSize=4;
             //源文件地址
             String sourcePath = "E:\\1\\4.txt";
             //目标文件地址
             String destnationPath = "E:\\2\\4.txt";
             //
             CountDownLatch latch = new CountDownLatch(threadSize);
             MultiDownloadFileThread m = new MultiDownloadFileThread(threadSize, sourcePath, destnationPath, latch);
             long startTime = System.currentTimeMillis();
             try {
                 m.excute();
                 latch.await();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             long endTime = System.currentTimeMillis();
             System.out.println("全部下载结束,共耗时" + (endTime - startTime) / 1000 + "s");
         }
    
    }
    

     

    线程类:

    import java.io.*;
    import java.nio.channels.FileChannel;
    import java.nio.channels.FileLock;
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @ProjectName: emp_customer
     * @Package: PACKAGE_NAME
     * @ClassName: MultiDownloadFileThread
     * @Author: Administrator
     * @Description: ${description}
     * @Date: 2019/10/11 15:03
     * @Version: 1.0
     */
    public class MultiDownloadFileThread {
    
        private int threadCount;
        private String sourcePath;
        private String targetPath;
        private CountDownLatch latch;
    
        public MultiDownloadFileThread(int threadCount, String sourcePath, String targetPath, CountDownLatch latch) {
            this.threadCount = threadCount;
            this.sourcePath = sourcePath;
            this.targetPath = targetPath;
            this.latch = latch;
        }
    
        public void excute() {
            File file = new File(sourcePath);
            int fileLength = (int) file.length();
            //分割文件
            int blockSize = fileLength / threadCount;
            for (int i = 1; i <= threadCount; i++) {
                //第一个线程下载的开始位置
                int startIndex = (i - 1) * blockSize;
                int endIndex = startIndex + blockSize - 1;
                if (i == threadCount) {
                    //最后一个线程下载的长度稍微长一点
                    endIndex = fileLength;
                }
                System.out.println("线程" + i + "下载:" + startIndex + "字节~" + endIndex + "字节");
                new DownLoadThread(i, startIndex, endIndex).start();
            }
        }
    
    
        public class DownLoadThread extends Thread {
            private int i;
            private int startIndex;
            private int endIndex;
    
            public DownLoadThread(int i, int startIndex, int endIndex) {
                this.i = i;
                this.startIndex = startIndex;
                this.endIndex = endIndex;
            }
    
            @Override
            public void run() {
                File file = new File(sourcePath);
                FileInputStream in = null;
                RandomAccessFile raFile = null;
                FileChannel fcin = null;
                FileLock flin = null;
                try {
                    in = new FileInputStream(file);
                    in.skip(startIndex);
                    //给要写的文件加锁
                    raFile = new RandomAccessFile(targetPath, "rwd");
                    fcin =raFile.getChannel();
                    while(true){
                        try {
                            flin = fcin.tryLock();
                            break;
                        } catch (Exception e) {
                            System.out.println("有其他线程正在操作该文件,当前线程休眠1000毫秒,当前进入的线程为:"+i);
                            sleep(1000);
                        }
                    }
                    //随机写文件的时候从哪个位置开始写
                    raFile.seek(startIndex);
                    int len = 0;
                    byte[] arr = new byte[1024];
                    //获取文件片段长度
                    int segLength = endIndex - startIndex + 1;
                    while ((len = in.read(arr)) != -1) {
                        if (segLength > len) {
                            segLength = segLength - len;
                            raFile.write(arr, 0, len);
                        } else {
                            raFile.write(arr, 0, segLength);
                            break;
                        }
                    }
                    System.out.println("线程" + i + "下载完毕");
                    //计数值减一
                    latch.countDown();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (in != null) {
                            in.close();
                        }
                        if (raFile != null) {
                            raFile.close();
                        }
    
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    运行结果:

     

    涉及到的相关知识点:

    1.CountDownLatch 

    2.RandomAccessFile

    3.FileLock

    下面我们具体讲解下

    一、FileLock :文件锁

    FileLock是java 1.4 版本后出现的一个类,它可以通过对一个可写文件(w)加锁,保证同时只有一个进程可以拿到文件的锁,这个进程从而可以对文件做访问;而其它拿不到锁的进程要么选择被挂起等待,要么选择去做一些其它的事情, 这样的机制保证了众进程可以顺序访问该文件。

    1. 概念

    • 共享锁: 共享读操作,但只能一个写(读可以同时,但写不能)。共享锁防止其他正在运行的程序获得重复的独占锁,但是允许他们获得重复的共享锁。
    • 独占锁: 只有一个读或一个写(读和写都不能同时)。独占锁防止其他程序获得任何类型的锁。

    2. lock()和tryLock()的区别:

    lock()阻塞的方法,锁定范围可以随着文件的增大而增加。无参lock()默认为独占锁;有参lock(0L, Long.MAX_VALUE, true)为共享锁。
    tryLock()非阻塞,当未获得锁时,返回null.
    3. FileLock的生命周期:在调用FileLock.release(),或者Channel.close(),或者JVM关闭

    4. FileLock是线程安全的
     

    二、RandomAccessFile

    java除了File类之外,还提供了专门处理文件的类,即RandomAccessFile(随机访问文件)类。该类是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。RandomAccessFile类支持“随机访问”方式,这里“随机”是指可以跳转到文件的任意位置处读写数据。在访问一个文件的时候,不必把文件从头读到尾,而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,这时使用RandomAccessFile类就是最佳选择。

    RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,但该类仅限于操作文件,不能访问其他的I/O设备,如网络、内存映像等。RandomAccessFile类的构造方法如下所示:

    RandomAccessFile(File file ,  String mode)
    //创建随机存储文件流,文件属性由参数File对象指定

    RandomAccessFile(String name ,  String mode)
    //创建随机存储文件流,文件名由参数name指定

    这两个构造方法均涉及到一个String类型的参数mode,它决定随机存储文件流的操作模式,其中mode值及对应的含义如下:

    “r”:以只读的方式打开,调用该对象的任何write(写)方法都会导致IOException异常
    “rw”:以读、写方式打开,支持文件的读取或写入。若文件不存在,则创建之。
    “rws”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。这里的“s”表示synchronous(同步)的意思
    “rwd”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。使用“rwd”模式仅要求将文件的内容更新到存储设备中,而使用“rws”模式除了更新文件的内容,还要更新文件的元数据(metadata),因此至少要求1次低级别的I/O操作

     

    三、CountDownLatch

    1.概念

    • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
    • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

    2.源码

    • countDownLatch类中只提供了一个构造器:
    //参数count为计数值
    public CountDownLatch(int count) {  };  
    
    • 类中有三个方法是最重要的:
    //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
    public void await() throws InterruptedException { };   
    //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
    //将count值减1
    public void countDown() { };  

    假如在我们的代码里面,我们把main方法里面的

    latch.await();

    注释掉

    如下所示:

    我们可以看到跟之前的输出结果相比,我们的主方法里面输出的:全部下载结束的输出信息,已经打印到我们执行文件下载的线程输出信息的前面了,说明主线程先执行完。这从而说明,await() 方法具有阻塞作用

     我们在把latch.await();放开,把文件下载线程里的latch.countDown();注释掉,

    如下:

    我们可以看到,主程序里的的输出;全部下载结束的输出信息,一直未输出,程序也一直未结束,由此可得,countDown() 方法具有唤醒阻塞线程的作用。

    那么如何让 CountdownLatch 尽早结束

    假如我们的程序执行到countDown()之前就抛出异常,这就可能导致一整情况,CountdownLatch 计数永远不会达到零并且 await() 永远不会终止。

    为了解决这个问题,我们在调用 await() 时添加一个超时参数。

     

    CountDownLatch总结:

        1、CountDownLatch end = new CountDownLatch(N); //构造对象时候 需要传入参数N

      2、end.await()  能够阻塞线程 直到调用N次end.countDown() 方法才释放线程,最好设置超时参数

      3、end.countDown() 可以在多个线程中调用  计算调用次数是所有线程调用次数的总和

     

    对于,本demo而言,加不加文件锁的意义不大,因为在进入线程写的时候,就已经告诉单个线程需要写的内容是哪一块到哪一块,不加锁,也会正常写入,切经本人测试无误,但若是对同一个文件,即要写,又要读话,就必须加锁,不然程序执行可能不完整,具体情况可以查看下面的这个博客:https://blog.csdn.net/gxy3509394/article/details/7435993

    展开全文
  • Java多线程 -

    千次阅读 2022-03-28 15:31:49
    指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Java中 volatile、synchronized 和 final 实现可见性。 原子性 如果一个操作是不可分割的,我们则称之为原子操作,也就是有原子性。比如i+...

    Java多线程 - 锁

    三性

    • 可见性

      指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Javavolatilesynchronizedfinal 实现可见性。

    • 原子性

      如果一个操作是不可分割的,我们则称之为原子操作,也就是有原子性。比如i++,就不是原子操作。在Javasynchronized和在lockunlock 中操作保证原子性

    • 有序性

      一系列操作是按照规定的顺序发生的。如果在本线程之内观察,所有的操作都是有序的,如果在其他线程观察,所有的操作都是无序的;前半句指“线程内表现为串行语义”后半句指“指令重排序”和“工作内存和主存同步延迟”。Java语言提供了volatilesynchronized 两个关键字来保证线程之间操作的有序性。volatile是因为其本身包含“禁止指令重排序”的语义,
      synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

    Volatile

    作用

    • 保证了可见性。
    • 防止指令重排序(有序性)。
    • 半个原子性: 对任意单个volatile变量的读/写具有原子性

    读-写时内存语义

    • 写的内存语义

    • 当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新会共享内存

    • 读的内存语义

      当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

    Volatile的内存屏障实现

    是否可以重排序第二个操作第二个操作第二个操作
    第一个操作普通读/写volatile读volatile写
    普通读/写YESYESNO
    volatile读NONONO
    volatile写YESNONO

    编译器生成字节码文件时会在指令序列中插入内存屏障来禁止特定类型的处理器排序。但是,在实际执行过程中,只要不改变volatile的内存语义,
    编译器可以根据实际情况省略部分不必要的内存屏障。

    放置的内存屏障

    在每个volatile写操作前面插入StoreStore屏障
    在每个volatile写操作后面插入StoreLoad屏障
    在每个volatile读操作后面插入LoadLoad屏障
    在每个volatile读操作后面插入LoadStore屏障

    什么是指令重排序

    执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序

    编译器优化的重排序

    在不改变单线程语义的情况下重新安排语句的执行顺序。比如,在第10行创建了临时变量,在使用前才进行初始化。或者,下面的情况。

    int a = 3;
    a = 5;
    
    int a = 5
    

    指令级并行重排序

    处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性,为了使 CPU 内部的运算单元能够尽量被充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。

    现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取址、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。指令流水线并不是串行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。

    比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。

    a++; 
    b=f(a);  //会阻塞直到a++有结果
    c--; //没有因果关系,所以,可以先计算。但是,这个先计算是发生在CPU执行时
    

    像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令进入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。比如将指令重排为:

    a++; c--; b=f(a);
    

    相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。

    内存系统的重排序

    因为使用了读写缓存区,使得看起来并不是顺序执行的。

    内存重排序实际上并不是真的相关操作被排序了,而是因为CPU引入高速缓存还没来得及刷新导致;

    寄存器是什么和高速缓存什么区别:

    CPU要取数据,处理数据,都要放到寄存器处理。一般寄存器不用太大,它只要存放指令一次操作的数据就够了。

    高速缓存是内存的部分拷贝,因为高速缓存速度快,把常用的数据放这里可以提高速度。

    每个CPU都有自己的缓存,为了提高共享变量的写操作,CPU把整个操作变成异步的了,如果写入操作还没来的及同步到其它CPU,就有可能发生其它CPU读取到的是旧的值,因此看起来这条指令还没执行一样。

    多CPU架构中,多个CPU高速缓冲区数据同步,依赖于缓存一致性协议

    缓存一致性协议

    在多处理器的情况下,每个处理器总是嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己对应的缓存对应的地址被修改,
    就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作的时候,会重新从系统中把数据督导处理器的缓存里。这个协议被称之为缓存一致性协议。

    CPU对于Volatile的支持

    在有volatile修饰的共享变量进行写操作的时候会多出一条带有lock前缀的汇编代码,而这个lock操作会做两件事:

    • 将当前处理器的缓存行的数据同步到系统内存。lock信号确保存在该信号期间CPU可以独占共享内存。在之前通过锁总线的方式,现在采用锁缓存的方式。
    • 这个写回操作会使其他处理器的缓存中缓存了该地址的缓存行无效。在下一次这些CPU需要使用这些地址的值时,强制要求去共享内存中读取。

    总结

    volatile提供了一种轻量级同步机制来完成同步,它可以保操作的可见性、有序性以及对于单个volatile变量的读/写具有原子性,对于符合操作等非原子操作不具有原子性。

    volatile通过添加内存屏障(特殊的字节码)及缓存一致性协议(lock指令)来完成对可见性的保证。

    Synchronized

    三种应用方式

    • 修饰实例方法,作用于当前实例(this)加锁,进入同步代码前要获得当前实例的锁

    • 修饰静态方法,作用于当前类对象(XX.class)加锁,进入同步代码前要获得当前类对象的锁

    • 修饰代码块,指定加锁对象,对给定对象(object)加锁,进入同步代码库前要获得给定对象的锁。

    Synchronized 锁升级

    在Java SE1.6里锁一共有四种状态,无锁状态偏向锁状态轻量级锁状态重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

    升级过程

    1. 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
    2. 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
    3. 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

    其中,偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

    Synchronized底层原理

    Java对象结构

    Java对象结构

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。(相当于C语言的内存对其)
    • 对象头:jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成。

    Java对象头结构

    虚拟机位数头对象结构说明
    32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

    Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

    锁状态25bit4bit1bit是否是偏向锁2bit 锁标志位
    无锁状态对象HashCode对象分代年龄001

    由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
    Mark Word特殊结构

    加上最初的默认状态,一共有5种变化。

    重量级锁

    Monitor对象

    Synchronized为重量级锁时,指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其monitor之间的关系有存在多种实现方式。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。

    JVM: 即Java Virtual Machine,JAVA编译后生成的JAVA字节码是在JVM上跑,须要由JVM把字节码翻译成机器指令。主流的JVM包括Hotspot、Jikes RVM等。JVM运行的码文件是.jar

    DVM:即Dalvik Virtual Machine,是安卓中使用的虚拟机。每一个进程相应着一个Dalvik虚拟机实例。Dalvik虚拟机运行的是Dalvik字节码。Dalvik使用即时编译(JIT)。因为JVM是Oracle公司(原SUN公司)的产品,Google担心版权的问题,既然Java是开源的,索性就研究了JVM,写出了DVM。Android执行的码文件是.dex

    ART:Android最新的虚拟机,使用AOT技术。

    JIT:JIT会在运行时分析应用程序的代码,识别哪些方法可以归类为热方法,这些方法会被JIT编译器编译成对应的汇编代码,然后存储到代码缓存中,以后调用这些方法时就不用解释执行了,可以直接使用代码缓存中已编译好的汇编代码。

    AOT:预编译。在运行之前,就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了。

    ObjectMonitor的主要数据结构如下:

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL; //指向持有ObjectMonitor对象的线程
        _WaitSet      = NULL; //保存ObjectWaiter对象列表.处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //保存ObjectWaiter对象列表.处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }
    

    每个等待锁的线程都会被封装成ObjectWaiter对象。

    当多个线程同时访问一段同步代码时:

    1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
    2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
    3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

    总结:monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

    Synchronized重量级锁底层原理

    对于Synchronized代码块,在字节码中使用monitorentermonitorexit实现。

    3: monitorenter  //进入同步代码
    //..........省略其他  
    15: monitorexit   //退出同步代码
    16: goto          24 //正常退出,继续执行
    //省略其他.......
    21: monitorexit //异常结束时被执行的释放monitor 的指令
    

    偏向锁

    HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁是为了在只有一个线程执行同步块时提高性能。

    优势

    当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

    流程
    1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
    2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)
    3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
    4. 如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着(因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁),如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
    5. 执行同步代码
    总结

    偏向锁只适用于一个线程竞争获得锁的情况,如果出现另一个线程竞争,立即升级为轻量级锁

    轻量级锁

    轻量级锁是为了在线程近乎交替执行同步块时提高性能。

    流程
    1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示。
      锁记录
    2. 拷贝对象头中的Mark Word复制到锁记录中。
    3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
    4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示。
      轻量级锁定状态内存图
    5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    Synchronized锁类型

    • synchronized 锁是非公平锁。

    • synchronized是可重入锁,所以不会自己把自己锁死

    Java锁

    CAS操作

    使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。

    CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。

    因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

    操作过程

    CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:

    • V 内存地址存放的实际值
    • O 预期的值(旧值)
    • N 更新的新值。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

    举例:

    1.在内存地址V当中,存储着值为10的变量。

    2.此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11。

    3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

    4.线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

    5.线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

    6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

    7.线程1进行交换,把地址V的值替换为B,也就是12.

    Java锁的种类

    公平锁/非公平锁

    • 公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。

    • 非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

    可重入锁/不可重入锁

    • 可重入锁:当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁

    Java的可重入锁有:reentrantLock(显式的可重入锁)、synchronized(隐式的可重入锁)

    • 不可重入锁:与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)。

    独享锁/共享锁

    • 独占锁:指该锁一次只能被一个线程所持有。

    对ReentrantLock和Synchronized而言都是独占锁

    • 共享锁:指该锁可以被多个线程锁持有。

    对ReentrantReadWriteLock其读锁是共享,其写锁是独占写的时候只能一个人写,但是读的时候,可以多个人同时读

    互斥锁/读写锁

    互斥锁

    在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

    如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。举个形象的例子:多个人抢一个马桶。

    读写锁

    读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

    读写锁有三种状态 :读加锁状态、写加锁状态和不加锁状态

    乐观锁/悲观锁

    乐观锁

    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐 观锁适用于多读的应用类型,这样可以提高吞吐量 。

    悲观锁

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁( 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程 )。

    分段锁

    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

    并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。

    容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。

    自旋锁

    自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

    自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间

    如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。

    展开全文
  • C++多线程并发(二)---线程同步之互斥

    万次阅读 多人点赞 2019-03-20 00:08:29
    一、何为线程同步 在前一篇文章《C++多线程并发编程...线程间通信:一个任务被分割为多个线程并发处理,多个线程可能都要处理某一共享内存的数据,多个线程对同一共享内存数据的访问需要准确有序。 如果像前一篇文...

    一、何为线程同步

    在前一篇文章《C++多线程并发(一)— 线程创建与管理》中解释多线程并发时说到两个比较重要的概念:

    • 多线程并发:在同一时间段内交替处理多个操作,线程切换时间片是很短的(一般为毫秒级),一个时间片多数时候来不及处理完对某一资源的访问;
    • 线程间通信:一个任务被分割为多个线程并发处理,多个线程可能都要处理某一共享内存的数据,多个线程对同一共享内存数据的访问需要准确有序。

    如果像前一篇文章中的示例,虽然创建了三个线程,但线程间不需要访问共同的内存分区数据,所以对线程间的执行顺序没有更多要求。但如果多个进程都需要访问相同的共享内存数据,如果都是读取数据还好,如果有读取有写入或者都要写入(数据并发访问或数据竞争),就需要使读写有序(同步化),否则可能会造成数据混乱,得不到我们预期的结果。下面再介绍两个用于理解线程同步的概念:

    • 同步:是指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
    • 互斥:是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

    多个线程对共享内存数据访问的竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。C++标准中对数据竞争的定义是:多个线程并发的去修改一个独立对象,数据竞争是未定义行为的起因。

    二、如何处理数据竞争

    从上面数据竞争形成的条件入手,数据竞争源于并发修改同一数据结构,那么最简单的处理数据竞争的方法就是对该数据结构采用某种保护机制,确保只有进行修改的线程才能看到数据被修改的中间状态,从其他访问线程的角度看,修改不是已经完成就是还未开始。C++标准库提供了很多类似的机制,最基本的就是互斥量,有一个< mutex >库文件专门支持对共享数据结构的互斥访问。

    2.1 lock与unlock保护共享资源

    Mutex全名mutual exclusion(互斥体),是个object对象,用来协助采取独占排他方式控制对资源的并发访问。这里的资源可能是个对象,或多个对象的组合。为了获得独占式的资源访问能力,相应的线程必须锁定(lock) mutex,这样可以防止其他线程也锁定mutex,直到第一个线程解锁(unlock) mutex。mutex类的主要操作函数见下表:
    mutex类操作函数
    从上表可以看出,mutex不仅提供了常规锁,还为常规锁可能造成的阻塞提供了尝试锁(带时间的锁需要带时间的互斥类timed_mutex支持,具体见下文)。下面先给出一段示例代码:

    // mutex1.cpp 		通过互斥体lock与unlock保护共享全局变量
    
    #include <chrono>
    #include <mutex>
    #include <thread>
    #include <iostream> 
    
    std::chrono::milliseconds interval(100);
     
    std::mutex mutex;
    int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
    int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
    
    //此线程只能修改 'job_shared'
    void job_1()
    {
        mutex.lock();
        std::this_thread::sleep_for(5 * interval);  //令‘job_1’持锁等待
        ++job_shared;
        std::cout << "job_1 shared (" << job_shared << ")\n";
        mutex.unlock();
    }
    
    // 此线程能修改'job_shared'和'job_exclusive'
    void job_2()
    {
        while (true) {    //无限循环,直到获得锁并修改'job_shared'
            if (mutex.try_lock()) {     //尝试获得锁成功则修改'job_shared'
                ++job_shared;
                std::cout << "job_2 shared (" << job_shared << ")\n";
                mutex.unlock();
                return;
            } else {      //尝试获得锁失败,接着修改'job_exclusive'
                ++job_exclusive;
                std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
                std::this_thread::sleep_for(interval);
            }
        }
    }
    
    int main() 
    {
        std::thread thread_1(job_1);
        std::thread thread_2(job_2);
     
        thread_1.join();
        thread_2.join();
    
        getchar();
        return 0;
    }
    

    互斥锁执行结果
    从上面的代码看,创建了两个线程和两个全局变量,其中一个全局变量job_exclusive是排他的,两线程并不共享,不会产生数据竞争,所以不需要锁保护。另一个全局变量job_shared是两线程共享的,会引起数据竞争,因此需要锁保护。线程thread_1持有互斥锁lock的时间较长,线程thread_2为免于空闲等待,使用了尝试锁try_lock,如果获得互斥锁则操作共享变量job_shared,未获得互斥锁则操作排他变量job_exclusive,提高多线程效率。

    2.2 lock_guard与unique_lock保护共享资源

    但lock与unlock必须成对合理配合使用,使用不当可能会造成资源被永远锁住,甚至出现死锁(两个线程在释放它们自己的lock之前彼此等待对方的lock)。是不是想起了C++另一对儿需要配合使用的对象new与delete,若使用不当可能会造成内存泄漏等严重问题,为此C++引入了智能指针shared_ptr与unique_ptr。智能指针借用了RAII技术(Resource Acquisition Is Initialization—使用类来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放)对普通指针进行封装,达到智能管理动态内存释放的效果。同样的,C++也针对lock与unlock引入了智能锁lock_guard与unique_lock,同样使用了RAII技术对普通锁进行封装,达到智能管理互斥锁资源释放的效果。lock_guard与unique_lock的区别如下:
    lock_guard操作函数
    unique_lock操作函数
    从上面两个支持的操作函数表对比来看,unique_lock功能丰富灵活得多。如果需要实现更复杂的锁策略可以用unique_lock,如果只需要基本的锁功能,优先使用更严格高效的lock_guard。两种锁的简单概述与策略对比见下表:

    类模板描述策略
    std::lock_guard严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权—使用std::adopt_lock策略),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁std::adopt_lock
    std::unique_lock更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁std::adopt_lock std::defer_lock std::try_to_lock

    如果将上面的普通锁lock/unlock替换为智能锁lock_guard,其中job_1函数代码修改如下:

    void job_1()
    {
        std::lock_guard<std::mutex> lockg(mutex);    //获取RAII智能锁,离开作用域会自动析构解锁
        std::this_thread::sleep_for(5 * interval);  //令‘job_1’持锁等待
        ++job_shared;
        std::cout << "job_1 shared (" << job_shared << ")\n";
    }
    

    如果也想将job_2的尝试锁try_lock也使用智能锁替代,由于lock_guard锁策略不支持尝试锁,只好使用unique_lock来替代,代码修改如下(其余代码和程序执行结果与上面相同):

    void job_2()
    {
        while (true) {    //无限循环,直到获得锁并修改'job_shared'
            std::unique_lock<std::mutex> ulock(mutex, std::try_to_lock);		//以尝试锁策略创建智能锁
            //尝试获得锁成功则修改'job_shared'
            if (ulock) {
                ++job_shared;
                std::cout << "job_2 shared (" << job_shared << ")\n";
                return;
            } else {      //尝试获得锁失败,接着修改'job_exclusive'
                ++job_exclusive;
                std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
                std::this_thread::sleep_for(interval);
            }
        }
    }
    

    2.3 timed_mutex与recursive_mutex提供更强大的锁

    前面介绍的互斥量mutex提供了普通锁lock/unlock和智能锁lock_guard/unique_lock,基本能满足我们大多数对共享数据资源的保护需求。但在某些特殊情况下,我们需要更复杂的功能,比如某个线程中函数的嵌套调用可能带来对某共享资源的嵌套锁定需求,mutex在一个线程中却只能锁定一次;再比如我们想获得一个锁,但不想一直阻塞,只想等待特定长度的时间,mutex也没提供可设定时间的锁。针对这些特殊需求,< mutex >库也提供了下面几种功能更丰富的互斥类,它们间的区别见下表:

    类模板描述
    std::mutex同一时间只可被一个线程锁定。如果它被锁住,任何其他lock()都会阻塞(block),直到这个mutex再次可用,且try_lock()会失败。
    std::recursive_mutex允许在同一时间多次被同一线程获得其lock。其典型应用是:函数捕获一个lock并调用另一函数而后者再次捕获相同的lock。
    std::timed_mutex额外允许你传递一个时间段或时间点,用来定义多长时间内它可以尝试捕获一个lock。为此它提供了try_lock_for(duration)和try_lock_until(timepoint)。
    std::recursive_timed_mutex允许同一线程多次取得其lock,且可指定期限。

    不同互斥类所支持的互斥锁类型总结如下表:
    互斥类与互斥锁
    继续用前面的例子,将mutex替换为timed_mutex,将job_2的尝试锁tyr_lock()替换为带时间的尝试锁try_lock_for(duration)。由于改变了尝试锁的时间,所以在真正获得锁之前的尝试次数也有变化,该变化体现在尝试锁失败后对排他变量job_exclusive的最终修改结果或修改次数上。更新后的代码如下所示:

    #include <chrono>
    #include <mutex>
    #include <thread>
    #include <iostream> 
    
    std::chrono::milliseconds interval(100);
     
    std::timed_mutex tmutex;
    
    int job_shared = 0; //两个线程都能修改'job_shared',mutex将保护此变量
    int job_exclusive = 0; //只有一个线程能修改'job_exclusive',不需要保护
    
    //此线程只能修改 'job_shared'
    void job_1()
    {
        std::lock_guard<std::timed_mutex> lockg(tmutex);    //获取RAII智能锁,离开作用域会自动析构解锁
        std::this_thread::sleep_for(5 * interval);  //令‘job_1’持锁等待
        ++job_shared;
        std::cout << "job_1 shared (" << job_shared << ")\n";
    }
    
    // 此线程能修改'job_shared'和'job_exclusive'
    void job_2()
    {
        while (true) {    //无限循环,直到获得锁并修改'job_shared'
            std::unique_lock<std::timed_mutex> ulock(tmutex,std::defer_lock);   //创建一个智能锁但先不锁定
            //尝试获得锁成功则修改'job_shared'
            if (ulock.try_lock_for(3 * interval)) {     //在3个interval时间段内尝试获得锁
                ++job_shared;
                std::cout << "job_2 shared (" << job_shared << ")\n";
                return;
            } else {      //尝试获得锁失败,接着修改'job_exclusive'
                ++job_exclusive;
                std::cout << "job_2 exclusive (" << job_exclusive << ")\n";
                std::this_thread::sleep_for(interval);
            }
        }
    }
    
    int main() 
    {
        std::thread thread_1(job_1);
        std::thread thread_2(job_2);
     
        thread_1.join();
        thread_2.join();
    
        getchar();
        return 0;
    }
    

    带时间尝试锁结果

    2.4 前章答疑

    前一篇文章中thread1.cpp程序运行结果可能会出现某行与其他行交叠错乱的情况,主要是由于不止一个线程并发访问了std::cout显示终端资源导致的,解决方案就是对cout << “somethings” << endl语句加锁,保证多个线程对cout资源的访问同步。为了尽可能降低互斥锁对性能的影响,应使用微粒锁,即只对cout资源访问语句进行加锁保护,cout资源访问完毕尽快解锁以供其他线程访问该资源。添加互斥锁保护后的代码如下:

    //thread2.cpp  增加对cout显示终端资源并发访问的互斥锁保护
    
    #include <iostream>
    #include <thread>
    #include <chrono>
    #include <mutex>
    
    using namespace std;
    
    std::mutex mutex1;
     
    void thread_function(int n)
    {
        std::thread::id this_id = std::this_thread::get_id();       //获取线程ID
    
        for(int i = 0; i < 5; i++){
            mutex1.lock();   
            cout << "Child function thread " << this_id<< " running : " << i+1 << endl;
            mutex1.unlock();
            std::this_thread::sleep_for(std::chrono::seconds(n));   //进程睡眠n秒
        }
    }
    
    class Thread_functor
    {
    public:
        // functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
        void operator()(int n)
        {
            std::thread::id this_id = std::this_thread::get_id();
    
            for(int i = 0; i < 5; i++){
                {
                    std::lock_guard<std::mutex> lockg(mutex1);
                    cout << "Child functor thread " << this_id << " running: " << i+1 << endl;
                }
                std::this_thread::sleep_for(std::chrono::seconds(n));   //进程睡眠n秒
            }
        }	
    };
    
     
    int main()
    {
        thread mythread1(thread_function, 1);      // 传递初始函数作为线程的参数
        if(mythread1.joinable())                  //判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
            mythread1.join();                     // 使用join()函数阻塞主线程直至子线程执行完毕
        
        Thread_functor thread_functor;
        thread mythread2(thread_functor, 3);     // 传递初始函数作为线程的参数
        if(mythread2.joinable())
            mythread2.detach();                  // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
    
        auto thread_lambda = [](int n){
            std::thread::id this_id = std::this_thread::get_id();
            for(int i = 0; i < 5; i++)
            {
                mutex1.lock();
                cout << "Child lambda thread " << this_id << " running: " << i+1 << endl;
                mutex1.unlock();
                std::this_thread::sleep_for(std::chrono::seconds(n));   //进程睡眠n秒
            }   
        };
    
        thread mythread3(thread_lambda, 4);     // 传递初始函数作为线程的参数
        if(mythread3.joinable())
            mythread3.join();                     // 使用join()函数阻塞主线程直至子线程执行完毕
    
        unsigned int n = std::thread::hardware_concurrency();       //获取可用的硬件并发核心数
        mutex1.lock();
        std::cout << n << " concurrent threads are supported." << endl;
        mutex1.unlock();
        std::thread::id this_id = std::this_thread::get_id();
        
        for(int i = 0; i < 5; i++){
            {
                std::lock_guard<std::mutex> lockg(mutex1);
                cout << "Main thread " << this_id << " running: " << i+1 << endl;
            }
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    
        getchar();
        return 0;
    }
    

    我们在使用mutex进行排他性的共享数据访问时,一般都会期望加锁不要阻塞,总是能立刻拿到锁,然后尽快访问数据,用完之后尽快解锁,这样才能不影响并发性和性能。但如果要等待某个条件的成立,在等待期间就不得不阻塞线程,常用的判断某条件是否成立的方法是不断轮询该条件是否成立,但如果轮询周期太短则太浪费CPU资源,如果轮询周期太长又可能会导致延误。有没有什么办法解决这个难题呢?可以参考下一篇文章《C++多线程并发(三)—线程同步之条件变量》

    更多文章:

    展开全文
  • 看到这个问题第一反应是不懂,然后查询了网上的一些说法,感觉略有不一。...然后,多个线程访问这个类的两个方法也有不同的形式,例如访问这个类的两个方法是通过一个类的实例对象来访问还是通过不同...
  • MFC多线程互斥使用

    千次阅读 2019-10-04 23:14:12
    第二部分实现两个线程互斥使用。 演示系统为Win10,平台为VS2017(MFC),主要使用类为CWinThread。 第二部分: 1.在原有基础上(见上一篇文章MFC线程的开启、暂停、继续和注销)添加一个EDIT控件和一个线程...
  • JUC多线程:synchronized机制原理 与 Lock机制

    万次阅读 多人点赞 2021-08-26 08:53:31
    synchronized 通过当前线程持有对象,从而拥有访问权限,而其他没有持有当前对象的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。synchronized 机制...
  • 1、线程锁的介绍 1.1 创建互斥:  (1) 静态互斥初始化:pthread_mutex_t mutex_x= PTHREAD_MUTEX_INITIALIZER;  此句创建后,可以直接使用 pthread_mutex_lock(&amp;mutex_x)和pthread_mutex_...
  • Java多线程安全问题和

    千次阅读 2020-02-28 17:32:26
    多个线程同时操作一个数据是,可能会出现数据不一样的情况,这就是线程安全问题。 线程安全机制用于保证多个线程访问数据时的一致性. 线程安全问题体现在三个方面: 1、原子性 2、可见性 3、有序性 原子操作是不...
  • 使用多线程及互斥样例:#include &lt;iostream&gt; #include &lt;windows.h&gt; using namespace std; HANDLE hMutex = NULL;//互斥量 //线程函数 DWORD WINAPI Fun(LPVOID lpParamter) { for ...
  • Java多线程的理解与使用

    万次阅读 多人点赞 2017-10-14 18:45:51
    作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等 )
  • 多线程之间如何实现同步?

    万次阅读 2019-07-01 11:56:53
    多个线程同时共享一个全局变量或静态变量做写的操作时候,可能会发生数据冲突问题,也就是线程安全问题,在读的操作不会发生数据冲突问题 下面看个简单的买票例子 案例:需求现在有100张火车票,有两个窗口同时抢...
  • 多线程中的常见分类

    千次阅读 2019-08-03 09:09:46
    对Java中的一些常见分类进行学习整理
  • 并发操作之——java多线程常用的

    千次阅读 2021-09-07 19:29:21
    也叫S/读,能查看但无法修改和删除的一种数据,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该可被多个线程所持有,用于资源数据共享 二、互斥 也叫X/排它/写/独占/独享...
  • Qt多线程基础(一)线程同步之互斥同步

    万次阅读 多人点赞 2016-11-04 13:45:08
    互斥同步方法:void run()函数中使用QMutex来实现同步,当多个线程访问共享变量时,应使用lock/trylock和unlock将对共享变量的操作代码包裹,以保证同步访问共享变量。(C++中引起线程安全的共享资源只有
  • 都需要访问/使用同一种资源; 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。 【同步】:   是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后...
  • ReadWriteLockLock一样也是一个接口,提供了readLock和writeLock两种的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。 使用场景 假设你的程序中涉及到对一些共享...
  • Java多线程的同步优化的6种方案

    万次阅读 2020-06-24 17:44:34
    Java中可以使用锁来解决多线程的同步问题,保障了数据的一致性,但也会代理很问题,本章总结了多线程同步的几种优化方案:包括读写、写时复制机制、细化等方案。
  • 当项目有中多个线程,如何查找死锁?

    万次阅读 多人点赞 2021-04-22 19:08:31
    二、使用步骤1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础...
  • Qt多线程使用互斥

    万次阅读 多人点赞 2018-09-04 22:08:06
    这样每次只有一个线程可以访问它(这类似于Java synchronized关键字)。 通常最好将互斥对象与QMutexLocker一起使用,因为这样可以很容易地确保一致地执行锁定和解锁。 2、官方示例: QMutex mutex; ...
  • 多线程锁的升级原理是什么?

    万次阅读 多人点赞 2019-05-20 11:04:06
    多线程锁的升级原理是什么? 的级别从低到高: 无锁 -> 偏向 -> 轻量级 -> 重量级 分级别原因: 没有优化以前,sychronized是重量级(悲观),使用 wait 和 notify、notifyAll 来切换...
  • Window多线程同步之(互斥)

    千次阅读 2017-12-22 14:41:45
    简述 互斥是用在多线程间对操作同一资源进行互斥的。一个线程占用了一个资源,那么别的线程就操作此资源,直到这个线程该释放互斥...比如多个线程对一个全局变量进行累加并打印。 源码 //共享资源 static int num
  • C# 多线程传递参数或多个参数

    万次阅读 2018-09-21 08:47:55
    using System; using System.IO; using System.Text; using System.Threading; ...namespace ConsoleApp7 ... Console.WriteLine("线程ID {0} {1} {2} {3}", threadId, id, money, name); } } } }
  • SpringBoot安全线程锁工具类

    万次阅读 2021-09-01 15:36:36
    这几日对接物联网项目,前端请求数据,后端接口发起TCP请求,由另一个线程来接收数据,这时候需要阻塞前端发起的请求,直到TCP接收数据完毕,再返回数据给前端。特此写了一个工具类 import java.util.concurrent....
  • 多线程并发之CountDownLatch(闭锁)使用详解

    万次阅读 多人点赞 2018-09-16 11:24:52
    是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行: 确保...
  • C#多线程锁

    千次阅读 2017-04-18 19:33:43
    c#多线程操作队列
  • Java使用循环创建多个线程

    千次阅读 2019-05-19 23:31:13
    使用start()方法启动线程,则立即开始创建下一个线程。 测试代码及结果如下: 情景:循环创建一类线程,这类线程的run()方法不能立即结束,如包含循环等。 问题:创建线程后,启动线程时,使用run(...
  • 多线程的实现和使用场景

    万次阅读 多人点赞 2021-06-09 22:06:20
    多线程的实现和使用场景一、多线程实现方式1.1 Thread实现1.2 Runnable实现二、多线程使用场景1.多线程使用场景1.1 多线程应该最多的场景:1.2多线程的常见应用场景:2.多线程小案列2.1 多线程计算2.2 多线程实现...
  • 谈谈多线程下为何需要

    万次阅读 2020-08-20 20:11:36
    多个线程运行的时候,共享了一块资源,在访问这块资源的时候就称为临界资源。为了解决这个问题,我们可以为这块资源加上一把,只允许一个线程访问这块资源。 那今天就讲讲互斥,自旋。 首先先看看不加锁的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,413,898
精华内容 965,559
关键字:

多个线程使用同多个锁