1、CAS
- Conmpare And Swap比较和交换,主要用于多个线程对共享内存的变量(全局变量)操作时的线程安全问题。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。
- 一个 CAS 涉及到以下操作
我们假设内存中的原数据V,旧的预期值A(线程从共享内存中取出的数据),需要修改的新值B。
比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。(交换)
返回操作是否成功。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS其实是一个乐观锁。
下图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。
ABA 问题
CAS 由三个步骤组成,分别是“读取->比较->写回”。
考虑这样一种情况,线程1和线程2同时执行 CAS 逻辑,两个线程的执行顺序如下:
时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走
时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B
时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A
时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。
然后用新值(newValue)写入内存中,完成 CAS 操作
如上流程,线程1并不知道原值已经被修改过了,在它看来并没什么变化,所以它会继续往下执行流程。
ABA的影响:比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化
对于 ABA 问题,通常的处理措施是对每一次 CAS 操作设置版本号
ABA问题的解决办法
1.在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
2.atomic包下的AtomicStampedReference类:其compareAndSet方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。
其他问题
CAS除了ABA问题,仍然存在循环时间长开销大和只能保证一个共享变量的原子操作
-
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 -
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁
2、我们经常使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:
(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中
在单线程状态下这个操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象。如何去解决这个问题呢?最常见的方式就是使用AtomicInteger来修饰a。而AtomicInteger底层就是由CAS实现的
3、synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
4、synchronized与Lock的区别
- synchronized是java内置关键字,在jvm层面,Lock是个java接口,有多种锁的实现类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- Lock只有代码块锁,而synchronized既有代码块锁,也有方法锁。
5、java.util.concurrent.locks包下常用的类
- Lock
Lock接口中的方法:lock()、tryLock()、tryLock(long time, TimeUnit
unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 - ReentrantLock
ReentrantLock,意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类 - ReadWriteLock
ReadWriteLock也是一个接口,只有两个方法,一个用来获取读锁,一个用来获取写锁 - ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口
6、锁的相关概念
- 可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2 - 可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()的用法时已经体现了Lock的可中断性。 - 公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
7、线程池
频繁创建(new)线程和销毁线程需要消耗大量时间资源(其实java中不仅是线程,创建和销毁任何对象都是要消耗不少资源的)
使用线程池使得线程可以复用:每个任务过来,就去线程池里面拿线程,处理完后,把线程放回线程池,避免频繁创建线程
- 在ThreadPoolExecutor类中有几个非常重要的方法:
execute()
submit() //实际上也是调用execute()方法
shutdown() //等待所有任务执行完毕再关掉线程池
shutdownNow() //立即关闭线程池 - ThreadPoolExecutor构造函数中的重要参数
public ThreadPoolExecutor(int corePoolSize, intmaximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,hreadFactory threadFactory,RejectedExecutionHandler handler)
1)corePoolSize(核心池大小)和maximumPoolSize
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
corePoolSize就是常规线程池大小,maximumPoolSize可以看成是线程池的一种补救措施,即任务量突然过大时的一种补救措施,最大的线程池只能到这里。
2)workQueue该线程池中的任务队列:维护着等待执行的 Runnable 对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
-
不过java不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了:
Executors.newCachedThreadPool(); //创建一个缓冲线程池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池
即上面的线程池的创建其实是调用类似下面的方法,只不过Java不推荐我们这样用
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue(5)); -
线程池工作原理
-
当提交一个新任务到线程池中时,线程池的处理流程如下:
1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
8、Java中线程间通信:
- syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
- ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
- 利用volatile
- 利用AtomicInteger
9、悲观锁适合写多读少,要确保数据安全的场景。悲观锁适合读多写少,提高系统吞吐的场景
10、垃圾回收器
查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version
jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk9环境下,默认使用G1回收器
垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存,内存泄露是指该内存空间使用完毕之后未回收
垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作
几个相关概念:
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
垃圾回收器分类:
-
1)Serial收集器(新生代收集器)(复制算法)
Serial收集器是一个基本的,古老的新生代垃圾收集器。这个垃圾收集器是一个单线程的收集器,在它进行垃圾回收的时候,其他的工作线程都会被暂停,直到它收集结束。这也就是说,如果Jvm参数配置有问题或者内存不够,导致频繁的gc,可能每隔一段时间应用就会暂停响应.尽管Serial收集器有如此多的缺点,但是从JDK1.3开始到JDK1.7都一直是默认的运行在Client模式下的新生代收集器。原因在于Serial收集器是一个简单高效的收集器,没有线程切换的开销等等,在一般的Client应用中需要回收的内存也不是很大,垃圾回收停顿的时间不是很长,是可以接受的 -
2)ParNew收集器(新生代收集器)(复制算法)
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为跟Serial收集器一样,进行垃圾回收的时候,其他工作的线程都会被暂停虽然ParNew收集器是多线程收集,但是它的性能并不一定比Serial收集器好。因为线程切换等开销的因素,在单CPU环境中它的性能是不如Serial收集器的,就算有2个CPU也不一定能说绝对比Serial好。但是随着CPU核数的增多,其最终效果肯定是优于Serial收集器的 -
3)Parallel Scavenge收集器(新生代收集器)(复制算法)
ParNew收集器也是并行的多线程收集器,主要关注的是回收内存的速度,尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。 -
4)Serial Old收集器(老年代)(标记-整理算法)
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器 -
5)Parallel Old收集器(老年代)(标记-整理算法) Parallel Old收集器是Parallel
Scavenge收集器的老年代版本。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。 -
6)CMS收集器(标记-清除算法)
-
7)G1收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
小数据量和小型应用,使用串行垃圾回收器即可。
对于对响应时间无特殊要求的,可以使用并行垃圾回收器和并发标记垃圾回收器。(中大型应用)
对于heap可以分配很大的中大型应用,使用G1垃圾回收器比较好,进一步优化和减少了GC暂停时间。
垃圾回收算法: -
1)引用计数法
假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计算器的值为0,就说明对象A没有引用了,可以被回收
无法解决循环引用问题。(最大的缺点) -
2)标记清除算法 是将垃圾回收分为2个阶段,分别是标记和清除
-
3)标记整理算法
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。 -
4) 复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
56、java中有两种方法:JAVA方法和本地方法。JAVA方法由JAVA编写,编译成字节码,存储在class文件中;本地方法由其它语言编写的,编译成和处理器相关的机器代码
11、java内存区域: -
(1)程序计数器
一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器线程私有,JVM内存中唯一没有定义OOM的区域 -
(2)虚拟机栈
运行java方法。线程私有,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用这个线程的一个方法就会为每个方法生成一个栈帧(Stack
Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等 -
(3)本地方法栈:运行本地方法,其他跟虚拟机栈差不多。线程私有
-
(4)堆区:
JVM内存中最大的一块。也是JVM GC管理的主要区域。存储对象实例。所有线程共享。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展。则OOM -
(5)方法区 线程共享,存储已经被虚拟机加载的类信息、常量、静态变量(static)、即时编译器编译后的代码等。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(对于HotSpot虚拟机来说),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
字符串常量池中的字符串只存在一份 String s1 = “hello,world!”; String s2 =
“hello,world!”; 即执行完第一行代码后,常量池中已存在
“hello,world!”,那么s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。
直接内存:除了JVM内存外的内存,比如机器一共有8G内存,JVM占了2G,直接内存就是6G
12、一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。以最简单的本地变量引用:Object obj = new Object()为例:
Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
new Object()作为实例对象数据存储在堆中;
堆中还记录了这个类–Object类本身的的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
13、GC
根据对象的存活时间,把对象划分到不同的内存区域:年轻代,年老代,永久代
-
(1)年轻代:对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,MinorGC并不代表年轻代内存不足,一般来说只是年轻代的某个区域满了:eden区或者存活区,整个年轻代内存还是够的。
年轻代可以分为3个区域:Eden区和两个存活区Survivor 0 、Survivor 1
绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10% -
(2)年老代:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 YoungGC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫Full GC
-
(3)永久代:也就是方法区,其上的垃圾收集主要是针对常量池的内存回收(没有引用了就回收)和对已加载类的卸载
14、Java内存模型(与Java内存区域属于不同层次的概念)
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。由此就会导致某个线程对共享变量的修改对其他线程不可见
15、volatile保证所有线程对共享变量的可见性:https://www.cnblogs.com/chengxiao/p/6528109.html
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁)
volatile更轻量级,开销更小
16、二叉树遍历方式 1).先序:根左右 2)中序:左根右 3)右序:左右根
17、其中Vector、HashTable、Properties是线程安全的。其中ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
.stringbuffer是线程安全的,stringbuilder是非线程安全的, String是不可变类,所以是线程安全的。所有不可变类都是线程安全的
18、类加载过程
-
装载:将编译后的二进制文件(.class文件)加载进JVM;
-
链接:
验证:确保被加载类的字节流信息符合虚拟机要求,不会危害到虚拟机安全
准备:为类的静态变量分配内存,并将其初始化为默认值(比如静态int变量a,默认初始值为0);
解析:把类中的符号引用转换为直接引用 -
初始化:为类的静态变量赋予正确的初始值(a的正确初始值为5);
19、静态变量是在类加载的时候就初始化,所以它属于类,不属于某个对象。非静态变量就是在执行代码的时候初始化
20、方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
21、hashmap和hashtable的区别:
- 1)两者的底层都是通过数组加链表的结构实现的
- 2)HashMap在并发时,如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key的hash值一样,这样可能会发生多个线程同时对Node数组进行扩容,扩容的时候就容易造成死循环。 所以hashmap是线程不安全的,hashtable使用了synchronized,是线程安全的
- 3)hashmap允许key为null,hashtable不允许
22、ArrayList和LinkedList,Vector的大致区别:
ArrayList是实现了基于动态数组的数据结构,适合读取操作
LinkedList是基于链表结构,适合写入操作
Vector也是基于数组结构,由于使用了synchronized方法-线程安全,所以性能上比ArrayList要差
23、GC算法:https://blog.csdn.net/windcake/article/details/54810052
- 1)标记-清除算法:先把所有活动的对象标记出来,然后把没有被标记的对象统一清除掉
- 2)复制算法:复制算法是将原有的内存空间分成两块,每次只使用其中的一块。在GC时,将正在使用的内存块中的存活对象复制到未使用的那一块中,然后清除正在使用的内存块中的所有对象,并交换两块内存的角色,完成一次垃圾回收。它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。
- 3)标记整理算法适用于存活对象较多的场合,它的标记阶段和标记-清除算法中的一样。整理阶段是将所有存活的对象压缩到内存的一端,之后清理边界外所有的空间。它的效率也不高。
24、记住一点:栈区存引用和基本数据类型,不能存对象,而堆区存对象。是比较地址,equals()比较对象内容。
1)String str1 = “abcd"的实现过程
首先栈区创建str引用,然后在String池(独立于栈和堆而存在,存储不可变量)中寻找其指向的内容为"abcd"的对象,
如果String池中没有,则创建一个,然后str指向String池中的对象,如果有,则直接将str1指向"abcd”";
如果后来又定义了字符串变量 str2 = “abcd”,则直接将str2引用指向String池中已经存在的“abcd”,不再重新创建对象;
当str1进行了重新赋值(str1=“abc”),则str1将不再指向"abcd",而是重新指String池中的"abc",
此时如果定义String str3 = “abc”,进行str1 == str3操作,返回值为true,因为他们的值一样,地址一样,
但如果内容为"abc"的str1进行了字符串+连接str1 = str1+“d”;此时str1指向的是在堆中新建的内容为"abcd"的对象,即此时进行str1str2,返回值false,因为地址不一样
也就是说用+进行连接生成的字符串,是在堆上面的,
2)String str3 = new String(“abcd”)的实现过程
直接在堆中创建对象。如果后来又有String str4 = new String(“abcd”),str4不会指向之前的对象,而是重新创建一个对象并指向它
所以如果此时进行str3==str4返回值是false,因为两个对象的地址不一样,如果是str3.equals(str4),返回true,因为内容相同。
25、为什么hashmap是线程不安全的
HashMap在并发时,如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key的hash值一样,
这样可能会发生多个线程同时对Node数组进行扩容,扩容的时候就容易造成死循环
26、如何线程安全的使用HashMap
了解了HashMap为什么线程不安全,那现在看看如何线程安全的使用HashMap。这个无非就是以下三种方式:
Hashtable
ConcurrentHashMap
Synchronized Map
例子:
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
27、hashmap, hashtable, concurrenthashmap
- 线程不安全的HashMap
多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。 - 效率低下的HashTable容器
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下 - ConcurrentHashMap分段锁技术
ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。ConcurrentHashMap中的分段锁称为Segment,类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。而且,其可以做到读取数据不加锁
线程占用其中一个Segment时,其他线程可正常访问其他段数据。Segment是一种可重入锁ReentrantLock
ConcurrentHashMap的并发度是什么
CCHM的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作CCHM,这也是CCHM对Hashtable的最大优势
concurrenthashmap使用注意事项: ConcurrentHashmap、Hashtable不支持key或者value为null,所以需要处理value不存在和存在两种情况。而HashMap是支持的
28、单例模式
单例的意思是这个类只有一个实例。单例有好几种写法,这里只列出两种:
1)饿汉单例模式
public class EHanSingleton {
//static final单例对象,类加载的时候就初始化
private static final EHanSingleton instance = new EHanSingleton();
//私有构造方法,使得外界不能直接new
private EHanSingleton() {
}
//公有静态方法,对外提供获取单例接口
public static EHanSingleton getInstance() {
return instance;
}
}
缺点:如果有大量的类都采用了饿汉单例模式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能
2)静态内部类实现单例模式(推荐使用这种模式)
public class StaticClassSingleton {
//私有的构造方法,防止new
private StaticClassSingleton() {
}
public static StaticClassSingleton getInstance() {
return StaticClassSingletonHolder.instance;
}
//静态内部类
private static class StaticClassSingletonHolder {
//第一次加载内部类的时候,实例化单例对象
private static final StaticClassSingleton instance = new StaticClassSingleton();
}
}
第一次加载StaticClassSingleton类时,并不会实例化instance。只有第一次调用getInstance方法时,
Java虚拟机才会去加载内部类:StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的
29、实现并启动线程有两种方法
1)写一个类继承自Thread类,重写run方法。用start方法启动线程;
2)写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动。
在start方法里面其实就是调用的run方法。那为什么不直接调用run方法?
因为如果直接调用run方法,并不会创建另一个线程,程序中只有主线程一个线程,所以并不是多线程。而start方法就是单独开一个线程去跑run里面的代码,与此同时,主线程也会同时进行。实现真正的多线程
30、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
31、冒泡排序
arr = [1, 7, 3, 6, 10, 2]
length = len(arr)
for i in range(length):
for j in range(length - i - 1):
if arr[j] > arr[j+1]: # 将最大值放到最右边,那么上一行代码的循环范围就必须是(0, length - i - 1)
tmp = arr[j+1]
arr[j+1] = arr[j]
arr[j] = tmp
print(arr)
二分查找
arr2 = [1, 3, 4, 6, 9, 10]
start = 0
tail = len(arr2) - 1
while start <= tail: # 一定要加 = 的情况,因为当start和tail相等的情况下,这个start值(也就是middle值,也就是tail值)其实就是所求值
middle = (start+tail)//2 # //为取整
if arr2[middle] == 6:
print(middle)
break
elif arr2[middle] < 6:
start = middle + 1
elif arr2[middle] > 6:
tail = middle - 1
来源:CSDN
作者:laogooooog
链接:https://blog.csdn.net/laojingyao/article/details/104851857