• 关于

    java单线程操作

    的搜索结果

问题

求java方向的前辈们戳入,关于单例的一些疑惑,求解~~~~? 400 报错

爱吃鱼的程序员 2020-06-01 10:30:42 0 浏览量 回答数 1

回答

现代操作系统在运行一个程序时, 会为其创建一个进程。 例如, 启动一个Java程序, 操作系统就会创建一个Java进程。 现代操作系统调度的最小单元是线程, 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行。在java中创建线程主要有继承Thread类和实现Runnable接口等方式,但需要注意的是,Thread和Runnable只有真正运行起来才会在一个新的线程中

houfeng2 2019-12-02 01:01:41 0 浏览量 回答数 0

回答

首先,你的代码没有问题,肯定是启动了3个线程来执行任务的。其次,分析下直接用main线程顺序执行三个操作,为什么比同时启动三个线程执行速度快呢?我认为这又可能跟操作系统处理IO的方式有关系,多线程并发处理IO时,操作系统底层响应IO的速度会影响线程的操作的。单线程环境下,顺序执行IO,操作系统可能不涉及到对IO请求的调用问题,但是多个线程同时发出IO请求命令时,操作系统底层的调度也有影响。你可以修正下你的测试内容,把Action中的Runnable的任务换成其他长时间计算任务,例如休眠操作,或者大数据计算操作,那么多线程的优势就体现出来了。修正测试内容: public void writeToTxt(String text) { int j=0; for(int i =0;i<1000000;i++){ j=i ; } System.out.println(Thread.currentThread().getName()+j); }最后,多线程提高效率体现在对整个功能完成时间上,是并行,而不会是纠结于单个任务完成过程中的处理时间;从理论上说,一个Runnable中执行完成的时间应该是差不多的,多线程的优势就是N个任务并行时,需要的总时间近似于完成一个任务的时间;而单线程时间则是N倍的单个任务的时间。而且多线程要考虑到线程池创建和调度的时间损耗,还是需要权衡的。像这种类似任务可以用java的线程池,可以免去线程创建和销毁的损耗。

蛮大人123 2019-12-02 02:39:56 0 浏览量 回答数 0

阿里云高校特惠,助力学生创业梦!0元体验,快速入门云计算!

学生动手场景应用,快速了解并掌握云服务器的各种新奇玩法!

回答

1)HashTable是线程安全的,其提供的public方法上都加了synchronized关键字,效率是比较低的;2)HashMap是非线程安全的,多线程操作会导致并发冲突异常,单线程场景高效;3)ConcurrentHashMap是线程安全的,与HashTable的实现机制不同,并没有通过加synchronized关键字的方式实现同步锁,在java8之前它是通过分段(segment)锁的方式来实现的,降低并发冲突,但是在java8中实现方式上有所调整,采取按行加锁,进一步降低并发冲突概率,具体可以参考jdk8源码实现

talishboy 2019-12-02 01:46:43 0 浏览量 回答数 0

回答

先补充一下概念:Java 内存模型中的可见性、原子性和有序性。可见性:  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。  在 Java 中 volatile、synchronized 和 final 实现可见性。原子性:  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。有序性:  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。下面内容摘录自《Java Concurrency in Practice》:  下面一段代码在多线程环境下,将存在问题。复制代码+ View code1 /** 2 * @author zhengbinMac 3 */ 4 public class NoVisibility { 5 private static boolean ready; 6 private static int number; 7 private static class ReaderThread extends Thread { 8 @Override 9 public void run() {10 while(!ready) {11 Thread.yield();12 }13 System.out.println(number);14 }15 }16 public static void main(String[] args) {17 new ReaderThread().start();18 number = 42;19 ready = true;20 }21 }复制代码  NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。甚至NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以明显地看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。  在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行春旭进行判断,无法得到正确的结论。  这个看上去像是一个失败的设计,但却能使JVM充分地利用现代多核处理器的强大性能。例如,在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中。此外,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。二、Volatile原理  Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。  当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。  而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。当一个变量定义为 volatile 之后,将具备两种特性:  1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。  2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。volatile 性能:  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

wangccsy 2019-12-02 01:48:10 0 浏览量 回答数 0

回答

多线程 Java多线程三大特性是啥? https://developer.aliyun.com/ask/282728 Java如何解决多线程之间线程安全问题? https://developer.aliyun.com/ask/282730Java为什么使用线程同步或使用锁能解决线程安全问题呢? https://developer.aliyun.com/ask/282731Java中多线程之间同步是什么? https://developer.aliyun.com/ask/282732Java 计数器 CountDownLatch有啥作用? https://developer.aliyun.com/ask/282733Java 屏障 CyclicBarrier有啥作用? https://developer.aliyun.com/ask/282734Java 计数信号量 Semaphore有啥作用? https://developer.aliyun.com/ask/282735Java在并发队列上JDK提供了你哪些实现? https://developer.aliyun.com/ask/282736Java阻塞队列与非阻塞队列的区别在哪? https://developer.aliyun.com/ask/282737Java并发队列ConcurrentLinkedQueue如何使用? https://developer.aliyun.com/ask/282729Java并发队列ArrayBlockingQueue如何使用? https://developer.aliyun.com/ask/282738Java并发队列LinkedBlockingQueue如何使用? https://developer.aliyun.com/ask/282739Java并发队列PriorityBlockingQueue如何使用? https://developer.aliyun.com/ask/282740Java并发队列SynchronousQueue如何使用? https://developer.aliyun.com/ask/282741Java如何使用BlockingQueue模拟生产者与消费者? https://developer.aliyun.com/ask/282742Java线程池有哪些创建方式? https://developer.aliyun.com/ask/282743Java线程池原理是啥? https://developer.aliyun.com/ask/282744Java如何自定义线程线程池? https://developer.aliyun.com/ask/282745Java如何合理配置线程池? https://developer.aliyun.com/ask/282746 SpringBoot - Java中一个典型的SpringBoot应用长什么样子呢? https://developer.aliyun.com/ask/282747Java @SpringBootApplication应该如何理解? https://developer.aliyun.com/ask/282748Java @Configuration应该如何理解? https://developer.aliyun.com/ask/282749Java @EnableAutoConfiguration应该如何理解? https://developer.aliyun.com/ask/282750Java @ComponentScan应该如何理解? https://developer.aliyun.com/ask/282751Java Spring Application执行流程有哪些? https://developer.aliyun.com/ask/282752Java SpringBoot应用如何添加自定义的ApplicationListener? https://developer.aliyun.com/ask/282753Java SpringBoot如何实现基于条件的自动配置? https://developer.aliyun.com/ask/282754Java SpringBoot如何调整自动配置的顺序? https://developer.aliyun.com/ask/282755Java SpringBoot对开发者有哪些影响? https://developer.aliyun.com/ask/282756对SpringBoot的行为进行干预的配置方式有哪些? https://developer.aliyun.com/ask/282757 正则表达式 Java 正则表达式是什么? https://developer.aliyun.com/ask/282759 Java正则表达式的语法组成部分有哪些? https://developer.aliyun.com/ask/282760Java正则表达式单个字符如何使用? https://developer.aliyun.com/ask/282761Java正则表达式字符组如何使用? https://developer.aliyun.com/ask/282762Java正则表达式量词如何使用? https://developer.aliyun.com/ask/282763Java正则表达式分组如何使用? https://developer.aliyun.com/ask/282764Java正则表达式特殊边界匹配如何使用? https://developer.aliyun.com/ask/282765Java正则表达式环视边界匹配如何使用? https://developer.aliyun.com/ask/282766Java中如何表示正则表达式? https://developer.aliyun.com/ask/282767Java中如何进行正则表达式切分? https://developer.aliyun.com/ask/282768Java中如何进行正则表达式验证? https://developer.aliyun.com/ask/282769Java中如何进行正则表达式查找? https://developer.aliyun.com/ask/282770Java中如何进行正则表达式替换? https://developer.aliyun.com/ask/282771 JDK8特性 Java8 Lambda表达式是什么? https://developer.aliyun.com/ask/282772 Java8 Lambda表达式语法有哪些? https://developer.aliyun.com/ask/282773Java如何检查是否是函数式接口? https://developer.aliyun.com/ask/282774Java8接口中默认方法是啥? https://developer.aliyun.com/ask/282775Java8接口中允许定义静态方法吗? https://developer.aliyun.com/ask/282776Java8集合之流式操作是啥? https://developer.aliyun.com/ask/282778Java8流式操作中串行和并行的流有啥区别? https://developer.aliyun.com/ask/282777Java8流式操作中中间操作有哪些? https://developer.aliyun.com/ask/282779

问问小秘 2020-06-02 15:05:40 0 浏览量 回答数 0

回答

HashMap的实现里确实没有锁的机制,因此它是线程不安全的。另外你说的也没错,只要有锁的机制,可以通过锁实现线程安全,你可以在读写HashMap对象的时候加锁,以保障这个对象的线程安全,但不代表HashMap本身是线程安全的,因为是外力(你自己加的锁)使然。为啥不在HashMap内部加锁让它变成线程安全? 这样会增加单线程访问的资源消耗,即使没有多线程访问,也要每次检查、加锁、解锁。 实际上有线程安全的Map,Collections里面有个静态方法可以返回一个线程安全版本的HashMappublic static Map synchronizedMap(Map m) { return new SynchronizedMap<K,V>(m); } 另外java5之后还提供了ConcurrentHashMap类,提供更高效的线程安全操作。

蛮大人123 2019-12-02 01:54:11 0 浏览量 回答数 0

回答

"总结起来,使用多线程编程具有如下几个优点。 进程之间不能共享内存,但线程之间共享内存非常容易。 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。 Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。 在实际应用中,多线程是非常有用的,一个浏览器必须能同时下载多个图片;一个We b服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收;图形用户界面(GUI)应用也需要启动单独的线程从主机环境收集用户界面事件……总之,多线程在实际编程中的应用是非常广泛的。"

星尘linger 2020-04-13 09:07:24 0 浏览量 回答数 0

回答

首先来讲一下传统的IO和NIO的区别,传统的IO又称BIO,即阻塞式IO,NIO就是非阻塞IO了。还有一种AIO就是异步IO,这里不加阐述了。 Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

万立超 2019-12-02 01:00:40 0 浏览量 回答数 0

问题

【Java问答学堂】13期 redis 和 memcached 有什么区别?

剑曼红尘 2020-05-06 14:37:41 0 浏览量 回答数 1

回答

我们都知道虚拟机的内存划分了多个区域,并不是一张大饼。那么为什么要划分为多块区域呢,直接搞一块区域,所有用到内存的地方都往这块区域里扔不就行了,岂不痛快。是的,如果不进行区域划分,扔的时候确实痛快,可用的时候再去找怎么办呢,这就引入了第一个问题,分类管理,类似于衣柜,系统磁盘等等,为了方便查找,我们会进行分区分类。另外如果不进行分区,内存用尽了怎么办呢?这里就引入了内存划分的第二个原因,就是为了方便内存的回收。如果不分,回收内存需要全部内存扫描,那就慢死了,内存根据不同的使用功能分成不同的区域,那么内存回收也就可以根据每个区域的特定进行回收,比如像栈内存中的栈帧,随着方法的执行栈帧进栈,方法执行完毕就出栈了,而对于像堆内存的回收就需要使用经典的回收算法来进行回收了,所以看起来分类这么麻烦,其实是大有好处的。 提到虚拟机的内存结构,可能首先想起来的就是堆栈。对象分配到堆上,栈上用来分配对象的引用以及一些基本数据类型相关的值。但是·虚拟机的内存结构远比此要复杂的多。除了我们所认识的(还没有认识完全)的堆栈以外,还有程序计数器,本地方法栈和方法区。我们平时所说的栈内存,一般是指的栈内存中的局部变量表。 从图中可以看到有5大内存区域,按照是否被线程所共享可分为两部分,一部分是线程独占区域,包括Java栈,本地方法栈和程序计数器。还有一部分是被线程所共享的,包括方法区和堆。什么是线程共享和线程独占呢,非常好理解,我们知道每一个Java进行都会有多个线程同时运行,那么线程共享区的这片区域就是被所有线程一起使用的,不管有多少个线程,这片空间始终就这一个。而线程的独占区,是每个线程都有这么一份内存空间,每个线程的这片空间都是独有的,有多少个线程就有多少个这么个空间。上图的区域的大小并不代表实际内存区域的大小,实际运行过程中,内存区域的大小也是可以动态调整的。下面来具体说说每一个区域的主要功能。 程序计数器,我们在写代码的过程中,开发工具一般都会给我们标注行号方便查看和阅读代码。那么在程序在运行过程中也有一个类似的行号方便虚拟机的执行,就是程序计数器,在c语言中,我们知道会有一个goto语句,其实就是跳转到了指定的行,这个行号就是程序计数器。存储的就是程序下一条所执行的指令。这部分区域是线程所独享的区域,我们知道线程是一个顺序执行流,每个线程都有自己的执行顺序,如果所有线程共用一个程序计数器,那么程序执行肯定就会出乱子。为了保证每个线程的执行顺序,所以程序计数器是被单个线程所独显的。程序计数器这块内存区域是唯一一个在jvm规范中没有规定内存溢出的。 java虚拟机栈,java虚拟机栈是程序运行的动态区域,每个方法的执行都伴随着栈帧的入栈和出栈。 栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。栈帧中包括了局部变量表,操作数栈,方法返回地址以及额外的一些附加信息,在编译过程中,局部变量表的大小已经确定,操作数栈深度也已经确定,因此栈帧在运行的过程中需要分配多大的内存是固定的,不受运行时影响。对于没有逃逸的对象也会在栈上分配内存,对象的大小其实在运行时也是确定的,因此即使出现了栈上内存分配,也不会导致栈帧改变大小。 一个线程中,可能调用链会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是最有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的字节码指令仅对当前栈帧进行操作。Ft5rk58GfiJxcdcCzGeAt8fjkFPkMRdf 局部变量表:我们平时所说的栈内存一般就是指栈内存中的局部变量表。这里主要是存储变量所用。对于基本数据类型直接存储其值,对于引用数据类型则存储其地址。局部变量表的最小存储单位是Slot,每个Slot都能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。 既然前面提到了数据类型,在此顺便说一下,一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress八种类型。前面六种不需要多解释,大家都认识,而后面的reference是对象的引用。虚拟机规范既没有说明它的长度,也没有明确指出这个引用应有怎样的结构,但是一般来说,虚拟机实现至少都应当能从此引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。而returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。 对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。Java语言中明确规定的64位的数据类型只有long和double两种(reference类型则可能是32位也可能是64位)。值得一提的是,这里把long和double数据类型读写分割为两次32读写的做法类似。不过,由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。 操作数栈是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到字节码文件中,关于字节码文件,后面我会具体的来描述。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。 举个例子,整数加法的字节码指令iadd在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值和并相加,然后将相加的结果入栈。 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。 本地方法栈 与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。 方法区经常会被人称之为永久代,但这俩并不是一个概念。首先永久代的概念仅仅在HotSpot虚拟机中存在,不幸的是,在jdk8中,Hotspot去掉了永久代这一说法,使用了Native Memory,也就是Metaspace空间。那么方法区是干嘛的呢?我们可以这么理解,我们要运行Java代码,首先需要编译,然后才能运行。在运行的过程中,我们知道首先需要加载字节码文件。也就是说要把字节码文件加载到内存中。好了,问题就来了,字节码文件放到内存中的什么地方呢,就是方法区中。当然除了编译后的字节码之外,方法区中还会存放常量,静态变量以及及时编译器编译后的代码等数据。 堆,一般来讲堆内存是Java虚拟机中最大的一块内存区域,同方法区一样,是被所有线程所共享的区域。此区域所存在的唯一目的就存放对象的实例(对象实例并不一定全部在堆中创建)。堆内存是垃圾收集器主要光顾的区域,一般来讲根据使用的垃圾收集器的不同,堆中还会划分为一些区域,比如新生代和老年代。新生代还可以再划分为Eden,Survivor等区域。另外为了性能和安全性的角度,在堆中还会为线程划分单独的区域,称之为线程分配缓冲区。更细致的划分是为了让垃圾收集器能够更高效的工作,提高垃圾收集的效率。 如果想要了解更多的关于虚拟机的内容,可以观看录制的<深入理解Java虚拟机>这套视频教程。

zwt9000 2019-12-02 00:21:07 0 浏览量 回答数 0

回答

【Java问答学堂】13期 redis 和 memcached 有什么区别?redis 的线程模型是什么?为什么 redis 单线程却能支撑高并发? 面试官心理分析 这个是问 redis 的时候,最基本的问题吧,redis 最基本的一个内部原理和特点,就是 redis 实际上是个单线程工作模型,你要是这个都不知道,那后面玩儿 redis 的时候,出了问题岂不是什么都不知道? 还有可能面试官会问问你 redis 和 memcached 的区别,但是 memcached 是早些年各大互联网公司常用的缓存方案,但是现在近几年基本都是 redis,没什么公司用 memcached 了。 面试题剖析 redis 和 memcached 有啥区别? redis 支持复杂的数据结构 redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。 redis 原生支持集群模式 在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。 性能对比 由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。 redis 的线程模型 redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。 文件事件处理器的结构包含 4 个部分: 多个 socketIO 多路复用程序文件事件分派器事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。 来看客户端与 redis 的一次通信过程: 要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。 首先,redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。 客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。 这样便完成了一次通信。关于 Redis 的一次通信过程,推荐读者阅读《Redis 设计与实现——黄健宏》进行系统学习。 为啥 redis 单线程模型也能效率这么高? 纯内存操作。核心是基于非阻塞的 IO 多路复用机制。C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。 往期回顾: 【Java问答学堂】1期 为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景? 【Java问答学堂】2期 如何保证消息队列的高可用? 【Java问答学堂】3期 如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性? 【Java问答学堂】4期 如何保证消息的可靠性传输?(如何处理消息丢失的问题?) 【Java问答学堂】5期 如何保证消息的顺序性? 【Java问答学堂】6期 如何解决消息队列的延时以及过期失效问题? 【Java问答学堂】7期 如果让你写一个消息队列,该如何进行架构设计? 【Java问答学堂】8期 es 的分布式架构原理能说一下么(es 是如何实现分布式的啊)? 【Java问答学堂】9期 es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊? 【Java问答学堂】10期 es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊? 【Java问答学堂】11期 es 生产集群的部署架构是什么?每个索引的数据量大概有多少? 【Java问答学堂】12期 项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果?

剑曼红尘 2020-05-06 14:37:53 0 浏览量 回答数 0

回答

我认为这2种写法没有什么明显区别 多线程单例可以考虑借助内部类或者枚举来实现 ###### 这2种写法有区别么? 要实现线程安全的单例,要么将synchronized边界扩大,要么借助内部类。 ###### 加一个volatile 关键字 ######volitile和synchronize有什么区别呀?###### http://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html ###### 卧槽,第一个写法 synchronized(instance) 不会空指针么?   其实你需要思考一个问题:基于 Java 的线程内存模型,线程中内存副本的修改什么时候会同步回主内存?   volatile 禁止指令重排序,就没你说的“ 但是JVM并不保证这两个操作的先后顺序 ”问题了。同时会通过失效缓存的方式保证内存可见性。 至于内存屏障这玩意儿,你就理解一个内存屏障就相当于同主内存进行一次同步,因为一个屏障里会重新进行 load 和 store 操作。 第二种多使用了一个栈帧,这个方法栈上加了锁,一般方法出栈也会把所做的修改更新回主内存,这里等于是换种方式达到 volatile 的效果。

爱吃鱼的程序员 2020-05-30 14:02:12 0 浏览量 回答数 0

回答

我认为这2种写法没有什么明显区别 多线程单例可以考虑借助内部类或者枚举来实现 ###### 这2种写法有区别么? 要实现线程安全的单例,要么将synchronized边界扩大,要么借助内部类。 ###### 加一个volatile 关键字 ######volitile和synchronize有什么区别呀?###### http://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html ###### 卧槽,第一个写法 synchronized(instance) 不会空指针么?   其实你需要思考一个问题:基于 Java 的线程内存模型,线程中内存副本的修改什么时候会同步回主内存?   volatile 禁止指令重排序,就没你说的“ 但是JVM并不保证这两个操作的先后顺序 ”问题了。同时会通过失效缓存的方式保证内存可见性。 至于内存屏障这玩意儿,你就理解一个内存屏障就相当于同主内存进行一次同步,因为一个屏障里会重新进行 load 和 store 操作。 第二种多使用了一个栈帧,这个方法栈上加了锁,一般方法出栈也会把所做的修改更新回主内存,这里等于是换种方式达到 volatile 的效果。

爱吃鱼的程序员 2020-06-02 15:02:50 0 浏览量 回答数 0

回答

我认为这2种写法没有什么明显区别 多线程单例可以考虑借助内部类或者枚举来实现 ###### 这2种写法有区别么? 要实现线程安全的单例,要么将synchronized边界扩大,要么借助内部类。 ###### 加一个volatile 关键字 ######volitile和synchronize有什么区别呀?###### http://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html ###### 卧槽,第一个写法 synchronized(instance) 不会空指针么?   其实你需要思考一个问题:基于 Java 的线程内存模型,线程中内存副本的修改什么时候会同步回主内存?   volatile 禁止指令重排序,就没你说的“ 但是JVM并不保证这两个操作的先后顺序 ”问题了。同时会通过失效缓存的方式保证内存可见性。 至于内存屏障这玩意儿,你就理解一个内存屏障就相当于同主内存进行一次同步,因为一个屏障里会重新进行 load 和 store 操作。 第二种多使用了一个栈帧,这个方法栈上加了锁,一般方法出栈也会把所做的修改更新回主内存,这里等于是换种方式达到 volatile 的效果。

优选2 2020-06-05 11:52:01 0 浏览量 回答数 0

回答

不矛盾啊,既然是单列,每次访问都是不同的线程,只要注意线程安全问题就可以了啊。所以在你的bean里面不能有成员变量,这样就不会有并发问题啊。######回复 @HUncle : no 3k,我也是新手,自己的理解而已。######回复 @妹夫 : 对,单例特别方便,但是有隐患,3Q######回复 @妹夫 : 还有一个就是单例能在程序中随处获得实例,很方便。个人觉得。######回复 @HUncle : 既然用了spring,都知道spring有依赖注入这么个东西,我们可以把需要依赖的bean交给spring去管理。如果使用了成员变量,操作它的时候就必须保证同步,这就是我说为什么不能使用成员变量,至少我没见过别人系统里会使用spring然后用同步方法去操作内部依赖的bean的。######回复 @HUncle : 单例能保证程序中这个实例是唯一的,减少java的内存回收,所以性能高。我不知道别人在哪些方面用单例,但我在写GUI程序的时候单例用的多,因为GUI程序并发少。并不是说不能存在成员变量,如果存在成员变量的时候你去操作它的时候必须保证线程同步,java中用synchronized。大量使用synchronized会导致性能降低######搜索一下 Bean的作用域,会找到你要的答案。######我说的就是singletion 的情形,此情况下如何处理多请求?######单例多线程######不知道底层如何规避线程安全的######这有矛盾吗?你多个请求过来 不就是多个线程访问单例么? ######有线程安全的问题######我也是有些迷惑,假如是单利的话,成员变量 是有危险的,但是如果每次来的都是不同的线程来调用这个单利 也是没有问题的,或者是单例调用单例应该也是没有问题的,就怕是多个线程重复的访问这个单例!######对的,正有此忧虑######这个问题问的好。 Spring默认的却是单例的,多线程和并发量特别大的情况下需要开发人员自己作出选择。singleton, prototype, request等######singleton的适用场合有哪些?######好像是TreadLocal的bean。前兩天專門查了查。######这个应该是spring自行管理的吧?######没错,核心就是ThreadLocal 去解决######对的,这个问题需要编码人员控制。如果多个线程操作同一个对象,是很危险的。特别是并发模式下。顶你,现在国内软件需要刨根问底的人。###### 学习Spring有段时间了,说说我的认识吧:并不是所有的bean都可以配置成单例的。对于Spring的系统,一般都分为Controller,Service,DAO;对于Service,一般会注入DAO,而DAO就回用到数据连接Connection,我们知道Connection是有状态的,在多线程环境下,肯定会遇到问题,Spring为我们考虑到了这种情况,对此做了特殊处理,只要使用Spring提供的线程绑定资源获取工具得到的Connection就是线程安全的,JDBC或iBatis对应DataSourceUtils,Hibernate对应SessionFactoryUtils,从相应的工具类中获取的Connection,通过了ThreadLocal处理,所以不会出现线程安全问题。另外Spring为我们做了更多事情,我们可以不必自己取获取Connection,Spring为我们提供了DAO模板类,JDBC对应JdbcTemplate,Hibernate对应HibernateTemplate,iBatis对应SqlMapClientTemplate,直接使用模板进行数据访问操作完全不用担心线程安全问题(其内部其实也是调用了相应的工具类)。所以我们的DAO完全可以成为singletion 对象。然后只要使用Spring提供的事务管理,我们的Service也同样可以成为singletion 对象。而我们的Controller,如果只注入了Service,而没有其他状态对象,同样可以成为singletion 对象。当然了,如果还包含其他有状态的成员属性,Spring也是无能为力的,这时候只能定义为request或者其他合适的对象了。 希望对你有所帮助。 ######讲的挺好,其实我只是想知道单例的情况,不过还是感谢你,3Q###### 引用来自“妹夫”的答案 不矛盾啊,既然是单列,每次访问都是不同的线程,只要注意线程安全问题就可以了啊。所以在你的bean里面不能有成员变量,这样就不会有并发问题啊。 我只看过struts源码,它在WEB应用方面,对象默认是非单例化的。其实Spring刚开始的时候不是作为WEB方向使用的,而作为最基本的容器使用的。虽然Spring的对象容器是synchronized的,但是只是容器操作时synchronized的,粒度太大。无法保证安全。而并发是测试过程最难定位的,国内和国外行业的区别就在这里。普通测试还行,但是需要定位到代码的BUG所在范围,就比较困难了。

爱吃鱼的程序员 2020-05-30 21:43:41 0 浏览量 回答数 0

回答

IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。 一、BIO 在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。 二、NIO NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题: 在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。 NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。 也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。 BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。 NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。 在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。   HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的http header外,这个链接是一直打开的状态的,这样在NIO处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),这样前面的线程还是可以去接受其他的请求,而后端的应用的处理只需要执行队列里面的就可以了,这样请求处理和后端应用是异步的.当后端处理完,到全局地方得到现场,产生响应,这个就实现了异步处理。 三、AIO 与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道: AsynchronousSocketChannel AsynchronousServerSocketChannel AsynchronousFileChannel AsynchronousDatagramChannel 其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。 BIO是一个连接一个线程。 NIO是一个请求一个线程。 AIO是一个有效请求一个线程。 先来个例子理解一下概念,以银行取款为例: 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写); 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API); 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回); 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

津崎平匡 2020-05-06 09:26:35 0 浏览量 回答数 0

问题

java NIO

java亨利 2019-12-01 21:51:49 1696 浏览量 回答数 0

问题

Redis 和 Memcached 的区别?Redis 的线程模型是什么?【Java问答学堂】31期

剑曼红尘 2020-06-03 20:28:14 28 浏览量 回答数 1

回答

关于线程和线程池的学习,我们可以从以下几个方面入手: 第一,什么是线程,线程和进程的区别是什么 第二,线程中的基本概念,线程的生命周期 第三,单线程和多线程 第四,线程池的原理解析 第五,常见的几种线程池的特点以及各自的应用场景 一、 线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。 二、 线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解: 第一步,是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。而我们在图解中可以看出,线程还具有一个则色的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程 三、 什么是单线程和多线程? 单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解 多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。 并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢? 并发,从宏观方面来说,并发就是同时进行多种时间,实际上,这几种时间,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情 而并发,则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础下完成。 还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,name意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,name,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。这个问题我们应该如何解决那?   有造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程战歌了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。在这里,同步代码块不多进行赘述,可以自行了解。 四,线程池 又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。 线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。 那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor 而我们创建时,一般使用它的子类:ThreadPoolExecutor. public ThreadPoolExecutor(int corePoolSize,                                int maximumPoolSize,                                long keepAliveTime,                                TimeUnit unit,                                BlockingQueue workQueue,                                ThreadFactory threadFactory,                                RejectedExecutionHandler handler)这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数: 又图中,我们可以看出,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收,maximumPoolSize就是线程池中可以容纳的最大线程的数量,而keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,而util,就是计算这个时间的一个单位,workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了知乎,拒绝执行某些任务。 线程池的执行流程又是怎样的呢? 有图我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。 handler的拒绝策略: 有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满              第二种DisCardPolicy:不执行新任务,也不抛出异常              第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行              第四种CallerRunsPolicy:直接调用execute来执行当前任务 五,四种常见的线程池: CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。 SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。 SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。 FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程 作者:weixin_40271838 来源:CSDN 原文:https://blog.csdn.net/weixin_40271838/article/details/79998327 版权声明:本文为博主原创文章,转载请附上博文链接!

auto_answer 2019-12-02 01:56:43 0 浏览量 回答数 0

回答

如果对什么是线程、什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内。 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。 很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程 并行与并发: 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。 并发与并行 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码: void transferMoney(User from, User to, float amount){ to.setMoney(to.getBalance() + amount); from.setMoney(from.getBalance() - amount); } 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。 好了,让我们开始吧。我准备分成几部分来总结涉及到多线程的内容: 扎好马步:线程的状态 内功心法:每个对象都有的方法(机制) 太祖长拳:基本线程类 九阴真经:高级多线程控制类 扎好马步:线程的状态 先来两张图: 线程状态 线程状态转换 各种状态一目了然,值得一提的是"blocked"这个状态:线程在Running的过程中可能会遇到阻塞(Blocked)情况 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable) 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。 此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。内功心法:每个对象都有的方法(机制) synchronized, wait, notify 是任何对象都具有的同步工具。让我们先来了解他们 monitor 他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。 wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。 当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。 再讲用法: synchronized单独使用: 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容 复制代码 public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } } } 复制代码 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。 public class Thread1 implements Runnable { public synchronized void run() { ..do something } } synchronized, wait, notify结合:典型场景生产者消费者问题 复制代码 /** * 生产者生产出来的产品交给店员 */ public synchronized void produce() { if(this.product >= MAX_PRODUCT) { try { wait(); System.out.println("产品已满,请稍候再生产"); } catch(InterruptedException e) { e.printStackTrace(); } return; } this.product++; System.out.println("生产者生产第" + this.product + "个产品."); notifyAll(); //通知等待区的消费者可以取出产品了 } /** * 消费者从店员取产品 */ public synchronized void consume() { if(this.product <= MIN_PRODUCT) { try { wait(); System.out.println("缺货,稍候再取"); } catch (InterruptedException e) { e.printStackTrace(); } return; } System.out.println("消费者取走了第" + this.product + "个产品."); this.product--; notifyAll(); //通知等待去的生产者可以生产产品了 } 复制代码 volatile 多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。 volatile 针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。太祖长拳:基本线程类 基本线程类指的是Thread类,Runnable接口,Callable接口Thread 类实现了Runnable接口,启动一个线程的方法:  MyThread my = new MyThread();  my.start(); Thread类相关方法:复制代码 //当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)public static Thread.yield() //暂停一段时间public static Thread.sleep() //在一个线程中调用other.join(),将等待other执行完后才继续本线程。    public join()//后两个函数皆可以被打断public interrupte() 复制代码 关于中断:它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。Thread.interrupted()检查当前线程是否发生中断,返回booleansynchronized在获锁的过程中是不能被中断的。 中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体 Thread类最佳实践:写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。 如何获取线程中的异常 不能用try,catch来获取线程中的异常Runnable 与Thread类似Callable future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态 ExecutorService e = Executors.newFixedThreadPool(3); //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.Future future = e.submit(new myCallable());future.isDone() //return true,false 无阻塞future.get() // return 返回值,阻塞直到该线程运行结束 九阴真经:高级多线程控制类 以上都属于内功心法,接下来是实际项目中常用到的工具了,Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。1.ThreadLocal类 用处:保存线程的独立变量。对一个线程类(继承自Thread)当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。 实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。2.原子类(AtomicInteger、AtomicBoolean……) 如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized //返回值为booleanAtomicInteger.compareAndSet(int expect,int update) 该方法可用于实现乐观锁,考虑文中最初提到的如下场景:a给b付款10元,a扣了10元,b要加10元。此时c给b2元,但是b的加十元代码约为:复制代码 if(b.value.compareAndSet(old, value)){ return ;}else{ //try again // if that fails, rollback and log} 复制代码 AtomicReference对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号3.Lock类  lock: 在java.util.concurrent包内。共有三个实现: ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock 主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。 区别如下:复制代码 lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。本质上和监视器锁(即synchronized是一样的)能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。和Condition类的结合。性能更高,对比如下图: 复制代码 synchronized和Lock性能对比 ReentrantLock    可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。使用方法是: 1.先new一个实例 static ReentrantLock r=new ReentrantLock(); 2.加锁       r.lock()或r.lockInterruptibly(); 此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch)     3.释放锁    r.unlock() 必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。 ReentrantReadWriteLock 可重入读写锁(读写锁的一个实现)   ReentrantReadWriteLock lock = new ReentrantReadWriteLock()  ReadLock r = lock.readLock();  WriteLock w = lock.writeLock(); 两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码4.容器类 这里就讨论比较常用的两个: BlockingQueueConcurrentHashMap BlockingQueue阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管  道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列),有兴趣可以研究 BlockingQueue在队列的基础上添加了多线程协作的功能: BlockingQueue 除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。 常见的阻塞队列有: ArrayListBlockingQueueLinkedListBlockingQueueDelayQueueSynchronousQueue ConcurrentHashMap高效的线程安全哈希map。请对比hashTable , concurrentHashMap, HashMap5.管理类 管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类 ThreadMXBeanThreadPoolExecutor如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:复制代码 ExecutorService e = Executors.newCachedThreadPool(); ExecutorService e = Executors.newSingleThreadExecutor(); ExecutorService e = Executors.newFixedThreadPool(3); // 第一种是可变大小线程池,按照任务数来分配线程, // 第二种是单线程池,相当于FixedThreadPool(1) // 第三种是固定大小线程池。 // 然后运行 e.execute(new MyRunnableImpl()); 复制代码 该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,本质上,他们都是ThreadPoolExecutor类的各种实现版本。请参见javadoc: ThreadPoolExecutor参数解释 翻译一下:复制代码 corePoolSize:池内线程初始值与最小值,就算是空闲状态,也会保持该数量线程。maximumPoolSize:线程最大值,线程的增长始终不会超过该值。keepAliveTime:当池内线程数高于corePoolSize时,经过多少时间多余的空闲线程才会被回收。回收前处于wait状态unit:时间单位,可以使用TimeUnit的实例,如TimeUnit.MILLISECONDS workQueue:待入任务(Runnable)的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死(starving)threadFactory:线程工厂类,有默认实现,如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。 阿里云优惠券地址https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=nb3paa5b

景凌凯 2019-12-02 01:40:35 0 浏览量 回答数 0

回答

一、Java内存分配     Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域存储不同类型的数据,这些区域的内存分配和销毁的时间也不同,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机管理的内存包括五个运行时数据区域,如下图所示:      1、方法区     方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(包括类的名称、方法信息、成员变量信息)、常量、静态变量、以及编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常。     运行时常量池(Runtime Constant Pool)是方法区的一部分,此区域会在两种情况下存储数据。     (1)class文件的常量池中的数据     class文件中的常量池用于存放编译期生成的各种字面值和常量,这部分内容在类被加载后存放到方法区的运行时常量池中。     字面值:private String name="zhangSan";private int age = 23+3;     常量:private final String TAG = "MainActivity";private final int age = 26;     (2)运行期间生成的常量     运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。String str = "abc".intern();当运行时常量池中存在字符串"abc时,将该字符串的引用返回,赋值给str,否则创建字符串"abc",加入运行时常量池中,并返回引用赋值给str。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。 2、虚拟机栈     虚拟机栈是线程私有的内存空间,每个线程都有一个线程栈,每个方法被执行时都会创建一个栈帧,方法执行完成,栈帧弹出,线程运行结束,线程栈被回收。虚拟机栈就是Java中的方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,这个栈帧用于存储局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表用来存储方法中的局部变量,包括方法中声明的变量以及函数形参。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小,并且在程序执行期间局部变量表的大小是不会改变的。程序中的所有计算过程都是在借助于操作数栈来完成的。指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向当前方法所属的类的运行时常量池。方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。     在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。 3、本地方法栈     本地方法栈也是线程私有的内存空间,本地方法栈与Java栈所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法,有的虚拟机直接把本地方法栈和虚拟机栈合二为一。 4、堆     Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此内存区域的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,在实现上,既可以实现固定大小的,也可以是扩展的。如果堆中没有足够的内存分配给实例,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常。     堆是运行时动态分配内存,对象在没有引用变量指向它的时候,才变成垃圾,但是仍然占着内存,在程序空闲的时候(没有工作线程运行,GC线程优先级最低)或者堆内存不足的时候(GC线程被触发),被垃圾回收器释放掉,由于要在运行时动态分配内存,存取速度较慢。 5、程序计数器     程序计数器的作用可以看做是当前线程所执行的字节码的行号指示。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空。 二、Java内存回收     对于虚拟机栈空间,当方法调用结束后,基本类型变量、引用类型变量、形参占据的空间会被自动释放,但引用类型指向的对象在堆中,堆中的无用内存由垃圾回收线程回收,GC线程优先级最低,只有当没有工作线程存在时GC线程才会执行,或者堆空间不足时会自动触发GC线程工作。除了回收内存,GC线程还负责整理堆中的碎片。 1、四种引用类型     Java中的对象引用分为四种,强引用类型、软引用类型、弱引用类型、虚引用类型。Java中提供这四种引用类型主要有两个目的:第一是可以让程序员通过代码的方式决定某些对象的生命周期;第二是有利于JVM进行垃圾回收。使用软引用和弱引用可以有效的避免oom。软引用关联的对象,只有软引用关联时,才可回收,如果有强引用同时关联,不会回收对象占用的内存,弱引用也如此。 (1)强引用     强引用是使用最普遍的引用,类似Object obj = new Object()、String str = "hello"。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 (2)软引用(SoftReference)     软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用通常用于网页缓存、图片缓存,防止内存溢出,在内存充足的时候,缓存对象会一直存在,在内存不足的时候,缓存对象占用的内存会被垃圾收集器回收。使用示例: public void testSoftReference() { Map<String,SoftReference<Bitmap>> imagesCache = new HashMap<String,SoftReference<Bitmap>>(); Bitmap bitmap = getBitmap(); SoftReference<Bitmap> image1 = new SoftReference<Bitmap>(bitmap); imagesCache.put("image1",image1); SoftReference<Bitmap> result_SoftReference = imagesCache.get("image1"); Bitmap result_Bitmap = result_SoftReference .get(); } import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get()); } } (3)弱引用(WeakReference)     弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,在java中用java.lang.ref.WeakReference类来表示。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以用于:单例类持有一个activity引用时,会造成内存泄露,把activity声明为弱引用,在activity销毁后,垃圾收集器扫描到activity对象时,会回收activity对象的内存。使用示例: public class SingleTon1 { private static final SingleTon1 mInstance = null; private WeakReference<Context> mContext; private SingleTon1(WeakReference<Context> context) { mContext = context; } public static SingleTon1 getInstance(WeakReference<Context> context) { if (mInstance == null) { synchronized (SingleTon1.class) { if (mInstance == null) { mInstance = new SingleTon1(context); } } } return mInstance; } } public class MyActivity extents Activity { public void onCreate (Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.main); SingleTon1 singleTon1 = SingleTon1.getInstance(new WeakReference<Context>(this)); } }import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReference<String> sr = new WeakReference<String>(new String("hello")); System.out.println(sr.get()); System.gc(); //通知JVM的gc进行垃圾回收 System.out.println(sr.get()); } } 输出结果: hellonull     第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。 (4)虚引用     虚引用和软引用、弱引用不同,它并不影响对象的生命周期,也无法通过虚引用来取得一个对象实例,在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用,如下: import java.lang.ref.PhantomReference;import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue); System.out.println(pr.get()); } } 2、垃圾回收算法 (1)标记-清除(Mark-Sweep)    标记-清除(Mark-Sweep)算法,分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 标记-清除算法主要问题是:1、效率问题,标记和清除过程的效率很低2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集 (2)复制(Copying)算法     复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 复制算法的主要问题是:1、复制算法将内存缩小为原来的一半,过于浪费2、对象存活率较高时就要执行较多的复制操作,造成频繁GC,效率将会变低 (3)标记-整理(Mark-Compact)     标记-整理算法的标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样连续的内存空间就比较多了。     如上图所示,所有存活的对象依次向左上角移动,(0,4)移动到(0,2),(1,0)移动到(0,3),依次类推,当所有的存活对象移动完成后,把剩余的所有空间清空,也就是清空(1,1)后的所有空间。 (4)分代回收(generational collection) 程序创建的大部分对象的生命周期都很短,只有一小部分对象的生命周期比较长,根据这样的规律,一般把Java堆分为Young Generation(新生代),Old Generation(老年代)和Permanent Generation(持久代),上面几种算法是通过分代回收混合在一起的,这样就可以根据各个年代的特点采用最适当的回收算法。 (1)新生代     在新生代中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to), 这两个区域大小相等,相当于copying算法中的两个区域,它们用来存放每次垃圾回收后存活下来的对象。在新生代中,垃圾回收一般用Copying的算法,速度快。     当新建对象无法放入eden区时,将触发minor collection(minorGC 是清理新生代的GC线程,eden的清理,from、to的清理都由MinorGC完成),将eden区与from区的存活对象复制到to区,经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象;当eden区再次满时,minor collection将eden区和to区的存活对象复制到from区,eden区和to区被清空,from区存放eden区和to区的存活对象,就这样from区和to区来回切换。如果进行minor collection的时候,发现to区放不下,则将eden区和from区的部分对象放入成熟代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟代。 (2)成熟代     在成熟代中主要存放应用程序中生命周期长的内存对象,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。如果成熟代放满对象,无法从新生代移入新的对象,那么将触发major collection(major GC清理整合OldGen的内存空间)。 (3)永久代    在永久代中,主要用来放JVM自己的反射对象,比如类对象、方法对象、成员变量对象、构造方法对象等。     此外,垃圾回收一般是在程序空闲的时候(没有工作线程,GC线程优先级较低)或者堆内存不足的时候自动触发,也可以调用System.gc()主动的通知Java虚拟机进行垃圾回收,但这只是个建议,Java虚拟机不一定马上执行,启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用 作者:喜六六 来源:CSDN 原文:https://blog.csdn.net/qq_29078329/article/details/78929457 版权声明:本文为博主原创文章,转载请附上博文链接!

auto_answer 2019-12-02 01:50:42 0 浏览量 回答数 0

回答

1.阻塞与同步2.BIO与NIO对比3.NIO简介4.缓冲区Buffer5.通道Channel6.反应堆7.选择器8.NIO源码分析9.AIO1.阻塞与同步1)阻塞(Block)和非租塞(NonBlock):阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候阻塞:往往需要等待缞冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回2)同步(Synchronization)和异步(Async)的方式:同步和异步都是基于应用程序私操作系统处理IO事件所采用的方式,比如同步:是应用程序要直接参与IO读写的操作。异步:所有的IO读写交给搡作系统去处理,应用程序只需要等待通知。同步方式在处理IO事件的时候,必须阻塞在某个方法上靣等待我们的IO事件完成(阻塞IO事件或者通过轮询IO事件的方式).对于异步来说,所有的IO读写都交给了搡作系统。这个时候,我们可以去做其他的事情,并不拓要去完成真正的IO搡作,当搡作完成IO后.会给我们的应用程序一个通知同步:阻塞到IO事件,阻塞到read成则write。这个时候我们就完全不能做自己的事情,让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大,参考:https://blog.csdn.net/CharJay_Lin/article/details/812598802.BIO与NIO对比block IO与Non-block IO1)区别IO模型 IO NIO方式 从硬盘到内存 从内存到硬盘通信 面向流(乡村公路) 面向缓存(高速公路,多路复用技术)处理 阻塞IO(多线程) 非阻塞IO(反应堆Reactor)触发 无 选择器(轮询机制)2)面向流与面向缓冲Java NIO和IO之间第一个最大的区别是,IO是面向流的.NIO是面向缓冲区的。Java IO面向流意味着毎次从流中读一个成多个字节,直至读取所有字节,它们没有被缓存在任何地方,此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的教据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,霱要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数裾。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。3)阻塞与非阻塞Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。4)选择器(Selector)Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择"通道:这些通里已经有可以处理的褕入,或者选择已准备写入的通道。这选怿机制,使得一个单独的线程很容易来管理多个通道。5)NIO和BIO读取文件BIO读取文件:链接BIO从一个阻塞的流中一行一行的读取数据image | left | 469x426NIO读取文件:链接通道是数据的载体,buffer是存储数据的地方,线程每次从buffer检查数据通知给通道image | left | 559x3946)处理数据的线程数NIO:一个线程管理多个连接BIO:一个线程管理一个连接3.NIO简介在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理处理,每一个操作在一步中产生或者消费一个数据库,按块处理要比按字节处理数据快的多。在NIO中有几个核心对象需要掌握:缓冲区(Buffer)、通道(Channel)、选择器(Selector)。参考:链接image2.png | center | 851x3834.缓冲区Buffer缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示:image3.png | center | 650x3681)其中的四个属性的含义分别如下:容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一个容量在缓冲区创建时被设定,并且永远不能改变。上界(Limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。位置(Position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。标记(Mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。2)Buffer的常见方法如下所示:flip(): 写模式转换成读模式rewind():将 position 重置为 0 ,一般用于重复读。clear() :compact(): 将未读取的数据拷贝到 buffer 的头部位。mark(): reset():mark 可以标记一个位置, reset 可以重置到该位置。Buffer 常见类型: ByteBuffer 、 MappedByteBuffer 、 CharBuffer 、 DoubleBuffer 、 FloatBuffer 、 IntBuffer 、 LongBuffer 、 ShortBuffer 。3)基本操作Buffer基础操作: 链接缓冲区分片,缓冲区分配,直接缓存区,缓存区映射,缓存区只读:链接4)缓冲区存取数据流程存数据时position会++,当停止数据读取的时候调用flip(),此时limit=position,position=0读取数据时position++,一直读取到limitclear() 清空 buffer ,准备再次被写入 (position 变成 0 , limit 变成 capacity) 。5.通道Channel通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。image4.png | center | 368x191在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。它们之间的继承关系如下图所示:image5.png | center | 650x5171)使用NIO读取数据在前面我们说过,任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。所以使用NIO读取数据可以分为下面三个步骤:从FileInputStream获取Channel 创建Buffer 将数据从Channel读取到Buffer中 例子:链接 2)使用NIO写入数据使用NIO写入数据与读取数据的过程类似,同样数据不是直接写入通道,而是写入缓冲区,可以分为下面三个步骤:从FileInputStream获取Channel 创建Buffer 将数据从Channel写入到Buffer中 例子:链接 6.反应堆1)阻塞IO模型在老的IO包中,serverSocket和socket都是阻塞式的,因此一旦有大规模的并发行为,而每一个访问都会开启一个新线程。这时会有大规模的线程上下文切换操作(因为都在等待,所以资源全都被已有的线程吃掉了),这时无论是等待的线程还是正在处理的线程,响应率都会下降,并且会影响新的线程。image6.png | center | 739x3362)NIOJava NIO是在jdk1.4开始使用的,它既可以说成“新IO”,也可以说成非阻塞式I/O。下面是java NIO的工作原理:1.由一个专门的线程来处理所有的IO事件,并负责分发。2.事件驱动机制:事件到的时候触发,而不是同步的去监视事件。3.线程通讯:线程之间通过wait,notify等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。image7.png | center | 689x251注:每个线程的处理流程大概都是读取数据,解码,计算处理,编码,发送响应。7.选择器传统的 server / client 模式会基于 TPR ( Thread per Request ) .服务器会为每个客户端请求建立一个线程.由该线程单独负贵处理一个客户请求。这种模式带未的一个问题就是线程数是的剧增.大量的线程会增大服务器的开销,大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这又带来了新的问题,如果线程池中有 200 个线程,而有 200 个用户都在进行大文件下载,会导致第 201 个用户的请求无法及时处理,即便第 201 个用户只想请求一个几 KB 大小的页面。传统的 Sorvor / Client 模式如下围所示:image8.png | center | 597x286NIO 中非阻塞IO采用了基于Reactor模式的工作方式,IO调用不会被阻塞,相反是注册感兴趣的特点IO事件,如可读数据到达,新的套接字等等,在发生持定率件时,系统再通知我们。 NlO中实现非阻塞IO的核心设计Selector,Selector就是注册各种IO事件的地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件。image9.png | center | 462x408当有读或者写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。使用NIO中非阻塞IO编写服务器处理程序,有三个步骤1.向Selector对象注册感兴趣的事件2.从Selector中获取感兴趣的事件3.根据不同事件进行相应的处理8.NIO源码分析Selector是NIO的核心epool模型1)SelectorSelector的open()方法:链接2)ServerSocketChannelServerSocketChannel.open() 链接9.AIOAsynchronous IO异步非阻塞IOBIO ServerSocketNIO ServerSocketChannelAIO AsynchronousServerSocketChannel

wangccsy 2019-12-02 01:46:51 0 浏览量 回答数 0

问题

我需要清理Java中的Thread对象吗?

小六码奴 2019-12-01 20:01:01 10 浏览量 回答数 1

回答

Java - Java编程语言(新版)介绍 Java 基本语法、Java 平台应用、 Java 的核心概念:JVM、JDK、JRE以及 java 面向对象思想,同时会学到如何在系统中搭建 Java 开发环境,以及如何利用第三方工具进行 Java 程序的开发。Java - Java进阶之设计模式介绍常用的设计模式以及 Java 语言的实现实例来学习 java 设计模式。从中我们可以学习到很多类型的设计模式,其中包括工厂模式、抽象工厂模式、单例模式、适配器模式、观察者模式、装饰者模式等等。Java - JDK 核心 API学习包括 java.lang 包,java.util 包,http://java.io 包以及泛型的相关知识SQL - MySQL基础课程该教程实验内容从MySQL的安装开始,介绍了MySQL基础、常用的操作,内容较为简单,如果想要更深入地学习SQL,请学习实验楼其他相关课程。Linux - 正则表达式基础在Linux Shell环境中学习正则表达式基本概念,并实践如何使用正则表达式对文本字符串进行处理。Java - JDBC 入门教程本实验通过学习 JDBC 定义和架构,回顾 SQL 语法,搭建 JDBC 的环境,通过实例来深入学习 JDBC。从中我们将学习到如何用 java 连接到数据库,并练习编写了一个信息管理的程序,在此基础上可以提高自己的数据库管理能力。Java - Java 8 新特性指南Java 8是近年来一个Java编程语言发行版本,由Oracle 2014年3月发布。该版本为Java带来许多新特性,是一个具有重大改变的版本。 本教程适用于Java初学者或者是具有一定编程经验的开发者,学习该课为自己的技能升级打补丁。Java - J2SE核心开发实战java基础的进阶课程,主要讲解IO、Util等常用类库的使用、Swing图形化编程、多线程编程等知识点。

inzaghi1984 2019-12-02 00:32:32 0 浏览量 回答数 0

问题

Java技术1000问(3)【精品问答】

问问小秘 2020-06-02 14:27:10 42 浏览量 回答数 1

问题

java后台接收请求处理是单线程的吗? 400 请求报错 

kun坤 2020-05-30 14:36:44 0 浏览量 回答数 1

回答

不矛盾啊,既然是单列,每次访问都是不同的线程,只要注意线程安全问题就可以了啊。所以在你的bean里面不能有成员变量,这样就不会有并发问题啊。######回复<aclass="referer"target="_blank">@HUncle:no3k,我也是新手,自己的理解而已。######回复<aclass="referer"target="_blank">@妹夫:对,单例特别方便,但是有隐患,3Q######回复<aclass="referer"target="_blank">@妹夫:还有一个就是单例能在程序中随处获得实例,很方便。个人觉得。######回复<aclass="referer"target="_blank">@HUncle:既然用了spring,都知道spring有依赖注入这么个东西,我们可以把需要依赖的bean交给spring去管理。如果使用了成员变量,操作它的时候就必须保证同步,这就是我说为什么不能使用成员变量,至少我没见过别人系统里会使用spring然后用同步方法去操作内部依赖的bean的。######回复<aclass="referer"target="_blank">@HUncle:单例能保证程序中这个实例是唯一的,减少java的内存回收,所以性能高。我不知道别人在哪些方面用单例,但我在写GUI程序的时候单例用的多,因为GUI程序并发少。并不是说不能存在成员变量,如果存在成员变量的时候你去操作它的时候必须保证线程同步,java中用synchronized。大量使用synchronized会导致性能降低######搜索一下<spanstyle="font-family:Arial;font-size:14px;line-height:26px;background-color:#FFFFFF;">Bean的作用域,会找到你要的答案。######我说的就是singletion的情形,此情况下如何处理多请求?######单例多线程######不知道底层如何规避线程安全的######这有矛盾吗?你多个请求过来不就是多个线程访问单例么?######有线程安全的问题######我也是有些迷惑,假如是单利的话,成员变量是有危险的,但是如果每次来的都是不同的线程来调用这个单利也是没有问题的,或者是单例调用单例应该也是没有问题的,就怕是多个线程重复的访问这个单例!######对的,正有此忧虑######这个问题问的好。Spring默认的却是单例的,多线程和并发量特别大的情况下需要开发人员自己作出选择。singleton,prototype,request等######singleton的适用场合有哪些?######好像是TreadLocal的bean。前兩天專門查了查。######这个应该是spring自行管理的吧?######没错,核心就是ThreadLocal去解决######对的,这个问题需要编码人员控制。如果多个线程操作同一个对象,是很危险的。特别是并发模式下。顶你,现在国内软件需要刨根问底的人。###### 学习Spring有段时间了,说说我的认识吧:并不是所有的bean都可以配置成单例的。对于Spring的系统,一般都分为Controller,Service,DAO;对于Service,一般会注入DAO,而DAO就回用到数据连接Connection,我们知道Connection是有状态的,在多线程环境下,肯定会遇到问题,Spring为我们考虑到了这种情况,对此做了特殊处理,只要使用Spring提供的线程绑定资源获取工具得到的Connection就是线程安全的,JDBC或iBatis对应DataSourceUtils,Hibernate对应SessionFactoryUtils,从相应的工具类中获取的Connection,通过了ThreadLocal处理,所以不会出现线程安全问题。另外Spring为我们做了更多事情,我们可以不必自己取获取Connection,Spring为我们提供了DAO模板类,JDBC对应JdbcTemplate,Hibernate对应HibernateTemplate,iBatis对应SqlMapClientTemplate,直接使用模板进行数据访问操作完全不用担心线程安全问题(其内部其实也是调用了相应的工具类)。所以我们的DAO完全可以成为<spanstyle="color:#FF6600;font-family:Verdana,sans-serif,宋体;line-height:normal;background-color:#FFFFFF;">singletion<spanstyle="color:#000000;">对象。然后只要使用Spring提供的事务管理,我们的Service也同样可以成为<spanstyle="color:#FF6600;font-family:Verdana,sans-serif,宋体;line-height:normal;background-color:#FFFFFF;">singletion<spanstyle="color:#000000;">对象。而我们的Controller,如果只注入了Service,而没有其他状态对象,同样可以成为<spanstyle="color:#FF6600;font-family:Verdana,sans-serif,宋体;line-height:normal;background-color:#FFFFFF;">singletion <spanstyle="color:#000000;">对象。当然了,如果还包含其他有状态的成员属性,Spring也是无能为力的,这时候只能定义为request或者其他合适的对象了。 希望对你有所帮助。######讲的挺好,其实我只是想知道单例的情况,不过还是感谢你,3Q######<divclass="ref"> 引用来自“妹夫”的答案<divclass="ref_body">不矛盾啊,既然是单列,每次访问都是不同的线程,只要注意线程安全问题就可以了啊。所以在你的bean里面不能有成员变量,这样就不会有并发问题啊。<divclass="a_body">我只看过struts源码,它在WEB应用方面,对象默认是非单例化的。其实Spring刚开始的时候不是作为WEB方向使用的,而作为最基本的容器使用的。虽然Spring的对象容器是synchronized的,但是只是容器操作时synchronized的,粒度太大。无法保证安全。而并发是测试过程最难定位的,国内和国外行业的区别就在这里。普通测试还行,但是需要定位到代码的BUG所在范围,就比较困难了。

优选2 2020-06-09 15:38:13 0 浏览量 回答数 0

问题

Map和List性能测试:报错

kun坤 2020-06-09 12:10:33 0 浏览量 回答数 1

回答

一.Lock接口(java.util.concurrent.locks): void lock():获取锁,阻塞方式;如果资源已被其他线程锁定,那么lock将会阻塞直到获取锁,锁阻塞期间不受线程的Interrupt的影响,在获取锁成功后,才会检测线程的interrupt状态,如果interrupt=true,则抛出异常。 unlock():释放锁 tryLock():尝试获取锁,并发环境中"闯入"行为,如果有锁可用,直接获取锁并返回true,否则范围false. lockInterruptibly():尝试获取锁,并支持"中断"请求。与lock的区别时,此方法的开始、结束和执行过程中,都会不断检测线程的interrupt状态,如果线程被中断,则立即抛出异常;而不像lock方法那样只会在获取锁之后才检测。 二.Lock接口实现类 Lock直接实现,只有3个类:ReentrantLock和WriteLock/ReadLock;这三种锁;Lock和java的synchronized(内置锁)的功能一致,均为排他锁. ReentrantLock为重入排他锁,对于同一线程,如果它已经持有了锁,那么将不会再次获取锁,而直接可以使用. ReentrantReadWriteLock并没有继承ReentrantLock,而是一个基于Lock接口的单独实现.它实现了 ReadWriteLock,即读写分离锁,是一种采用锁分离技巧的API. 尽管在API级别ReentrantReadWriteLock和ReentrantLock没有直接继承关系,但是ReentrantReadWriteLock中的ReadLock和WriteLock都具有ReentrantLock的全部语义(简单说,就是把ReentrantLock的代码copy了一下.),即锁的可重入性.WriteLock支持Condition(条件),ReadLock不支持. Lock的实现类中,都包含了2中锁等待策略:公平和非公平;其实他们的实现也非常简单,底层都是使用了queue来维持锁请求顺序.[参考:http://shift-alt-ctrl.iteye.com/blog/1839142] 公平锁,就是任何锁请求,首先将请求加入队列,然后再有队列机制来决定,是阻塞还是分配锁. 非公平,就是允许"闯入",当然公平锁,也无法干扰"闯入",对于任何锁请求,首先检测锁状态是否可用,如果可用直接获取,否则加入队列.. ReentrantLock本质上和synchronized修饰词是同一语义,如果一个线程lock()之后,其他线程进行lock时必须阻塞,直到当前线程的前续线程unlock.[执行lock操作时,将会被队列化(假如在公平模式下),获取lock的线程都将具有前续/后继线程,前续线程就是当前线程之前执行lock操作而阻塞的线程,后继线程就是当前线程之后执行lock操作的线程;那么对于unlock操作就是"解锁"信号的传递,如果当前线程unlock,那么将会触发后继线程被"唤醒",即它因为lock操作阻塞状态被解除.];这是ReentrantLock的基本原理,但是当ReentrantLock在Conditon情况下,事情就变得更加复杂.[参加下述] 三.Condition:锁条件 Condition与Lock形成happen-before关系。Condition将Object的监视器方法(wait,notify,notifyAll)分解成截然不同的对象,以便通过这些对象与任意Lock实现组合。使Lock具有等待“集合”的特性,或者“类型”;Lock替代了synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。(synchronized + object.wait对应Lock + Condition.await) Condition又称条件队列,为线程提供了一个含义,以便在某种状态条件现在可能为true的其他线程通知它之前,一直挂起该线程。即多个线程,其中一个线程因为某个条件而阻塞,其他线程当“条件”满足时,则“通知”哪些阻塞的线程。这,几乎和object中wait和notify的机制一样。 Condition和wait一样,阻塞时也将原子性的释放锁(间接执行了release()方法)。并挂起线程。Condition必须与Lock形成关系,只有获取lock权限的,才能进行Condition操作。Condition底层基于AQS实现,条件阻塞,将以队列的方式,LockSupport支持。其实现类有ConditionObject,这也是Lock.newCondition()的返回实际类型,在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition 应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。 void await() throws InterruptedException:当前线程阻塞,并原子性释放对象锁。如下条件将触发线程唤醒: 当线程被中断(支持中断响应), 其他线程通过condition.signal()方法,且碰巧选中当前线程唤醒 其他线程通过condition.signalAll()方法 发生虚假唤醒 底层实现,await()方法将当前线程信息添加到Conditon内部维护的"await"线程队列的尾部(此队列的目的就是为singal方法保持亟待唤醒的线程的顺序),然后释放锁(执行tryRelease()方法,注意此处释放锁,仅仅是释放了锁信号,并不是unlock,此时其他线程仍不能获取锁--lock方法阻塞),然后使用LockSupport.park(this)来强制剥夺当前线程执行权限。await方法会校验线程的中断标记。 由此可见,await()方法执行之后,因为已经"归还"了锁信号,那么其他线程此时执行lock方法,将不再阻塞.. void awaitUninterruptibly():阻塞,直到被唤醒。此方法不响应线程中断请求。即当线程被中断时,它将继续等待,直到接收到signal信号(你应该能想到"陷阱"),当最终从此方法返回时,仍然将设置其中断状态。 void signal()/signalAll():唤醒一个/全部await的线程。 对于signal()方法而言,底层实现为,遍历await"线程队列,找出此condition上最先阻塞的线程,并将此阻塞线程unpark.至此为止,我们似乎发现"锁信号"丢失了,因为在线程await时通过tryRelease时释放了一次信号.那么被signal成功的线程,首先执行一次acquire(增加锁信号),然后校验自己是否被interrupted,如果锁信号获取成功且线程状态正常,此时才正常的从await()方法退出.经过这么复杂的分析,终于明白了ReentrantLock + Condition情况下,锁状态变更和线程控制的来龙去脉... Java代码 收藏代码 //////例子: private Lock lock = new ReentrantLock(); private Condition full = lock.newCondition(); private Condition empty = lock.newCondition(); public Object take(){ lock.lock(); try{ while(isEmpty()){ empty.await() } Object o = get() full.signalAll(); return o; }finally{ lock.unlock(); } } public void put(Object o){ lock.lock(); try{ while(isFull()){ full.await(); } put(o); empty.signalAll(); }finally{ lock.unlock(); } } 四.机制 Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。 Lock接口具有的方法: void lock():获取锁,阻塞直到获取。 void lockInterruptibly() throws InterrutedException:获取锁,阻塞直到获取成功,支持中断响应。 boolean tryLock():尝试获取锁,返回是否获取的结果。如果碰巧获取成功,则返回true,此时已经持有锁。 boolean tryLock(long time,TimeUnit) throws InterruptedException:尝试获取锁,获取成功返回true,超时时且没有获取锁则返回false。 void unlock():释放锁。约定只有持有锁者才能释放锁,否则抛出异常。 void newCondition():返回绑定到lock的条件。 五.ReadWriteLock ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer(写锁),读取锁可以由多个 reader 线程同时保持(共享锁)。写入锁是独占的。所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。 与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。 Lock readLock():返回读锁。 Lock writeLock():返回写锁。 六.ReentrantLock ReentrantLock,重入排它锁,它和synchronized具有相同的语义以及在监视器上具有相同的行为,但是功能更加强大。 ReetrantLock将由最近成功获得锁且还没有释放锁的线程标记为“锁占有者”;当锁没有被线程持有时,调用lock方法将会成功获取锁并返回,如果当前线程为锁持有者,再次调用lock将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ReentrantLock的构造方法,允许接收一个“公平策略”参数,“公平策略”下,多个线程竞争获取锁时,将会以队列化锁请求者,并将锁授予队列的head。在“非公平策略”下,则不完全保证锁获取的顺序,允许闯入行为(tryLock)。 ReentrantLock基于AQS机制,锁信号量为1,如果信号量为1且当前锁持有者不为自己,则不能获取锁。释放锁时,如果当前锁持有者不是自己,也将抛出“IllegalMonitorStateException”。由此可见,对于ReentrantLock,lock和release方法是需要组合出现。 七.ReentrantReadWriteLock:可重入读写分离锁 重入性 :当前线程可以重新获取相应的“读锁”或者“写锁”,在写入线程保持的所有写入锁都已经释放后,才允许重入reader(读取线程)使用它们。writer线程可以获取读锁,但是reader线程却不能直接获取写锁。 锁降级:重入还允许写入锁降级为读锁,其实现方式为:先获取写入锁,然后获取读取锁,最后释放写入锁。但是读取锁不能升级为写入锁。 Conditon的支持:只有写入锁支持conditon,对于读取锁,newConditon方法直接抛出UnsupportedOperationException。 ReentrantReadWriteLock目前在java api中无直接使用。ReentrantReadWriteLock并没有继承自 ReentrantLock,而是单独重新实现。其内部仍然支持“公平性”“非公平性”策略。 ReentrantReadWriteLock基于AQS,但是AQS只有一个state来表示锁的状态,所以如果一个state表示2种类型的锁状态,它做了一个很简单的策略,“位运算”,将一个int类型的state拆分为2个16位段,左端表示readlock锁引用计数,右端16位表示write锁。在readLock、writeLock进行获取锁或者释放锁时,均是通过有效的位运算和位控制,来达到预期的效果。 八.ReadLock void lock():获取读取锁,伪代码如下: Java代码 收藏代码 //如果当前已经有“写锁”,且持有写锁者不是当前线程(如果是当前线程,则支持写锁,降级为读锁),则获取锁失败 //即任何读锁的获取,必须等待队列中的写锁释放 //c为实际锁引用量(exclusiveCount方法实现为:c & ((1<<16) -1) if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current) return -1; //CAS操作,操作state的左端16位。 if(CAS(c,c + (1<<16))){ return 1; } void unlock():释放read锁,即共享锁,伪代码如下: Java代码 收藏代码 //CAS锁引用 for (;;) { int c = getState(); int nextc = c - (1<<16);//位操作,释放一个锁。 if (compareAndSetState(c, nextc)) return nextc == 0; } 九.WriteLock void lock():获取写入锁,伪代码如下: Java代码 收藏代码 //当前线程 Thread current = Thread.currentThread(); //实际的锁引用state int c = getState(); //右端16位,通过位运算获取“写入锁”的state int w = exclusiveCount(c); //如果有锁引用 if (c != 0) { //且所引用不是自己 if (w == 0 || current != getExclusiveOwnerThread()){ return false; } } //如果写入锁state为0,且CAS成功,则设置state和独占线程信息 if ((w == 0 && writerShouldBlock(current)) ||!compareAndSetState(c, c + acquires)){ return false; } setExclusiveOwnerThread(current); return true; void unlock():释放写入锁,伪代码如下: Java代码 收藏代码 //计算释放锁的信号量 int nextc = getState() - releases; //对于写入锁,则校验当前线程是否为锁持有者,否则不可以释放(死锁) if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); //释放锁,且重置独占线程信息 if (exclusiveCount(nextc) == 0) { setExclusiveOwnerThread(null); setState(nextc); return true; } else { setState(nextc); return false; } 十.LockSupport:用来创建锁和其他同步类的基本线程阻塞原语。 底层基于hotspot的实现unsafe。park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法。三种形式的 park(即park,parkNanos(Object blocker,long nanos),parkUntil(Object blocker,long timestamp)) 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。 在锁实现中提供的作为 blocker 的普通参数是 this。 static void park(Object blocker):阻塞当前线程,直到如下情况发生: 其他线程,调用unpark方法,并将此线程作为目标而唤醒 其他线程中断当前线程此方法不报告,此线程是何种原因被放回,需要调用者重新检测,而且此方法也经常在while循环中执行 Java代码 收藏代码 while(//condition,such as:queue.isEmpty){ LockSupport.park(queue);//此时queue对象作为“阻塞”点传入,以便其他监控工具查看,queue的状态 //检测当前线程是否已经中断。 if(Thread.interrupted()){ break; } } void getBlocker(Thread t):返回提供最近一次尚未解除阻塞的park的阻塞点。可以返回null。 void unpark(Thread t):解除指定线程阻塞,使其可用。参数null则无效果。 LockSupport实例(不过不建议在实际代码中直接使用LockSupport,很多时候,你可以使用锁来控制): Java代码 收藏代码 /////////////Demo public class LockSupportTestMain { /** * @param args */ public static void main(String[] args) throws Exception{ System.out.println("Hear!"); BlockerObject blocker = new BlockerObject(); LThread tp = new LThread(blocker, false); LThread tt = new LThread(blocker, true); tp.start(); tt.start(); Thread.sleep(1000); } static class LThread extends Thread{ private BlockerObject blocker; boolean take; LThread(BlockerObject blocker,boolean take){ this.blocker = blocker; this.take = take; } @Override public void run(){ if(take){ while(true){ Object o = blocker.take(); if(o != null){ System.out.println(o.toString()); } } }else{ Object o = new Object(); System.out.println("put,,," + o.toString()); blocker.put(o); } } } static class BlockerObject{ Queue<Object> inner = new LinkedList<Object>(); Queue<Thread> twaiters = new LinkedList<Thread>(); Queue<Thread> pwaiters = new LinkedList<Thread>(); public void put(Object o){ inner.offer(o); pwaiters.offer(Thread.currentThread()); Thread t = twaiters.poll(); if(t != null){ LockSupport.unpark(t); } System.out.println("park"); LockSupport.park(Thread.currentThread()); System.out.println("park is over"); } public Object take(){ Thread t = pwaiters.poll(); if(t != null){ System.out.println("unpark"); LockSupport.unpark(t); System.out.println("unpark is OK"); } //twaiters.offer(Thread.currentThread()); return inner.poll(); } } } 备注:有时候会疑惑wait()/notify() 和Unsafe.park()/unpark()有什么区别?区别是wait和notify是Object类的方法,它们首选需要获得“对象锁”,并在synchronized同步快中执行。park和unpark怎不需要这么做。wait和park都是有当前线程发起,notify和unpark都是其他线程发起。wait针对的是对象锁,park针对的线程本身,但是最终的效果都是导致当前线程阻塞。Unsafe不建议开发者直接使用。

景凌凯 2020-04-24 16:41:16 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 企业信息查询 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站