java 工程创建
@@ -1,2 +0,0 @@
|
||||
\|
|
||||
具体应该参考相应的jdk进行,了解相应的方法。这里只说明类的主要作用,和自己的相关理解,以及部分常用的方法。
|
||||
@@ -1,370 +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关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
|
||||
|
||||
好了,让我们开始吧。我准备分成几部分来总结涉及到多线程的内容:
|
||||
|
||||
1. 扎好马步:线程的状态
|
||||
|
||||
2. 内功心法:每个对象都有的方法(机制)
|
||||
|
||||
3. 太祖长拳:基本线程类
|
||||
|
||||
4. 九阴真经:高级多线程控制类
|
||||
|
||||
**扎好马步:线程的状态**
|
||||
|
||||
先来两张图:
|
||||
|
||||

|
||||
|
||||
线程状态
|
||||
|
||||

|
||||
|
||||
线程状态转换
|
||||
|
||||
各种状态一目了然,值得一提的是"blocked"这个状态:
|
||||
|
||||
线程在Running的过程中可能会遇到阻塞(Blocked)情况
|
||||
|
||||
1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
|
||||
|
||||
2. 调用wait(),使该线程处于等待池(wait blocked
|
||||
pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool
|
||||
),释放同步锁使线程回到可运行状态(Runnable)
|
||||
|
||||
3. 对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()检查当前线程是否发生中断,返回boolean
|
||||
|
||||
synchronized在获锁的过程中是不能被中断的。
|
||||
|
||||
中断是一个状态!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包内。共有三个实现:
|
||||
|
||||
ReentrantLock ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.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.容器类**
|
||||
|
||||
这里就讨论比较常用的两个:
|
||||
|
||||
BlockingQueue ConcurrentHashMap
|
||||
|
||||
**BlockingQueue**
|
||||
|
||||
阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管
|
||||
道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列),有兴趣可以研究
|
||||
|
||||
BlockingQueue在队列的基础上添加了多线程协作的功能:
|
||||
|
||||

|
||||
|
||||
BlockingQueue
|
||||
|
||||
除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队
|
||||
列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。
|
||||
|
||||
常见的阻塞队列有:
|
||||
|
||||
ArrayListBlockingQueue LinkedListBlockingQueue DelayQueue SynchronousQueue
|
||||
|
||||
**ConcurrentHashMap**
|
||||
|
||||
高效的线程安全哈希map。请对比hashTable , concurrentHashMap, HashMap
|
||||
|
||||
**5.管理类**
|
||||
|
||||
管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。
|
||||
|
||||
了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类
|
||||
ThreadMXBean
|
||||
|
||||
**ThreadPoolExecutor**
|
||||
|
||||
如果不了解这个类,应该了解前面提到的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接口并作为参数传入。
|
||||
@@ -1 +0,0 @@
|
||||
以功能为单位进行记录。主要说明实现某个具体功能时用到的相关类。以及类的主要用法。
|
||||
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,56 +0,0 @@
|
||||
# DAO模式的原理说明
|
||||
|
||||
DAO(Data Access
|
||||
Object)是一个数据访问接口,数据访问:顾名思义就是与数据库打交道,夹在业务逻辑与数据库资源中间。
|
||||
|
||||
在核心J2EE模式中DAO的定义是:为了建立一个健壮的J2EE应用,应该将所有对数据源的访问操作抽象封装在一个公共API中。用程序设计的语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口在逻辑上对应这个特定的数据存储。
|
||||
|
||||
DAO层本质上就是MVC中的Model部分的具体实现。相当于整个工程的javabean,实现了对数据库的增删查改。
|
||||
|
||||

|
||||
|
||||
# DAO模式的具体实现
|
||||
|
||||
\-VO类:提供javabean,表示数据库中的一条记录,主要有getter和setter方法
|
||||
|
||||
\-DAO类:提供了DAOmodel的接口,供控制器调用
|
||||
|
||||
\-DAOimplement:实现了DAO类提供的外部接口,主要是通过jdbc链接数据库,实现了数据库的增删查改操作。
|
||||
|
||||
\-DAOFactory:通过工厂类,获取一个DAO的实例化对象。
|
||||
|
||||
在当前的工程中分别对应一下包:
|
||||
|
||||
.domain
|
||||
|
||||
.dao
|
||||
|
||||
.dao.impl
|
||||
|
||||
.service.impl
|
||||
|
||||
这四个类分别代daobean,dao接口,dao的jdbc实现,dao的工厂类,在这里实现工程需要的所有的数据库的操作,提供给其他部分调用。
|
||||
|
||||
最根本的事通过jdbcUtils提供了访问数据库的链接
|
||||
|
||||
逻辑结构非常清晰明显
|
||||
|
||||
# 最后说明dao的工作原理
|
||||
|
||||

|
||||
|
||||
关于到的层级在此说明
|
||||
|
||||
数据库数据存储
|
||||
|
||||
\|-jdbc提供数据库的链接
|
||||
|
||||
\|-daoBean数据保存
|
||||
|
||||
\|-daoImpl的具体实现
|
||||
|
||||
\|-dao提供数据库访问的接口
|
||||
|
||||
\|-daoFactory实例化数据库的接口提供具体的数据操作
|
||||
|
||||
控制器数据操作需求
|
||||
@@ -1,9 +0,0 @@
|
||||
java本身就是一个依赖各种库来构建工程的语言,实际上需要自己写的东西并不多,就是调用各种已经编辑好的库,来实现各自的功能。
|
||||
|
||||
大部分库都是由jre/jdk内部自带的java库提供的。包括各种不同的版本,导入的时候必须要注意,可能是1.6,1.7,1.8等。
|
||||
|
||||
在做javaEE开发过程中,部分依赖的网络编程的库org.apache.\*是由Tomcat服务器插件提供的
|
||||
|
||||
在实现软件测试的过程中,junit4相关的库是由第三方插件提供,也就是说,在构建工程的时候,下载了eclemma插件,提供了org.junit.\*库和junit.\*库。并且提供了net.mooctest.plugin.\*库,实现与mooctest的网站的交互。
|
||||
|
||||
所以在构建工程过程中,必须得考虑相关库的导入。可能是来自于jdk/jre的官方标准库,也可能是来自于其他特定功能的库(tomcat),也许是来自相关插件的库(如adt插件、eclemma插件、junit4插件)等。
|
||||
@@ -1,15 +0,0 @@
|
||||
服务器端:
|
||||
|
||||
java Servlet
|
||||
|
||||
资源配置文件
|
||||
|
||||
与前端结合:
|
||||
|
||||
jsp:java脚本,直接在前端页面jsp嵌入前端脚本实现动态化。
|
||||
|
||||
jsp:jsp标签,使用jsp标签来实现基础的控制
|
||||
|
||||
jsp:EL表达式,使用简化的表达式实现动态数据获取
|
||||
|
||||
jsp:tag标签,包括标准标签库和自定义标签库的使用。
|
||||
@@ -1,11 +0,0 @@
|
||||
jsp代码编写属于后端,因为前端开发看不懂JSP代码,他们追求的是网页效果。而JSP其实就是JAVA代码来拼接前端页面而已,本身也是Servlet,因此JAVA
|
||||
WEB工程师也要学习一些HTML,JS,CSS等,实际开法中前端工程师写好网页,Java
|
||||
web开发人员负责填写JSP脚本,也可以反过来,后端定义好标签库,让前端以标签的方式来写代码。但近年来,大部分项目都是用Ajax来调用后端接口了,做到前后分离,直接写JSP的少了。
|
||||
|
||||
作者:清浅池塘
|
||||
|
||||
链接:https://www.zhihu.com/question/52695070/answer/228804504
|
||||
|
||||
来源:知乎
|
||||
|
||||
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
|
||||
@@ -1,11 +0,0 @@
|
||||
## JSP的内置对象
|
||||
|
||||
request对象
|
||||
|
||||
通过from表单能够发送GET和POST方法的Request。
|
||||
|
||||
request.getParameter("name"):得到键值对的值
|
||||
|
||||
request.setCharacterEncoding(utf-8);解决编码问题
|
||||
|
||||
getProtocol
|
||||
@@ -1,120 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import queue
|
||||
import re
|
||||
import urllib2
|
||||
import time
|
||||
import urllib2
|
||||
headers=("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36")
|
||||
opener=urllib2.build_opener()
|
||||
opener.addheaders=[headers]
|
||||
#global the opener
|
||||
urllib2.install_opener(opener)
|
||||
listurl=[]
|
||||
|
||||
#use the procy server
|
||||
def use_proxy(proxy_addr,url):
|
||||
try:
|
||||
import urllib2
|
||||
data = urllib2.urlopen(url).read().decode('utf-8')
|
||||
return data
|
||||
except urllib2.URLError as e:
|
||||
if hasattr(e,"code"):
|
||||
print(e.code)
|
||||
if hasattr(e,"reason"):
|
||||
print(e.reason)
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
print("exception:"+str(e))
|
||||
time.sleep(1)
|
||||
# get the picture
|
||||
def craw(url,number):
|
||||
html1=urllib2.urlopen(url).read().decode('utf-8')
|
||||
html1=html1
|
||||
pat1='<tr><td class="contentstyle54611">(.*?)</body></html>'
|
||||
result1=re.compile(pat1,re.S).findall(html1)
|
||||
result1=result1[0]
|
||||
pat2='<IMG border="0" src="(.*?).jpg"'
|
||||
imagelist=re.compile(pat2,re.S).findall(result1)
|
||||
x=1;
|
||||
for imageurl in imagelist:
|
||||
imagename="E:/hello/"+str(x)+str(number)+".jpg"
|
||||
imageurl = imageurl.replace('../../', "")
|
||||
imageurl = "http://zdhxy.nwpu.edu.cn/" + imageurl+'.jpg'
|
||||
try:
|
||||
urllib2.urlretrieve(imageurl,filename=imagename)
|
||||
except urllib2.URLError as e:
|
||||
if hasattr(e, "code"):
|
||||
print(e.code)
|
||||
if hasattr(e, "reason"):
|
||||
print(e.reason)
|
||||
x+=1;
|
||||
|
||||
|
||||
#get the content through the link
|
||||
def getcontent(listurl,proxy):
|
||||
i=0
|
||||
#set local files's html code
|
||||
html1='''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://
|
||||
www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>computer</title>
|
||||
</head>
|
||||
<body>'''
|
||||
fh=open("E:/hello/2.html","wb")
|
||||
fh.write(html1.encode("utf-8"))
|
||||
fh.close()
|
||||
fh=open("E:/hello/2.html","ab")
|
||||
for i in range(0,len(listurl)):
|
||||
for j in range(0,len(listurl[i])):
|
||||
try:
|
||||
url=listurl[i][j]
|
||||
url=url.replace("\"","")
|
||||
url = url.replace('../', "")
|
||||
url="http://jsj.nwpu.edu.cn/"+url
|
||||
data2=use_proxy(proxy,url)
|
||||
craw(url,j)
|
||||
titlepat='<title>(.*?)</title>'
|
||||
contentpat='<tr><td class="contentstyle54611">(.*?)</body></html>'
|
||||
title=re.compile(titlepat).findall(data2)
|
||||
content=re.compile(contentpat,re.S).findall(data2)
|
||||
thistitle=" "
|
||||
thiscontent=" "
|
||||
|
||||
if(title!=[]):
|
||||
thistitle=title[0]
|
||||
if(content!=[]):
|
||||
thiscontent=content[0]
|
||||
dataall="<p>title is :"+thistitle+"</p><p>content is :"+thiscontent+"</p><br>"
|
||||
fh.write(dataall.encode("utf-8"))
|
||||
print("第"+str(i)+"第个网站"+str(j)+"次处理")
|
||||
except urllib2.URLError as e:
|
||||
if hasattr(e, "code"):
|
||||
print(e.code)
|
||||
if hasattr(e, "reason"):
|
||||
print(e.reason)
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
print("exception:" + str(e))
|
||||
time.sleep(1)
|
||||
fh.close()
|
||||
html2='''</body>
|
||||
</html>
|
||||
'''
|
||||
fh=open("E:/hello/2.html","ab")
|
||||
fh.write(html2.encode("utf-8"))
|
||||
fh.close()
|
||||
|
||||
proxy="123.145.128.139:8118"
|
||||
pagestart=1
|
||||
pageend=2
|
||||
|
||||
url="http://jsj.nwpu.edu.cn/index/xyxw.htm"
|
||||
data1=use_proxy(proxy,url)
|
||||
listurlpat='<a class="c54630" href="(.*?)"'
|
||||
listurl.append(re.compile(listurlpat,re.S).findall(data1))
|
||||
getcontent(listurl,proxy)
|
||||
#print(title);
|
||||
#listurl=getlisturl(key,pagestart,pageend,proxy)
|
||||
#getcontent(listurl,proxy)
|
||||
@@ -1,157 +0,0 @@
|
||||
JDBC操作数据库的基本步骤:
|
||||
|
||||
1)加载(注册)数据库驱动(到JVM)。
|
||||
|
||||
2)建立(获取)数据库连接。
|
||||
|
||||
3)创建(获取)数据库操作对象。
|
||||
|
||||
4)定义操作的SQL语句。
|
||||
|
||||
5)执行数据库操作。
|
||||
|
||||
6)获取并操作结果集。
|
||||
|
||||
7)关闭对象,回收数据库资源(关闭结果集--\>关闭数据库操作对象--\>关闭连接)。
|
||||
|
||||
**[java]** [view plain](http://blog.csdn.net/hpu_a/article/details/51354867)
|
||||
[copy](http://blog.csdn.net/hpu_a/article/details/51354867)
|
||||
|
||||
1. **package** com.yangshengjie.jdbc;
|
||||
|
||||
2. **import** java.sql.Connection;
|
||||
|
||||
3. **import** java.sql.DriverManager;
|
||||
|
||||
4. **import** java.sql.ResultSet;
|
||||
|
||||
5. **import** java.sql.SQLException;
|
||||
|
||||
6. **import** java.sql.Statement;
|
||||
|
||||
7.
|
||||
|
||||
8. **public** **class** JDBCTest {
|
||||
|
||||
9. /\*\*
|
||||
|
||||
10. \* 使用JDBC连接并操作mysql数据库
|
||||
|
||||
11. \*/
|
||||
|
||||
12. **public** **static** **void** main(String[] args) {
|
||||
|
||||
13. // 数据库驱动类名的字符串
|
||||
|
||||
14. String driver = "com.mysql.jdbc.Driver";
|
||||
|
||||
15. // 数据库连接串
|
||||
|
||||
16. String url = "jdbc:mysql://127.0.0.1:3306/jdbctest";
|
||||
|
||||
17. // 用户名
|
||||
|
||||
18. String username = "root";
|
||||
|
||||
19. // 密码
|
||||
|
||||
20. String password = "mysqladmin";
|
||||
|
||||
21. Connection conn = **null**;
|
||||
|
||||
22. Statement stmt = **null**;
|
||||
|
||||
23. ResultSet rs = **null**;
|
||||
|
||||
24. **try** {
|
||||
|
||||
25. // 1、加载数据库驱动(
|
||||
成功加载后,会将Driver类的实例注册到DriverManager类中)
|
||||
|
||||
26. Class.forName(driver );
|
||||
|
||||
27. // 2、获取数据库连接
|
||||
|
||||
28. conn = DriverManager.getConnection(url, username, password);
|
||||
|
||||
29. // 3、获取数据库操作对象
|
||||
|
||||
30. stmt = conn.createStatement();
|
||||
|
||||
31. // 4、定义操作的SQL语句
|
||||
|
||||
32. String sql = "select \* from user where id = 100";
|
||||
|
||||
33. // 5、执行数据库操作
|
||||
|
||||
34. rs = stmt.executeQuery(sql);
|
||||
|
||||
35. // 6、获取并操作结果集
|
||||
|
||||
36. **while** (rs.next()) {
|
||||
|
||||
37. System.out.println(rs.getInt("id"));
|
||||
|
||||
38. System.out.println(rs.getString("name"));
|
||||
|
||||
39. }
|
||||
|
||||
40. } **catch** (Exception e) {
|
||||
|
||||
41. e.printStackTrace();
|
||||
|
||||
42. } **finally** {
|
||||
|
||||
43. // 7、关闭对象,回收数据库资源
|
||||
|
||||
44. **if** (rs != **null**) { //关闭结果集对象
|
||||
|
||||
45. **try** {
|
||||
|
||||
46. rs.close();
|
||||
|
||||
47. } **catch** (SQLException e) {
|
||||
|
||||
48. e.printStackTrace();
|
||||
|
||||
49. }
|
||||
|
||||
50. }
|
||||
|
||||
51. **if** (stmt != **null**) { // 关闭数据库操作对象
|
||||
|
||||
52. **try** {
|
||||
|
||||
53. stmt.close();
|
||||
|
||||
54. } **catch** (SQLException e) {
|
||||
|
||||
55. e.printStackTrace();
|
||||
|
||||
56. }
|
||||
|
||||
57. }
|
||||
|
||||
58. **if** (conn != **null**) { // 关闭数据库连接对象
|
||||
|
||||
59. **try** {
|
||||
|
||||
60. **if** (!conn.isClosed()) {
|
||||
|
||||
61. conn.close();
|
||||
|
||||
62. }
|
||||
|
||||
63. } **catch** (SQLException e) {
|
||||
|
||||
64. e.printStackTrace();
|
||||
|
||||
65. }
|
||||
|
||||
66. }
|
||||
|
||||
67. }
|
||||
|
||||
68. }
|
||||
|
||||
69. }
|
||||
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 41 KiB |
@@ -1 +0,0 @@
|
||||
不能用过度的MVC框架的知识来理解简单的jsp页面,怎么说,现在写的jsp的级别就是将这些内容组合起来,一个页面与另一个页面杂糅式的交互。起始php与python这种直接接触框架的学习,反而非常不好。有时间多做东西救火啊,提高效率。才能有更多的理解。
|
||||
@@ -1,55 +0,0 @@
|
||||
# 序列化
|
||||
|
||||
对序列化的理解
|
||||
|
||||
JAVA序列化是指把JAVA对象转换为字节序列的过程。JAVA反序列化是吧字节恢复为JAVA对象。
|
||||
|
||||
应用:当两个进程进行远程通信时,可以相互发送各种类型的数据。包括文本、图片、音频、视频,这些数据都是以二进制序列的形式在网络上传送。
|
||||
|
||||
两个JAVA进程通信的时候,也可以传送对象,将JAVA序列化为字节序列,装载为字节流,然后进行传送。.接收端将字节流反序列化为JAVA对象。
|
||||
|
||||
作用:实现了数据的持久化,通过序列化,能够将数据永久的保存到硬盘上。利用序列化可以实现数据流的输入输出和远程通信。
|
||||
|
||||
实现方法:
|
||||
|
||||
java.io.ObjectOuptutStream:对象输出流writeObject()
|
||||
|
||||
java.io.ObjectInputStream:对象输入流readObject()
|
||||
|
||||
对象能被序列化的要求
|
||||
|
||||
implement了Serializable接口,通过默认的方法进行序列化
|
||||
|
||||
implement了Serializable接口,并且实现了相应的方法,然后通过自己的方式进行序列化和反序列化
|
||||
|
||||
序列化的步骤:
|
||||
|
||||
步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
|
||||
|
||||
ObjectOutputStream out = new ObjectOutputStream(new
|
||||
fileOutputStream(“D:\\\\objectfile.obj”));
|
||||
|
||||
步骤二:通过对象输出流的writeObject()方法写对象:
|
||||
|
||||
out.writeObject(“Hello”);
|
||||
|
||||
out.writeObject(new Date());
|
||||
|
||||
反序列化的步骤:
|
||||
|
||||
步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
|
||||
|
||||
ObjectInputStream in = new ObjectInputStream(new
|
||||
fileInputStream(“D:\\\\objectfile.obj”));
|
||||
|
||||
步骤二:通过对象输出流的readObject()方法读取对象:
|
||||
|
||||
String obj1 = (String)in.readObject();
|
||||
|
||||
Date obj2 = (Date)in.readObject();
|
||||
|
||||
# transient关键字说明
|
||||
|
||||
可以理解为瞬态、不可序列化。
|
||||
|
||||
简单的说,truansient 变量就是在对象序列化的过程中没有被序列化变量。
|
||||
@@ -1,27 +0,0 @@
|
||||

|
||||
|
||||
使用appium的原理及过程说明:
|
||||
|
||||
运行测试脚本的电脑,我们称为Client。
|
||||
|
||||
打开Appium,就开启了Appium Server,默认监听4723端口。
|
||||
|
||||
Appium
|
||||
Server接收到Client命令(测试脚本),翻译成测试机器可以理解的语言,然后发送给测试机器运行。
|
||||
|
||||
测试机器运行结束后,再把测试结果返回给Appium Server,之后Appium
|
||||
Server再把测试结果返回给Client。
|
||||
|
||||

|
||||
|
||||
1.appium是c/s模式的
|
||||
|
||||
2.appium是基于webdriver协议添加对移动设备自动化api扩展而成的,所以具有和webdriver一样的特性,比如多语言支持
|
||||
|
||||
3.webdriver是基于http协议的,第一连接会建立一个session会话,并通过post发送一个json告知服务端相关测试信息
|
||||
|
||||
4.对于android来说,4.2以后是基于uiautomator框架实现查找注入事件的,4.2以前则是instrumentation框架的,并封装成一个叫Selendroid这玩意提供服务
|
||||
|
||||
5.客户端只需要发送http请求实现通讯,意味着客户端就是多语言支持的
|
||||
|
||||
6.appium服务端是node.js写的,所以你安装的时候无论哪个平台都是先装node
|
||||
@@ -1,96 +0,0 @@
|
||||
WEB-INF web.xml
|
||||
|
||||
文件用来配置整个工程。
|
||||
|
||||
\<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
|
||||
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
|
||||
|
||||
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
|
||||
|
||||
version="2.4"\>
|
||||
|
||||
\<servlet\>
|
||||
|
||||
\<servlet-name\>FirstServlet\</servlet-name\>
|
||||
|
||||
\<servlet-class\>lesson.j2ee.ex1.FirstServlet\</servlet-class\>
|
||||
|
||||
\</servlet\>
|
||||
|
||||
\<servlet-mapping\>
|
||||
|
||||
\<servlet-name\>FirstServlet\</servlet-name\>
|
||||
|
||||
\<url-pattern\>/Serv1\</url-pattern\>
|
||||
|
||||
\</servlet-mapping\>
|
||||
|
||||
\</web-app\>
|
||||
|
||||
(也许讲过,但是感觉从来没有学过怎么开发,知识知道Javaservlet的一些基础理论知识,真的需要学习一下这方面的东西了。如何真正的使用Java开发一个工程。)
|
||||
|
||||
Servlet通过回调函数管理自己的生命周期
|
||||
|
||||
init()-\>初始化
|
||||
|
||||
sevice()-\>提供servlet服务。
|
||||
|
||||
\-doGet()用于相应GET方法的函数,HTTPSevlet类中实现
|
||||
|
||||
\-doPost()用于响应POST方法的函数,HTTPSevlet类中实现
|
||||
|
||||
\-doHead()用于响应HEAD方法的函数,HTTPSevlet类中实现。
|
||||
|
||||
destroy()-\>销毁
|
||||
|
||||
getServletConfig()用来获取Servlet配置信息。
|
||||
|
||||
getSeviceInfo()用来获取其他信息。
|
||||
|
||||
关于Servlet的继承关系,必须知道每一级继承都实现了什么玩意。
|
||||
|
||||
Servlet \<-- GenericServlet \<-- HttpServlet \<--- MyServlet
|
||||
|
||||
关于servlet中常用到的两个对象
|
||||
|
||||
ServletConfig对象
|
||||
|
||||
每个servlet都有一个ServletConfig对象;用于向servlet传递部署时的信息;用于访问ServletContext;其参数在DD中配置
|
||||
|
||||
ServletContext对象
|
||||
|
||||
每个web应用都有一个ServletContext;用于访问web应用的参数(在DD中配置);用于获取服务器信息;存放其它公用信息
|
||||
|
||||
请求与响应
|
||||
|
||||
ServletRequest \<--- HTTPServletRequest \<--- realRequest
|
||||
|
||||
SerbletResponse \<---- HTTPSerbletResponse \<---realResponse
|
||||
|
||||
java
|
||||
与你写的python还是有很多不同的,面向对象思想比较深刻。所以,每次设计到一个内容,首先了解有哪些类-对象,
|
||||
每个类-对象由哪些属性 , 每个类-对象有那些方法;
|
||||
因为每个类的功能和作用比较固定,所以属性和方法一般是与此功能相关的,笔记哦啊容易记忆。
|
||||
|
||||
已经涉及到的对象
|
||||
|
||||
HttpServlet -对象,每次请求会实例化。
|
||||
|
||||
Request -对象
|
||||
|
||||
Response -对象
|
||||
|
||||
ServletConfig - 对象
|
||||
|
||||
SerletContext - 对象
|
||||
|
||||
还是第一次真正的用到cookie,那么思考一下如何使用cookie。
|
||||
|
||||
用户携带cookie值(如果有的话)请求一个网页。
|
||||
|
||||
服务器端读取cookie值,如果存在cookie值,则添加到页面当中。
|
||||
|
||||
当前用的这个东西存在一定的弊端。直接通过cookie获取当前用户的身份。
|
||||
@@ -1,27 +0,0 @@
|
||||
感觉配置环境的过程中,对这些东西一知半解。
|
||||
|
||||
1\. 首先安装eclipse,jdk/jre,adt插件,完成eclipse开发Android环境的基础设备。
|
||||
|
||||
2.
|
||||
安装了node.js并配置了相关的环境,使得系统能够编译并运行node.js的代码。然而跟我们的工程有什么联系,一点都不知道。
|
||||
|
||||
:经过了解得知,安装node的原因是因为,node的服务器端,是由node.js写的,也就是说,node并不是用来进行编译客户端的代码的,而是用来运行appium的服务器端程序的。
|
||||
|
||||
3.
|
||||
安装Appium软件(这玩意不是系统插件),是一个调试的监视工具吧。具体怎么运作也不知道。
|
||||
|
||||
:经过了解得知,这个东西封装了对各种移动设备框架的调试方法,通过服务器端,连接客户端和移动设备。客户端通过url的形式访问服务器端,完成对移动设备的调试。
|
||||
|
||||
4\. APPium调试过程依赖的jar包包括测试类库:
|
||||
|
||||
Selenum类库(selenium.jar)
|
||||
|
||||
Appium类库(client.jar)不知道是干啥用的
|
||||
|
||||
5\. 关联了相关的文件(apk appt appt.exe)不知道是干啥用的
|
||||
|
||||
6\. 启动了Android模拟器,或者连接了真机。
|
||||
|
||||
7\. 启动Appium工具,并通过junit的方式对软件进行调试。
|
||||
|
||||
总之,今天最大的收获,可能就是对eclipse自动构建java工程结构有了更多的了解。
|
||||
@@ -1,30 +0,0 @@
|
||||
下面来介绍将小数值舍入为整数的几个方法:Math.ceil()、Math.floor()和Math.round()。
|
||||
这三个方法分别遵循下列舍入规则:
|
||||
|
||||
Math.ceil()执行向上舍入,即它总是将数值向上舍入为最接近的整数;
|
||||
|
||||
Math.floor()执行向下舍入,即它总是将数值向下舍入为最接近的整数;
|
||||
|
||||
Math.round()执行标准舍入,即它总是将数值四舍五入为最接近的整数(这也是我们在数学课上学到的舍入规则)。
|
||||
|
||||
下面来看几个例子:
|
||||
|
||||
Math.ceil(25.9) //26
|
||||
|
||||
Math.ceil(25.5) //26
|
||||
|
||||
Math.ceil(25.1) //26
|
||||
|
||||
Math.ceil(25.0) //25
|
||||
|
||||
Math.round(25.9) //26
|
||||
|
||||
Math.round(25.5) //26
|
||||
|
||||
Math.round(25.1) //25
|
||||
|
||||
Math.floor(25.9) //25
|
||||
|
||||
Math.floor(25.5) //25
|
||||
|
||||
Math.floor(25.1) //25
|
||||
@@ -1,622 +0,0 @@
|
||||
# Java IO
|
||||
<!-- GFM-TOC -->
|
||||
* [Java IO](#java-io)
|
||||
* [一、概览](#一概览)
|
||||
* [二、磁盘操作](#二磁盘操作)
|
||||
* [三、字节操作](#三字节操作)
|
||||
* [实现文件复制](#实现文件复制)
|
||||
* [装饰者模式](#装饰者模式)
|
||||
* [四、字符操作](#四字符操作)
|
||||
* [编码与解码](#编码与解码)
|
||||
* [String 的编码方式](#string-的编码方式)
|
||||
* [Reader 与 Writer](#reader-与-writer)
|
||||
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
|
||||
* [五、对象操作](#五对象操作)
|
||||
* [序列化](#序列化)
|
||||
* [Serializable](#serializable)
|
||||
* [transient](#transient)
|
||||
* [六、网络操作](#六网络操作)
|
||||
* [InetAddress](#inetaddress)
|
||||
* [URL](#url)
|
||||
* [Sockets](#sockets)
|
||||
* [Datagram](#datagram)
|
||||
* [七、NIO](#七nio)
|
||||
* [流与块](#流与块)
|
||||
* [通道与缓冲区](#通道与缓冲区)
|
||||
* [缓冲区状态变量](#缓冲区状态变量)
|
||||
* [文件 NIO 实例](#文件-nio-实例)
|
||||
* [选择器](#选择器)
|
||||
* [套接字 NIO 实例](#套接字-nio-实例)
|
||||
* [内存映射文件](#内存映射文件)
|
||||
* [对比](#对比)
|
||||
* [八、参考资料](#八参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
## 一、概览
|
||||
|
||||
Java 的 I/O 大概可以分成以下几类:
|
||||
|
||||
- 磁盘操作:File
|
||||
- 字节操作:InputStream 和 OutputStream
|
||||
- 字符操作:Reader 和 Writer
|
||||
- 对象操作:Serializable
|
||||
- 网络操作:Socket
|
||||
- 新的输入/输出:NIO
|
||||
|
||||
## 二、磁盘操作
|
||||
|
||||
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
|
||||
|
||||
递归地列出一个目录下所有文件:
|
||||
|
||||
```java
|
||||
public static void listAllFiles(File dir) {
|
||||
if (dir == null || !dir.exists()) {
|
||||
return;
|
||||
}
|
||||
if (dir.isFile()) {
|
||||
System.out.println(dir.getName());
|
||||
return;
|
||||
}
|
||||
for (File file : dir.listFiles()) {
|
||||
listAllFiles(file);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从 Java7 开始,可以使用 Paths 和 Files 代替 File。
|
||||
|
||||
## 三、字节操作
|
||||
|
||||
### 实现文件复制
|
||||
|
||||
```java
|
||||
public static void copyFile(String src, String dist) throws IOException {
|
||||
FileInputStream in = new FileInputStream(src);
|
||||
FileOutputStream out = new FileOutputStream(dist);
|
||||
|
||||
byte[] buffer = new byte[20 * 1024];
|
||||
int cnt;
|
||||
|
||||
// read() 最多读取 buffer.length 个字节
|
||||
// 返回的是实际读取的个数
|
||||
// 返回 -1 的时候表示读到 eof,即文件尾
|
||||
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
|
||||
out.write(buffer, 0, cnt);
|
||||
}
|
||||
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
```
|
||||
|
||||
### 装饰者模式
|
||||
|
||||
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
|
||||
|
||||
- InputStream 是抽象组件;
|
||||
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
|
||||
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9709694b-db05-4cce-8d2f-1c8b09f4d921.png" width="650px"> </div><br>
|
||||
|
||||
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
|
||||
|
||||
```java
|
||||
FileInputStream fileInputStream = new FileInputStream(filePath);
|
||||
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
|
||||
```
|
||||
|
||||
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
|
||||
|
||||
## 四、字符操作
|
||||
|
||||
### 编码与解码
|
||||
|
||||
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
|
||||
|
||||
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
|
||||
|
||||
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
|
||||
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
|
||||
- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
|
||||
|
||||
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
|
||||
|
||||
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
|
||||
|
||||
### String 的编码方式
|
||||
|
||||
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
|
||||
|
||||
```java
|
||||
String str1 = "中文";
|
||||
byte[] bytes = str1.getBytes("UTF-8");
|
||||
String str2 = new String(bytes, "UTF-8");
|
||||
System.out.println(str2);
|
||||
```
|
||||
|
||||
在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
|
||||
|
||||
```java
|
||||
byte[] bytes = str1.getBytes();
|
||||
```
|
||||
|
||||
### Reader 与 Writer
|
||||
|
||||
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
|
||||
|
||||
- InputStreamReader 实现从字节流解码成字符流;
|
||||
- OutputStreamWriter 实现字符流编码成为字节流。
|
||||
|
||||
### 实现逐行输出文本文件的内容
|
||||
|
||||
```java
|
||||
public static void readFileContent(String filePath) throws IOException {
|
||||
|
||||
FileReader fileReader = new FileReader(filePath);
|
||||
BufferedReader bufferedReader = new BufferedReader(fileReader);
|
||||
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
|
||||
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
|
||||
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
|
||||
// 因此只要一个 close() 调用即可
|
||||
bufferedReader.close();
|
||||
}
|
||||
```
|
||||
|
||||
## 五、对象操作
|
||||
|
||||
### 序列化
|
||||
|
||||
序列化就是将一个对象转换成字节序列,方便存储和传输。
|
||||
|
||||
- 序列化:ObjectOutputStream.writeObject()
|
||||
- 反序列化:ObjectInputStream.readObject()
|
||||
|
||||
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
|
||||
|
||||
### Serializable
|
||||
|
||||
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) throws IOException, ClassNotFoundException {
|
||||
|
||||
A a1 = new A(123, "abc");
|
||||
String objectFile = "file/a1";
|
||||
|
||||
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
|
||||
objectOutputStream.writeObject(a1);
|
||||
objectOutputStream.close();
|
||||
|
||||
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
|
||||
A a2 = (A) objectInputStream.readObject();
|
||||
objectInputStream.close();
|
||||
System.out.println(a2);
|
||||
}
|
||||
|
||||
private static class A implements Serializable {
|
||||
|
||||
private int x;
|
||||
private String y;
|
||||
|
||||
A(int x, String y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "x = " + x + " " + "y = " + y;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### transient
|
||||
|
||||
transient 关键字可以使一些属性不会被序列化。
|
||||
|
||||
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
|
||||
|
||||
```java
|
||||
private transient Object[] elementData;
|
||||
```
|
||||
|
||||
## 六、网络操作
|
||||
|
||||
Java 中的网络支持:
|
||||
|
||||
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
|
||||
- URL:统一资源定位符;
|
||||
- Sockets:使用 TCP 协议实现网络通信;
|
||||
- Datagram:使用 UDP 协议实现网络通信。
|
||||
|
||||
### InetAddress
|
||||
|
||||
没有公有的构造函数,只能通过静态方法来创建实例。
|
||||
|
||||
```java
|
||||
InetAddress.getByName(String host);
|
||||
InetAddress.getByAddress(byte[] address);
|
||||
```
|
||||
|
||||
### URL
|
||||
|
||||
可以直接从 URL 中读取字节流数据。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
URL url = new URL("http://www.baidu.com");
|
||||
|
||||
/* 字节流 */
|
||||
InputStream is = url.openStream();
|
||||
|
||||
/* 字符流 */
|
||||
InputStreamReader isr = new InputStreamReader(is, "utf-8");
|
||||
|
||||
/* 提供缓存功能 */
|
||||
BufferedReader br = new BufferedReader(isr);
|
||||
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
|
||||
br.close();
|
||||
}
|
||||
```
|
||||
|
||||
### Sockets
|
||||
|
||||
- ServerSocket:服务器端类
|
||||
- Socket:客户端类
|
||||
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1e6affc4-18e5-4596-96ef-fb84c63bf88a.png" width="550px"> </div><br>
|
||||
|
||||
### Datagram
|
||||
|
||||
- DatagramSocket:通信类
|
||||
- DatagramPacket:数据包类
|
||||
|
||||
## 七、NIO
|
||||
|
||||
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
|
||||
|
||||
### 流与块
|
||||
|
||||
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
|
||||
|
||||
面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
|
||||
|
||||
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
|
||||
|
||||
I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
|
||||
|
||||
### 通道与缓冲区
|
||||
|
||||
#### 1. 通道
|
||||
|
||||
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
|
||||
|
||||
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
|
||||
|
||||
通道包括以下类型:
|
||||
|
||||
- FileChannel:从文件中读写数据;
|
||||
- DatagramChannel:通过 UDP 读写网络中数据;
|
||||
- SocketChannel:通过 TCP 读写网络中数据;
|
||||
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
|
||||
|
||||
#### 2. 缓冲区
|
||||
|
||||
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
|
||||
|
||||
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
|
||||
|
||||
缓冲区包括以下类型:
|
||||
|
||||
- ByteBuffer
|
||||
- CharBuffer
|
||||
- ShortBuffer
|
||||
- IntBuffer
|
||||
- LongBuffer
|
||||
- FloatBuffer
|
||||
- DoubleBuffer
|
||||
|
||||
### 缓冲区状态变量
|
||||
|
||||
- capacity:最大容量;
|
||||
- position:当前已经读写的字节数;
|
||||
- limit:还可以读写的字节数。
|
||||
|
||||
状态变量的改变过程举例:
|
||||
|
||||
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
|
||||
|
||||
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
|
||||
|
||||
③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
|
||||
|
||||
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
|
||||
|
||||
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
|
||||
|
||||
### 文件 NIO 实例
|
||||
|
||||
以下展示了使用 NIO 快速复制文件的实例:
|
||||
|
||||
```java
|
||||
public static void fastCopy(String src, String dist) throws IOException {
|
||||
|
||||
/* 获得源文件的输入字节流 */
|
||||
FileInputStream fin = new FileInputStream(src);
|
||||
|
||||
/* 获取输入字节流的文件通道 */
|
||||
FileChannel fcin = fin.getChannel();
|
||||
|
||||
/* 获取目标文件的输出字节流 */
|
||||
FileOutputStream fout = new FileOutputStream(dist);
|
||||
|
||||
/* 获取输出字节流的文件通道 */
|
||||
FileChannel fcout = fout.getChannel();
|
||||
|
||||
/* 为缓冲区分配 1024 个字节 */
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
|
||||
|
||||
while (true) {
|
||||
|
||||
/* 从输入通道中读取数据到缓冲区中 */
|
||||
int r = fcin.read(buffer);
|
||||
|
||||
/* read() 返回 -1 表示 EOF */
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 切换读写 */
|
||||
buffer.flip();
|
||||
|
||||
/* 把缓冲区的内容写入输出文件中 */
|
||||
fcout.write(buffer);
|
||||
|
||||
/* 清空缓冲区 */
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 选择器
|
||||
|
||||
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
|
||||
|
||||
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
|
||||
|
||||
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
|
||||
|
||||
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
|
||||
|
||||
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/093f9e57-429c-413a-83ee-c689ba596cef.png" width="350px"> </div><br>
|
||||
|
||||
#### 1. 创建选择器
|
||||
|
||||
```java
|
||||
Selector selector = Selector.open();
|
||||
```
|
||||
|
||||
#### 2. 将通道注册到选择器上
|
||||
|
||||
```java
|
||||
ServerSocketChannel ssChannel = ServerSocketChannel.open();
|
||||
ssChannel.configureBlocking(false);
|
||||
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
```
|
||||
|
||||
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
|
||||
|
||||
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
|
||||
|
||||
- SelectionKey.OP_CONNECT
|
||||
- SelectionKey.OP_ACCEPT
|
||||
- SelectionKey.OP_READ
|
||||
- SelectionKey.OP_WRITE
|
||||
|
||||
它们在 SelectionKey 的定义如下:
|
||||
|
||||
```java
|
||||
public static final int OP_READ = 1 << 0;
|
||||
public static final int OP_WRITE = 1 << 2;
|
||||
public static final int OP_CONNECT = 1 << 3;
|
||||
public static final int OP_ACCEPT = 1 << 4;
|
||||
```
|
||||
|
||||
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
|
||||
|
||||
```java
|
||||
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
|
||||
```
|
||||
|
||||
#### 3. 监听事件
|
||||
|
||||
```java
|
||||
int num = selector.select();
|
||||
```
|
||||
|
||||
使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
|
||||
|
||||
#### 4. 获取到达的事件
|
||||
|
||||
```java
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
if (key.isAcceptable()) {
|
||||
// ...
|
||||
} else if (key.isReadable()) {
|
||||
// ...
|
||||
}
|
||||
keyIterator.remove();
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 事件循环
|
||||
|
||||
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
|
||||
|
||||
```java
|
||||
while (true) {
|
||||
int num = selector.select();
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
if (key.isAcceptable()) {
|
||||
// ...
|
||||
} else if (key.isReadable()) {
|
||||
// ...
|
||||
}
|
||||
keyIterator.remove();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 套接字 NIO 实例
|
||||
|
||||
```java
|
||||
public class NIOServer {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
Selector selector = Selector.open();
|
||||
|
||||
ServerSocketChannel ssChannel = ServerSocketChannel.open();
|
||||
ssChannel.configureBlocking(false);
|
||||
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
ServerSocket serverSocket = ssChannel.socket();
|
||||
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
|
||||
serverSocket.bind(address);
|
||||
|
||||
while (true) {
|
||||
|
||||
selector.select();
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
|
||||
while (keyIterator.hasNext()) {
|
||||
|
||||
SelectionKey key = keyIterator.next();
|
||||
|
||||
if (key.isAcceptable()) {
|
||||
|
||||
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
|
||||
|
||||
// 服务器会为每个新连接创建一个 SocketChannel
|
||||
SocketChannel sChannel = ssChannel1.accept();
|
||||
sChannel.configureBlocking(false);
|
||||
|
||||
// 这个新连接主要用于从客户端读取数据
|
||||
sChannel.register(selector, SelectionKey.OP_READ);
|
||||
|
||||
} else if (key.isReadable()) {
|
||||
|
||||
SocketChannel sChannel = (SocketChannel) key.channel();
|
||||
System.out.println(readDataFromSocketChannel(sChannel));
|
||||
sChannel.close();
|
||||
}
|
||||
|
||||
keyIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
while (true) {
|
||||
|
||||
buffer.clear();
|
||||
int n = sChannel.read(buffer);
|
||||
if (n == -1) {
|
||||
break;
|
||||
}
|
||||
buffer.flip();
|
||||
int limit = buffer.limit();
|
||||
char[] dst = new char[limit];
|
||||
for (int i = 0; i < limit; i++) {
|
||||
dst[i] = (char) buffer.get(i);
|
||||
}
|
||||
data.append(dst);
|
||||
buffer.clear();
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class NIOClient {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Socket socket = new Socket("127.0.0.1", 8888);
|
||||
OutputStream out = socket.getOutputStream();
|
||||
String s = "hello world";
|
||||
out.write(s.getBytes());
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 内存映射文件
|
||||
|
||||
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
|
||||
|
||||
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
|
||||
|
||||
下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
|
||||
|
||||
```java
|
||||
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
|
||||
```
|
||||
|
||||
### 对比
|
||||
|
||||
NIO 与普通 I/O 的区别主要有以下两点:
|
||||
|
||||
- NIO 是非阻塞的;
|
||||
- NIO 面向块,I/O 面向流。
|
||||
|
||||
## 八、参考资料
|
||||
|
||||
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
|
||||
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
|
||||
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
|
||||
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
|
||||
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
|
||||
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
|
||||
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
|
||||
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
|
||||
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
|
||||
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
|
||||
@@ -1,755 +0,0 @@
|
||||
# Java 虚拟机
|
||||
<!-- GFM-TOC -->
|
||||
* [Java 虚拟机](#java-虚拟机)
|
||||
* [一、运行时数据区域](#一运行时数据区域)
|
||||
* [程序计数器](#程序计数器)
|
||||
* [Java 虚拟机栈](#java-虚拟机栈)
|
||||
* [本地方法栈](#本地方法栈)
|
||||
* [堆](#堆)
|
||||
* [方法区](#方法区)
|
||||
* [运行时常量池](#运行时常量池)
|
||||
* [直接内存](#直接内存)
|
||||
* [二、垃圾收集](#二垃圾收集)
|
||||
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
|
||||
* [引用类型](#引用类型)
|
||||
* [垃圾收集算法](#垃圾收集算法)
|
||||
* [垃圾收集器](#垃圾收集器)
|
||||
* [三、内存分配与回收策略](#三内存分配与回收策略)
|
||||
* [Minor GC 和 Full GC](#minor-gc-和-full-gc)
|
||||
* [内存分配策略](#内存分配策略)
|
||||
* [Full GC 的触发条件](#full-gc-的触发条件)
|
||||
* [四、类加载机制](#四类加载机制)
|
||||
* [类的生命周期](#类的生命周期)
|
||||
* [类加载过程](#类加载过程)
|
||||
* [类初始化时机](#类初始化时机)
|
||||
* [类与类加载器](#类与类加载器)
|
||||
* [类加载器分类](#类加载器分类)
|
||||
* [双亲委派模型](#双亲委派模型)
|
||||
* [自定义类加载器实现](#自定义类加载器实现)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
本文大部分内容参考 **周志明《深入理解 Java 虚拟机》** ,想要深入学习的话请看原书。
|
||||
|
||||
## 一、运行时数据区域
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5778d113-8e13-4c53-b5bf-801e58080b97.png" width="400px"> </div><br>
|
||||
|
||||
### 程序计数器
|
||||
|
||||
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
|
||||
|
||||
### Java 虚拟机栈
|
||||
|
||||
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8442519f-0b4d-48f4-8229-56f984363c69.png" width="400px"> </div><br>
|
||||
|
||||
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:
|
||||
|
||||
```java
|
||||
java -Xss2M HackTheJava
|
||||
```
|
||||
|
||||
该区域可能抛出以下异常:
|
||||
|
||||
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
|
||||
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
|
||||
|
||||
### 本地方法栈
|
||||
|
||||
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a6899d-c6b0-4a47-8569-9d08f0baf86c.png" width="300px"> </div><br>
|
||||
|
||||
### 堆
|
||||
|
||||
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
|
||||
|
||||
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
|
||||
|
||||
- 新生代(Young Generation)
|
||||
- 老年代(Old Generation)
|
||||
|
||||
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
|
||||
|
||||
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
|
||||
|
||||
```java
|
||||
java -Xms1M -Xmx2M HackTheJava
|
||||
```
|
||||
|
||||
### 方法区
|
||||
|
||||
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||
|
||||
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
|
||||
|
||||
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
|
||||
|
||||
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
|
||||
|
||||
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
|
||||
|
||||
### 运行时常量池
|
||||
|
||||
运行时常量池是方法区的一部分。
|
||||
|
||||
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
|
||||
|
||||
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
|
||||
|
||||
### 直接内存
|
||||
|
||||
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
|
||||
|
||||
## 二、垃圾收集
|
||||
|
||||
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
|
||||
|
||||
### 判断一个对象是否可被回收
|
||||
|
||||
#### 1. 引用计数算法
|
||||
|
||||
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
|
||||
|
||||
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
|
||||
public Object instance = null;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Test a = new Test();
|
||||
Test b = new Test();
|
||||
a.instance = b;
|
||||
b.instance = a;
|
||||
a = null;
|
||||
b = null;
|
||||
doSomething();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
|
||||
|
||||
#### 2. 可达性分析算法
|
||||
|
||||
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
|
||||
|
||||
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
|
||||
|
||||
- 虚拟机栈中局部变量表中引用的对象
|
||||
- 本地方法栈中 JNI 中引用的对象
|
||||
- 方法区中类静态属性引用的对象
|
||||
- 方法区中的常量引用的对象
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/83d909d2-3858-4fe1-8ff4-16471db0b180.png" width="350px"> </div><br>
|
||||
|
||||
|
||||
#### 3. 方法区的回收
|
||||
|
||||
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
|
||||
|
||||
主要是对常量池的回收和对类的卸载。
|
||||
|
||||
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
|
||||
|
||||
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
|
||||
|
||||
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
|
||||
- 加载该类的 ClassLoader 已经被回收。
|
||||
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
|
||||
|
||||
#### 4. finalize()
|
||||
|
||||
类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
|
||||
|
||||
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
|
||||
|
||||
### 引用类型
|
||||
|
||||
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
|
||||
|
||||
Java 提供了四种强度不同的引用类型。
|
||||
|
||||
#### 1. 强引用
|
||||
|
||||
被强引用关联的对象不会被回收。
|
||||
|
||||
使用 new 一个新对象的方式来创建强引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
```
|
||||
|
||||
#### 2. 软引用
|
||||
|
||||
被软引用关联的对象只有在内存不够的情况下才会被回收。
|
||||
|
||||
使用 SoftReference 类来创建软引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
SoftReference<Object> sf = new SoftReference<Object>(obj);
|
||||
obj = null; // 使对象只被软引用关联
|
||||
```
|
||||
|
||||
#### 3. 弱引用
|
||||
|
||||
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
|
||||
|
||||
使用 WeakReference 类来创建弱引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
WeakReference<Object> wf = new WeakReference<Object>(obj);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
#### 4. 虚引用
|
||||
|
||||
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
|
||||
|
||||
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
|
||||
|
||||
使用 PhantomReference 来创建虚引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
### 垃圾收集算法
|
||||
|
||||
#### 1. 标记 - 清除
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/005b481b-502b-4e3f-985d-d043c2b330aa.png" width="400px"> </div><br>
|
||||
|
||||
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
|
||||
|
||||
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
|
||||
|
||||
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
|
||||
|
||||
不足:
|
||||
|
||||
- 标记和清除过程效率都不高;
|
||||
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
|
||||
|
||||
#### 2. 标记 - 整理
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ccd773a5-ad38-4022-895c-7ac318f31437.png" width="400px"> </div><br>
|
||||
|
||||
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
|
||||
|
||||
优点:
|
||||
|
||||
- 不会产生内存碎片
|
||||
|
||||
不足:
|
||||
|
||||
- 需要移动大量对象,处理效率比较低。
|
||||
|
||||
#### 3. 复制
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b2b77b9e-958c-4016-8ae5-9c6edd83871e.png" width="400px"> </div><br>
|
||||
|
||||
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
|
||||
|
||||
主要不足是只使用了内存的一半。
|
||||
|
||||
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
|
||||
|
||||
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
|
||||
|
||||
#### 4. 分代收集
|
||||
|
||||
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
|
||||
|
||||
一般将堆分为新生代和老年代。
|
||||
|
||||
- 新生代使用:复制算法
|
||||
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
|
||||
|
||||
### 垃圾收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
|
||||
|
||||
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
|
||||
|
||||
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
|
||||
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
|
||||
|
||||
#### 1. Serial 收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
|
||||
|
||||
Serial 翻译为串行,也就是说它以串行的方式执行。
|
||||
|
||||
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
|
||||
|
||||
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
|
||||
|
||||
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
|
||||
|
||||
#### 2. ParNew 收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
|
||||
|
||||
它是 Serial 收集器的多线程版本。
|
||||
|
||||
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
|
||||
|
||||
#### 3. Parallel Scavenge 收集器
|
||||
|
||||
与 ParNew 一样是多线程收集器。
|
||||
|
||||
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
|
||||
|
||||
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
|
||||
|
||||
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
|
||||
|
||||
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
|
||||
|
||||
#### 4. Serial Old 收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
|
||||
|
||||
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
|
||||
|
||||
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
|
||||
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
|
||||
|
||||
#### 5. Parallel Old 收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
|
||||
|
||||
是 Parallel Scavenge 收集器的老年代版本。
|
||||
|
||||
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
|
||||
|
||||
#### 6. CMS 收集器
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
|
||||
|
||||
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
|
||||
|
||||
分为以下四个流程:
|
||||
|
||||
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
|
||||
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
|
||||
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
|
||||
- 并发清除:不需要停顿。
|
||||
|
||||
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
|
||||
|
||||
具有以下缺点:
|
||||
|
||||
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
|
||||
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
|
||||
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
|
||||
|
||||
#### 7. G1 收集器
|
||||
|
||||
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
|
||||
|
||||
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
|
||||
|
||||
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
|
||||
|
||||
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
|
||||
|
||||
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
|
||||
|
||||
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
|
||||
|
||||
- 初始标记
|
||||
- 并发标记
|
||||
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
|
||||
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
|
||||
|
||||
具备如下特点:
|
||||
|
||||
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
|
||||
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
|
||||
|
||||
## 三、内存分配与回收策略
|
||||
|
||||
### Minor GC 和 Full GC
|
||||
|
||||
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
|
||||
|
||||
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
|
||||
|
||||
### 内存分配策略
|
||||
|
||||
#### 1. 对象优先在 Eden 分配
|
||||
|
||||
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
|
||||
|
||||
#### 2. 大对象直接进入老年代
|
||||
|
||||
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
|
||||
|
||||
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
|
||||
|
||||
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
|
||||
|
||||
#### 3. 长期存活的对象进入老年代
|
||||
|
||||
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
|
||||
|
||||
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
|
||||
|
||||
#### 4. 动态对象年龄判定
|
||||
|
||||
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
|
||||
|
||||
#### 5. 空间分配担保
|
||||
|
||||
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
|
||||
|
||||
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
|
||||
|
||||
### Full GC 的触发条件
|
||||
|
||||
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
|
||||
|
||||
#### 1. 调用 System.gc()
|
||||
|
||||
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
|
||||
|
||||
#### 2. 老年代空间不足
|
||||
|
||||
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
|
||||
|
||||
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
|
||||
|
||||
#### 3. 空间分配担保失败
|
||||
|
||||
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
|
||||
|
||||
#### 4. JDK 1.7 及以前的永久代空间不足
|
||||
|
||||
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
|
||||
|
||||
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
|
||||
|
||||
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
|
||||
|
||||
#### 5. Concurrent Mode Failure
|
||||
|
||||
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
|
||||
|
||||
## 四、类加载机制
|
||||
|
||||
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
|
||||
|
||||
### 类的生命周期
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/335fe19c-4a76-45ab-9320-88c90d6a0d7e.png" width="600px"> </div><br>
|
||||
|
||||
包括以下 7 个阶段:
|
||||
|
||||
- **加载(Loading)**
|
||||
- **验证(Verification)**
|
||||
- **准备(Preparation)**
|
||||
- **解析(Resolution)**
|
||||
- **初始化(Initialization)**
|
||||
- 使用(Using)
|
||||
- 卸载(Unloading)
|
||||
|
||||
### 类加载过程
|
||||
|
||||
包含了加载、验证、准备、解析和初始化这 5 个阶段。
|
||||
|
||||
#### 1. 加载
|
||||
|
||||
加载是类加载的一个阶段,注意不要混淆。
|
||||
|
||||
加载过程完成以下三件事:
|
||||
|
||||
- 通过类的完全限定名称获取定义该类的二进制字节流。
|
||||
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
|
||||
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
|
||||
|
||||
|
||||
其中二进制字节流可以从以下方式中获取:
|
||||
|
||||
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
|
||||
- 从网络中获取,最典型的应用是 Applet。
|
||||
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
|
||||
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
|
||||
|
||||
#### 2. 验证
|
||||
|
||||
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
|
||||
|
||||
#### 3. 准备
|
||||
|
||||
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
|
||||
|
||||
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
|
||||
|
||||
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
|
||||
|
||||
```java
|
||||
public static int value = 123;
|
||||
```
|
||||
|
||||
如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
|
||||
|
||||
```java
|
||||
public static final int value = 123;
|
||||
```
|
||||
|
||||
#### 4. 解析
|
||||
|
||||
将常量池的符号引用替换为直接引用的过程。
|
||||
|
||||
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
|
||||
|
||||
#### 5. 初始化
|
||||
|
||||
<div data="modify -->"></div>
|
||||
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit\>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit\>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
由于父类的 <clinit\>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
|
||||
|
||||
```java
|
||||
static class Parent {
|
||||
public static int A = 1;
|
||||
static {
|
||||
A = 2;
|
||||
}
|
||||
}
|
||||
|
||||
static class Sub extends Parent {
|
||||
public static int B = A;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(Sub.B); // 2
|
||||
}
|
||||
```
|
||||
|
||||
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit\>() 方法。但接口与类不同的是,执行接口的 <clinit\>() 方法不需要先执行父接口的 <clinit\>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit\>() 方法。
|
||||
|
||||
虚拟机会保证一个类的 <clinit\>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit\>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit\>() 方法完毕。如果在一个类的 <clinit\>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
### 类初始化时机
|
||||
|
||||
#### 1. 主动引用
|
||||
|
||||
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
|
||||
|
||||
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
|
||||
|
||||
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
|
||||
|
||||
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
|
||||
|
||||
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
|
||||
|
||||
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
|
||||
|
||||
#### 2. 被动引用
|
||||
|
||||
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
|
||||
|
||||
- 通过子类引用父类的静态字段,不会导致子类初始化。
|
||||
|
||||
```java
|
||||
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
|
||||
```
|
||||
|
||||
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
|
||||
|
||||
```java
|
||||
SuperClass[] sca = new SuperClass[10];
|
||||
```
|
||||
|
||||
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
|
||||
|
||||
```java
|
||||
System.out.println(ConstClass.HELLOWORLD);
|
||||
```
|
||||
|
||||
### 类与类加载器
|
||||
|
||||
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
|
||||
|
||||
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
|
||||
|
||||
### 类加载器分类
|
||||
|
||||
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
|
||||
|
||||
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
|
||||
|
||||
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
|
||||
|
||||
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
|
||||
|
||||
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME\>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
|
||||
|
||||
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME\>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
|
||||
|
||||
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
|
||||
|
||||
### 双亲委派模型
|
||||
|
||||
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
|
||||
|
||||
下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dd2d40a-5b2b-4d45-b176-e75a4cd4bdbf.png" width="500px"> </div><br>
|
||||
|
||||
#### 1. 工作过程
|
||||
|
||||
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
|
||||
|
||||
#### 2. 好处
|
||||
|
||||
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
|
||||
|
||||
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
|
||||
|
||||
#### 3. 实现
|
||||
|
||||
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
|
||||
|
||||
```java
|
||||
public abstract class ClassLoader {
|
||||
// The parent class loader for delegation
|
||||
private final ClassLoader parent;
|
||||
|
||||
public Class<?> loadClass(String name) throws ClassNotFoundException {
|
||||
return loadClass(name, false);
|
||||
}
|
||||
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
// First, check if the class has already been loaded
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c == null) {
|
||||
try {
|
||||
if (parent != null) {
|
||||
c = parent.loadClass(name, false);
|
||||
} else {
|
||||
c = findBootstrapClassOrNull(name);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
// ClassNotFoundException thrown if class not found
|
||||
// from the non-null parent class loader
|
||||
}
|
||||
|
||||
if (c == null) {
|
||||
// If still not found, then invoke findClass in order
|
||||
// to find the class.
|
||||
c = findClass(name);
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义类加载器实现
|
||||
|
||||
以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
|
||||
|
||||
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
|
||||
|
||||
```java
|
||||
public class FileSystemClassLoader extends ClassLoader {
|
||||
|
||||
private String rootDir;
|
||||
|
||||
public FileSystemClassLoader(String rootDir) {
|
||||
this.rootDir = rootDir;
|
||||
}
|
||||
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
byte[] classData = getClassData(name);
|
||||
if (classData == null) {
|
||||
throw new ClassNotFoundException();
|
||||
} else {
|
||||
return defineClass(name, classData, 0, classData.length);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getClassData(String className) {
|
||||
String path = classNameToPath(className);
|
||||
try {
|
||||
InputStream ins = new FileInputStream(path);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int bufferSize = 4096;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int bytesNumRead;
|
||||
while ((bytesNumRead = ins.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, bytesNumRead);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String classNameToPath(String className) {
|
||||
return rootDir + File.separatorChar
|
||||
+ className.replace('.', File.separatorChar) + ".class";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
|
||||
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
|
||||
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
|
||||
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
|
||||
- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
|
||||
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
|
||||
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
|
||||
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
|
||||
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
|
||||
- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
|
||||
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
|
||||
- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
|
||||
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
|
||||
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
|
||||
@@ -1,117 +0,0 @@
|
||||
## 新的事件监听机制
|
||||
|
||||
-----
|
||||
|
||||
* 之前是定义一个组件对象(事件源),队组建对象绑定一个监听器对象,在监听器对象中有监听同一事件不同动作的函数,函数内部对事件进行处理。从监听器的角度对事件和动作进行处理。
|
||||
* 现在是直接将静态事件绑定到事件源上,通过process函数运行运行事件发生时的处理方式。而且这些静态事件是已经存在于awtEvent类中的。
|
||||
|
||||
```
|
||||
|
||||
package EventHandle;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* @author 宙斯
|
||||
* 准确的说这并不是先前的事件监听机制吧。它不存在指明的监听器。
|
||||
* 应该更像一种组件自带的事件匹配功能。
|
||||
* 具体步骤:
|
||||
* 打开某一类型的事件匹配功能。
|
||||
* 使用process函数运行某个事件
|
||||
* 当事件发生时通过事件的ID匹配具体的动作。
|
||||
* 匹配后设置相应的处理方式。
|
||||
*/
|
||||
|
||||
public class baseEvent extends Frame{
|
||||
private int x = 0,y = 0,flag = 0;
|
||||
private Image img;
|
||||
private int dx = 0,dy = 0;
|
||||
|
||||
public static void main(String[] args) {
|
||||
baseEvent bE = new baseEvent();
|
||||
}
|
||||
public baseEvent(){
|
||||
super();
|
||||
setSize(500,500);
|
||||
this.setVisible(true);
|
||||
|
||||
Toolkit tk = Toolkit.getDefaultToolkit();
|
||||
img = tk.getImage("first.png");
|
||||
|
||||
//相当于添加了对Component事件的监听器,似乎监听器类在底层已经直接绑定。
|
||||
enableEvents(AWTEvent.COMPONENT_EVENT_MASK);
|
||||
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
|
||||
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
|
||||
|
||||
repaint();
|
||||
|
||||
}
|
||||
|
||||
//直接调用底层的事件处理函数
|
||||
public void processComponentEvent(ComponentEvent e){
|
||||
if(e.getID() == ComponentEvent.COMPONENT_MOVED){
|
||||
System.out.println(e.getSource());
|
||||
System.out.println(e.getID());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void processWindowEvent(WindowEvent e){
|
||||
if(e.getID() == WindowEvent.WINDOW_CLOSING){
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
/*
|
||||
public void processMouseEvent(MouseEvent e){
|
||||
if(e.getID() == MouseEvent.MOUSE_PRESSED){
|
||||
System.out.println("pressed");
|
||||
if((e.getX()>= x)&&(e.getX()<=x+100)&&(e.getY()>=y)&&(e.getY()<=y+100)){
|
||||
flag = 1;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
}
|
||||
}
|
||||
if((e.getID()==MouseEvent.MOUSE_RELEASED)&&(flag == 1)){
|
||||
x = e.getX();
|
||||
y = e.getY();
|
||||
repaint();
|
||||
flag = 0;
|
||||
}
|
||||
}
|
||||
*/
|
||||
public void processMouseEvent(MouseEvent e){
|
||||
if(e.getID() == MouseEvent.MOUSE_PRESSED){
|
||||
System.out.println("pressed");
|
||||
if((e.getX()>= x)&&(e.getX()<=x+100)&&(e.getY()>=y)&&(e.getY()<=y+100)){
|
||||
dx = e.getX()-x;
|
||||
dy = e.getY()-y;
|
||||
flag = 1;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
}
|
||||
}
|
||||
if(e.getID()== MouseEvent.MOUSE_MOVED&&flag ==1){
|
||||
x = e.getX()-dx;
|
||||
y = e.getY()-dy;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
repaint();
|
||||
|
||||
}
|
||||
if((e.getID()==MouseEvent.MOUSE_RELEASED)&&(flag == 1)){
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
}
|
||||
public void paint(Graphics g){
|
||||
g.drawImage(img, x, y,100,100, this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
@@ -1,84 +0,0 @@
|
||||
# 画图的标准步骤
|
||||
|
||||
------
|
||||
|
||||
package painting;
|
||||
|
||||
import java.awt.Frame;
|
||||
import java.awt.Graphics;
|
||||
|
||||
/**
|
||||
* 画图过程的整体框架.
|
||||
* 继承了Frame类能够使用相关的框体数据。
|
||||
* 实现了Rnnable接口,本类能够作为一个线程被执行。
|
||||
* @author 宙斯
|
||||
*
|
||||
*/
|
||||
public class framePaint extends Frame implements Runnable{
|
||||
public static void main(String [] args){
|
||||
framePaint workStart = new framePaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 无参构造函数.
|
||||
* 设置了窗体的基本属性。
|
||||
* 创建了与本类相关的线程并且执行。
|
||||
* 也就是说,这个类的对象在创建的时候,就已经执行了相关的画图代码,并且产生了一个看见的图形对象。
|
||||
* 而且这个绘制的过程,会作为一个普通的线程被执行。
|
||||
*/
|
||||
public framePaint(){
|
||||
super("framePaint");
|
||||
setSize(350,350);
|
||||
setVisible(true);
|
||||
|
||||
new Thread(this).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现多线程的方法.
|
||||
* run方法时Runnable线程的主要方法,当线程开始是,执行这个方法,所以画图方法在这个线程方法中被调用
|
||||
* 这个方法相当于画图过程的总方法。
|
||||
* @see java.lang.Runnable#run()
|
||||
*/
|
||||
public void run(){
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现画图的过程.
|
||||
* 主要的画图方法,在调用画图对象的repaint方法时,这个方法被自动加载。
|
||||
* @see java.awt.Window#paint(java.awt.Graphics)
|
||||
*/
|
||||
public void paint(Graphics g){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
# Graphics类中的主要方法
|
||||
|
||||
-----
|
||||
|
||||
> 能绘制的图形:
|
||||
> String 字符串
|
||||
> Line 直线
|
||||
> Rect 长方形
|
||||
> Oval 椭圆形
|
||||
> Arc 弧线
|
||||
> Polygon 多边形
|
||||
>
|
||||
|
||||
* draw系列
|
||||
绘制线条图形
|
||||
* fill系列
|
||||
绘制平面图形,用前景色填充内容物
|
||||
* clearRect()
|
||||
清除指定区域内的图形
|
||||
* clipRect()
|
||||
截取制定区域内的图形
|
||||
* copyArea()
|
||||
赋值制定区域内的图形到指定区域
|
||||
* get/setColor/Font
|
||||
设置颜色字体等
|
||||
* setClip()
|
||||
截取制定形状内的图形
|
||||
@@ -1,5 +0,0 @@
|
||||
# 底层事件处理
|
||||
|
||||
-----
|
||||
|
||||
* 在java.awt学习的时候,将监听器绑定到事件源上,当指定事件发生时监听器会自动匹配到相应的动作,执行相应的处理方式。但在底层事件处理过程中,不许要监听器,能够直接获取当前发生的底层事件,然后进行匹配处理。
|
||||
@@ -1,117 +0,0 @@
|
||||
## 新的事件监听机制
|
||||
|
||||
-----
|
||||
|
||||
* 之前是定义一个组件对象(事件源),队组建对象绑定一个监听器对象,在监听器对象中有监听同一事件不同动作的函数,函数内部对事件进行处理。从监听器的角度对事件和动作进行处理。
|
||||
* 现在是直接将静态事件绑定到事件源上,通过process函数运行运行事件发生时的处理方式。而且这些静态事件是已经存在于awtEvent类中的。
|
||||
|
||||
```
|
||||
|
||||
package EventHandle;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* @author 宙斯
|
||||
* 准确的说这并不是先前的事件监听机制吧。它不存在指明的监听器。
|
||||
* 应该更像一种组件自带的事件匹配功能。
|
||||
* 具体步骤:
|
||||
* 打开某一类型的事件匹配功能。
|
||||
* 使用process函数运行某个事件
|
||||
* 当事件发生时通过事件的ID匹配具体的动作。
|
||||
* 匹配后设置相应的处理方式。
|
||||
*/
|
||||
|
||||
public class baseEvent extends Frame{
|
||||
private int x = 0,y = 0,flag = 0;
|
||||
private Image img;
|
||||
private int dx = 0,dy = 0;
|
||||
|
||||
public static void main(String[] args) {
|
||||
baseEvent bE = new baseEvent();
|
||||
}
|
||||
public baseEvent(){
|
||||
super();
|
||||
setSize(500,500);
|
||||
this.setVisible(true);
|
||||
|
||||
Toolkit tk = Toolkit.getDefaultToolkit();
|
||||
img = tk.getImage("first.png");
|
||||
|
||||
//相当于添加了对Component事件的监听器,似乎监听器类在底层已经直接绑定。
|
||||
enableEvents(AWTEvent.COMPONENT_EVENT_MASK);
|
||||
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
|
||||
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
|
||||
|
||||
repaint();
|
||||
|
||||
}
|
||||
|
||||
//直接调用底层的事件处理函数
|
||||
public void processComponentEvent(ComponentEvent e){
|
||||
if(e.getID() == ComponentEvent.COMPONENT_MOVED){
|
||||
System.out.println(e.getSource());
|
||||
System.out.println(e.getID());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void processWindowEvent(WindowEvent e){
|
||||
if(e.getID() == WindowEvent.WINDOW_CLOSING){
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
/*
|
||||
public void processMouseEvent(MouseEvent e){
|
||||
if(e.getID() == MouseEvent.MOUSE_PRESSED){
|
||||
System.out.println("pressed");
|
||||
if((e.getX()>= x)&&(e.getX()<=x+100)&&(e.getY()>=y)&&(e.getY()<=y+100)){
|
||||
flag = 1;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
}
|
||||
}
|
||||
if((e.getID()==MouseEvent.MOUSE_RELEASED)&&(flag == 1)){
|
||||
x = e.getX();
|
||||
y = e.getY();
|
||||
repaint();
|
||||
flag = 0;
|
||||
}
|
||||
}
|
||||
*/
|
||||
public void processMouseEvent(MouseEvent e){
|
||||
if(e.getID() == MouseEvent.MOUSE_PRESSED){
|
||||
System.out.println("pressed");
|
||||
if((e.getX()>= x)&&(e.getX()<=x+100)&&(e.getY()>=y)&&(e.getY()<=y+100)){
|
||||
dx = e.getX()-x;
|
||||
dy = e.getY()-y;
|
||||
flag = 1;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
}
|
||||
}
|
||||
if(e.getID()== MouseEvent.MOUSE_MOVED&&flag ==1){
|
||||
x = e.getX()-dx;
|
||||
y = e.getY()-dy;
|
||||
System.out.println(e.getX()+"..."+e.getY());
|
||||
|
||||
repaint();
|
||||
|
||||
}
|
||||
if((e.getID()==MouseEvent.MOUSE_RELEASED)&&(flag == 1)){
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
}
|
||||
public void paint(Graphics g){
|
||||
g.drawImage(img, x, y,100,100, this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
# 多线程共享受限资源
|
||||
|
||||
----
|
||||
|
||||
## >多线程共享受限资源存在的问题
|
||||
|
||||
|
||||
----
|
||||
|
||||
* **这个描述???**
|
||||
当你坐在桌子旁边时,手里有有一把叉子,准备插起盘子里最后一块十五,当叉子碰到十五的时候,它忽然消失了。
|
||||
|
||||
* **解决方法**
|
||||
给这个资源加锁,每个线程访问这个资源时上锁,访问结束后开锁。
|
||||
@@ -1,14 +0,0 @@
|
||||
# 多线程共享受限资源
|
||||
|
||||
----
|
||||
|
||||
## >多线程共享受限资源存在的问题
|
||||
|
||||
|
||||
----
|
||||
|
||||
* **这个描述???**
|
||||
当你坐在桌子旁边时,手里有有一把叉子,准备插起盘子里最后一块十五,当叉子碰到十五的时候,它忽然消失了。
|
||||
|
||||
* **解决方法**
|
||||
给这个资源加锁,每个线程访问这个资源时上锁,访问结束后开锁。
|
||||
@@ -1,48 +0,0 @@
|
||||
# 多线程理论补充
|
||||
|
||||
------
|
||||
|
||||
## >让步机制
|
||||
|
||||
-----
|
||||
|
||||
* **yield()**
|
||||
thread.yield()线程执行这个函数会主动放弃执行的优先级,但只是暗示别的线程能够抢到更多的资源,没有确定的机制保证cpu资源一定被其他线程使用。
|
||||
|
||||
## >休眠机制
|
||||
|
||||
---
|
||||
|
||||
* **sleep()**
|
||||
thread.sleep(30)线程停止执行30ms,可能会跑出异常,此线程调用interrupt()方法,中断了线程的执行。
|
||||
|
||||
## >优先权机制
|
||||
|
||||
----
|
||||
|
||||
* **setPriority()**
|
||||
thread.setPriority()通过thread类中的常量对优先级进行设定。thread.getPriority()能够获取当前线程的优先级。
|
||||
|
||||
## >后台进程
|
||||
|
||||
-----
|
||||
|
||||
* **后台线程(守护线程)thread.setDaemon()**
|
||||
程序运行时,在后台提供的一种通用的线程,当所有的非后台线程结束时,后台线程(守护线程)自动终止。必须在线程启动之前,调用方法setDaemon(),将线程设置成后台线程。isDeamon()判断是否为后台线程。
|
||||
|
||||
## >加入线程
|
||||
|
||||
----
|
||||
|
||||
* **t.join(a)**
|
||||
t线程此时将被挂起,直到a线程结束才会被执行。当a线程结束时,t.isAlive()返回为假,线程恢复。
|
||||
|
||||
## >编码变体
|
||||
|
||||
-----
|
||||
|
||||
* **implements Runnable**
|
||||
通过实现接口Runnable来达到作为线程类的方法。必须实现run方法。
|
||||
|
||||
* **建立有响应的用户界面**
|
||||
让运算作为一个独立的线程在后台独立运行。
|
||||
@@ -1,40 +0,0 @@
|
||||
# 多线程的应用
|
||||
|
||||
------
|
||||
|
||||
## 多线程停止线程
|
||||
|
||||
----
|
||||
|
||||
* stop方法已经过时不能使用,只能当run方法结束时,才能终止线程。开启多线程程运行时,代码通常是循环结构,只要控制住线程,通常可以让run方法结束。
|
||||
* 应当设计可以修改的无限循环标志。跳出无限循环,则会终止线程。
|
||||
* 当线程在循环内进入等待状态时,及时线程的循环条件不满足,必须终止线程,但是无法执行到判断语句进行线程的终止,此时,必须使用interrupt()函数来达到要求。
|
||||
* interrupt将处于冻结状态的线程强制转换到运行状态。此时wait()就会跑出我们处理已久的中断异常。
|
||||
* 当没有指定的方式让冻结的线程回复到运行状态时,这是需要对冻结进行清除。Thread提供了interrupt方法。
|
||||
|
||||
|
||||
## 多线程 守护线程
|
||||
|
||||
-----
|
||||
|
||||
* 守护线程就是后台线程,也是一种依赖线程
|
||||
* 特点:
|
||||
当前台线程结束后,后台线程会自动终止,作为依赖线程,守护线程,不用强调结束。
|
||||
* 方法:
|
||||
setDemon()设置某个线程为守护线程
|
||||
|
||||
## 多线程join方法
|
||||
|
||||
----
|
||||
|
||||
* t1.join() t1抢夺cpu执行权,主线程处于冻结状态,t1优先执行。相当于顺序执行两个线程。主线程碰到谁的join就会等谁执行。
|
||||
* 当A线程执行到了B线程的Join方法时,A线程就会等待,等B线程都执行完成,A才会执行。join才会临时加入线程执行。当B线程进入wait时,A线程也能继续执行。
|
||||
* toString 方法能够显示线程的名称,线程的优先级,线程当前的分组(线程组谁调用就是谁的线程组的)
|
||||
* 所有线程包括主线程,默认是5。数越大优先级越高。MAX_PRIORITY,MIN_PRIORITY,
|
||||
* yield()方法,当线程读到这里是,会释放执行权。这样会使所有的线程都有平均执行的效果。
|
||||
|
||||
## 什么时候会使用到多线程
|
||||
|
||||
----
|
||||
|
||||
* 当程序独立运算相互之间不相关的时候,可以用多线程封装一下,提高程序执行的速度
|
||||
@@ -1,40 +0,0 @@
|
||||
# 多线程的应用
|
||||
|
||||
------
|
||||
|
||||
## 多线程停止线程
|
||||
|
||||
----
|
||||
|
||||
* stop方法已经过时不能使用,只能当run方法结束时,才能终止线程。开启多线程程运行时,代码通常是循环结构,只要控制住线程,通常可以让run方法结束。
|
||||
* 应当设计可以修改的无限循环标志。跳出无限循环,则会终止线程。
|
||||
* 当线程在循环内进入等待状态时,及时线程的循环条件不满足,必须终止线程,但是无法执行到判断语句进行线程的终止,此时,必须使用interrupt()函数来达到要求。
|
||||
* interrupt将处于冻结状态的线程强制转换到运行状态。此时wait()就会跑出我们处理已久的中断异常。
|
||||
* 当没有指定的方式让冻结的线程回复到运行状态时,这是需要对冻结进行清除。Thread提供了interrupt方法。
|
||||
|
||||
|
||||
## 多线程 守护线程
|
||||
|
||||
-----
|
||||
|
||||
* 守护线程就是后台线程,也是一种依赖线程
|
||||
* 特点:
|
||||
当前台线程结束后,后台线程会自动终止,作为依赖线程,守护线程,不用强调结束。
|
||||
* 方法:
|
||||
setDemon()设置某个线程为守护线程
|
||||
|
||||
## 多线程join方法
|
||||
|
||||
----
|
||||
|
||||
* t1.join() t1抢夺cpu执行权,主线程处于冻结状态,t1优先执行。相当于顺序执行两个线程。主线程碰到谁的join就会等谁执行。
|
||||
* 当A线程执行到了B线程的Join方法时,A线程就会等待,等B线程都执行完成,A才会执行。join才会临时加入线程执行。当B线程进入wait时,A线程也能继续执行。
|
||||
* toString 方法能够显示线程的名称,线程的优先级,线程当前的分组(线程组谁调用就是谁的线程组的)
|
||||
* 所有线程包括主线程,默认是5。数越大优先级越高。MAX_PRIORITY,MIN_PRIORITY,
|
||||
* yield()方法,当线程读到这里是,会释放执行权。这样会使所有的线程都有平均执行的效果。
|
||||
|
||||
## 什么时候会使用到多线程
|
||||
|
||||
----
|
||||
|
||||
* 当程序独立运算相互之间不相关的时候,可以用多线程封装一下,提高程序执行的速度
|
||||
@@ -1,166 +0,0 @@
|
||||
# 多线程间的通讯
|
||||
|
||||
-----
|
||||
|
||||
## 多线程通讯的定义:
|
||||
|
||||
----
|
||||
|
||||
- 多个不同的线程对共同的数据进行不同的操作。
|
||||
|
||||
## 多线程通讯间的安全问题
|
||||
|
||||
-----
|
||||
|
||||
* 安全问题的原因
|
||||
例如当多个线程对同一个数据进行不同操作时,导致各种操作的先后顺序出现混乱。
|
||||
* 安全问题的解决方式
|
||||
对这些线程操作数据的部分进行同步处理,使用相同的锁,将不同的部分锁起。
|
||||
|
||||
## 线程间通讯等待唤醒机制
|
||||
|
||||
-----
|
||||
|
||||
* 可以模仿锁的工作原理(设置标志位,记录当前是够线程占用锁内的程序,实现只能有一个线程执行锁内代码的现象)
|
||||
* 步骤:
|
||||
1. 设置标志位flag
|
||||
2. 当标志位处于输入状态时,执行输入程序,执行完成后修改为输出状态。
|
||||
3. 当标志位处于输出状态时,执行输出程序,执行完成后修改为输入状态。
|
||||
## 等待唤醒机制的具体实现
|
||||
|
||||
----
|
||||
|
||||
* wait()和notify()函数必须在同步代码块或者同步函数当中使用。注意wait()会抛出中断异常。对持有监视器(锁)的线程进行操作。
|
||||
* wait()和notify()的操作对象是同步中持有当前锁的线程。
|
||||
* 线程的等待唤醒机制必须制定一把确定的锁。锁是任意的对象,任意对象都能成为锁,成为锁之后都能调用wait和notify的方法。而且这些方法都定义在Object类中。只有同一个锁上的被等待线程可以被同一个notify唤醒。等待唤醒必须是同一个锁。
|
||||
|
||||
## 使用新的工具类实现程序锁定和解锁
|
||||
|
||||
----
|
||||
|
||||
package painting;
|
||||
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @author 宙斯
|
||||
*
|
||||
*/
|
||||
public class LockerUsing {
|
||||
|
||||
/**
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
Resource2 r = new Resource2();
|
||||
Produce2 pro = new Produce2(r);
|
||||
Consumer2 con = new Consumer2(r);
|
||||
|
||||
Thread t1 = new Thread(pro);
|
||||
Thread t2 = new Thread(con);
|
||||
Thread t3 = new Thread(pro);
|
||||
Thread t4 = new Thread(con);
|
||||
t1.start();
|
||||
t2.start();
|
||||
t3.start();
|
||||
t4.start();
|
||||
}
|
||||
|
||||
}
|
||||
class Resource2{
|
||||
private String name;
|
||||
private int count =1;
|
||||
private boolean flag = false;
|
||||
private Lock lock = new ReentrantLock();//新建了一个锁对象
|
||||
private Condition condition_con = lock.newCondition();//生成了一个与锁相关的控制对象
|
||||
private Condition condition_pro = lock.newCondition();
|
||||
public void set(String name) throws InterruptedException{
|
||||
lock.lock();
|
||||
try{
|
||||
while(flag)
|
||||
condition_pro.await();
|
||||
|
||||
this.name= name+"..."+count++;
|
||||
System.out.println(Thread.currentThread().getName()+"生产者:"+this.name);
|
||||
flag = true;
|
||||
condition_con.signal();
|
||||
}
|
||||
finally{
|
||||
lock.unlock();//异常处理当中释放锁的动过一定要被执行
|
||||
|
||||
}
|
||||
}
|
||||
public void out() throws InterruptedException{
|
||||
lock.lock();
|
||||
try{
|
||||
while(!flag)
|
||||
condition_con.await();
|
||||
System.out.println(Thread.currentThread().getName()+"消费者:"+this.name);
|
||||
flag = false;
|
||||
condition_pro.signalAll();
|
||||
}
|
||||
finally{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
class Produce2 implements Runnable{
|
||||
private Resource2 res;
|
||||
Produce2(Resource2 res){
|
||||
this.res = res;
|
||||
|
||||
}
|
||||
public void run()
|
||||
{
|
||||
while(true){
|
||||
try {
|
||||
res.set("商品");
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Consumer2 implements Runnable{
|
||||
private Resource2 res;
|
||||
Consumer2(Resource2 res){
|
||||
this.res = res;
|
||||
|
||||
}
|
||||
public void run()
|
||||
{
|
||||
while(true){
|
||||
try {
|
||||
res.out();
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
* 代码的解释:
|
||||
|
||||
1. 单线程 -----多线程-----多线程操作同一个数据源的时候实行同步机制------带有满足条件的同步机制(睡眠唤醒机制)
|
||||
2. 实现了多线程不同操作相同的程序,这个类具有模板的价值.对资源的不同操作写到资源类中,并使用this资源类的锁,对各种不同的操作进行上锁
|
||||
3. 而非写到其他操作类中,这样能够将同步和冲突解决都封装到资源类中便于理解和操作。
|
||||
4. 当线程数多于两个,比如此程序中有两个在生产两个在消费,那么标准的方式应该是通过循环判断标志位是否合格,
|
||||
因为当某个线程判断满足后,但在进入之前肯能被其他线程修改标志位。
|
||||
而且必须使用notifyAll()唤醒所有的线程。
|
||||
5. 使用Locker及其相关类的好处
|
||||
一个Locker上可以有很多condition对象执行操作,也就是只唤醒对方的condition
|
||||
jdk1.5实现了多线程的升级解决方案
|
||||
将同步设置Synchronize替换成Lock操作,可见
|
||||
将Object中wait,notify,notifyAll替换成Condition对象。该对象可以Locker锁进行获取
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# 多线程间的通讯
|
||||
|
||||
-----
|
||||
|
||||
## 多线程通讯的定义:
|
||||
|
||||
----
|
||||
|
||||
- 多个不同的线程对共同的数据进行不同的操作。
|
||||
|
||||
## 多线程通讯间的安全问题
|
||||
|
||||
-----
|
||||
|
||||
* 安全问题的原因
|
||||
例如当多个线程对同一个数据进行不同操作时,导致各种操作的先后顺序出现混乱。
|
||||
* 安全问题的解决方式
|
||||
对这些线程操作数据的部分进行同步处理,使用相同的锁,将不同的部分锁起。
|
||||
|
||||
## 线程间通讯等待唤醒机制
|
||||
|
||||
-----
|
||||
|
||||
* 可以模仿锁的工作原理(设置标志位,记录当前是够线程占用锁内的程序,实现只能有一个线程执行锁内代码的现象)
|
||||
* 步骤:
|
||||
1. 设置标志位flag
|
||||
2. 当标志位处于输入状态时,执行输入程序,执行完成后修改为输出状态。
|
||||
3. 当标志位处于输出状态时,执行输出程序,执行完成后修改为输入状态。
|
||||
## 等待唤醒机制的具体实现
|
||||
|
||||
----
|
||||
|
||||
* wait()和notify()函数必须在同步代码块或者同步函数当中使用。注意wait()会抛出中断异常。对持有监视器(锁)的线程进行操作。
|
||||
* wait()和notify()的操作对象是同步中持有当前锁的线程。
|
||||
* 线程的等待唤醒机制必须制定一把确定的锁。锁是任意的对象,任意对象都能成为锁,成为锁之后都能调用wait和notify的方法。而且这些方法都定义在Object类中。只有同一个锁上的被等待线程可以被同一个notify唤醒。等待唤醒必须是同一个锁。
|
||||
|
||||
## 使用新的工具类实现程序锁定和解锁
|
||||
|
||||
----
|
||||
|
||||
package painting;
|
||||
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @author 宙斯
|
||||
*
|
||||
*/
|
||||
public class LockerUsing {
|
||||
|
||||
/**
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
Resource2 r = new Resource2();
|
||||
Produce2 pro = new Produce2(r);
|
||||
Consumer2 con = new Consumer2(r);
|
||||
|
||||
Thread t1 = new Thread(pro);
|
||||
Thread t2 = new Thread(con);
|
||||
Thread t3 = new Thread(pro);
|
||||
Thread t4 = new Thread(con);
|
||||
t1.start();
|
||||
t2.start();
|
||||
t3.start();
|
||||
t4.start();
|
||||
}
|
||||
|
||||
}
|
||||
class Resource2{
|
||||
private String name;
|
||||
private int count =1;
|
||||
private boolean flag = false;
|
||||
private Lock lock = new ReentrantLock();//新建了一个锁对象
|
||||
private Condition condition_con = lock.newCondition();//生成了一个与锁相关的控制对象
|
||||
private Condition condition_pro = lock.newCondition();
|
||||
public void set(String name) throws InterruptedException{
|
||||
lock.lock();
|
||||
try{
|
||||
while(flag)
|
||||
condition_pro.await();
|
||||
|
||||
this.name= name+"..."+count++;
|
||||
System.out.println(Thread.currentThread().getName()+"生产者:"+this.name);
|
||||
flag = true;
|
||||
condition_con.signal();
|
||||
}
|
||||
finally{
|
||||
lock.unlock();//异常处理当中释放锁的动过一定要被执行
|
||||
|
||||
}
|
||||
}
|
||||
public void out() throws InterruptedException{
|
||||
lock.lock();
|
||||
try{
|
||||
while(!flag)
|
||||
condition_con.await();
|
||||
System.out.println(Thread.currentThread().getName()+"消费者:"+this.name);
|
||||
flag = false;
|
||||
condition_pro.signalAll();
|
||||
}
|
||||
finally{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
class Produce2 implements Runnable{
|
||||
private Resource2 res;
|
||||
Produce2(Resource2 res){
|
||||
this.res = res;
|
||||
|
||||
}
|
||||
public void run()
|
||||
{
|
||||
while(true){
|
||||
try {
|
||||
res.set("商品");
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Consumer2 implements Runnable{
|
||||
private Resource2 res;
|
||||
Consumer2(Resource2 res){
|
||||
this.res = res;
|
||||
|
||||
}
|
||||
public void run()
|
||||
{
|
||||
while(true){
|
||||
try {
|
||||
res.out();
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
* 代码的解释:
|
||||
|
||||
1. 单线程 -----多线程-----多线程操作同一个数据源的时候实行同步机制------带有满足条件的同步机制(睡眠唤醒机制)
|
||||
2. 实现了多线程不同操作相同的程序,这个类具有模板的价值.对资源的不同操作写到资源类中,并使用this资源类的锁,对各种不同的操作进行上锁
|
||||
3. 而非写到其他操作类中,这样能够将同步和冲突解决都封装到资源类中便于理解和操作。
|
||||
4. 当线程数多于两个,比如此程序中有两个在生产两个在消费,那么标准的方式应该是通过循环判断标志位是否合格,
|
||||
因为当某个线程判断满足后,但在进入之前肯能被其他线程修改标志位。
|
||||
而且必须使用notifyAll()唤醒所有的线程。
|
||||
5. 使用Locker及其相关类的好处
|
||||
一个Locker上可以有很多condition对象执行操作,也就是只唤醒对方的condition
|
||||
jdk1.5实现了多线程的升级解决方案
|
||||
将同步设置Synchronize替换成Lock操作,可见
|
||||
将Object中wait,notify,notifyAll替换成Condition对象。该对象可以Locker锁进行获取
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# 底层事件处理
|
||||
|
||||
-----
|
||||
|
||||
* 在java.awt学习的时候,将监听器绑定到事件源上,当指定事件发生时监听器会自动匹配到相应的动作,执行相应的处理方式。但在底层事件处理过程中,不许要监听器,能够直接获取当前发生的底层事件,然后进行匹配处理。
|
||||
@@ -1,48 +0,0 @@
|
||||
# 多线程理论补充
|
||||
|
||||
------
|
||||
|
||||
## >让步机制
|
||||
|
||||
-----
|
||||
|
||||
* **yield()**
|
||||
thread.yield()线程执行这个函数会主动放弃执行的优先级,但只是暗示别的线程能够抢到更多的资源,没有确定的机制保证cpu资源一定被其他线程使用。
|
||||
|
||||
## >休眠机制
|
||||
|
||||
---
|
||||
|
||||
* **sleep()**
|
||||
thread.sleep(30)线程停止执行30ms,可能会跑出异常,此线程调用interrupt()方法,中断了线程的执行。
|
||||
|
||||
## >优先权机制
|
||||
|
||||
----
|
||||
|
||||
* **setPriority()**
|
||||
thread.setPriority()通过thread类中的常量对优先级进行设定。thread.getPriority()能够获取当前线程的优先级。
|
||||
|
||||
## >后台进程
|
||||
|
||||
-----
|
||||
|
||||
* **后台线程(守护线程)thread.setDaemon()**
|
||||
程序运行时,在后台提供的一种通用的线程,当所有的非后台线程结束时,后台线程(守护线程)自动终止。必须在线程启动之前,调用方法setDaemon(),将线程设置成后台线程。isDeamon()判断是否为后台线程。
|
||||
|
||||
## >加入线程
|
||||
|
||||
----
|
||||
|
||||
* **t.join(a)**
|
||||
t线程此时将被挂起,直到a线程结束才会被执行。当a线程结束时,t.isAlive()返回为假,线程恢复。
|
||||
|
||||
## >编码变体
|
||||
|
||||
-----
|
||||
|
||||
* **implements Runnable**
|
||||
通过实现接口Runnable来达到作为线程类的方法。必须实现run方法。
|
||||
|
||||
* **建立有响应的用户界面**
|
||||
让运算作为一个独立的线程在后台独立运行。
|
||||
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 225 KiB |
@@ -1,321 +0,0 @@
|
||||
# java并发控制
|
||||
|
||||
> 参考文献
|
||||
> * [并发编程](https://www.cnblogs.com/flashsun/p/10776168.html)
|
||||
> * [java高并发编程](https://blog.csdn.net/cx105200/article/details/80220937)
|
||||
|
||||
|
||||
synchronized 关键字
|
||||
可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。可能锁对象包括: this, 临界资源对象,Class 类对象
|
||||
|
||||
同步方法
|
||||
同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时, 需同步执行。
|
||||
|
||||
public synchronized void test(){
|
||||
System.out.println("测试一下");
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
同步代码块
|
||||
同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。
|
||||
锁定临界对象
|
||||
同步代码块在执行时,是锁定 object 对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。
|
||||
|
||||
public void test(){
|
||||
synchronized(o){
|
||||
System.out.println("测试一下");
|
||||
}
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
锁定当前对象
|
||||
|
||||
public void test(){
|
||||
synchronized(this){
|
||||
System.out.println("测试一下");
|
||||
}
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
锁的底层实现
|
||||
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方法 并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
|
||||
对象内存简图
|
||||
|
||||
对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。
|
||||
实例变量:存放类的属性数据信息,包括父类的属性信息
|
||||
填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
|
||||
当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁) 的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
|
||||
在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。
|
||||
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,以及_Owner 标记。其中_WaitSet 是用于管理等待队列(wait)线程的,_EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。线程状态图如下:
|
||||
|
||||
这里写图片描述
|
||||
|
||||
当多线程并发访问同一个同步代码时,首先会进入_EntryList,当线程获取锁标记后,
|
||||
monitor 中的_Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在_EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算,并将_Owner 标记赋值为 null,代表放弃锁,执行线程进如_WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入_EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner 标记赋值为 null,且计数器赋值为 0 计算。
|
||||
|
||||
锁的种类
|
||||
Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。
|
||||
锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。
|
||||
锁只能升级,不能降级。
|
||||
|
||||
重量级锁
|
||||
|
||||
在锁的底层实现中解释的就是重量级锁。
|
||||
|
||||
偏向锁
|
||||
|
||||
是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在 Monitor 中有变量 ACC_SYNCHRONIZED。当变量值使用的时候, 代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。
|
||||
|
||||
轻量级锁
|
||||
过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。
|
||||
两个线程也可能出现重量级锁。
|
||||
|
||||
自旋锁
|
||||
是一个过渡锁,是偏向锁和轻量级锁的过渡。
|
||||
当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。
|
||||
|
||||
volatile 关键字
|
||||
变量的线程可见性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile 修饰的变量是线程可见的,当 JVM 解释 volatile 修饰的变量时,会通知 CPU,在计算过程中, 每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。
|
||||
volatile 只是通知底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同步。
|
||||
|
||||
volatile int count = 0;
|
||||
1
|
||||
wait¬ify
|
||||
AtomicXxx 类型组
|
||||
原子类型。
|
||||
在 concurrent.atomic 包中定义了若干原子类型,这些类型中的每个方法都是保证了原子操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如果某数据需要多个线程同时操作,且要求计算原子性,可以考虑使用原子类型对象。
|
||||
|
||||
AtomicInteger count = new AtomicInteger(0);
|
||||
void m(){
|
||||
count.incrementAndGet();
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
注意:原子类型中的方法是保证了原子操作,但多个方法之间是没有原子性的。如:
|
||||
|
||||
AtomicInteger i = new AtomicInteger(0);
|
||||
if(i.get() != 5){
|
||||
i.incrementAndGet();
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
在上述代码中,get 方法和 incrementAndGet 方法都是原子操作,但复合使用时,无法保证原子性,仍旧可能出现数据错误。
|
||||
|
||||
CountDownLatch 门闩
|
||||
门闩是 concurrent 包中定义的一个类型,是用于多线程通讯的一个辅助类型。
|
||||
门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门
|
||||
|
||||
闩数量大于 0,线程会阻塞等待。当线程调用 countDown 时,会递减门闩的数量,当门闩数量为 0 时,await 阻塞线程可执行。
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(5);
|
||||
|
||||
void m1(){
|
||||
try {
|
||||
latch.await();// 等待门闩开放。
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("m1() method");
|
||||
}
|
||||
|
||||
void m2(){
|
||||
for(int i = 0; i < 10; i++){
|
||||
if(latch.getCount() != 0){
|
||||
System.out.println("latch count : " + latch.getCount());
|
||||
latch.countDown(); // 减门闩上的锁。
|
||||
}
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("m2() method : " + i);
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
21
|
||||
22
|
||||
23
|
||||
24
|
||||
25
|
||||
锁的重入
|
||||
在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。
|
||||
当线程持有锁时,会在 monitor 的计数器中执行递增计算,若当前线程调用其他同步代码,且同步代码的锁对象相同时,monitor 中的计数器继续递增。每个同步代码执行结束,
|
||||
monitor 中的计数器都会递减,直至所有同步代码执行结束,monitor 中的计数器为 0 时,释放锁标记,_Owner 标记赋值为 null。
|
||||
|
||||
ReentrantLock
|
||||
重入锁,建议应用的同步方式。相对效率比 synchronized 高。量级较轻。
|
||||
synchronized 在 JDK1.5 版本开始,尝试优化。到 JDK1.7 版本后,优化效率已经非常好了。在绝对效率上,不比 reentrantLock 差多少。
|
||||
使用重入锁,必须必须必须手工释放锁标记。一般都是在 finally 代码块中定义释放锁标记的 unlock 方法。
|
||||
|
||||
公平锁
|
||||
|
||||
这里写图片描述
|
||||
|
||||
private static ReentrantLock lock = new ReentrantLock(true);
|
||||
public void run(){
|
||||
for(int i = 0; i < 5; i++){
|
||||
lock.lock();
|
||||
try{
|
||||
System.out.println(Thread.currentThread().getName() + " get lock");
|
||||
}finally{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
8ThreadLocal
|
||||
|
||||
remove 问题
|
||||
这里写图片描述
|
||||
|
||||
同步容器
|
||||
解决并发情况下的容器线程安全问题的。给多线程环境准备一个线程安全的容器对象。线程安全的容器对象: Vector, Hashtable。线程安全容器对象,都是使用 synchronized
|
||||
方法实现的。
|
||||
concurrent 包中的同步容器,大多数是使用系统底层技术实现的线程安全。类似 native。
|
||||
Java8 中使用 CAS。
|
||||
|
||||
Map/Set
|
||||
ConcurrentHashMap/ConcurrentHashSet
|
||||
|
||||
底层哈希实现的同步 Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。量级较 synchronized 低。key 和 value 不能为 null。
|
||||
|
||||
ConcurrentSkipListMap/ConcurrentSkipListSet
|
||||
|
||||
底层跳表(SkipList)实现的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低。
|
||||
|
||||
这里写图片描述
|
||||
|
||||
List
|
||||
CopyOnWriteArrayList
|
||||
|
||||
写时复制集合。写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组。
|
||||
|
||||
Queue
|
||||
ConcurrentLinkedQueue
|
||||
|
||||
基础链表同步队列。
|
||||
|
||||
LinkedBlockingQueue
|
||||
阻塞队列,队列容量不足自动阻塞,队列容量为 0 自动阻塞。
|
||||
|
||||
ArrayBlockingQueue
|
||||
底层数组实现的有界队列。自动阻塞。根据调用 API(add/put/offer)不同,有不同特性。
|
||||
当容量不足的时候,有阻塞能力。
|
||||
add 方法在容量不足的时候,抛出异常。
|
||||
put 方法在容量不足的时候,阻塞等待。
|
||||
offer 方法,
|
||||
单参数 offer 方法,不阻塞。容量不足的时候,返回 false。当前新增数据操作放弃。三参数 offer 方法(offer(value,times,timeunit)),容量不足的时候,阻塞 times 时长(单
|
||||
位为 timeunit),如果在阻塞时长内,有容量空闲,新增数据返回 true。如果阻塞时长范围
|
||||
内,无容量空闲,放弃新增数据,返回 false。
|
||||
|
||||
DelayQueue
|
||||
延时队列。根据比较机制,实现自定义处理顺序的队列。常用于定时任务。如:定时关机。
|
||||
|
||||
LinkedTransferQueue
|
||||
转移队列,使用 transfer 方法,实现数据的即时处理。没有消费者,就阻塞。
|
||||
|
||||
SynchronusQueue
|
||||
同步队列,是一个容量为 0 的队列。是一个特殊的 TransferQueue。必须现有消费线程等待,才能使用的队列。
|
||||
add 方法,无阻塞。若没有消费线程阻塞等待数据,则抛出异常。
|
||||
put 方法,有阻塞。若没有消费线程阻塞等待数据,则阻塞。
|
||||
|
||||
ThreadPool&Executor
|
||||
Executor
|
||||
线程池顶级接口。
|
||||
常用方法 - void execute(Runnable)
|
||||
作用是: 启动线程任务的。
|
||||
|
||||
ExecutorService
|
||||
Executor 接口的子接口。
|
||||
常见方法 - Future submit(Callable), Future submit(Runnable)
|
||||
|
||||
Future
|
||||
未来结果,代表线程任务执行结束后的结果。
|
||||
|
||||
Callable
|
||||
可执行接口。
|
||||
接口方法 : Object call();相当于 Runnable 接口中的 run 方法。区别为此方法有返回值。不能抛出已检查异常。
|
||||
和 Runnable 接口的选择 - 需要返回值或需要抛出异常时,使用 Callable,其他情况可任意选择。
|
||||
|
||||
Executors
|
||||
工具类型。为 Executor 线程池提供工具方法。类似 Arrays,Collections 等工具类型的功用。
|
||||
|
||||
FixedThreadPool
|
||||
容量固定的线程池
|
||||
queued tasks - 任务队列
|
||||
completed tasks - 结束任务队列
|
||||
|
||||
CachedThreadPool
|
||||
缓存的线程池。容量不限(Integer.MAX_VALUE)。自动扩容。默认线程空闲 60 秒,自动销毁。
|
||||
|
||||
ScheduledThreadPool
|
||||
计划任务线程池。可以根据计划自动执行任务的线程池。
|
||||
|
||||
SingleThreadExceutor
|
||||
单一容量的线程池。
|
||||
|
||||
ForkJoinPool
|
||||
分支合并线程池(mapduce 类似的设计思想)。适合用于处理复杂任务。初始化线程容量与 CPU 核心数相关。
|
||||
线程池中运行的内容必须是 ForkJoinTask 的子类型(RecursiveTask,RecursiveAction)。
|
||||
|
||||
WorkStealingPool
|
||||
JDK1.8 新增的线程池。工作窃取线程池。当线程池中有空闲连接时,自动到等待队列中窃取未完成任务,自动执行。
|
||||
初始化线程容量与 CPU 核心数相关。此线程池中维护的是精灵线程。
|
||||
ExecutorService.newWorkStealingPool();
|
||||
|
||||
ThreadPoolExecutor
|
||||
线程池底层实现。除 ForkJoinPool 外,其他常用线程池底层都是使用 ThreadPoolExecutor
|
||||
实现的。
|
||||
public ThreadPoolExecutor
|
||||
(int corePoolSize, // 核心容量
|
||||
|
||||
int maximumPoolSize, // 最大容量
|
||||
long keepAliveTime, // 生命周期,0 为永久
|
||||
TimeUnit unit, // 生命周期单位
|
||||
BlockingQueue workQueue // 任务队列,阻塞队列。
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# java并发机制与底层实现原理
|
||||
## 1 volatile
|
||||
volatile是轻量级的synchronize,它在多处理器开发中保证了共享变量的“可见性”,因为它不会引起线程上下文的切换和调度,所以比synchronize的使用和执行成本更底。
|
||||
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。使用volatile变量,在操作后,JVM会发出lock指令
|
||||
|
||||
将当前处理器缓存行的数据写回到系统内存
|
||||
这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效
|
||||
|
||||
|
||||
|
||||
## 2 synchronize
|
||||
### 同步基础
|
||||
synchronize实现同步的基础,具体表现为三种形式
|
||||
|
||||
* 对于普通同步方法,锁是当前实例对象
|
||||
* 对于静态同步方法,锁是当前类的class对象
|
||||
* 对于同步方法块,锁是Synchronize括号里配置的对象
|
||||
* 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在那里,锁里会存储什么信息。
|
||||
|
||||
### java对象头
|
||||
synchonize用的锁是存在java对象头里的。如果对象是数组类型,则JVM用三个字宽存储对象头,如果对象为非数组类型,则用二个字宽存储对象头。32位中,一字宽等于四字节(32bit)
|
||||
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<td>长度</td>
|
||||
<td>内容</td>
|
||||
<td>说明</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>32/64bit</td>
|
||||
<td>Mark Word</td>
|
||||
<td>存储对象的hashCode或锁信息等。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>32/64bit</td>
|
||||
<td>Class Metadata Address</td>
|
||||
<td>存储到对象类型数据的指针</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>32/64bit</td>
|
||||
<td>Array length</td>
|
||||
<td>数组的长度(如果当前对象是数组)</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
在运行期间Mark Word里存存储的数据会随着锁标志位的变化而变化。会成为下面的一种
|
||||
|
||||

|
||||
|
||||
|
||||
## 3 锁类型
|
||||
为了减少获得锁与释放锁所带来的性能消耗,引入“偏向锁”和“轻量级锁'.所以在java中存在四种状态
|
||||
|
||||
* 无锁状态
|
||||
* 偏向锁状态
|
||||
* 轻量级锁状态
|
||||
* 自旋锁
|
||||
* 重量级锁状态
|
||||
|
||||
|
||||
它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁
|
||||
|
||||
### 偏向锁
|
||||
Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁。
|
||||
|
||||
流程图中展示偏向锁的获取释放以及升级至轻量锁
|
||||
|
||||

|
||||
|
||||
### 轻量级锁
|
||||
1. 轻量级锁加锁:
|
||||
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
|
||||
|
||||
2. 轻量级锁解锁
|
||||
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。
|
||||
|
||||
|
||||
借用网上流程图如下:
|
||||

|
||||
|
||||
### 自旋锁
|
||||
当竟争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程(线程切换平均消耗8K个时钟周期),让线程多做几个空操作(自旋)
|
||||
|
||||
1. 如果同步块过长,自旋失败,会降低系统性能
|
||||
2. 如果同步块很短,自旋成功,节省线程挂起切换时间,担升系统性能
|
||||
|
||||
## 4 锁对比
|
||||
|
||||
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<td>锁</td>
|
||||
<td align="center">优点</td>
|
||||
<td align="center">缺点</td>
|
||||
<td align="center">适用场景</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>偏向锁</td>
|
||||
<td>加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。</td>
|
||||
<td>如果线程间存在锁竞争,会带来额外的锁撤销的消耗。</td>
|
||||
<td>适用于只有一个线程访问同步块场景。</td</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>轻量级锁</td>
|
||||
<td>竞争的线程不会阻塞,提高了程序的响应速度。</td>
|
||||
<td>如果始终得不到锁竞争的线程使用自旋会消耗CPU</td>
|
||||
<td>追求响应时间。同步块执行速度非常快。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>重量级锁</td>
|
||||
<td>线程竞争不使用自旋,不会消耗CPU。</td>
|
||||
<td>线程阻塞,响应时间缓慢。</td>
|
||||
<td>追求吞吐量。同步块执行速度较长。</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
|
||||
|
||||
总结
|
||||
* 偏向锁,轻量级锁,自旋锁不是JAVA语言层上的优化方法
|
||||
* 内置于JVM中的获取锁的优化方法与获取锁的步骤
|
||||
* 偏向锁可用可先尝试偏向锁
|
||||
* 轻量级锁可用可先尝试轻量级锁
|
||||
* 1与2都失败,则尝试自旋锁
|
||||
* 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起
|
||||
@@ -1,284 +0,0 @@
|
||||
# java并发机制
|
||||
|
||||
|
||||
|
||||
## java多线程
|
||||
|
||||
### 继承Thread类
|
||||
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动方法就是通过继承了Thread类的start()实例方法。执行run()方法(重写的)。就可以启动新线程并执行自己定义。例如:
|
||||
```java
|
||||
//实现方法的类
|
||||
public class Demo1 extends Thread {
|
||||
|
||||
public void run(){
|
||||
System.out.println("继承Thread类");
|
||||
}
|
||||
}
|
||||
|
||||
//执行的方法
|
||||
public static void main(String[] args) {
|
||||
|
||||
Demo1 demo1=new Demo1();
|
||||
demo1.start();
|
||||
}
|
||||
```
|
||||
|
||||
### 实现Runnable接口
|
||||
由于java是单继承的,那么在平时开发中就提倡使用接口的方式实现。则需要实现多线程的类通过实现Runnable接口的run方法。通过Thread的start()方法进行启动,例如:
|
||||
|
||||
```java
|
||||
//实现的方法类:
|
||||
public class Demo2 implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("实现runnable接口");
|
||||
}
|
||||
|
||||
//执行方法:
|
||||
public static void main(String[] args) {
|
||||
|
||||
Demo2 demo2=new Demo2();
|
||||
Thread thread=new Thread(demo2);
|
||||
thread.start();
|
||||
}
|
||||
```
|
||||
### 通过内部类的方式实现多线程
|
||||
直接可以通过Thread类的start()方法进行实现,因为Thread类实现了Runnable接口,并重写了run方法,在run方法中实现自己的逻辑,例如:
|
||||
|
||||
```java
|
||||
//这里通过了CountDownLatch,来进行阻塞,来观察两个线程的启动,这样更加体现的明显一些:
|
||||
public static CountDownLatch countDownLatch=new CountDownLatch(2);
|
||||
|
||||
public static void main(String[] args) {
|
||||
new Thread(()->{
|
||||
countDownLatch.countDown();
|
||||
try {
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("T1");
|
||||
}).start();
|
||||
|
||||
new Thread(()->{
|
||||
countDownLatch.countDown();
|
||||
try {
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("T2");
|
||||
}).start();
|
||||
}
|
||||
```
|
||||
|
||||
### 通过实现Callable接口
|
||||
通过实现Callable接口的call方法,可以通过FutureTask的get()方法来获取call方法中的返回值,具体实现如下:
|
||||
|
||||
```java
|
||||
//实现类方法:
|
||||
public class Demo3 implements Callable {
|
||||
|
||||
@Override
|
||||
public Object call() {
|
||||
return "1";
|
||||
}
|
||||
}
|
||||
|
||||
//执行方法:
|
||||
public static void main(String[] args) {
|
||||
|
||||
//创建实现类对象
|
||||
Callable demo3=new Demo3();
|
||||
FutureTask oneTask = new FutureTask(demo3);
|
||||
Thread thread=new Thread(oneTask);
|
||||
thread.start();
|
||||
Object o = null;
|
||||
try {
|
||||
//获取返回值
|
||||
o = oneTask.get();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println(o);
|
||||
}
|
||||
```
|
||||
|
||||
### 通过线程池来实现多线程
|
||||
线程池可以根据不同的场景来选择不同的线程池来进行实现,这里我仅使用其中之一进行演示,后续会单独写一个线程池相关的单独介绍:
|
||||
|
||||
```java
|
||||
//实现代码如下:
|
||||
public class Demo5 {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(5);
|
||||
for(int i=0;i<5;i++){
|
||||
int finalI = i;
|
||||
executorService.execute(()-> {
|
||||
System.out.println(finalI);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通过Timer定时器来实现多线程
|
||||
就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,同样根据参数得不同存在多种执行方式,例如其中延迟定时任务这样:
|
||||
|
||||
```
|
||||
//具体代码如下:
|
||||
public class Demo6 {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Timer timer=new Timer();
|
||||
timer.schedule(new TimerTask(){
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println(1);
|
||||
}
|
||||
},2000l,1000l);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通过stream实现多线程
|
||||
jdk1.8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
|
||||
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
|
||||
具体简单代码实现如下:
|
||||
|
||||
```
|
||||
//代码实现:
|
||||
public class Demo7 {
|
||||
|
||||
//为了更形象体现并发,通过countDownLatch进行阻塞
|
||||
static CountDownLatch countDownLatch=new CountDownLatch(6);
|
||||
public static void main(String[] args) {
|
||||
List list=new ArrayList<>();
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
list.add(5);
|
||||
list.add(6);
|
||||
|
||||
list.parallelStream().forEach(p->{
|
||||
//将所有请求在打印之前进行阻塞,方便观察
|
||||
countDownLatch.countDown();
|
||||
try {
|
||||
System.out.println("线程执行到这里啦");
|
||||
Thread.sleep(10000);
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println(p);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
## java异步IO
|
||||
|
||||
|
||||
### java NIO
|
||||
> 参考文献
|
||||
> * [java nio一篇博客](https://blog.csdn.net/forezp/article/details/88414741)
|
||||
> * [java nio并发编程网](http://ifeve.com/overview/)
|
||||
|
||||
|
||||
|
||||
### java akka
|
||||
|
||||
1. 前情提要
|
||||
面向对象编程理论中,对象之间通信,依赖的是消息,但java里,对象之间通信,用的是对象方法
|
||||
|
||||
2. Actor模型
|
||||
计算模型,计算单位Actor,所有的计算都在Actor中执行。Actor中一切都是actor,actor之间完全隔离,不共享任何变量。不共享变量,就不会有并发问题。java本身不支持actor模型,需要引入第三方类库Akka
|
||||
|
||||
3. 代码范例
|
||||
```
|
||||
//该Actor当收到消息message后,
|
||||
//会打印Hello message
|
||||
static class HelloActor
|
||||
extends UntypedActor {
|
||||
@Override
|
||||
public void onReceive(Object message) {
|
||||
System.out.println("Hello " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
//创建Actor系统
|
||||
ActorSystem system = ActorSystem.create("HelloSystem");
|
||||
//创建HelloActor
|
||||
ActorRef helloActor =
|
||||
system.actorOf(Props.create(HelloActor.class));
|
||||
//发送消息给HelloActor
|
||||
helloActor.tell("Actor", ActorRef.noSender());
|
||||
}
|
||||
```
|
||||
actor之间通信完美遵循了消息机制。而不是通过调用对象的方式
|
||||
|
||||
4. 消息和对象方法的区别
|
||||
actor内部有一个邮箱mailbox,接受到的消息先放到邮箱,如果有积压,新消息不会马上得到处理。actor是单线程处理消息。所以不会有并发问题
|
||||
说白了,就是消费者线程的生产者-消费者模式
|
||||
|
||||
5. 区别
|
||||
对相关的方法调用,一般是同步的,而actor的消息机制是异步的。
|
||||
|
||||
6. Actor规范定义
|
||||
1. 处理能力,处理接收到的消息
|
||||
2. 存储能力,actor可以存储自己的内部状态
|
||||
3. 通信能力,actor可以和其他actor之间通信
|
||||
7. actor实现线程安全的累加器
|
||||
无锁算法,因为只有1个线程在消费,不会存在并发问题
|
||||
|
||||
```
|
||||
//累加器
|
||||
static class CounterActor extends UntypedActor {
|
||||
private int counter = 0;
|
||||
@Override
|
||||
public void onReceive(Object message){
|
||||
//如果接收到的消息是数字类型,执行累加操作,
|
||||
//否则打印counter的值
|
||||
if (message instanceof Number) {
|
||||
counter += ((Number) message).intValue();
|
||||
} else {
|
||||
System.out.println(counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
//创建Actor系统
|
||||
ActorSystem system = ActorSystem.create("HelloSystem");
|
||||
//4个线程生产消息
|
||||
ExecutorService es = Executors.newFixedThreadPool(4);
|
||||
//创建CounterActor
|
||||
ActorRef counterActor =
|
||||
system.actorOf(Props.create(CounterActor.class));
|
||||
//生产4*100000个消息
|
||||
for (int i=0; i<4; i++) {
|
||||
es.execute(()->{
|
||||
for (int j=0; j<100000; j++) {
|
||||
counterActor.tell(1, ActorRef.noSender());
|
||||
}
|
||||
});
|
||||
}
|
||||
//关闭线程池
|
||||
es.shutdown();
|
||||
//等待CounterActor处理完所有消息
|
||||
Thread.sleep(1000);
|
||||
//打印结果
|
||||
counterActor.tell("", ActorRef.noSender());
|
||||
//关闭Actor系统
|
||||
system.shutdown();
|
||||
}
|
||||
```
|
||||
8. 总结
|
||||
actor计算模型,基本计算单元。消息通信。
|
||||
|
||||
9. 应用
|
||||
spark,filink,play
|
||||
@@ -1,159 +0,0 @@
|
||||
RMI的定义
|
||||
|
||||
RPC (Remote Procedure
|
||||
Call):远程方法调用,用于一个进程调用另一个进程中的过程,从而提供了过程的分布能力。
|
||||
|
||||
RMI(Remote Method
|
||||
Invocation):远程方法调用,即在RPC的基础上有向前迈进了一步,提供分布式对象间的通讯。允许运行在一个java
|
||||
虚拟机的对象调用运行在另一个java虚拟机上对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
|
||||
|
||||
RMI的作用
|
||||
|
||||
RMI的全称宗旨就是尽量简化远程接口对象的调用。
|
||||
|
||||
RMI大大增强了java开发分布式应用的能力,例如可以将计算方法复杂的程序放在其他的服务器上,主服务器只需要去调用,而真正的运算是在其他服务器上进行,最后将运算结果返回给主服务器,这样就减轻了主服务器的负担,提高了效率(但是也有其他的开销)。
|
||||
|
||||
RMI网络模型
|
||||
|
||||
在设计初始阶段,我们真正想要的是这样一种机制,客户端程序员以常规方式进行方法调用,而无需操心将数据发送到网络上或者解析响应之类的问题。所以才有了如下的网络模型:在客户端为远程对象安装一个代理。代理是位于客户端虚拟机中的一个对象,它对于客户端程序来说,就像是要访问的远程对象一样。客户端调用此代理时,只需进行常规的方法调用。而客户端代理则负责使用网络协议与服务器进行联系。
|
||||
|
||||

|
||||
|
||||
现在的问题在于代理之间是如何进行通信的?通常有三种方法:
|
||||
|
||||
1、CORBA:通过对象请求代理架构,支持任何编程语言编写的对象之间的方法调用。
|
||||
|
||||
2、SOAP
|
||||
|
||||
3、RMI:JAVA的远程方法调用技术,支持java的分布式对象之间的方法调用。
|
||||
|
||||
其中CORBA与SOAP都是完全独立于言语的,可以使用C、C++、JAVA来编写,而RMI只适用于JAVA。
|
||||
|
||||
RMI的工作原理
|
||||
|
||||
**术语介绍**
|
||||
|
||||
> 1、存根:当客户端要调用远程对象的一个方法时,实际上调用的是代理对象上的一个普通方法,**我们称此代理对象为存根(stub)。**存根位于客户端机器上,而非服务器上。
|
||||
|
||||
> 2、参数编组:存根会将**远程方法所需的参数打包成一组字节**,对参数编码的过程就称为参数编组。参数编组的目的是将参数转换成适合在虚拟机之间进行传递的格式,在RMI协议中,对象是使用序列化机制进行编码的。
|
||||
|
||||
编程模型
|
||||
|
||||
为了介绍RMI的编程模型,我下面会编写一个DEMO。远程对象表示的是一个仓库,而客户端程序向仓库询问某个产品的价格。
|
||||
|
||||
**接口定义**
|
||||
|
||||
远程对象的能力是由在客户端和服务器之间共享的接口所表示的:
|
||||
|
||||

|
||||
|
||||
复制代码
|
||||
|
||||
package rmi; import java.rmi.Remote; import java.rmi.RemoteException; public
|
||||
interface Warehouse extends Remote { double getPrice(String description) throws
|
||||
RemoteException; }
|
||||
|
||||
远程对象的接口必须扩展Remote接口,它位于java.rmi包中。接口中所有的方法必须声明抛出RemoteException异常。这是因为远程方法总是存在失败的可能,所以java编程语言要求每一次远程方法的调用都必须捕获RemoteException,并且指明当调用不成功时应执行的相应处理操作。
|
||||
|
||||
**接口的实现**
|
||||
|
||||

|
||||
|
||||
复制代码
|
||||
|
||||
package rmi; import java.rmi.RemoteException; import
|
||||
java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import
|
||||
java.util.Map; public class WarehouseImpl extends UnicastRemoteObject implements
|
||||
Warehouse { private static final long serialVersionUID = 1L; private
|
||||
Map\<String,Double\> prices; protected WarehouseImpl() throws RemoteException {
|
||||
prices = new HashMap\<String,Double\>(); prices.put("mate7",3700.00); } public
|
||||
double getPrice(String description) throws RemoteException { Double price =
|
||||
prices.get(description); return price == null? 0 : price; }}
|
||||
|
||||
你可以看出这个类是远程方法调用的目标,因为它扩展自UnicastRemoteObject,这个类的构造器使得它的对象可供远程访问。
|
||||
|
||||
**RMI注册表:通过JNDI发布RMI服务**
|
||||
|
||||
1、要访问服务器上的一个远程对象时,客户端必须先得到一个本地的存根对象,也就是客户端机器上的代理对象。那么问题来了,如何才能得到这个存根呢?
|
||||
|
||||
2、为此,JDK提供了自举注册服务(bootstrap registry
|
||||
service),服务器程序应该使用自举注册服务来注册至少一个远程对象。
|
||||
|
||||
3、而要注册一个远程对象,需要一个RMI URL和一个对实现对象的引用。
|
||||
|
||||
4、RMI
|
||||
的URL以rmi:开头,后接域名或IP地址(host),紧接着是端口号(port),最后是服务名(service)。
|
||||
|
||||
如:rmi://regserver.mycompany.cmo:99/central_warehouse
|
||||
|
||||
如果我们是在本地发布RMI服务,那么host就是“localhost”,此外RMI默认的端口号是“1099”,当然我们也可以自行设置,只要不与其他端口重复即可。service实际上是基于同一个host与port下唯一的服务名。
|
||||
|
||||
发布RMI服务:
|
||||
|
||||
1 package rmi; 2 3 import java.net.MalformedURLException; 4 import
|
||||
java.rmi.AlreadyBoundException; 5 import java.rmi.Naming; 6 import
|
||||
java.rmi.RemoteException; 7 import java.rmi.registry.LocateRegistry; 8 9 import
|
||||
javax.naming.NamingException; 10 11 12 public class WarehouseServer 13 { 14
|
||||
public static void main(String[] args) throws RemoteException, NamingException,
|
||||
MalformedURLException, AlreadyBoundException 15 { 16
|
||||
System.out.println("Constructing server implementation"); 17 WarehouseImpl
|
||||
centralWarehouse = new WarehouseImpl(); 18 19 System.out.println("Binding server
|
||||
implementation to registry"); 20 LocateRegistry.createRegistry(1099); 21
|
||||
Naming.bind("rmi://localhost:1099/central_warehoues",centralWarehouse); 22 23
|
||||
System.out.println("Waiting for invocations from clients ..."); 24 } 25 }
|
||||
|
||||
运行结果:
|
||||
|
||||
Constructing server implementation Binding server implementation to registry
|
||||
Waiting for invocations from clients ...
|
||||
|
||||
1、第20行只需提供一个port,就在JNDI中创建了一个注册表。
|
||||
|
||||
2、第21行通过bind方法绑定了RMI地址与RMI服务实现类。
|
||||
|
||||
3、执行这个方法后,相当于自动发布了RMI服务。接下来要做的事情就是写一个RM客户端调用已发布的RMI服务。
|
||||
|
||||
**客户端调用RMI服务**
|
||||
|
||||
1 package rmi; 2 3 import java.net.MalformedURLException; 4 import
|
||||
java.rmi.Naming; 5 import java.rmi.NotBoundException; 6 import
|
||||
java.rmi.RemoteException; 7 import javax.naming.NamingException; 8 9 public
|
||||
class WarehouseClient 10 { 11 public static void main(String[] args) throws
|
||||
NamingException, RemoteException, MalformedURLException, NotBoundException 12 {
|
||||
13 System.out.println("RMI registry binding:"); 14 String url =
|
||||
"rmi://localhost:1099/central_warehoues"; 15 Warehouse centralWarehouse =
|
||||
(Warehouse) Naming.lookup(url); 16 String descr = "mate7"; 17 double price =
|
||||
centralWarehouse.getPrice(descr); 18 System.out.println(descr + ":" + price); 19
|
||||
} 20 }
|
||||
|
||||
运行结果:
|
||||
|
||||
RMI registry binding: mate7:3700.0
|
||||
|
||||
补充说明:
|
||||
|
||||
> 服务调用只需要知道两个东西:1、RMI请求路径;2、RMI接口名
|
||||
|
||||
> 第15行,这里用的是接口名Warehouse,而不是实现类。一定不能RMI接口的实现类,否则就是本地调用了。
|
||||
|
||||
> 查看运行结果,我们知道这次DEMO展示的远程调用成功了。
|
||||
|
||||
**下面我们来看下RMI的网络示意图:**
|
||||
|
||||

|
||||
|
||||
1、借助JNDI这个所谓的命名与目录服务,我们成功地发布并调用了RMI服务。实际上,JNDI就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。
|
||||
|
||||
2、在服务端我们发布了RMI服务,并在JNDI中进行了注册,此时就在服务端创建了一个Skeleton(骨架),当客户端第一次成功连接JNDI并获取远程服务对象后,立马在本地创建了一个Stub(存根)。
|
||||
|
||||
3、远程通信实际是通过Skeleton与Stub来完成的,数据是基于TCP/IP协议,在“传输层”上发送的。
|
||||
|
||||
4、毋庸置疑,理论上RMI一定比WebService要快,毕竟WebService是基于http协议的,而http所携带的数据是通过“应用层”来传输的。传输层较应用层更为底层,越底层越快。
|
||||
|
||||
RMI的局限性
|
||||
|
||||
1、只能实现JAVA系统之间的调用,而WebService可以实现跨语言实现系统之间的调用。
|
||||
|
||||
2、RMI使用了JAVA默认的序列化方式,对于性能要求比较高的系统,可能需要其他的序列化方案来解决。
|
||||
|
||||
3、RMI服务在运行时难免会存在故障,例如,如果RMI服务无法连接了,就会导致客户端无法响应的现象。
|
||||
@@ -1,140 +0,0 @@
|
||||
String与StringBuffer的区别
|
||||
|
||||
简单地说,就是一个变量和常量的关系。StringBuffer对象的内容可以修改;而String对象一旦产生后就不可以被修改,重新赋值其实是两个对象。
|
||||
|
||||
StringBuffer的内部实现方式和String不同,StringBuffer在进行字符串处理时,不生成新的对象,在内存使用上要优于String类。所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入、删除等操作,使用StringBuffer要更加适合一些。
|
||||
|
||||
String:在String类中没有用来改变已有字符串中的某个字符的方法,由于不能改变一个java字符串中的某个单独字符,所以在JDK文档中称String类的对象是不可改变的。然而,不可改变的字符串具有一个很大的优点:编译器可以把字符串设为共享的。
|
||||
|
||||
StringBuffer:StringBuffer类属于一种辅助类,可预先分配指定长度的内存块建立一个字符串缓冲区。这样使用StringBuffer类的append方法追加字符
|
||||
比 String使用 + 操作符添加字符 到 一个已经存在的字符串后面有效率得多。因为使用 +
|
||||
操作符每一次将字符添加到一个字符串中去时,字符串对象都需要寻找一个新的内存空间来容纳更大的字符串,这无凝是一个非常消耗时间的操作。添加多个字符也就意味着要一次又一次的对字符串重新分配内存。使用StringBuffer类就避免了这个问题。
|
||||
|
||||
StringBuffer是线程安全的,在多线程程序中也可以很方便的进行使用,但是程序的执行效率相对来说就要稍微慢一些。
|
||||
|
||||
StringBuffer的常用方法
|
||||
|
||||
StringBuffer类中的方法要偏重于对字符串的变化例如追加、插入和删除等,这个也是StringBuffer和String类的主要区别。
|
||||
|
||||
1、append方法
|
||||
|
||||
public StringBuffer append(boolean b)
|
||||
|
||||
该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也发生改变,例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“abc”);
|
||||
|
||||
sb.append(true);
|
||||
|
||||
则对象sb的值将变成”abctrue”。
|
||||
|
||||
使用该方法进行字符串的连接,将比String更加节约内容,例如应用于数据库SQL语句的连接,例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
String user = “test”;
|
||||
|
||||
String pwd = “123”;
|
||||
|
||||
sb.append(“select \* from userInfo where username=“)
|
||||
|
||||
.append(user)
|
||||
|
||||
.append(“ and pwd=”)
|
||||
|
||||
.append(pwd);
|
||||
|
||||
这样对象sb的值就是字符串“select \* from userInfo where username=test and
|
||||
pwd=123”。
|
||||
|
||||
2、deleteCharAt方法
|
||||
|
||||
public StringBuffer deleteCharAt(int index)
|
||||
|
||||
该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“Test”);
|
||||
|
||||
sb. deleteCharAt(1);
|
||||
|
||||
该代码的作用删除字符串对象sb中索引值为1的字符,也就是删除第二个字符,剩余的内容组成一个新的字符串。所以对象sb的值变为”Tst”。
|
||||
|
||||
还存在一个功能类似的delete方法:
|
||||
|
||||
public StringBuffer delete(int start,int end)
|
||||
|
||||
该方法的作用是删除指定区间以内的所有字符,包含start,不包含end索引值的区间。例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“TestString”);
|
||||
|
||||
sb. delete (1,4);
|
||||
|
||||
该代码的作用是删除索引值1(包括)到索引值4(不包括)之间的所有字符,剩余的字符形成新的字符串。则对象sb的值是”TString”。
|
||||
|
||||
3、insert方法
|
||||
|
||||
public StringBuffer insert(int offset, String s)
|
||||
|
||||
该方法的作用是在StringBuffer对象中插入内容,然后形成新的字符串。例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“TestString”);
|
||||
|
||||
sb.insert(4,“false”);
|
||||
|
||||
该示例代码的作用是在对象sb的索引值4的位置插入字符串false,形成新的字符串,则执行以后对象sb的值是”TestfalseString”。
|
||||
|
||||
4、reverse方法
|
||||
|
||||
public StringBuffer reverse()
|
||||
|
||||
该方法的作用是将StringBuffer对象中的内容反转,然后形成新的字符串。例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“abc”);
|
||||
|
||||
sb.reverse();
|
||||
|
||||
经过反转以后,对象sb中的内容将变为”cba”。
|
||||
|
||||
5、setCharAt方法
|
||||
|
||||
public void setCharAt(int index, char ch)
|
||||
|
||||
该方法的作用是修改对象中索引值为index位置的字符为新的字符ch。例如:
|
||||
|
||||
StringBuffer sb = new StringBuffer(“abc”);
|
||||
|
||||
sb.setCharAt(1,’D’);
|
||||
|
||||
则对象sb的值将变成”aDc”。
|
||||
|
||||
6、trimToSize方法
|
||||
|
||||
public void trimToSize()
|
||||
|
||||
该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费。
|
||||
|
||||
7、构造方法:
|
||||
|
||||
StringBuffer s0=new StringBuffer();分配了长16字节的字符缓冲区
|
||||
|
||||
StringBuffer s1=new StringBuffer(512);分配了512字节的字符缓冲区
|
||||
|
||||
8、获取字符串的长度: length()
|
||||
|
||||
StringBuffer s = new StringBuffer("www");
|
||||
|
||||
int i=s.length();
|
||||
|
||||
m.返回字符串的一部分值
|
||||
|
||||
substring(int start) //返回从start下标开始以后的字符串
|
||||
|
||||
substring(int start,int end) //返回从start到 end-1字符串
|
||||
|
||||
9.替换字符串
|
||||
|
||||
replace(int start,int end,String str)
|
||||
|
||||
s.replace(0,1,"qqq");
|
||||
|
||||
10.转换为不变字符串:toString()。
|
||||
@@ -1,189 +0,0 @@
|
||||
简介
|
||||
|
||||
**作用:**
|
||||
|
||||
用来传输和存储数据
|
||||
|
||||
**定义:**
|
||||
|
||||
可扩展标记语言。
|
||||
|
||||
**特点:**
|
||||
|
||||
自行定义标签,具有自我描述性。
|
||||
|
||||
能够跨越各种平台
|
||||
|
||||
**具体应用:**
|
||||
|
||||
用于描述可用的web服务的WSDL
|
||||
|
||||
树结构
|
||||
|
||||
**范例**
|
||||
|
||||
\<?xml version="1.0"
|
||||
encoding="UTF-8"?\>\<note\>\<to\>Tove\</to\>\<from\>Jani\</from\>\<heading\>Reminder\</heading\>\<body\>Don't
|
||||
forget me this weekend!\</body\>\</note\>
|
||||
|
||||
说明:第一行是XML文档声明,定义了版本号和文档的编码方式。
|
||||
|
||||
**树结构:**
|
||||
|
||||

|
||||
|
||||
**元素:**
|
||||
|
||||
起始标记、结束标记、内容(可以是字符文本、其他元素、混合物)
|
||||
|
||||
**属性:**
|
||||
|
||||
属性名=属性值
|
||||
|
||||
**语法规则:**
|
||||
|
||||
标签闭合,正确嵌套
|
||||
|
||||
有一个根元素
|
||||
|
||||
可以有树枝也可以由实体引用
|
||||
|
||||
XML声明可选,放在第一行
|
||||
|
||||
XML标签对大小写敏感
|
||||
|
||||
属性值必须加引号,应当尽量用子元素代替属性。数据的数据存储为属性,而数据本身应当存储为元素。
|
||||
|
||||
id属性可以用来标识XML元素
|
||||
|
||||
注释方式\<!-- This is a comment --\>
|
||||
|
||||
部分字符使用转义字符
|
||||
|
||||
| < | \< | less than |
|
||||
|---------|----|----------------|
|
||||
| > | \> | greater than |
|
||||
| \& | & | ampersand |
|
||||
| \' | ' | apostrophe |
|
||||
| " | " | quotation mark |
|
||||
|
||||
命名空间
|
||||
|
||||
**名字冲突**
|
||||
|
||||
同时使用相同的名字
|
||||
|
||||
**限定名字**
|
||||
|
||||
使用名字空间。在标签之前加上前缀
|
||||
|
||||
> \<h:table xmlns:h="http://www.w3.org/TR/html4/"\> \<h:tr\>
|
||||
> \<h:td\>Apples\</h:td\> \<h:td\>Bananas\</h:td\> \</h:tr\> \</h:table\>
|
||||
|
||||
使用默认的名字空间
|
||||
|
||||
> \<table xmlns="http://www.w3.org/TR/html4/"\> \<tr\> \<td\>Apples\</td\>
|
||||
> \<td\>Bananas\</td\> \</tr\> \</table\>
|
||||
|
||||
XML定义方式
|
||||
|
||||
**XML DTD**
|
||||
|
||||
DTD 的目的是定义 XML 文档的结构。它使用一系列合法的元素来定义文档结构:
|
||||
|
||||
> \<!DOCTYPE note
|
||||
|
||||
> [
|
||||
|
||||
> \<!ELEMENT note (to,from,heading,body)\>
|
||||
|
||||
> \<!ELEMENT to (\#PCDATA)\>
|
||||
|
||||
> \<!ELEMENT from (\#PCDATA)\>
|
||||
|
||||
> \<!ELEMENT heading (\#PCDATA)\>
|
||||
|
||||
> \<!ELEMENT body (\#PCDATA)\>
|
||||
|
||||
> ]\>
|
||||
|
||||
**XML Schema**
|
||||
|
||||
W3C 支持一种基于 XML 的 DTD 代替者,它名为 XML Schema:
|
||||
|
||||
> \<xs:element name="note"\>
|
||||
|
||||
> \<xs:complexType\>
|
||||
|
||||
> \<xs:sequence\>
|
||||
|
||||
> \<xs:element name="to" type="xs:string"/\>
|
||||
|
||||
> \<xs:element name="from" type="xs:string"/\>
|
||||
|
||||
> \<xs:element name="heading" type="xs:string"/\>
|
||||
|
||||
> \<xs:element name="body" type="xs:string"/\>
|
||||
|
||||
> \</xs:sequence\>
|
||||
|
||||
> \</xs:complexType\>
|
||||
|
||||
> \</xs:element\>
|
||||
|
||||
XML验证方式
|
||||
|
||||
对xml语法进行检查
|
||||
|
||||
XML显示方式
|
||||
|
||||
**源代码**
|
||||
|
||||
**CSS格式显示**
|
||||
|
||||
> CATALOG { background-color: \#ffffff; width: 100%; } CD { display: block;
|
||||
> margin-bottom: 30pt; margin-left: 0; } TITLE { color: \#FF0000; font-size:
|
||||
> 20pt; } ARTIST { color: \#0000FF; font-size: 20pt; }
|
||||
> COUNTRY,PRICE,YEAR,COMPANY { display: block; color: \#000000; margin-left:
|
||||
> 20pt; }
|
||||
|
||||
**XSLT格式显示XML**
|
||||
|
||||
将XML转化为对应的HTML语言
|
||||
|
||||
> \<?xml version="1.0" encoding="ISO-8859-1"?\>
|
||||
|
||||
> \<!-- Edited by XMLSpy® --\>
|
||||
|
||||
> \<html xsl:version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
> xmlns="http://www.w3.org/1999/xhtml"\>
|
||||
|
||||
> \<body style="font-family:Arial;font-size:12pt;background-color:\#EEEEEE"\>
|
||||
|
||||
> \<xsl:for-each select="breakfast_menu/food"\>
|
||||
|
||||
> \<div style="background-color:teal;color:white;padding:4px"\>
|
||||
|
||||
> \<span style="font-weight:bold"\>\<xsl:value-of select="name"/\>\</span\>
|
||||
|
||||
> \- \<xsl:value-of select="price"/\>
|
||||
|
||||
> \</div\>
|
||||
|
||||
> \<div style="margin-left:20px;margin-bottom:1em;font-size:10pt"\>
|
||||
|
||||
> \<p\>\<xsl:value-of select="description"/\>.
|
||||
|
||||
> \<span style="font-style:italic"\>
|
||||
|
||||
> \<xsl:value-of select="calories"/\> (calories per serving)
|
||||
|
||||
> \</span\>.\</p\>
|
||||
|
||||
> \</div\>
|
||||
|
||||
> \</xsl:for-each\>
|
||||
|
||||
> \</body\>
|
||||
|
||||
> \</html\>
|
||||
@@ -1,71 +0,0 @@
|
||||
1)确定是数据源和数据目的(输入还是输出)
|
||||
|
||||
源:输入流 InputStream Reader
|
||||
|
||||
目的:输出流 OutputStream Writer
|
||||
|
||||
2)明确操作的数据对象是否是纯文本
|
||||
|
||||
是:字符流Reader,Writer
|
||||
|
||||
否:字节流InputStream,OutputStream
|
||||
|
||||
3)明确具体的设备。
|
||||
|
||||
是硬盘文件:File++:
|
||||
|
||||
读取:FileInputStream,, FileReader,
|
||||
|
||||
写入:FileOutputStream,FileWriter
|
||||
|
||||
是内存用数组
|
||||
|
||||
byte[]:ByteArrayInputStream, ByteArrayOutputStream
|
||||
|
||||
是char[]:CharArrayReader, CharArrayWriter
|
||||
|
||||
是String:StringBufferInputStream(已过时,因为其只能用于String的每个字符都是8位的字符串),
|
||||
StringReader, StringWriter
|
||||
|
||||
是网络用Socket流
|
||||
|
||||
是键盘:用System.in(是一个InputStream对象)读取,用System.out(是一个OutoutStream对象)打印
|
||||
|
||||
3)是否需要转换流
|
||||
|
||||
是,就使用转换流,从Stream转化为Reader,Writer:InputStreamReader,OutputStreamWriter
|
||||
|
||||
4)是否需要缓冲提高效率
|
||||
|
||||
是就加上Buffered:BufferedInputStream, BufferedOuputStream, BuffereaReader,
|
||||
BufferedWriter
|
||||
|
||||
5)是否需要格式化输出
|
||||
|
||||
例:将一个文本文件中数据存储到另一个文件中。
|
||||
|
||||
1)数据源和数据目的:读取流,InputStream Reader 输出:OutputStream Writer
|
||||
|
||||
2)是否纯文本:是!这时就可以选择Reader Writer。
|
||||
|
||||
3)设备:是硬盘文件。Reader体系中可以操作文件的对象是 FileReader FileWriter。
|
||||
|
||||
FileReader fr = new FileReader("a.txt");
|
||||
|
||||
FileWriter fw = new FileWriter("b.txt");
|
||||
|
||||
4)是否需要提高效率:是,加Buffer
|
||||
|
||||
BufferedReader bfr = new BufferedReader(new FileReader("a.txt"); );
|
||||
|
||||
BufferedWriter bfw = new BufferedWriter(new FileWriter("b.txt"); );
|
||||
|
||||
PrintStream
|
||||
|
||||
PrintStream在OutputStream基础之上提供了增强的功能,即可以方便地输出各种类型的数据(而不仅限于byte型)的格式化表示形式。PrintStream的方法从不抛出IOEceptin
|
||||
|
||||
PrintWriter
|
||||
|
||||
PrintWriter提供了PrintStream的所有打印方法,其方法也从不抛出IOException。
|
||||
|
||||
与PrintStream的区别:作为处理流使用时,PrintStream只能封装OutputStream类型的字节流,而PrintWriter既可以封装OutputStream类型的字节流,还能够封装Writer类型的字符输出流并增强其功能。
|
||||
@@ -1,15 +0,0 @@
|
||||
**值得参考的网址**
|
||||
|
||||
**http://tutorials.jenkov.com/java-io/stringreader.html**
|
||||
|
||||

|
||||
|
||||
应该使用以下格式
|
||||
|
||||
- 作用分析
|
||||
|
||||
- 功能方法
|
||||
|
||||
- 代码示例
|
||||
|
||||
- 注意事项
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 44 KiB |