精华内容
下载资源
问答
  • 多线程中集合被共享吗
    千次阅读
    2021-05-20 19:17:33

    一组并发线程运行在一个进程的上下文中,每个线程都有它自己独立的线程上下文,例如:栈、程序计数器、线程ID、条件码等,每个线程和其它的线程一起共享除此之外的进程上下文的剩余部分,包括整个用户的虚拟地址空间,当然也共享同样的打开的文件的集合。,这里有一点要特别注意,就是寄存器是从不共享的,而虚拟存储器总是共享的。

    有了共享就要防止在对共享变量进行操作的过程中得到一个不可知的值,在Linux内核中有个原子类型与原子操作这么个概念,因为用户态下没有这么一个原子操作存在,那么在我们用户态下就需要要对操作这个共享变量的线程进行同步。为什么要进行同步呢?

    因为假设我们在一个程序中有一个全局变量cnt,初始值为0,接下去我们创建了两个线程,完成的功能都是在一个循环中对这个变量进行+1操作,想象一下在这两个线程操作完成后会出现什么状况。

    点击(此处)折叠或打开

    void *thread(void* value)

    {

    int max = *((int*)value)

    for(int i=0;i

    {

    cnt++;

    }

    return NULL;

    }假设我们这里的max为10000,那么我们想要得到的结果的结果当然是20000,可是在执行之后结果并不是我们所期望的20000,而是一个小于20000的值。为什么会出现这个现象呢?

    这里就是我们为什么需要对线程进行同步了。

    因为在C语言的层面来说,cnt++就是一条语句,实际上我们在心里默认把它当作了一个原子操作,事实上,就这么一条操作语句,在汇编代码中是分三步执行的:

    1)、将这个值从内存中取出来,放入一个寄存器中;

    2)、将寄存器中的值进行+1操作;

    3)、将寄存器中的值放入内存中去。

    因为对与多线程来说我们不知道何时会执行哪个线程,所以执行的顺序是不可知的。我们所想的是先让一边执行完,然后再开始执行另外一边。

    现在我们不妨将这个问题极端化,也就是两线程交叉执行,假设左边的执行线程为A,右边为B,假设A先执行,A从内存中取出cnt的值,那么现在在R1里的值为0,接下去,A线程被B线程打断,A停止执行,B开始执行,B又从内存中取出cnt的值,现在在R2中的值也为0。然后又轮到A执行,进行加1操作,则R1为1,接下去轮到B执行,进行加1操作,则R2为1。然后A将值写回到内存中,B也将值写回到内存中。这次我们知道内存中的值为1而并非我们所期望的2。

    那么怎么能让它进行正确的执行顺序呢?同步,可以用加锁来完成同步操作。

    点击(此处)折叠或打开

    for(int i=0;i

    {

    P(&mutex);

    cnt++;

    V(&mutex);

    }在对cnt加1的操作时,对这个操作加锁,这就意味着当下只有这一个线程执行这个操作,其它的线程都得等在外面,等这个线程解锁出来,其他的线程才可以有机会进去。

    加锁之后我们再来看看上面的那张图的执行过程,也假设是在一个极端的情况:

    A先加锁,然后完成那三个步骤(因为此时只有它一个线程有操作的权限),解锁;现在内存中的值为1,A加锁,然后一样完成三个步骤,解锁;现在内存中的值为2。与所期望的相同。当然了,对于加锁的问题还要防止出现死锁现象,这里就不讨论了。

    更多相关内容
  • 多线程概念:线程的独有和共享

    千次阅读 2021-10-12 13:12:53
    特点是:这个进程的所有pcb共用一块虚拟地址空间,共用进程的大部分资源(程序运行时,操作系统为程序运行所分配的所有资源),所以这些pcb线程称为轻量级进程。 而在Linux下,这些执行流又是通过pcb来实现的...

    线程概念:

    理解:
    在Linux内核中,实质上是没有线程这个概念的,也没有提供创建线程的系统调用接口;(线程创建等接口是在库函数中完成的,因为早期Linux根本不认为有线程这个东西)
    当我们需要完成多个功能时,如果单单按照进程的概念,一个进程就执行一个功能,就需要多进程来完成。而由于这些功能有可能在内存中的位置是相同的,我们能不能通过一些方式来节省资源呢?
    Linux通过多个pcb共享一个虚拟地址空间来实现完成不同的功能,而这些pcb相对于进程pcb而言是是相对轻松的,因此被称为轻量级进程。
    在广泛意义上讲(各种操作系统),又有一个概念是:线程是进程的一条执行流,完成进程的各种功能。而Linux对这点,已经用轻量级进程来实现了,为了统一概念,干脆就说Linux下轻量级进程就是线程。
    总结:
    一个pcb映射一个虚拟地址空间,这就叫有一个轻量级进程的进程。
    多个pcb映射一个虚拟地址空间,这就叫有多个轻量级进程的进程。
    在这里插入图片描述
    总结:
    进程和线程:
    进程是一个pcb,Linux下是一个task_struct结构体实现的,对一个运行的程序进行动态的描述,通过这个描述实现对程序运行的管理。
    线程是进程中的一条执行流,但是Linux下执行流通过pcb实现,一个进程中可以有多个pcb,这些pcb共享进程中的大部分资源,相较于传统的pcb较为轻量化,因此也称轻量级进程。
    进程是操作系统资源分配的基本单位;
    线程是CPU调度的基本单元

    线程之间的独有和共享

    独有:标识符,寄存器,栈,信号屏蔽字,errno,优先级
    共享:虚拟地址空间(代码段和数据段),文件描述符表,信号的处理回调函数,用户ID/组ID/工作路径;
    

    虚拟地址空间共享:
    创建线程是创建一个pcb,然后通过指针指向同一块虚拟地址空间,同一个页表,因此映射的也是同一块物理内存,数据段代码段堆栈都是同一块。这意味着线程之间的通信是非常灵活的,只需要知道变量的地址就可以进行通信。
    tips:线程间的通信可以通过进程通信的方式,除此之外全局变量、函数传参也可以进行通信。
    信号屏蔽字为什么独有:
    为什么信号是先注销在处理?
    信号是针对整个通知事件进行处理的,但一个信号只应被处理一次就好了。然而一个进程有多个执行流,到底谁来处理这个信号呢?
    答案是:谁拿到时间片,谁能就处理谁就处理。
    若不先注销信号,当第一个线程将信号处理完毕,切换时间片到另一个线程,这个线程还能检测到有信号未处理,就要去处理。这样就造成一个信号被处理多次。
    有的线程不希望操作被信号打断,就可以独立屏蔽这个信号。所以信号屏蔽字是独有的。
    寄存器独有:
    线程在处理数据时,会将cpu上寄存器的数据(上下文数据,程序计数器等)拷贝过来,每个线程在切换完时间片后再次运行时就会知道上次运行的位置。方便继续运行。
    栈独有:
    在使用vfork()创建子进程时,父子进程共用同一个虚拟地址空间,但是需要阻塞父进程,直到子进程exit退出或者程序替换才可以。
    否则就会造成共用一块栈区,在函数压栈是,如果父进程和子进程单独调用函数,就不知道哪一个函数先出栈了,这样就是栈混乱。
    多线程调用也是一样,因为线程之间是共用同一快虚拟地址空间的,也会遇到栈混乱的情况,唯一能解决这个问题的方法就是调用栈独有。
    但是这种栈独有也是相对的(因为虚拟地址空间是共享的),当遇到一些特殊情况时,传入栈的地址也可以让线程之间使用用一个栈。

    errno独有:
    errno是一个全局变量,保存系统调用使用完成后的错误编号,多个线程同时执行,都会使用系统调用;都会修改errno,若error共用,就会存在二义问题。不知道哪个errno是哪个进程的。

    多线程/多进程进行多任务处理的优缺点分析:
    多线程的优点:
    1.线程间通信灵活方便,(除了进程间通信方式之外还有全局变量以及函数传参,共用一块虚拟地址空间,只要知道地址就可以访问同一块物理空间)
    2.线程的创建和销毁成本更低(创建线程创建pcb只要使用一个指针就可以指向同一处就好了)
    3.同一个进程中的线程间调度成本更低(因为使用的同一个页表,只需要切换页表信息,而进程调度需要切换整个页表)
    多进程的优点:
    多进程的鲁棒性、稳定性更高(异常(比如内存访问错误、逻辑错误)以及一些系统调用exit直接针对整个进程生效,多进程有一个进程退出,不会影响其他进程。而如果是在多线程中,进程退出,线程都得退出。出现问题就很容易造成整个程序崩溃)
    应用场景:对于主程序安全性要求比较高的场景,比如shell场景服务器。
    应用场景:
    1.IO密集型程序:多任务并行处理,(单磁盘可以并行压缩IO等待事件/多磁盘可以实现同时处理)
    IO密集型:程序中大量进程IO操作,对CPU要求不高因此执行流个数没有太大要求,使用多线程很好。

    2.CPU密集型程序:程序中进行大量的数据运算处理,cpu资源足够,就可以同时处理,提高效率,(通常执行流的个数是cpu核心数+1),创建线程很多的话,而cpu资源不够,就会造成大量的线程切换,调度成本提高。使用多进程比较好。

    展开全文
  • java多线程--集合并发

    千次阅读 2021-02-08 10:56:11
    但是这个代码在多线程情况下就会出 java.lang.ArrayIndexOutOfBoundsException 1.2 为什么呢? 如下测试代码 @Test public void testSynchronizedList() throws Exception { long fisrtTime = System

    1.问题引入:

    1.1

    常见的list获取最后一个对象。

    Object haha = list.get(list.size() - 1);
    

    大致看起来没有问题。但是这个代码在多线程情况下就会出
    java.lang.ArrayIndexOutOfBoundsException

    1.2 为什么呢?

    如下测试代码

    
        @Test
        public void testSynchronizedList() throws Exception {
            long fisrtTime = System.currentTimeMillis();
    
            List<Integer> mlist = Collections.synchronizedList(new ArrayList<>());
            mlist.add(1);
            mlist.add(3);
            mlist.add(2);
            mlist.add(1);
            for (int i = 0; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 100000; j++) {
                          list.add(new Random().nextInt(100));
                            synchronized (list){
                                int index1 = list.size() - 1;
                                int index2 = list.size() - 1;
                                int index3 = list.size() - 1;
                                int index4 = list.size() - 1;
                                int index5 = list.size() - 1;
                                if (index1 != index2) {
                                    System.out.println("testCopyOnWriteArrayList index1=" + index1
                                            + "  index2=" + index2
                                            + "  index3=" + index3
                                            + "  index4=" + index4
                                            + "  index5=" + index5
                                    );
                                }
    
                            }
                            list.remove(0);
                            }
                        }
                        long endTime = System.currentTimeMillis();
                        System.out.println("useSynchronized all time = " + (endTime - fisrtTime));
                    }
                }).start();
            }
            Thread.sleep(5000);
            System.out.println("size = " + mlist.size());
        }
    

    打印log:其中发现 int index1 = mlist.size() - 1;每次获取的都不一样,因为其他线程在操作数组。

    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=4  index2=5  index3=5  index4=5  index5=5
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    testCopyOnWriteArrayList index1=5  index2=4  index3=4  index4=4  index5=4
    useCopyWrit all time = 3239
    useCopyWrit all time = 3248
    size = 4
    

    1.3 怎么获取集合中最后一个元素?

    1. 前提排除list,array形式因为这两种都是 list.get(list.size() - 1)形式
    2. 剩余方案1: deque的getLast();
    3. 剩余方案2: set.last();

    2. 多线程集合整理

    1. 第一类:java.util.concurrent 包下并发类

    	1. ConcurrentHashMap:并发版HashMap
    	2. CopyOnWriteArrayList:并发版ArrayList
    	3. CopyOnWriteArraySet:并发Set
    	4. ConcurrentLinkedQueue:并发队列(基于链表)
    	5. ConcurrentLinkedDeque:并发队列(基于双向链表)
    	6. ConcurrentSkipListMap:基于跳表的并发Map
    	7. ConcurrentSkipListSet:基于跳表的并发Set
    	8. ArrayBlockingQueue:阻塞队列(基于数组)
    	9. LinkedBlockingQueue:阻塞队列(基于链表)
    	10. LinkedBlockingDeque:阻塞队列(基于双向链表)
    	11. PriorityBlockingQueue:线程安全的优先队列
    	12. SynchronousQueue:读写成对的队列
    	13. LinkedTransferQueue:基于链表的数据交换队列
    	14. DelayQueue:延时队列
    

    并发容器

    • 针对于同步容器的巨大缺陷。java.util.concurrent中提供了并发容器。并发容器包注重以下特性:

      1. 根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
      2. 并发容器定义了一些线程安全的复合操作。
      3. 并发容器在迭代时,可以不封闭在synchronized中。但是未必每次看到的都是"最新的、当前的"数据。如果说将迭代操作包装在synchronized中,可以达到"串行"的并发安全性,那么并发容器的迭代达到了"脏读"。
    • CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象。

    • ConcurrentLinkedQuerue是Query实现,是一个先进先出的队列。一般的Queue实现中操作不会阻塞,如果队列为空,那么取元素的操作将返回空。Queue一般用LinkedList实现的,因为去掉了List的随机访问需求,因此并发性更好。

    • BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,那么获取操作将阻塞,直到队列中有一个可用的元素。如果队列已满,那么插入操作就阻塞,直到队列中出现可用的空间。

    2. 第二类:java.util.Collections 创建的

     Collection c = Collections.synchronizedCollection(new ArrayList());
     List list = Collections.synchronizedList(new ArrayList());    // 包装List
     Set set = Collections.synchronizedSet(new HashSet());         // 包装Set
     Map map = Collections.synchronizedMap(new HashMap());         // 包装Map
    

    同步容器类

    • JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装。对每个共有方法都进行同步

      同步容器的问题
      同步容器类在单个方法被使用时可以保证线程安全。复合操作则需要额外的客户端加锁来保护。

      迭代器与ConcurrentModificationException
      使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。想要避免出现ConcurrentModificationException,就必须在迭代过程持有容器的锁。但是若容器较大,则迭代的时间也会较长。那么需要访问该容器的其他线程将会长时间等待。从而会极大降低性能。

    List<Integer> lists = new ArrayList<>();
    lists.add(1);
    lists.add(2);
    synchronized(lists){
        for(Integer i : lists){
            System.out.println(i);
        }
    }
    
    若不希望在迭代期间对容器加锁,可以使用"克隆"容器的方式。使用线程封闭,由于其他线程不会对容器进行修改,可以避免ConcurrentModificationException。但是在创建副本的时候,存在较大性能开销。
    
    **隐式迭代**
    toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate,也即可能抛出ConcurrentModificationException。
    

    3. 详细并发容器:

    1. ConcurrentHashMap 并发版HashMap
      最常见的并发容器之一,可以用作并发场景下的缓存。底层依然是哈希表,但在JAVA 8中有了不小的改变,而JAVA 7和JAVA 8都是用的比较多的版本,因此经常会将这两个版本的实现方式做一些比较(比如面试中)。

      一个比较大的差异就是,JAVA 7中采用分段锁来减少锁的竞争,JAVA 8中放弃了分段锁,采用CAS(一种乐观锁),同时为了防止哈希冲突严重时退化成链表(冲突时会在该位置生成一个链表,哈希值相同的对象就链在一起),会在链表长度达到阈值(8)后转换成红黑树(比起链表,树的查询效率更稳定)。

    2. CopyOnWriteArrayList 并发版ArrayList
      并发版ArrayList,底层结构也是数组,和ArrayList不同之处在于:当新增和删除元素时会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。

      适用场景:由于读操作不加锁,写(增、删、改)操作加锁,因此适用于读多写少的场景。

      局限:由于读的时候不会加锁(读的效率高,就和普通ArrayList一样),读取的当前副本,因此可能读取到脏数据。如果介意,建议不用。

    看看源码感受下:

    
    public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
        final transient ReentrantLock lock = new ReentrantLock();
        private transient volatile Object[] array;
        // 添加元素,有锁
        public boolean add(E e) {
            final ReentrantLock lock = this.lock;
            lock.lock(); // 修改时加锁,保证并发安全
            try {
                Object[] elements = getArray(); // 当前数组
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len + 1); // 创建一个新数组,比老的大一个空间
                newElements[len] = e; // 要添加的元素放进新数组
                setArray(newElements); // 用新数组替换原来的数组
                return true;
            } finally {
                lock.unlock(); // 解锁
            }
        }
        // 读元素,不加锁,因此可能读取到旧数据
        public E get(int index) {
            return get(getArray(), index);
        }
    }
    
    1. CopyOnWriteArraySet 并发Set
      基于CopyOnWriteArrayList实现(内含一个CopyOnWriteArrayList成员变量),也就是说底层是一个数组,意味着每次add都要遍历整个集合才能知道是否存在,不存在时需要插入(加锁)。

      适用场景:在CopyOnWriteArrayList适用场景下加一个,集合别太大(全部遍历伤不起)。

    2. ConcurrentLinkedQueue 并发队列(基于链表)
      基于链表实现的并发队列,使用乐观锁(CAS)保证线程安全。因为数据结构是链表,所以理论上是没有队列大小限制的,也就是说添加数据一定能成功。

    3. ConcurrentLinkedDeque 并发队列(基于双向链表)
      基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出(FIFO),也可以先进后出(FILO),当然先进后出的话应该叫它栈了。

    4. ConcurrentSkipListMap 基于跳表的并发Map
      SkipList即跳表,跳表是一种空间换时间的数据结构,通过冗余数据,将链表一层一层索引,达到类似二分查找的效果

    5. ConcurrentSkipListSet 基于跳表的并发Set
      类似HashSet和HashMap的关系,ConcurrentSkipListSet里面就是一个ConcurrentSkipListMap,就不细说了。

    6. ArrayBlockingQueue 阻塞队列(基于数组)
      基于数组实现的可阻塞队列,构造时必须制定数组大小,往里面放东西时如果数组满了便会阻塞直到有位置(也支持直接返回和超时等待),通过一个锁ReentrantLock保证线程安全。
      用offer操作举个例子:

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
        /**
         * 读写共用此锁,线程间通过下面两个Condition通信
         * 这两个Condition和lock有紧密联系(就是lock的方法生成的)
         * 类似Object的wait/notify
         */
        final ReentrantLock lock;
        /** 队列不为空的信号,取数据的线程需要关注 */
        private final Condition notEmpty;
        /** 队列没满的信号,写数据的线程需要关注 */
        private final Condition notFull;
        // 一直阻塞直到有东西可以拿出来
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0)
                    notEmpty.await();
                return dequeue();
            } finally {
                lock.unlock();
            }
        }
        // 在尾部插入一个元素,队列已满时等待指定时间,如果还是不能插入则返回
        public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {
            checkNotNull(e);
            long nanos = unit.toNanos(timeout);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly(); // 锁住
            try {
                // 循环等待直到队列有空闲
                while (count == items.length) {
                    if (nanos <= 0)
                        return false;// 等待超时,返回
                    // 暂时放出锁,等待一段时间(可能被提前唤醒并抢到锁,所以需要循环判断条件)
                    // 这段时间可能其他线程取走了元素,这样就有机会插入了
                    nanos = notFull.awaitNanos(nanos);
                }
                enqueue(e);//插入一个元素
                return true;
            } finally {
                lock.unlock(); //解锁
            }
        }
    

    乍一看会有点疑惑,读和写都是同一个锁,那要是空的时候正好一个读线程来了不会一直阻塞吗?

    答案就在notEmpty、notFull里,这两个出自lock的小东西让锁有了类似synchronized + wait + notify的功能。

    1. LinkedBlockingQueue 阻塞队列(基于链表)
      基于链表实现的阻塞队列,想比与不阻塞的ConcurrentLinkedQueue,它多了一个容量限制,如果不设置默认为int最大值。

    2. LinkedBlockingDeque 阻塞队列(基于双向链表)
      类似LinkedBlockingQueue,但提供了双向链表特有的操作。

    3. PriorityBlockingQueue 线程安全的优先队列
      构造时可以传入一个比较器,可以看做放进去的元素会被排序,然后读取的时候按顺序消费。某些低优先级的元素可能长期无法被消费,因为不断有更高优先级的元素进来。

    4. SynchronousQueue 数据同步交换的队列
      一个虚假的队列,因为它实际上没有真正用于存储元素的空间,每个插入操作都必须有对应的取出操作,没取出时无法继续放入。

      一个简单的例子感受一下:

    import java.util.concurrent.*;
    public class Main {
        public static void main(String[] args) {
            SynchronousQueue<Integer> queue = new SynchronousQueue<>();
            new Thread(() -> {
                try {
                    // 没有休息,疯狂写入
                    for (int i = 0; ; i++) {
                        System.out.println("放入: " + i);
                        queue.put(i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(() -> {
                try {
                    // 咸鱼模式取数据
                    while (true) {
                        System.out.println("取出: " + queue.take());
                        Thread.sleep((long) (Math.random() * 2000));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    /* 输出:
    放入: 0
    取出: 0
    放入: 1
    取出: 1
    放入: 2
    取出: 2
    放入: 3
    取出: 3
    */
    

    可以看到,写入的线程没有任何sleep,可以说是全力往队列放东西,而读取的线程又很不积极,读一个又sleep一会。输出的结果却是读写操作成对出现。

    JAVA中一个使用场景就是Executors.newCachedThreadPool(),创建一个缓存线程池。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(
            0, // 核心线程为0,没用的线程都被无情抛弃
            Integer.MAX_VALUE, // 最大线程数理论上是无限了,还没到这个值机器资源就被掏空了
            60L, TimeUnit.SECONDS, // 闲置线程60秒后销毁
            new SynchronousQueue<Runnable>()); // offer时如果没有空闲线程取出任务,则会失败,线程池就会新建一个线程
    }
    
    1. LinkedTransferQueue 基于链表的数据交换队列
      实现了接口TransferQueue,通过transfer方法放入元素时,如果发现有线程在阻塞在取元素,会直接把这个元素给等待线程。如果没有人等着消费,那么会把这个元素放到队列尾部,并且此方法阻塞直到有人读取这个元素。和SynchronousQueue有点像,但比它更强大。

    2. DelayQueue 延时队列

    可以使放入队列的元素在指定的延时后才被消费者取出,元素需要实现Delayed接口。

    4。阻塞队列LinkedBlockingQueue 以及非阻塞队列ConcurrentLinkedQueue 的区别

    阻塞队列:线程安全

    • 按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列检索操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。

    注意:

    1. 必须要使用take()方法在获取的时候达成阻塞结果
    2. 使用poll()方法将产生非阻塞效果

    非阻塞队列

    • 基于链接节点的、无界的、线程安全。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许 null 元素。

    5.其他知识点

    1. 锁粒度:
      1. ConcurrentHashMap【本文concurrentHashMap是jdk1.7中的实现,jdk1.8中使用的不是Segment,特此说明】
      2. 快速存取<Key, Value>键值对使用HashMap;多线程并发存取<Key, Value>键值对使用ConcurrentHashMap;
      3. 我们知道,HashTable和和Collections类中提供的同步HashTable是线程安全的,但是他们线程安全是通过在进行读写操作时对整个map加锁来实现的,故此性能比较低。那既然是由于锁粒度(加锁的范围叫锁粒度)太大造成的性能低下,可不可以从锁粒度着手去改良呢?由此,就引申出了ConcurrentHashMap。
      4. ConcurrentHashMap采取了“锁分段”技术来细化锁的粒度:把整个map划分为一系列被成为segment的组成单元,一个segment相当于一个小的hashtable。这样,加锁的对象就从整个map变成了一个更小的范围——一个segment。ConcurrentHashMap线程安全并且提高性能原因就在于:对map中的读是并发的,无需加锁;只有在put、remove操作时才加锁,而加锁仅是对需要操作的segment加锁,不会影响其他segment的读写,由此,不同的segment之间可以并发使用,极大地提高了性能。
    2. 跳表:
      1. TreeMap使用红黑树按照key的顺序(自然顺序、自定义顺序)来使得键值对有序存储,但是只能在单线程下安全使用;多线程下想要使键值对按照key的顺序来存储,则需要使用ConcurrentSkipListMap。
      2. ConcurrentSkipListMap的底层是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。
      3. 跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。在查找时,首先按照层级查找,比如:当前跳表最高层级为3,即每个结点中不仅记录了next结点(层级1),还记录了next的next(层级2)、next的next的next(层级3)结点。现在查找一个结点,则从头结点开始先按高层级开始查:head->head的next的next的next->。。。直到找到结点或者当前结点q的值大于所查结点,则此时当前查找层级的q的前一节点p开始,在p~q之间进行下一层级(隔1个结点)的查找…直到最终迫近、找到结点。此法使用的就是“先大步查找确定范围,再逐渐缩小迫近”的思想进行的查找。

    引用参考

    1. 14个Java并发容器,你用过几个?
    2. Java并发——同步容器与并发容器
    3. Java中Synchronized的用法
    4. (原创)JAVA阻塞队列LinkedBlockingQueue 以及非阻塞队列ConcurrentLinkedQueue 的区别
    5. Java并发容器——ConcurrentSkipListMap和ConcurrentHashMap
    展开全文
  • Java多线程变量共享与隔离

    千次阅读 2021-11-12 11:38:42
    线程相关 线程的相关API Thread.currentThread().getName():获取当前线程的名字 start():1.启动当前线程2.调用线程中的run方法 run():通常需要重写Thread类...join():在线程中插入执行另一个线程,该线程被阻塞,直


    线程相关

    线程的相关API

    1. Thread.currentThread().getName():获取当前线程的名字
    2. start():1.启动当前线程2.调用线程中的run方法
    3. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
    4. currentThread():静态方法,返回执行当前代码的线程
    5. getName():获取当前线程的名字
    6. setName():设置当前线程的名字
    7. yield():主动释放当前线程的执行权
    8. join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
    9. stop():过时方法。当执行此方法时,强制结束当前线程。
    10. sleep(long millitime):线程休眠一段时间
    11. isAlive():判断当前线程是否存活

    线程的调度

    调度策略:

    1. 时间片:线程的调度采用时间片轮转的方式
    2. 抢占式:高优先级的线程抢占CPU

    Java的调度方法:

    1. 对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
    2. 对高优先级,使用优先调度的抢占式策略

    线程的优先级

    等级:
    MAX_PRIORITY:10
    MIN_PRIORITY:1
    NORM_PRIORITY:5

    方法:
    getPriority():返回线程优先级
    setPriority(int newPriority):改变线程的优先级

    注意事项:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。

    方法和变量的线程安全问题

    静态方法

    与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到JVM关闭。

    非静态方法

    又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。不会常驻内存,当实例对象被JVM 回收之后,也跟着消失。

    静态变量

    线程非安全,静态变量即类变量,位于方法区,为所有该类下的对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。

    实例变量

    实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。

    局部变量

    线程安全,每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。

    变量共享

    共享变量线程安全问题

    可见性

    如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性。如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量。

    Java多线程里对于共享变量的操作往往需要考虑进行一定的同步互斥操作,原来是因为Java内存模型导致的共享内存对于线程不可见。

    Java 内存模型规定,将所有的变量都存放在主内存中。

    1. 线程对共享变量的所有操作必须在工作内存中进行,不能直接操作主内存。
    2. 不同线程间不能访问彼此的工作内存中的变量,线程间变量值的传递都必须经过主内存。

    在这里插入图片描述
    多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作内存中成为一个副本,对这个副本进行改动之后,再更新回主内存中变量所在的地方。

    由于CPU时间片是以线程为最小单位,所以这里的工作内存实际上就是指的物理缓存,CPU运算时获取数据的地方;而主内存也就是指的是内存,也就是原始的共享变量存放的位置。

    一个线程A对共享变量1的修改对线程B可见,需要经过下列步骤:

    1. 线程A将更改变量1后的值更新到主内存
    2. 主内存将更新后的变量1的值更新到线程B的工作内存中变量1的副本

    要实现共享变量的可见性必须保证下列两点:

    1. 线程对工作内存中副本的更改能够及时的更新到主内存上
    2. 其他线程能够及时的将主内存上共享变量的更新刷新到自己工作内存的该变量的副本上

    可见性举例

    一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。
    在这里插入图片描述
    1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。

    2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。

    3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。

    共享变量可见性的实现

    Java中可以通过synchronized、volatile、java concurrent类来实现共享变量的可见性。

    synchronized

    使用synchronized可以保证原子性(synchronized代码块内容要么不执行,要执行就保证全部执行完毕)和可见性,修改后的代码为在write和read方法上加synchronized关键字。

    JMM关于Synchronized的两条规定:

    1. 线程解锁前,必须把共享变量的最新值刷新到主内存中;
    2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)。

    synchronized 实际上是对访问修改共享变量的代码块进行加互斥锁,多个线程对synchronized代码块的访问时,某一时刻仅仅有一个线程在访问和修改代码块中的内容(加锁),其他所有的线程等待该线程离开代码块时(释放锁)才有机会进入synchronized代码块。

    某一个线程进入synchronized代码块前后,执行过程入如下:

    1. 线程获得互斥锁
    2. 清空工作内存
    3. 从主内存拷贝共享变量最新的值到工作内存成为副本
    4. 执行代码
    5. 将修改后的副本的值刷新回主内存中
    6. 线程释放锁

    随后,其他代码在进入synchronized代码块的时候,所读取到的工作内存上共享变量的值都是上一个线程修改后的最新值。

    注意,synchronized加锁后用到的变量才会从主内存拉取、才会修改后刷新回主内存。

    举例说明:
    在这里插入图片描述
    该代码程序不会进入死循环,分析执行过程:

    1. 创建o对象;
    2. 创建新线程,异步执行;
    3. 执行while,此时isStop=false;进入循环;
    4. 遇到synchronized锁,得到锁;
    5. 清空主线程的工作内存;
      从主内存拷贝共享变量最新的值到工作内存成为副本(该步骤省略,因为所里面没有代码块(不知道是否可以这么理解));
      执行代码(该步骤省略,因为没有代码);
      将修改后的副本的值刷新会主内存中(该步骤省略,因为没有代码);
    6. 释放锁
    7. 重新执行while,此时工作内存中没有isStop,从主内存中获取最新值,如果isStop=false,继续执行4、5、6、7步骤,知道2秒钟后子线程将isStop值改为true,并刷新回主线程,此时步骤7从主内存中获得的isStop=true,结束while循环,主线程结束。

    特别情况:
    在这里插入图片描述
    在上个例子的基础上加上一段代码:System.out.println(isStop),查看打印日志:
    在这里插入图片描述
    没有达到自己预期的效果:结束前一次貌似应该打印true;
    上面代码其实有两种结果:

    1. 最后一次打印isStop为true(几率小)
    2. 最后一次打印isStop为false(几率大)
      具体原因:
      在这里插入图片描述
      System.out.println(boolean x)方法其实也含有一个synchronized锁。
      结果1出现的情况:where时,isStop=false,第一个锁o对象锁清空工作内存后重新从内存得到isStop=true(几率小)
      结果2出现的情况:where时,isStop=false,第一个锁o对象锁清空工作内存后重新从内存得到isStop=false,println得到的参数值为false(值传递),println的锁PrintStream对象锁清空工作内存,再次where时,isStop=true(几率大)。

    volatile

    volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存。这样一来,不同的线程都能及时的看到该变量的最新值。

    但是volatile不能保证变量更改的原子性
    比如number++,这个操作实际上是三个操作的集合(读取number,number加1,将新的值写回number),volatile只能保证每一 步的操作对所有线程是可见的,但是假如两个线程都需要执行number++,那么这一共6个操作集合,之间是可能会交叉执行的,那么最后导致number 的结果可能会不是所期望的。

    举例说明:
    在这里插入图片描述
    程序不会进入死循环,原因:
    isStop是被volatile修饰的,所有每次while时都是从主内存中获取isStop的值,当子线程2秒钟后修改了isStop的值为true,并刷新进了住内存,此后while从主内存中获取的isStop值为true,结束循环。

    AtomicInteger:一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

    synchronized和volatile比较

    1. volatile不需要同步操作,所以效率更高,不会阻塞线程,但是适用情况比较窄

    2. volatile读变量相当于加锁(即进入synchronized代码块),而写变量相当于解锁(退出synchronized代码块)

    3. synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性

    volatile适用情况

    1. 对变量的写入操作不依赖当前值
      比如自增自减、number = number + 5等(不满足)

    2. 当前volatile变量不依赖于别的volatile变量
      比如 volatile_var > volatile_var2这个不等式(不满足)

    特殊操作会从主内存中拉取值

    1. 变量被赋值
    2. 变量参与计算(比如加减乘除)
    3. 字符串拼接(验证过)
    4. ++、–等操作
    5. 线程休眠(验证过)

    以上操作会从主内存拉去值到工作内存,所有如果在上面的例子的weile循环中有这些操作,不会造成死循环。
    在这里插入图片描述

    变量隔离

    多线程之间就是因为数据共享在多个线程才导致了线程不安全,这就要求线程间的数据需要隔离,从根本上解决了线程安全问题。

    ThreadLocal

    提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。

    使用ThreadLocal的好处

    1. 达到线程安全
    2. 不需要加锁,提高执行效率
    3. 更高效地利用内存节省开销,用ThreadLocal可以节省内存和开销。
    4. 免去传参的繁琐,不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅

    ThreadLocal主要方法

    主要是initialValue、set、get、remove这几个方法

    • initialValue方法返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
    • 当线程第一次使用get方法访问变量时,将调用initialValue方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法。
    • 通常,每个线程最多调用一次initialValue()方法,但如果已经调用了一次remove()后,再调用get(),则可以再次调用initialValue(),相当于第一次调用get()。
    • 如果不重写initialValue()方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

    ThreadLocal源码分析

    Thread、ThreadLocal、ThreadLocalMap三者的关系:
    在这里插入图片描述
    每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal。

    get方法
    get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。

    注意:这个map以及map中的key和value都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中

    getMap方法:
    获取到当前线程内的ThreadLocalMap对象。每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null。

    set方法
    把当前线程需要全局共享的value传入

    initialValue方法
    这个方法没有默认实现,如果要用initialValue方法,需要自己实现,通常使用匿名内部类的方式实现

    remove方法
    删除对应这个线程的值。

    ThreadLocalMap类
    ThreadLocalMap类,也就是Thread.threadLocals。

    // 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    ThreadLocalMap 类是每个线程Thread类里面的变量,但ThreadLocalMap这个静态内部类定义在ThreadLocal类中,其中发现这一行代码

    private Entry[] table;
    

    里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:

    • 键:这个ThreadLocal
    • 值:实际需要的成员变量,比如User或者SimpleDateFormat对象

    这个思路和HashMap一样,那么我们可以把它想象成HashMap来分析,但是实现上略有不同。

    比如处理冲突方式不同,HashMap采用链地址法,而ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

    通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry

    ThreadLocal注意事项

    1.ThreadLocal内存泄漏问题
    ThreadLocalMap中的Entry继承自 WeakReference,是弱引用,ThreadLocal可能出现Value泄漏!

    什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收

    弱引用:通过WeakReference类实现的,在GC的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal中的key就用到了弱引用,有利于内存回收。
    强引用:我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象。

    ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时,每个 Entry 都包含了一个对value的强引用,如下:

    static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
         Object value;
         Entry(ThreadLocal<?> k, Object v) {
             super(k); // key值给WeakReference处理
             value = v; // value直接用变量保存,是强引用
         }
     }
    

    正常情况下,当线程终止,保存在ThreadLocalMap里的value会被垃圾回收,因为没有任何强引用了。但如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:

    Thread---->ThreadLocalMap---->Entry(key为null,弱引用被回收)---->value

    因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。

    JDK已经考虑到了这个问题,所以在set, remove, rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。比如rehash里面调用resize,如果key回收了,那么value也设置为null,断开强引用链路,便于垃圾回收。

     private void resize() {
               ......省略代码
              ThreadLocal<?> k = e.get();
               if (k == null) {
                   e.value = null; // Help the GC
               } 
               ......
    
    

    但是如果一个ThreadLocal不被使用,那么实际上set, remove, rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏。

    ThreadLocal如何避免内存泄漏
    及时调用remove方法,就会删除对应的Entry对象,可以避免内存remove泄漏,所以使用完ThreadLocal之后,应该调用remove方法。
    比如拦截器获取到用户信息,用户信息存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove清除User对象,这样就能稳妥的保证不会内存泄漏。

    注意事项

    1. 共享对象问题
      如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
    2. 不要强行使用ThreadLocal
      如果可以不使用ThreadLocal就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。
    3. 优先使用框架的支持,而不是自己创造
      在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。

    参考文章:
    Java多线程里共享变量线程安全问题的原因
    Java多线程共享变量控制
    Java多线程超详解
    ThreadLocal详解

    展开全文
  • 在开发C/C++多线程程序时,STL集合类是我们经常用到的类,比如vector. 而C/C++的这些集合类并没有同步版本,所以在多线程访问时,如果某个线程正在修改集合类A, 而其他线程正在访问A,那么会造成数据冲突,导致程序抛...
  • 在上篇我们深入学习了JUC的lock锁与synchronized关键字的区别,以及一些关键问题,特点的探讨,这一篇我们继续进行JUC的学习。 集合的安全性问题 我先附上一段代码,希望小伙伴们先理解如下代码 package ...
  • 多线程之间共享哪些资源?

    千次阅读 2018-10-20 23:59:16
    同一进程间的线程究竟共享哪些资源呢,而又各自独享哪些资源呢? 共享的资源有 a. 堆 由于堆是在进程空间开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部...
  • java多线程创建创建线程的方式Runnable和Callable的区别Thread类的start()和run()方法有什么区别?什么导致线程阻塞?3. 多线程同步和锁怎么检测一个线程是否持有对象监视器Condition?4. 线程池CyclicBarrier和...
  • Java的集合+I/O流+多线程的练习题和面试题
  • 日常开发如果用到多线程编程,也一定会涉及到线程安全问题 线程安全这个问题就不太好理解 正因为如此,程序猿们才尝试发明出更多的编程模型来处理并发编程的任务 例如:多进程、多线程、actor、csp、async+await、...
  • 操作系统--多线程之间共享哪些资源?

    万次阅读 多人点赞 2018-08-21 18:23:57
    【同一进程间的线程共享的资源有】 a. 堆 由于堆是在进程空间开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的) b. 全局变量 它是与具体某一...
  • 多线程中的可见性问题

    千次阅读 2020-08-16 21:34:38
    可见性问题是指一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。 对于串行程序来说,可见性问题是不存在的,因为你在任何一个操作步骤修改了某个变量,在后续的步骤读取这个变量的值时...
  • 线程自己基本上不拥有系统资源,只拥有一点在运行必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 一个线程可以创建和撤销另一个线程; 同一个进程...
  • 集合是编程最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5...
  • 一、同一进程间的线程共享内存地址空间,其中进程用户空间的五个段 英文: .bss 、.data 、 .text 、 stack 、heap 。 中文:BSS段(bss segment),数据段:数据段(data segment),代码段:代码段(code segme...
  • 多线程中,如果要并发的修改一个数据结构,那么很有可能会破坏这个数据结构。例如,一个线程可能要向一个散列表插入一个元素,假如在调整各个桶之间的链接关系时剥夺了控制权,而此时正好有另外一个线程正在...
  • php 实现多线程

    千次阅读 2021-04-27 01:09:15
    php本身是不支持多线程的,那么如何在php实现多线程呢?可以想一下,WEB服务器本身都是支持多线程的。每一个访问者,当访问WEB页面的时候,都将调用新的线程,通过这一点我们可以利用WEB服务器自身的线程来解决PHP...
  • Java多线程编程(超详细总结)

    千次阅读 2020-11-21 00:54:28
    Java多线程编程(超详细总结) ** 一、线程与多线程的概念 线程,即单线程,是程序的一条执行线索,执行路径,是程序使用cpu的最小单位。线程本身不能运行,它只能运行在程序,线程是依赖于程序存在的。 ...
  • Java集合java集合框架的结构List接口List接口通常表示一个列表(数组、队列、链表、栈等),其中的元素可以重复,常用实现类为ArrayList和LinkedList,另外还有不常用的Vector。另外,LinkedList还是实现了Queue接口...
  • 关于Java程序员面试多线程问题总结 很多核心Java面试题多线程(Multi-Threading)和集合框架(CollectionsFramework)理解核心线程概念时娴熟的实际经验是必需的这篇文章收集了Java线程方面一些典型的问题这些问题...
  • java 程序怎么保证多线程的运行安全? 线程的安全性问题体现在: 原子性:一个或者多个操作在 CPU 执行的过程中断的特性 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到 有序性:程序...
  • Java多线程共享变量控制

    千次阅读 2017-09-07 15:39:48
    线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作内存成为一个副本,对这个副本进行改动之后,再更新回主内存变量所在的地方。 (由于CPU时间片是以线程为最小单位
  • Java多线程超详解

    万次阅读 多人点赞 2019-06-11 01:00:30
    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...
  • 【Python 爬虫】多线程爬取

    千次阅读 2022-03-20 10:44:40
    文章目录前言一、多进程库(multiprocessing)二、多线程爬虫三、案例实操四、案例解析1、获取网页内容2、获取每一章链接3、获取每一章的正文并返回章节名和正文4、将每一章保存到本地5、多线程爬取文章 前言 简单...
  • 多线程带来的问题 为什么需要多线程 其实说白了,时代变了,现在的机器都是多核的了,为了榨干机器最后的性能我们引入单线程。 为了充分利用CPU资源,为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不...
  • Java并发(一):多线程干货总结

    千次阅读 2021-02-25 18:45:33
    一、进程 线程进程:一个进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂停时...
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2015-03-14 13:13:17
    本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 186,934
精华内容 74,773
关键字:

多线程中集合被共享吗