日期: 2021 年 6 月 1 日

40个多线程问题总结

1、多线程有什么用?

一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓”知其然知其所以然”,”会用”只是”知其然”,”为什么用”才是”知其所以然”,只有达到”知其然知其所以然”的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

(3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

2、创建线程的方式

比较常见的一个问题了,一般就是两种:

(1)继承Thread类

(2)实现Runnable接口

至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。

 

3、start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

4、Runnable接口和Callable接口的区别

有点深的问题了,也看出一个Java程序员学习知识的广度。

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

5、CyclicBarrier和CountDownLatch的区别

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行

(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务

(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

6、volatile关键字的作用

一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个:

(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是*新的数据

(2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率

从实践角度而言,volatile(自身仅能保证可见性,不能保证操作的原子性)的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

7、什么是线程安全

又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地*好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

(1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

(2)*对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上*大多数都不是线程安全的,不过*对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

(3)相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

(4)线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

8、Java中如何获取到线程dump文件

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是*好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,

9、一个线程如果出现了运行时异常会怎么样

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

 

10、如何在两个线程之间共享数据

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

11、sleep方法和wait方法有什么区别

这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

 

12、生产者消费者模型的作用是什么

这个问题很理论,但是很重要:

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型*重要的作用

(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

 

13、ThreadLocal有什么用

简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

 

14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

 

16、为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

17、怎么检测一个线程是否持有对象监视器

我也是在网上看到一道多线程面试题才知道有方法可以判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。

18、synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

(2)ReentrantLock可以获取各种锁的信息

(3)ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

19、ConcurrentHashMap的并发度是什么

ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着*多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的*大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

20、ReadWriteLock是什么

首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

21、FutureTask是什么

这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

22、Linux环境下如何查找哪个线程使用CPU*长

这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:

(1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

(2)top -H -p pid,顺序不能改变

这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下。

使用”top -H -p pid”+”jps pid”可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。

*后提一点,”top -H -p pid”打出来的LWP是十进制的,”jps pid”打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。

23、Java编程写一个会导致死锁的程序

*次看到这个题目,觉得这是一个非常好的问题。很多人都知道死锁是怎么一回事儿:线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了,问一下怎么写一个死锁的程序就不知道了,这种情况说白了就是不懂什么是死锁,懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的。

真正理解什么是死锁,这个问题其实不难,几个步骤:

(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

(2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁

(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的

这样,线程1″睡觉”睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。代码就不写了,占的篇幅有点多,Java多线程7:死锁这篇文章里面有,就是上面步骤的代码实现。

24、怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

25、不可变对象对多线程有什么帮助

前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

 

26、什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

27、如果你提交任务时,线程池队列已满,这时会发生什么

这里区分一下:

  1. 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
  2. 如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒*策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

 

28、Java中用到的线程调度算法是什么

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

 

29、Thread.sleep(0)的作用是什么

这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

30、什么是自旋

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

31、什么是Java内存模型

Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:

(1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将*新的值更新到主内存中去

(2)定义了几个原子操作,用于操作主内存和工作内存中的变量

(3)定义了volatile变量的使用规则

(4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

32、什么是CAS

CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中*新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

 

33、什么是乐观锁和悲观锁

(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

34、什么是AQS

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的*个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

35、单例模式的线程安全性

老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:非线程安全

(3)双检锁单例模式的写法:线程安全

36、Semaphore有什么作用

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码*多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

37、Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:

(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是*新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

(2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码*终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句”return count”假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完*句,线程就切换了。

 

38、线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

39、同步方法和同步块,哪个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然*常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。

 

40、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

这是我在并发编程网上看到的一个问题,把这个问题放在*后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是*步,增加服务器是第二步,至于线程池的设置,设置参考(2)。*后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

Java中的增强for循环的实现原理与坑

在JAVA中,遍历集合和数组一般有以下三种形式:

for (int i = 0; i < list.size(); i++) {
   System.out.print(list.get(i) + ",");
}

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
   System.out.print(iterator.next() + ",");
}

for (Integer i : list) {
   System.out.print(i + ",");
}

*种是普通的for循环遍历、第二种是使用迭代器进行遍历,第三种我们一般称之为增强for循环(for each)。

实现原理

可以看到,第三种形式是JAVA提供的语法糖,这里我们剖析一下,这种增强for循环底层是如何实现的。

我们对以下代码进行反编译

for (Integer : list) {
   System.out.println(i);
}

反编译后:

Integer i;
for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(i)){
   i = (Integer)iterator.next();        
}

反编译后的代码其实比较复杂,我们按照执行顺序拆解一下:

Integer i; 定义一个临时变量i

Iterator iterator = list.iterator(); 获取List的迭代器

iterator.hasNext(); 判断迭代器中是否有未遍历过的元素

i = (Integer)iterator.next(); 获取*个未遍历的元素,赋值给临时变量i

System.out.println(i) 输出临时变量i的值

如此循环往复,直到遍历完List中的所有元素。

通过反编译,我们看到,其实JAVA中的增强for循环底层是通过迭代器模式来实现的。

增强for循环的坑

这里说是增强for循环的坑,其实主要是因为有些人不了解增强for循环的实现原理而可能踩入的坑。

既然增强for循环通过迭代器实现,那么必然有迭代器的特性。

Java中有fail-fast机制。在使用迭代器遍历元素的时候,在对集合进行删除的时候一定要注意,使用不当有可能发生ConcurrentModificationException,这是一种运行时异常,编译期并不会发生。只有在程序真正运行时才会爆发。

如以下代码:

for (Student stu : students) {    
   if (stu.getId() == 2)     
       students.remove(stu);    
}

会抛出ConcurrentModificationException异常。

Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出

java.util.ConcurrentModificationException异常。

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。

但你可以使用 Iterator 本身的方法 remove() 来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

正确的在遍历的同时删除元素的姿势:

Iterator<Student> stuIter = students.iterator();    
while (stuIter.hasNext()) {    
   Student student = stuIter.next();    
   if (student.getId() == 2)    
       stuIter.remove();//这里要使用Iterator的remove方法移除当前对象,如果使用List的remove方法,则同样会出现ConcurrentModificationException    
}

好啦,这里给你介绍了增强for循环的实现原理,以及使用不当可能踩入的坑。所以,虽然是一个简单的for-each语法,但是也要了解其原理,不然可能导致一些莫名其妙的问题。

转载自:http://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121134&idx=1&sn=a34a1bd547f00e479e9f6dbde8848fe4&chksm=f36bbe8fc41c3799d1bb2c781f81f51e28651d2fb8eb5670a31caac5ba782b66416e5fdf1b1c&mpshare=1&scene=1&srcid=0413o4rBaecPGUVy6Bi9lzt7#rd

js中let和var的区别

转载自:https://www.cnblogs.com/asand/p/7205632.html

let变量之前没见过,刚遇到,探探究竟。

以下转自:http://blog.csdn.net/nfer_zhuang/article/details/48781671

声明后未赋值,表现相同

复制代码

复制代码

(function() {
      var varTest;
      let letTest;
      console.log(varTest); //输出undefined
      console.log(letTest); //输出undefined
    }());

复制代码

复制代码

使用未声明的变量,表现不同:

复制代码

复制代码

(function() {
  console.log(varTest); //输出undefined(注意要注释掉下面一行才能运行)
  console.log(letTest); //直接报错:ReferenceError: letTest is not defined

  var varTest = 'test var OK.';
  let letTest = 'test let OK.';
}());

复制代码

复制代码

重复声明同一个变量时,表现不同:

复制代码

复制代码

(function() {
      "use strict";
      var varTest = 'test var OK.';
      let letTest = 'test let OK.';

      var varTest = 'varTest changed.';
      let letTest = 'letTest changed.'; //直接报错:SyntaxError: Identifier 'letTest' has already been declared

      console.log(varTest); //输出varTest changed.(注意要注释掉上面letTest变量的重复声明才能运行)
      console.log(letTest);
    }());

复制代码

复制代码

变量作用范围,表现不同:

复制代码

复制代码

(function() {
  var varTest = 'test var OK.';
  let letTest = 'test let OK.';

  {
    var varTest = 'varTest changed.';
    let letTest = 'letTest changed.';
  }

  console.log(varTest); //输出"varTest changed.",内部"{}"中声明的varTest变量覆盖外部的letTest声明
  console.log(letTest); //输出"test let OK.",内部"{}"中声明的letTest和外部的letTest不是同一个变量
}());

复制代码

复制代码

备注:

使用 let 语句声明一个变量,该变量的范围限于声明它的块中。  可以在声明变量时为变量赋值,也可以稍后在脚本中给变量赋值。

使用 let 声明的变量,在声明前无法使用,否则将会导致错误。

如果未在 let 语句中初始化您的变量,则将自动为其分配 JavaScript 值 undefined

activemq持久订阅工作原理

对activemq消息订阅模式来说有两种:持久订阅/非持久订阅。

非持久订阅consumer只能消费在该consumer激活状态时传送给对应topic的消息才能被该consumer消费,一旦该consumer 挂掉到下次启动期间发布到该topic的消息不能被该consumer重新恢复时使用!!!

持久订阅:订阅之后,无论消息是否是在该consumer激活或者down掉期间发送的,*终都会被该consumer接收到,直到被显示取消持久订阅(session.unscribe(“topic名字”))!!!

 

那么持久订阅到底是如何实现的呢,笔者在这里将展现其中的奥秘:

先来看下TopicRegion的addConsumer方法

public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
if (info.isDurable()) {
//看该消息是否是持久化订阅

ActiveMQDestination destination = info.getDestination();
if (!destination.isPattern()) {
// Make sure the destination is created.
lookup(context, destination,true);
}
String clientId = context.getClientId();
String subscriptionName = info.getSubscriptionName();
SubscriptionKey key = new SubscriptionKey(clientId, subscriptionName);
DurableTopicSubscription sub = durableSubscriptions.get(key);
if (sub != null) {
if (sub.isActive()) {
throw new JMSException(“Durable consumer is in use for client: ” + clientId + ” and subscriptionName: ” + subscriptionName);
}
// 看下该订阅者的消息筛选项是否变化
if (hasDurableSubChanged(info, sub.getConsumerInfo())) {
// 如果变化了那么首先移除该订阅者对应的DurableTopicSubscription,然后再追加*新创建的DurableTopicSubscription
durableSubscriptions.remove(key);
destinationsLock.readLock().lock();
try {
for (Destination dest : destinations.values()) {
//Account for virtual destinations
if (dest instanceof Topic){
Topic topic = (Topic)dest;
topic.deleteSubscription(context, key);
}
}
} finally {
destinationsLock.readLock().unlock();
}
super.removeConsumer(context, sub.getConsumerInfo());
super.addConsumer(context, info);
sub = durableSubscriptions.get(key);
} else {
// 如果消息筛选项没有变化,那么直接将刚恢复连接的订阅者id与之前的DurableTopicSubscription 关联起来
if (sub.getConsumerInfo().getConsumerId() != null) {
subscriptions.remove(sub.getConsumerInfo().getConsumerId());
}
subscriptions.put(info.getConsumerId(), sub);
}
} else {
super.addConsumer(context, info);
sub = durableSubscriptions.get(key);
if (sub == null) {
throw new JMSException(“Cannot use the same consumerId: ” + info.getConsumerId() + ” for two different durable subscriptions clientID: ” + key.getClientId()
+ ” subscriberName: ” + key.getSubscriptionName());
}
}
sub.activate(usageManager, context, info, broker);
return sub;
} else {
return super.addConsumer(context, info);
}
}

上面代码是订阅者连接到消息提供者时的处理代码,下面看下更核心的持久订阅与消息提供者断开连接时的处理:

@Override
public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
if (info.isDurable()) {
SubscriptionKey key = new SubscriptionKey(context.getClientId(), info.getSubscriptionName());
DurableTopicSubscription sub = durableSubscriptions.get(key);
if (sub != null) {
sub.deactivate(keepDurableSubsActive);
}

} else {
super.removeConsumer(context, info);
}
}

从上面代码可以看到,针对持久订阅者来说,当其与消息提供者断开连接时,provider并没有将该连接移除,仅仅是将断开连接者对应的DurableTopicSubscription状态设置为非激活状态,改状态不影响provider将发送到该topic的消息保存下来,非持久订阅者则在与provider失去连接这段期间无法接收该时间段发送的消息!

pkcs1与pkcs8格式RSA私钥互相转换

注:亲验可用

转载自:https://www.jianshu.com/p/08e41304edab

1、PKCS1私钥生成

openssl genrsa -out private.pem 1024

private.pem 的内容如下:

  1. —–BEGIN RSA PRIVATE KEY—–
  2. MIICXAIBAAKBgQC5BW6T9GVaaG/epGDjPpY3wN0DrBt+NojvxkEgpUdOAxgAepqe
  3. GbSqtXAd+MOOBbHxIOEwrFC9stkypQgxrB49tXDI+4Jj8MuKI15HEmI8k7+tRDOl
  4. J5TFSL2J9KA3GuQbyVAhlpxl+YnV7yjxP9l1dkbApg1ixSd5KOPbaQ00WQIDAQAB
  5. AoGAYiqzpOTC8dj/og1tKqUGZsZ5fX1PiQO+XBnAbGXFE2sozPhAGSpiZUCnH//h
  6. IfV7mAht8rk6java+bf+RPyhfg0zW7oXy0pm8DwoW7+0fOzQ4sEYeoqza/VrkYwR
  7. 5BxBa+KyT1HCi4uXogyDlQT1p0ZT0iaqZBfTApdyVkmcQEECQQDhfPl+ILl0bh0H
  8. 8ORoMmmxAZMn293+de441OlAjL3CsF4yhUUdavAYWM0RAV5MJtKUTR4ZpRXkB/pq
  9. kgyTxpr9AkEA0g6pQRpcGxulr2758ZlOLdL8B1n1ubre464IKQ0zNfERKhR/j7U8
  10. LGF+3mhZuoSEdklwLCJ8ZMvIhkV0v8JjjQJBANtqXOyas1vUenNruRabV7ViLuuu
  11. S0p9Px4WMBMb4Ns9+6t1e1ew44kNgB54EmZPsMGWeR/DQJXwHYDuNUbnD5ECQA7S
  12. Gf8N7RG8kaQfIGN7fZieGkoqfrvsA23tCYZb+BEGQT/G0nlBQE2hU2I92pbeYro1
  13. 1ERI6p3yAuP2YpZlEMECQGNzhqshYfDiWwU4Q3aZWkRrv74uIXk1HQoFH1BthzQJ
  14. TbzKH/LEqZN8WVau3bf41yAx2YoaOsIJJtOUTYcfh14=
  15. —–END RSA PRIVATE KEY—–

2、PKCS1私钥转换为PKCS8(该格式一般Java调用)

  1. openssl pkcs8 -topk8 -inform PEM -in private.pem -outform pem -nocrypt -out pkcs8.pem

pkcs8.pem文件内容

  1. —–BEGIN PRIVATE KEY—–
  2. MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALkFbpP0ZVpob96k
  3. YOM+ljfA3QOsG342iO/GQSClR04DGAB6mp4ZtKq1cB34w44FsfEg4TCsUL2y2TKl
  4. CDGsHj21cMj7gmPwy4ojXkcSYjyTv61EM6UnlMVIvYn0oDca5BvJUCGWnGX5idXv
  5. KPE/2XV2RsCmDWLFJ3ko49tpDTRZAgMBAAECgYBiKrOk5MLx2P+iDW0qpQZmxnl9
  6. fU+JA75cGcBsZcUTayjM+EAZKmJlQKcf/+Eh9XuYCG3yuTqNq9r5t/5E/KF+DTNb
  7. uhfLSmbwPChbv7R87NDiwRh6irNr9WuRjBHkHEFr4rJPUcKLi5eiDIOVBPWnRlPS
  8. JqpkF9MCl3JWSZxAQQJBAOF8+X4guXRuHQfw5GgyabEBkyfb3f517jjU6UCMvcKw
  9. XjKFRR1q8BhYzREBXkwm0pRNHhmlFeQH+mqSDJPGmv0CQQDSDqlBGlwbG6Wvbvnx
  10. mU4t0vwHWfW5ut7jrggpDTM18REqFH+PtTwsYX7eaFm6hIR2SXAsInxky8iGRXS/
  11. wmONAkEA22pc7JqzW9R6c2u5FptXtWIu665LSn0/HhYwExvg2z37q3V7V7DjiQ2A
  12. HngSZk+wwZZ5H8NAlfAdgO41RucPkQJADtIZ/w3tEbyRpB8gY3t9mJ4aSip+u+wD
  13. be0Jhlv4EQZBP8bSeUFATaFTYj3alt5iujXUREjqnfIC4/ZilmUQwQJAY3OGqyFh
  14. 8OJbBThDdplaRGu/vi4heTUdCgUfUG2HNAlNvMof8sSpk3xZVq7dt/jXIDHZiho6
  15. wgkm05RNhx+HXg==
  16. —–END PRIVATE KEY—–

3、PKCS8格式私钥转换为PKCS1(传统私钥格式)

openssl rsa -in pkcs8.pem -out pkcs1.pem

pkcs1.pem文件内容如下:

  1. —–BEGIN RSA PRIVATE KEY—–
  2. MIICXAIBAAKBgQC5BW6T9GVaaG/epGDjPpY3wN0DrBt+NojvxkEgpUdOAxgAepqe
  3. GbSqtXAd+MOOBbHxIOEwrFC9stkypQgxrB49tXDI+4Jj8MuKI15HEmI8k7+tRDOl
  4. J5TFSL2J9KA3GuQbyVAhlpxl+YnV7yjxP9l1dkbApg1ixSd5KOPbaQ00WQIDAQAB
  5. AoGAYiqzpOTC8dj/og1tKqUGZsZ5fX1PiQO+XBnAbGXFE2sozPhAGSpiZUCnH//h
  6. IfV7mAht8rk6java+bf+RPyhfg0zW7oXy0pm8DwoW7+0fOzQ4sEYeoqza/VrkYwR
  7. 5BxBa+KyT1HCi4uXogyDlQT1p0ZT0iaqZBfTApdyVkmcQEECQQDhfPl+ILl0bh0H
  8. 8ORoMmmxAZMn293+de441OlAjL3CsF4yhUUdavAYWM0RAV5MJtKUTR4ZpRXkB/pq
  9. kgyTxpr9AkEA0g6pQRpcGxulr2758ZlOLdL8B1n1ubre464IKQ0zNfERKhR/j7U8
  10. LGF+3mhZuoSEdklwLCJ8ZMvIhkV0v8JjjQJBANtqXOyas1vUenNruRabV7ViLuuu
  11. S0p9Px4WMBMb4Ns9+6t1e1ew44kNgB54EmZPsMGWeR/DQJXwHYDuNUbnD5ECQA7S
  12. Gf8N7RG8kaQfIGN7fZieGkoqfrvsA23tCYZb+BEGQT/G0nlBQE2hU2I92pbeYro1
  13. 1ERI6p3yAuP2YpZlEMECQGNzhqshYfDiWwU4Q3aZWkRrv74uIXk1HQoFH1BthzQJ
  14. TbzKH/LEqZN8WVau3bf41yAx2YoaOsIJJtOUTYcfh14=
  15. —–END RSA PRIVATE KEY—–

eclipse中调试时无法进入jdk源码

转载自:https://blog.csdn.net/u013352983/article/details/50633637

亲试有效

今天在eclipse中进行断点调试时,无法进入jdk源码中跟踪代码,eclipse默认是使用的java环境是JRE(Java Runtime Environment 即java运行时环境)环境,jre环境是不支持调试的;需要将eclipse的环境换成JDK的。查看eclipse运行环境方法如下: window  –>  preference  –>  java  –>  Installed JREs –> 右侧 会看到 eclipse的java环境了。

既然JRE不允许调试那就将eclipse的环境换成jdk的配置如下:

*步:找到eclipse环境如下图:

%title插图%num%title插图%num

 

 

第二步:点击add找到本地安装jdk的目录

点击add按钮之后如图:

%title插图%num%title插图%num

点击Next:如图:

%title插图%num%title插图%num

点击Directory….选择jdk的安装文件(是jdk不是jre)选择完成后点击Finish,设置jdk为默认环境如图:

%title插图%num

第三步:将要调试的工程的引用的环境换成jdk的(即  Buil Path)如图:

%title插图%num

现在可以进行jdk源码的调试和跟踪了

NIO 之 ByteBuffer实现原理

转载自:https://www.jianshu.com/p/451cc865d413

前言

Java NIO 主要由下面3部分组成:

  • Buffer
  • Channel
  • Selector

在传统IO中,流是基于字节的方式进行读写的。
在NIO中,使用通道(Channel)基于缓冲区数据块的读写。

流是基于字节一个一个的读取和写入。
通道是基于块的方式进行读取和写入。

Buffer 类结构图

Buffer 的类结构图如下:

 

%title插图%num

Buffer类结构图

从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。

Buffer类分析

Filed

每个缓冲区都有这4个属性,无论缓冲区是何种类型都有相同的方法来设置这些值

  1. private int mark = -1;
  2. private int position = 0;
  3. private int limit;
  4. private int capacity;

1. 标记(mark)

初始值-1,表示未标记。
标记一个位置,方便以后reset重新从该位置读取数据。

  1. public final Buffer mark() {
  2. mark = position;
  3. return this;
  4. }
  5. public final Buffer reset() {
  6. int m = mark;
  7. if (m < 0)
  8. throw new InvalidMarkException();
  9. position = m;
  10. return this;
  11. }

2. 位置(position)

缓冲区中读取或写入的下一个位置。这个位置从0开始,*大值等于缓冲区的大小

  1. //获取缓冲区的位置
  2. public final int position() {
  3. return position;
  4. }
  5. //设置缓冲区的位置
  6. public final Buffer position(int newPosition) {
  7. if ((newPosition > limit) || (newPosition < 0))
  8. throw new IllegalArgumentException();
  9. position = newPosition;
  10. if (mark > position) mark = -1;
  11. return this;
  12. }

3. 限度(limit)

  1. //获取limit位置
  2. public final int limit() {
  3. return limit;
  4. }
  5. //设置limit位置
  6. public final Buffer limit(int newLimit) {
  7. if ((newLimit > capacity) || (newLimit < 0))
  8. throw new IllegalArgumentException();
  9. limit = newLimit;
  10. if (position > limit) position = limit;
  11. if (mark > limit) mark = -1;
  12. return this;
  13. }

4. 容量(capacity)

缓冲区可以保存元素的*大数量。该值在创建缓存区时指定,一旦创建完成后就不能修改该值。

  1. //获取缓冲区的容量
  2. public final int capacity() {
  3. return capacity;
  4. }

filp 方法

  1. public final Buffer flip() {
  2. limit = position;
  3. position = 0;
  4. mark = –1;
  5. return this;
  6. }
  1. 将limit设置成当前position的坐标
  2. 将position设置为0
  3. 取消标记

rewind 方法

  1. public final Buffer rewind() {
  2. position = 0;
  3. mark = –1;
  4. return this;
  5. }

从源码中发现,rewind修改了position和mark,而没有修改limit。

  1. 将position设置为0
  2. 取消mark标记

clear 方法

  1. public final Buffer clear() {
  2. position = 0;
  3. limit = capacity;
  4. mark = –1;
  5. return this;
  6. }
  1. 将position坐标设置为0
  2. limit设置为capacity
  3. 取消标记

从clear方法中,我们发现Buffer中的数据没有清空,如果通过Buffer.get(i)的方式还是可以访问到数据的。如果再次向缓冲区中写入数据,他会覆盖之前存在的数据。

remaining 方法

查看当前位置和limit之间的元素数。

  1. public final int remaining() {
  2. return limit – position;
  3. }

hasRemaining 方法

判断当前位置和limit之间是否还有元素

  1. public final boolean hasRemaining() {
  2. return position < limit;
  3. }

ByteBuffer 类分析

%title插图%num

ByteBuffer类结果图

从图中我们可以发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类

HeapByteBuffer:在堆中创建的缓冲区。就是在jvm中创建的缓冲区。
MappedByteBuffer:直接缓冲区。物理内存中创建缓冲区,而不在堆中创建。

allocate 方法(创建堆缓冲区)

  1. public static ByteBuffer allocate(int capacity) {
  2. if (capacity < 0)
  3. throw new IllegalArgumentException();
  4. return new HeapByteBuffer(capacity, capacity);
  5. }

我们发现allocate方法创建的缓冲区是创建的HeapByteBuffer实例。

HeapByteBuffer 构造

  1. HeapByteBuffer(int cap, int lim) { // package-private
  2. super(-1, 0, lim, cap, new byte[cap], 0);
  3. }

从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中创建一个byte[]数组。

allocateDirect创建直接缓冲区

  1. public static ByteBuffer allocateDirect(int capacity) {
  2. return new DirectByteBuffer(capacity);
  3. }

我们发现allocate方法创建的缓冲区是创建的DirectByteBuffer实例。

DirectByteBuffer构造

%title插图%num

DirectByteBuffer 构造方法

 

直接缓冲区是通过java中Unsafe类进行在物理内存中创建缓冲区。

wrap 方法

  1. public static ByteBuffer wrap(byte[] array)
  2. public static ByteBuffer wrap(byte[] array, int offset, int length);

可以通过wrap类把字节数组包装成缓冲区ByteBuffer实例。
这里需要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。如果array数组中的值更改,则ByteBuffer中的数据也会更改的。

get 方法

  1. public byte get()
    获取position坐标元素,并将position+1;
  2. public byte get(int i)
    获取指定索引下标的元素
  3. public ByteBuffer get(byte[] dst)
    从当前position中读取元素填充到dst数组中,每填充一个元素position+1;
  4. public ByteBuffer get(byte[] dst, int offset, int length)
    从当前position中读取元素到dst数组的offset下标开始填充length个元素。

put 方法

  1. public ByteBuffer put(byte x)
    写入一个元素并position+1
  2. public ByteBuffer put(int i, byte x)
    指定的索引写入一个元素
  3. public final ByteBuffer put(byte[] src)
    写入一个自己数组,并position+数组长度
  4. public ByteBuffer put(byte[] src, int offset, int length)
    从一个自己数组的offset开始length个元素写入到ByteBuffer中,并把position+length
  5. public ByteBuffer put(ByteBuffer src)
    写入一个ByteBuffer,并position加入写入的元素个数

视图缓冲区

%title插图%num

Paste_Image.png

 

ByteBuffer可以转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。

压缩缓冲区

  1. public ByteBuffer compact() {
  2. System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
  3. position(remaining());
  4. limit(capacity());
  5. discardMark();
  6. return this;
  7. }

1、把缓冲区positoin到limit中的元素向前移动positoin位
2、设置position为remaining()
3、 limit为缓冲区容量
4、取消标记

例如:ByteBuffer.allowcate(10);
内容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]

compact前

[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
pos=4
lim=10
cap=10

compact后

[4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
pos=6
lim=10
cap=10

slice方法

  1. public ByteBuffer slice() {
  2. return new HeapByteBuffer(hb,
  3. 1,
  4. 0,
  5. this.remaining(),
  6. this.remaining(),
  7. this.position() + offset);
  8. }

创建一个分片缓冲区。分配缓冲区与主缓冲区共享数据。
分配的起始位置是主缓冲区的position位置
容量为limit-position。
分片缓冲区无法看到主缓冲区positoin之前的元素。

直接缓冲区和堆缓冲区性能对比

下面我们从缓冲区创建的性能和读取性能两个方面进行性能对比。

读写性能对比

  1. public static void directReadWrite() throws Exception {
  2. int time = 10000000;
  3. long start = System.currentTimeMillis();
  4. ByteBuffer buffer = ByteBuffer.allocate(4*time);
  5. for(int i=0;i<time;i++){
  6. buffer.putInt(i);
  7. }
  8. buffer.flip();
  9. for(int i=0;i<time;i++){
  10. buffer.getInt();
  11. }
  12. System.out.println(“堆缓冲区读写耗时 :”+(System.currentTimeMillis()-start));
  13. start = System.currentTimeMillis();
  14. ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
  15. for(int i=0;i<time;i++){
  16. buffer2.putInt(i);
  17. }
  18. buffer2.flip();
  19. for(int i=0;i<time;i++){
  20. buffer2.getInt();
  21. }
  22. System.out.println(“直接缓冲区读写耗时:”+(System.currentTimeMillis()-start));
  23. }

输出结果:

  1. 堆缓冲区创建耗时 :70
  2. 直接缓冲区创建耗时:47

从结果中我们发现堆缓冲区读写比直接缓冲区读写耗时更长。

  1. public static void directAllocate() throws Exception {
  2. int time = 10000000;
  3. long start = System.currentTimeMillis();
  4. for (int i = 0; i < time; i++) {
  5. ByteBuffer buffer = ByteBuffer.allocate(4);
  6. }
  7. System.out.println(“堆缓冲区创建时间:”+(System.currentTimeMillis()-start));
  8. start = System.currentTimeMillis();
  9. for (int i = 0; i < time; i++) {
  10. ByteBuffer buffer = ByteBuffer.allocateDirect(4);
  11. }
  12. System.out.println(“直接缓冲区创建时间:”+(System.currentTimeMillis()-start));
  13. }

输出结果:

  1. 堆缓冲区创建时间:73
  2. 直接缓冲区创建时间:5146

从结果中发现直接缓冲区创建分配空间比较耗时。

对比结论

直接缓冲区比较适合读写操作,*好能重复使用直接缓冲区并多次读写的操作。
堆缓冲区比较适合创建新的缓冲区,并且重复读写不会太多的应用。

建议:如果经过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,否则不建议使用直接缓冲区。

NIO 之 Buffer 图解

转载自:https://www.jianshu.com/p/12c81abb5387

 

Buffer 类 结构

%title插图%num

对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。

概述

缓冲区 Buffer 内部就是用数组实现的。 Buffer 包含了下面4个属性:

  • 容量( Capacity)
    缓冲区能够容纳的数据元素的*大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
  • 上界( Limit)
    缓冲区的*个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
  • 位置( Position)
    下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
  • 标记( Mark)
    一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。

这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity

示例

下面展示了一个新创建的容量为 10 的 ByteBuffer 逻辑视图

ByteBuffer.allocate(10);

%title插图%num

图1

位置(Position)被设为 0,而且容量( Capacity)和上界( Limit)被设为 10,刚好经过缓冲区能够容纳的*后一个字节。
标记(mark)*初未定义。
容量(Capacity)是固定的,但另外的三个属性可以在使用缓冲区时改变。

put() 方法

让我们看一个例子。 我们将代表“abcde”字符串的 ASCII 码载入一个名为 buffer 的
ByteBuffer 对象中。当在图1 中所新建的缓冲区上执行以下代码后。

buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');

缓冲区的结果状态如图 2所示:

 

%title插图%num

图2

flip() 方法

我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通
道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。

flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素
的释放状态。在翻转之后,图 2 的缓冲区会变成图 3 中的样子。

%title插图%num

图3

rewind() 方法

rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使
用 rewind()后退,重读已经被翻转的缓冲区中的数据。
图2 的缓冲区调用 rewind() 方法会变成图4 中的样子。

 

%title插图%num

图4

如果将缓冲区翻转两次会怎样呢?

compact() 方法

有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这
一点,未读的数据元素需要下移以使*个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。图 5显示了一个读取了两个元素(position 现在为2),并且现在我们想要对其进行压缩的缓冲区。

%title插图%num

图5

buffer.compact();

压缩后的结果如下图

 

%title插图%num

图6

duplicate() 方法

duplicate() 方法创建了一个与原始缓冲区一样的新缓冲区。两个缓冲区共享数据,拥有同样的 capacity ,但每个缓冲区都拥有自己的 position,limit 和 mark 属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

  1. public ByteBuffer duplicate() {
  2. return new HeapByteBufferR(hb,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. offset);
  8. }

重新创建一个 ByteBuffer,并且使用同一个数组。所有一个byteBuffer 变动,会影响另一个 ByteBuffer。 但 position、limit、mark 都是独立的。

duplicate() 方法

您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与
duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数
将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出
ReadOnlyBufferException 异常。

  1. public ByteBuffer asReadOnlyBuffer() {
  2. return new HeapByteBufferR(hb,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. offset);
  8. }

HeapByteBufferR 分析

  1. class HeapByteBufferR
  2. extends HeapByteBuffer{
  3. public ByteBuffer put(byte x) {
  4. throw new ReadOnlyBufferException();
  5. }
  6. public ByteBuffer put(int i, byte x) {
  7. throw new ReadOnlyBufferException();
  8. }
  9. public ByteBuffer putInt(int x) {
  10. throw new ReadOnlyBufferException();
  11. }
  12. ……
  13. }

HeapByteBufferR 继承 HeapByteBuffer 类,并重写了所有的可修改 buffer 的方法。把所有能修改 buffer 的方法都直接 throw ReadOnlyBufferException,来保证只读。

slice() 方法

slice() 分割缓冲区。创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量( limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。

原 ByteBuffer如下图:

 

%title插图%num

slice() 分割后的 ByteBuffer

 

%title插图%num

  1. public ByteBuffer slice() {
  2. return new HeapByteBuffer(hb,
  3. 1,
  4. 0,
  5. this.remaining(),
  6. this.remaining(),
  7. this.position() + offset);
  8. }

 

AndroidStudio制作登录和注册功能的实现,界面的布局介绍

设计思路

当我们面临制作登录和注册功能的实现时,我们需要先设计登录界面的布局和注册界面的布局,做到有完整的思路时才开始实现其功能效果会更好。

我们需要做个标题栏,登陆界面,实现登陆界面的功能代码块,注册界面,实现测试界面的功能模块即可完成。

标题栏的设计思路
每个APP都基本上有个标题栏,即是显示标题,标题栏的两侧大多数都有一个返回建。那么标题栏即是一个返回键和一个标题栏的制作布局。

为了避免大多数代码的冗杂,我们把这个标题栏的制作布局独立起来,标题的显示我们可以在每块主题模块上,用setText()方法来显示不同的标题。

接下来我们创建main_title_bar.xml布局文件:
具体代码如下:

<?xml version=”1.0″ encoding=”utf-8″?>
<!–这里代码的是创建一个标题栏,左边是返回键–>
<!–我们设置RelativeLayout布局,id = “title_bar”–>
<RelativeLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/title_bar”
android:layout_width=”match_parent”
android:layout_height=”50dp”
android:background=”@android:color/transparent”>
<!–一个是显示返回键,一个是显示标题框–>
<!–通过TextView来显示,id : tv_back , tv_main_title –>
<TextView
android:id=”@+id/tv_back”
android:layout_width=”50dp”
android:layout_height=”50dp”
android:layout_alignParentLeft=”true”
android:background=”@drawable/go_back_selector” />
<TextView
android:id=”@+id/tv_main_title”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerInParent=”true”
android:textColor=”@android:color/white”
android:textSize=”20sp”/>
<!–其中android:background=”@drawable/go_back_selector”为点击回退键时,会变化效果,其实就是一个点击更换个图片而已。–>
<!–我们先用这种老方法,接下来以后的文章才做代码优化效果–>
</RelativeLayout>
补充:
现在标题栏布局做好了,我们需要了解怎么换图片,就是在android:background=”@drawable/go_back_selector”,其实就是在drawable中创建这个go_back_selector.xml文件而已,用到了android:state_pressed=”true”这个属性,当点击时就是变化的图片效果,记住state_pressed就OK。

登录界面布局
创建登录界面,我们需要标题栏显示“登录”,那么就要通过<include>标签。

我们需要设计想好美化登录界面,需要以下图片:登录背景图片login_bg.png,默认的头像图片default_icon,输入用户名的背景图片login_user_name_bg,在用户名前需要一个小标图user_name_icon,同理,输入密码框需要图片有login_psw_bg,psw_icon,按钮需要图片加以美观register_selector,根据需要的图片可自行制作。

登录界面布局模块代码
创建activity_login.xml布局文件,具体代码如下:

<?xml version=”1.0″ encoding=”utf-8″?>
<!–登录界面,用LinearLayout–>
<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:background=”@drawable/login_bg”
android:orientation=”vertical”>
<!–标题栏–>
<include layout=”@layout/main_title_bar”></include>
<!–显示头像,记得加入id iv_head –>
<ImageView
android:id=”@+id/iv_head”
android:layout_width=”70dp”
android:layout_height=”70dp”
android:layout_marginTop=”25dp”
android:layout_gravity=”center_horizontal”
android:background=”@drawable/default_icon”/>
<!–输入框–>
<EditText
android:id=”@+id/et_user_name”
android:layout_width=”fill_parent”
android:layout_height=”48dp”
android:layout_marginTop=”35dp”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:layout_gravity=”center_horizontal”
android:background=”@drawable/login_user_name_bg”
android:drawableLeft=”@drawable/user_name_icon”
android:drawablePadding=”10dp”
android:paddingLeft=”8dp”
android:gravity=”center_vertical”
android:hint=”请输入用户名”
android:singleLine=”true”
android:textColor=”#000000″
android:textColorHint=”#a3a3a3″
android:textSize=”14sp”/>
<!–输入框–>
<EditText
android:id=”@+id/et_psw”
android:layout_width=”fill_parent”
android:layout_height=”48dp”
android:layout_gravity=”center_horizontal”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:background=”@drawable/login_psw_bg”
android:drawableLeft=”@drawable/psw_icon”
android:drawablePadding=”10dp”
android:paddingLeft=”8dp”
android:gravity=”center_vertical”
android:hint=”请输入密码”
android:inputType=”textPassword”
android:singleLine=”true”
android:textColor=”#000000″
android:textColorHint=”#a3a3a3″
android:textSize=”14sp”/>
<!–按钮–>
<Button
android:id=”@+id/btn_login”
android:layout_width=”fill_parent”
android:layout_height=”40dp”
android:layout_marginTop=”15dp”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:layout_gravity=”center_horizontal”
android:background=”@drawable/register_selector”
android:text=”登 录”
android:textColor=”@android:color/white”
android:textSize=”18sp”/>
<!–显示tv register , find_psw –>
<LinearLayout
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:layout_marginTop=”8dp”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:gravity=”center_horizontal”
android:orientation=”horizontal”>
<TextView
android:id=”@+id/tv_register”
android:layout_width=”0dp”
android:layout_height=”wrap_content”
android:layout_weight=”1″
android:gravity=”center_horizontal”
android:padding=”8dp”
android:text=”立即注册”
android:textColor=”@android:color/white”
android:textSize=”14sp” />
<!–layout_weight=”1″ layout_width=”0dp”实现均分效果–>
<TextView
android:id=”@+id/tv_find_psw”
android:layout_width=”0dp”
android:layout_height=”wrap_content”
android:layout_weight=”1″
android:gravity=”center_horizontal”
android:padding=”8dp”
android:text=”找回密码?”
android:textColor=”@android:color/white”
android:textSize=”14sp” />
</LinearLayout>
</LinearLayout>
同理注册界面布局设计思路
创建注册界面,我们需要标题栏显示“注册”,那么就要通过<include>标签。那么我做了效果图,提供思路参考:

注册界面思路图
注册布局模块代码
创建activity_register.xml布局文件,具体代码如下:

<?xml version=”1.0″ encoding=”utf-8″?>
<!–注册界面–>
<!–这里的布局放置是: 1 个 ImageView 控件,用于显示用户头像;3 个 EditText 控件,用于输入用户名、密码、再次输入密码;1 个 Button 控件为注册按钮–>
<!–修改 activity_register.xml 为 LinearLayout 布局–>
<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/activity_register”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:background=”@drawable/register_bg”
android:orientation=”vertical”>
<include layout=”@layout/main_title_bar”></include><!–引入标题栏–>
<ImageView
android:layout_width=”70dp”
android:layout_height=”70dp”
android:layout_gravity=”center_horizontal”
android:layout_marginTop=”25dp”
android:src=”@drawable/default_icon”/>
<!–三个编辑框–>
<EditText
android:id=”@+id/et_user_name”
android:layout_width=”fill_parent”
android:layout_height=”48dp”
android:layout_gravity=”center_horizontal”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:layout_marginTop=”35dp”
android:background=”@drawable/register_user_name_bg”
android:drawableLeft=”@drawable/user_name_icon”
android:drawablePadding=”10dp”
android:gravity=”center_vertical”
android:hint=”请输入用户名”
android:paddingLeft=”8dp”
android:singleLine=”true”
android:textColor=”#000000″
android:textColorHint=”#a3a3a3″
android:textSize=”14sp”/>
<EditText
android:id=”@+id/et_psw”
android:layout_width=”fill_parent”
android:layout_gravity=”center_horizontal”
android:layout_height=”48dp”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:background=”@drawable/register_psw_bg”
android:drawableLeft=”@drawable/psw_icon”
android:drawablePadding=”10dp”
android:hint=”请输入密码”
android:inputType=”textPassword”
android:paddingLeft=”8dp”
android:singleLine=”true”
android:textColor=”#000000″
android:textColorHint=”#a3a3a3″
android:textSize=”14sp”/>
<EditText
android:id=”@+id/et_psw_again”
android:layout_width=”fill_parent”
android:layout_height=”48dp”
android:layout_gravity=”center_horizontal”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:background=”@drawable/register_psw_again_bg”
android:drawableLeft=”@drawable/psw_icon”
android:drawablePadding=”10dp”
android:hint=”请再次输入密码”
android:inputType=”textPassword”
android:paddingLeft=”8dp”
android:singleLine=”true”
android:textColor=”#000000″
android:textColorHint=”#a3a3a3″
android:textSize=”14sp”/>
<Button
android:id=”@+id/btn_register”
android:layout_width=”fill_parent”
android:layout_height=”40dp”
android:layout_gravity=”center_horizontal”
android:layout_marginLeft=”35dp”
android:layout_marginRight=”35dp”
android:layout_marginTop=”15dp”
android:background=”@drawable/register_selector”
android:text=”注 册”
android:textColor=”@android:color/white”
android:textSize=”18sp”/>
</LinearLayout>
MD5算法
MD5 为 Message-Digest Algorithm 5(信息–摘要算法),记住几个要点就可以了。

Message
Digest
MessageDigest
MessageDigest.getInstance( );
由于注册登录涉及密码,我们需要对用户的密码进行 MD5 算法加密,MD5 算法是把任意长度的字符串变成固定长度(通常是128位)的16进制字符串,且此算法不可逆。
具体代码如下:

package cn.edu.gdmec.android.androidstudiodemo.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Utils {
//md5 加密算法
public static String md5(String text) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance(“md5”);
// 数组 byte[] result -> digest.digest( ); 文本 text.getBytes();
byte[] result = digest.digest(text.getBytes());
//创建StringBuilder对象 然后建议StringBuffer,安全性高
//StringBuilder sb = new StringBuilder();
StringBuffer sb = new StringBuffer();
// result数组,digest.digest ( ); -> text.getBytes();
// for 循环数组byte[] result;
for (byte b : result){
// 0xff 为16进制
int number = b & 0xff;
// number值 转换 字符串 Integer.toHexString( );
String hex = Integer.toHexString(number);
if (hex.length() == 1){
sb.append(“0″+hex);
}else {
sb.append(hex);
}
}
//sb StringBuffer sb = new StringBuffer();对象实例化
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
//发送异常return空字符串
return “”;
}
}
}
实现注册逻辑功能代码
完成注册页面的布局与 MD5 工具类后,进行注册界面的逻辑编写。

当在注册界面点击注册按钮后,需要获取用户名,用户密码和再次确认密码,当两次密码相同时,将用户名和密码(经过 MD5 加密)保存到 SharedPreferences 中,同时当注册成功后,需要将用户名传递到登录界面中。

RegisterActivity.java具体代码如下:

package cn.edu.gdmec.android.androidstudiodemo;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import cn.edu.gdmec.android.androidstudiodemo.utils.MD5Utils;

public class RegisterActivity extends AppCompatActivity {
private TextView tv_main_title;//标题
private TextView tv_back;//返回按钮
private Button btn_register;//注册按钮
//用户名,密码,再次输入的密码的控件
private EditText et_user_name,et_psw,et_psw_again;
//用户名,密码,再次输入的密码的控件的获取值
private String userName,psw,pswAgain;
//标题布局
private RelativeLayout rl_title_bar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置页面布局 ,注册界面
setContentView(R.layout.activity_register);
//设置此界面为竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
init();
}

private void init() {
//从main_title_bar.xml 页面布局中获取对应的UI控件
tv_main_title=findViewById(R.id.tv_main_title);
tv_main_title.setText(“注册”);
tv_back=findViewById(R.id.tv_back);
//布局根元素
rl_title_bar=findViewById(R.id.title_bar);
rl_title_bar.setBackgroundColor(Color.TRANSPARENT);
//从activity_register.xml 页面中获取对应的UI控件
btn_register=findViewById(R.id.btn_register);
et_user_name=findViewById(R.id.et_user_name);
et_psw=findViewById(R.id.et_psw);
et_psw_again=findViewById(R.id.et_psw_again);
tv_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//返回键
RegisterActivity.this.finish();
}
});
//注册按钮
btn_register.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//获取输入在相应控件中的字符串
getEditString();
//判断输入框内容
if(TextUtils.isEmpty(userName)){
Toast.makeText(RegisterActivity.this, “请输入用户名”, Toast.LENGTH_SHORT).show();
return;
}else if(TextUtils.isEmpty(psw)){
Toast.makeText(RegisterActivity.this, “请输入密码”, Toast.LENGTH_SHORT).show();
return;
}else if(TextUtils.isEmpty(pswAgain)){
Toast.makeText(RegisterActivity.this, “请再次输入密码”, Toast.LENGTH_SHORT).show();
return;
}else if(!psw.equals(pswAgain)){
Toast.makeText(RegisterActivity.this, “输入两次的密码不一样”, Toast.LENGTH_SHORT).show();
return;
/**
*从SharedPreferences中读取输入的用户名,判断SharedPreferences中是否有此用户名
*/
}else if(isExistUserName(userName)){
Toast.makeText(RegisterActivity.this, “此账户名已经存在”, Toast.LENGTH_SHORT).show();
return;
}else{
Toast.makeText(RegisterActivity.this, “注册成功”, Toast.LENGTH_SHORT).show();
//把账号、密码和账号标识保存到sp里面
/**
* 保存账号和密码到SharedPreferences中
*/
saveRegisterInfo(userName, psw);
//注册成功后把账号传递到LoginActivity.java中
// 返回值到loginActivity显示
Intent data = new Intent();
data.putExtra(“userName”, userName);
setResult(RESULT_OK, data);
//RESULT_OK为Activity系统常量,状态码为-1,
// 表示此页面下的内容操作成功将data返回到上一页面,如果是用back返回过去的则不存在用setResult传递data值
RegisterActivity.this.finish();
}
}
});
}
/**
* 获取控件中的字符串
*/
private void getEditString(){
userName=et_user_name.getText().toString().trim();
psw=et_psw.getText().toString().trim();
pswAgain=et_psw_again.getText().toString().trim();
}
/**
* 从SharedPreferences中读取输入的用户名,判断SharedPreferences中是否有此用户名
*/
private boolean isExistUserName(String userName){
boolean has_userName=false;
//mode_private SharedPreferences sp = getSharedPreferences( );
// “loginInfo”, MODE_PRIVATE
SharedPreferences sp=getSharedPreferences(“loginInfo”, MODE_PRIVATE);
//获取密码
String spPsw=sp.getString(userName, “”);//传入用户名获取密码
//如果密码不为空则确实保存过这个用户名
if(!TextUtils.isEmpty(spPsw)) {
has_userName=true;
}
return has_userName;
}
/**
* 保存账号和密码到SharedPreferences中SharedPreferences
*/
private void saveRegisterInfo(String userName,String psw){
String md5Psw = MD5Utils.md5(psw);//把密码用MD5加密
//loginInfo表示文件名, mode_private SharedPreferences sp = getSharedPreferences( );
SharedPreferences sp=getSharedPreferences(“loginInfo”, MODE_PRIVATE);
//获取编辑器, SharedPreferences.Editor editor -> sp.edit();
SharedPreferences.Editor editor=sp.edit();
//以用户名为key,密码为value保存在SharedPreferences中
//key,value,如键值对,editor.putString(用户名,密码);
editor.putString(userName, md5Psw);
//提交修改 editor.commit();
editor.commit();
}
}
实现登录逻辑功能代码
完成登录界面布局后,来实现登录界面的逻辑代码。

当点击登录按钮时,需判断用户名和密码是否为空。

若为空,则提示请输入用户名或密码,这里的判断事项比较一开始凌乱,需要细细品味;若不为空,则获取用户输入的用户名,由于用的是本地数据,需要根据用户名在 SharedPreferences 中查询是否有对应的密码,若有对应的密码且与用户输入的密码(需通过 MD5 加密)比对一致情况,则登录成功。

LoginActivity.java具体代码如下:

package cn.edu.gdmec.android.androidstudiodemo;

import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import cn.edu.gdmec.android.androidstudiodemo.utils.MD5Utils;

public class LoginActivity extends AppCompatActivity{
private TextView tv_main_title;//标题
private TextView tv_back,tv_register,tv_find_psw;//返回键,显示的注册,找回密码
private Button btn_login;//登录按钮
private String userName,psw,spPsw;//获取的用户名,密码,加密密码
private EditText et_user_name,et_psw;//编辑框
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
//设置此界面为竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
init();
}
//获取界面控件
private void init() {
//从main_title_bar中获取的id
tv_main_title=findViewById(R.id.tv_main_title);
tv_main_title.setText(“登录”);
tv_back=findViewById(R.id.tv_back);
//从activity_login.xml中获取的
tv_register=findViewById(R.id.tv_register);
tv_find_psw=findViewById(R.id.tv_find_psw);
btn_login=findViewById(R.id.btn_login);
et_user_name=findViewById(R.id.et_user_name);
et_psw=findViewById(R.id.et_psw);
//返回键的点击事件
tv_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//登录界面销毁
LoginActivity.this.finish();
}
});
//立即注册控件的点击事件
tv_register.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//为了跳转到注册界面,并实现注册功能
Intent intent=new Intent(LoginActivity.this,RegisterActivity.class);
startActivityForResult(intent, 1);
}
});
//找回密码控件的点击事件
tv_find_psw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//跳转到找回密码界面(此页面暂未创建)
}
});
//登录按钮的点击事件
btn_login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//开始登录,获取用户名和密码 getText().toString().trim();
userName=et_user_name.getText().toString().trim();
psw=et_psw.getText().toString().trim();
//对当前用户输入的密码进行MD5加密再进行比对判断, MD5Utils.md5( ); psw 进行加密判断是否一致
String md5Psw= MD5Utils.md5(psw);
// md5Psw ; spPsw 为 根据从SharedPreferences中用户名读取密码
// 定义方法 readPsw为了读取用户名,得到密码
spPsw=readPsw(userName);
// TextUtils.isEmpty
if(TextUtils.isEmpty(userName)){
Toast.makeText(LoginActivity.this, “请输入用户名”, Toast.LENGTH_SHORT).show();
return;
}else if(TextUtils.isEmpty(psw)){
Toast.makeText(LoginActivity.this, “请输入密码”, Toast.LENGTH_SHORT).show();
return;
// md5Psw.equals(); 判断,输入的密码加密后,是否与保存在SharedPreferences中一致
}else if(md5Psw.equals(spPsw)){
//一致登录成功
Toast.makeText(LoginActivity.this, “登录成功”, Toast.LENGTH_SHORT).show();
//保存登录状态,在界面保存登录的用户名 定义个方法 saveLoginStatus boolean 状态 , userName 用户名;
saveLoginStatus(true, userName);
//登录成功后关闭此页面进入主页
Intent data=new Intent();
//datad.putExtra( ); name , value ;
data.putExtra(“isLogin”,true);
//RESULT_OK为Activity系统常量,状态码为-1
// 表示此页面下的内容操作成功将data返回到上一页面,如果是用back返回过去的则不存在用setResult传递data值
setResult(RESULT_OK,data);
//销毁登录界面
LoginActivity.this.finish();
//跳转到主界面,登录成功的状态传递到 MainActivity 中
startActivity(new Intent(LoginActivity.this, MainActivity.class));
return;
}else if((spPsw!=null&&!TextUtils.isEmpty(spPsw)&&!md5Psw.equals(spPsw))){
Toast.makeText(LoginActivity.this, “输入的用户名和密码不一致”, Toast.LENGTH_SHORT).show();
return;
}else{
Toast.makeText(LoginActivity.this, “此用户名不存在”, Toast.LENGTH_SHORT).show();
}
}
});
}
/**
*从SharedPreferences中根据用户名读取密码
*/
private String readPsw(String userName){
//getSharedPreferences(“loginInfo”,MODE_PRIVATE);
//”loginInfo”,mode_private; MODE_PRIVATE表示可以继续写入
SharedPreferences sp=getSharedPreferences(“loginInfo”, MODE_PRIVATE);
//sp.getString() userName, “”;
return sp.getString(userName , “”);
}
/**
*保存登录状态和登录用户名到SharedPreferences中
*/
private void saveLoginStatus(boolean status,String userName){
//saveLoginStatus(true, userName);
//loginInfo表示文件名 SharedPreferences sp=getSharedPreferences(“loginInfo”, MODE_PRIVATE);
SharedPreferences sp=getSharedPreferences(“loginInfo”, MODE_PRIVATE);
//获取编辑器
SharedPreferences.Editor editor=sp.edit();
//存入boolean类型的登录状态
editor.putBoolean(“isLogin”, status);
//存入登录状态时的用户名
editor.putString(“loginUserName”, userName);
//提交修改
editor.commit();
}
/**
* 注册成功的数据返回至此
* @param requestCode 请求码
* @param resultCode 结果码
* @param data 数据
*/
@Override
//显示数据, onActivityResult
//startActivityForResult(intent, 1); 从注册界面中获取数据
//int requestCode , int resultCode , Intent data
// LoginActivity -> startActivityForResult -> onActivityResult();
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//super.onActivityResult(requestCode, resultCode, data);
super.onActivityResult(requestCode, resultCode, data);
if(data!=null){
//是获取注册界面回传过来的用户名
// getExtra().getString(“***”);
String userName=data.getStringExtra(“userName”);
if(!TextUtils.isEmpty(userName)){
//设置用户名到 et_user_name 控件
et_user_name.setText(userName);
//et_user_name控件的setSelection()方法来设置光标位置
et_user_name.setSelection(userName.length());
}
}
}
}
补充
如做了效果,需要在清单文件中实现该类,文件的跳转,可以自己了解一下。主要介绍注册模块,登录模块。里面的注解我写的如果有不全的或者错误点,可以联系讨论。

接下来你看到如上代码有点多,那么我们可以进行代码的优化来减少代码量。

 

总结
本文讲了AndroidStudio制作登录和注册功能的实现,界面的布局介绍,如果您还有更好地理解,欢迎沟通
定位:分享 Android&Java知识点,有兴趣可以继续关注

Android开发中登录注册界面的框架实现

小项目框架
今天用QQ的时候想到了,不如用android studio 做一个类似于这样的登录软件。当然QQ的实现的功能特别复杂,UI界面也很多,不是单纯的一时新奇就可以做出来的。就是简单的实现了一些功能,做了三个界面;1.登录界面。2.注册界面。3.登陆后的界面。

功能描述
登录按钮——按钮实现跳转到下一个界面,并且判断输入的账号、密码是否符合规则(不为空),提示,登陆成功或失败
注册按钮——按钮实现跳转到注册界面

登录界面

%title插图%num

main_activity.xml

<LinearLayout
android:id=”@+id/number”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@+id/iv”
android:layout_centerVertical=”true”
android:layout_marginBottom=”5dp”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”15dp”
android:background=”#ffffff”>
<TextView
android:id=”@+id/tv_number”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”账号”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:id=”@+id/et_username”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”5dp”
android:background=”@null”
android:padding=”10dp” />
</LinearLayout>
<LinearLayout
android:id=”@+id/password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@+id/number”
android:layout_centerVertical=”true”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:background=”#ffffff”>
<TextView
android:id=”@+id/tv_password”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”密码”
android:textSize=”20dp”
android:textColor=”#000″ />
<EditText
android:id=”@+id/et_password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”5dp”
android:layout_toRightOf=”@id/tv_password”
android:background=”@null”
android:inputType=”textPassword”
android:padding=”10dp” />
</LinearLayout>
<Button
android:id=”@+id/button_login”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/password”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”60dp”
android:background=”#3c8dc4″
android:text=”登录”
android:textColor=”#ffffff”
android:textSize=”20dp” />
<Button
android:id=”@+id/button_register”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/button_login”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”30dp”
android:background=”#b7585556″
android:text=”注册”
android:textColor=”#ffffff”
android:textSize=”20dp” />

<CheckBox
android:checked=”true”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”记住密码”
android:id=”@+id/checkBox”
android:layout_below=”@+id/password”
android:layout_marginLeft=”10dp”
android:layout_marginTop=”5dp”/>

注册界面
确定注册——按钮实现注册,判断以上四个注册信息是否符合规则,判断两次输入密码是否一样,并且不为空。并且显示提示信息
返回登录——按钮实现跳转到刚才的登录界面

%title插图%num

main_activity.java

public class MainActivity extends AppCompatActivity {
private EditText et_username;
private EditText et_password;
private EditText et_password2;
private EditText et_mail;
private Button btn_login;
private Button btn_register;
private CheckBox checkbox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Map<String, String> userInfo = SaveInfo.getSaveInformation(this);
if (userInfo != null) {
et_username.setText(userInfo.get(“username”));
et_password.setText(userInfo.get(“password”));
}
et_username =(EditText) findViewById(R.id.et_username);
et_password =(EditText) findViewById(R.id.et_password);
et_password2 =(EditText) findViewById(R.id.reg_password2);
et_mail = (EditText) findViewById(R.id.reg_mail);
checkbox = (CheckBox) findViewById(R.id.checkBox);
btn_login =(Button) findViewById(R.id.button_login);
btn_register =(Button) findViewById(R.id.button_register);
btn_login.setOnClickListener(new MyButton());
btn_register.setOnClickListener(new MyButton());
}
public class MyButton implements View.OnClickListener{
@Override
public void onClick(View view){
String username =et_username.getText().toString().trim();
String password =et_password.getText().toString().trim();
switch (view.getId()) {
//当点击登录按钮时
case R.id.button_login:
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(password)){
Toast.makeText(MainActivity.this,”密码或账号不能为空”,Toast.LENGTH_SHORT).show();
} else {
if(checkbox.isChecked()){
//保存密码的操作
}
Toast.makeText(MainActivity.this,”登录成功”,Toast.LENGTH_SHORT).show();
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
}
break;
//当点击注册按钮事件时
case R.id.button_register:
Intent intent = new Intent(MainActivity.this,RegisterActivity.class);
startActivity(intent);
break;

}
}
}
}

register_activity

<TextView
android:layout_marginTop=”60dp”
android:id=”@+id/reg_number1″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”账号:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@+id/reg_number1″
android:layout_toRightOf=”@+id/reg_number1″
android:id=”@+id/reg_username”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number2″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number1″
android:padding=”10dp”
android:text=”密码:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number2″
android:layout_toRightOf=”@+id/reg_number2″
android:id=”@+id/reg_password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number3″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number2″
android:padding=”10dp”
android:text=”密码:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number3″
android:layout_toRightOf=”@+id/reg_number3″
android:id=”@+id/reg_password2″
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number4″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number3″
android:padding=”10dp”
android:text=”邮箱:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number4″
android:layout_toRightOf=”@+id/reg_number4″
android:id=”@+id/reg_mail”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”确定注册”
android:background=”#74e674″
android:id=”@+id/reg_btn_sure”
android:layout_marginTop=”38dp”
android:layout_below=”@+id/reg_mail”
android:layout_marginLeft=”50dp” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”返回登录”
android:background=”#f27758″
android:id=”@+id/reg_btn_login”
android:layout_alignBottom=”@id/reg_btn_sure”
android:layout_toRightOf=”@id/reg_btn_sure”
android:layout_marginLeft=”60dp”
/>
<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”账号注册”
android:textSize=”30dp”
android:layout_marginTop=”5dp”
android:layout_alignParentLeft=”true”
android:layout_alignParentStart=”true” />
</RelativeLayout>

registeractivity.java

public class RegisterActivity extends AppCompatActivity {
private EditText reg_username;
private EditText reg_password;
private EditText reg_password2;
private EditText reg_mail;
private Button reg_btn_sure;
private Button reg_btn_login;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
reg_username = (EditText) findViewById(R.id.reg_username);
reg_password = (EditText) findViewById(R.id.reg_password);
reg_password2 = (EditText) findViewById(R.id.reg_password2);
reg_mail = (EditText) findViewById(R.id.reg_mail);
reg_btn_sure = (Button) findViewById(R.id.reg_btn_sure);
reg_btn_login = (Button) findViewById(R.id.reg_btn_login);
reg_btn_sure.setOnClickListener(new RegisterButton());
reg_btn_login.setOnClickListener(new RegisterButton());
}

public class RegisterButton implements View.OnClickListener {
@Override
public void onClick(View v) {
String username = reg_username.getText().toString().trim();
String password = reg_password.getText().toString().trim();
String password2 = reg_password2.getText().toString().trim();
String mail = reg_mail.getText().toString().trim();
switch (v.getId()) {
//注册开始,判断注册条件
case R.id.reg_btn_sure:
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password) || TextUtils.isEmpty(password2) || TextUtils.isEmpty(mail)) {
Toast.makeText(RegisterActivity.this, “各项均不能为空”, Toast.LENGTH_SHORT).show();
} else {
if (TextUtils.equals(password, password2)) {
//执行注册操作
SaveInfo.SaveInformation(RegisterActivity.this,username,password,password2,mail);
Toast.makeText(RegisterActivity.this,”注册成功,请返回登录”,Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(RegisterActivity.this, “两次输入的密码不一样”, Toast.LENGTH_SHORT).show();
}
}
break;
case R.id.reg_btn_login:
Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
startActivity(intent);
break;

}
}
}
}

登录成功界面创建一个布局文件就可以了,写上你想要的东西,我自己就是创建了一个布局,什么都没有,所以就在这里不写了
在这里因为要做一个保存操作,所以创建了一个java工具类,其中定义了两个方法,一个保存登录名和密码,一个负责调用保存的登录名和密码
saveinfo

public class SaveInfo {
public static boolean SaveInformation(Context context, String username, String password, String password2, String mail) {
try {
FileOutputStream fos = context.openFileOutput(“data.txt”, Context.MODE_APPEND);
fos.write((“用户名:” + username + ” 密码:” + password + “邮箱:” + mail).getBytes());
fos.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

public static Map<String, String> getSaveInformation(Context context) {
try {
FileInputStream fis = context.openFileInput(“data.txt”);
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
String str = br.readLine();
String[] infos = str.split(“用户名:”+”密码:”+”邮箱:”);
Map<String, String> map = new HashMap<String, String>();
map.put(“username”, infos[0]);
map.put(“password”, infos[1]);
fis.close();
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

示例图片

 

%title插图%num %title插图%num %title插图%num %title插图%num %title插图%num

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速