JAVA基础
Posted novalist
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础相关的知识,希望对你有一定的参考价值。
ghuan1993A
基本语法
多态
解藕,包括重载和重写;
重载:编译时多态,从JVM的角度来讲,这是一种静态分派;
重写:运行时多态,从JVM的角度来讲,这是一种动态分派。
static/final/private,子类不能重写父类
final
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变(什么不可以被改变呢,是变量的引用?还是变量里面的内容?还是两者都不可以被改变)
引用不可变,引用指向的内容可变
static
被static修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要new出一个类来
被static修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要new出一个类来
静态代码块是严格按照父类静态代码块->子类静态代码块的顺序加载的,且只加载一次。
static一般情况下来说是不可以修饰类的,如果static要修饰一个类,说明这个类是一个静态内部类
序列化
/ 反序列化 / transient
序列化之后保存的是对象的信息
Java为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法
进行序列化、反序列化时,虚拟机会首先试图调用对象里的writeObject和readObject方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。换言之,利用自定义的writeObject方法和readObject方法,用户可以自己控制序列化和反序列化的过程。
什么时候不会被序列化?
被声明为transient的属性不会被序列化,这就是transient关键字的作用
被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于对象的,因此序列化的时候不会序列化它
实现方式
1.原生实现序列化接口Serializable,
即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化。
需要注意的是JavaBean实体类必须实现Serializable接口,否则无法序列化。
2.Json序列化
Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。
ObjectMapper mapper = new ObjectMapper(); mapper.writeValueAsBytes mapper.readValue(writeValueAsBytes, User.class)
3.FastJson序列化
4、ProtoBuff序列化
用java原生序列化方式的缺点:
多语言环境下,使用Java序列化方式进行存储后,很难用其他语言还原出结果
占用的字节数比较大,而且序列化、反序列化效率也不高
String
StringBuilder、StringBuffer
String 的 “+” 底层是通过StringBuilder,然后调用append,最后toString
StringBuilder底层是一个char数组,在toString()的时候再通过new String(),缺点是当空间不足的时候需要扩容,而且不是线程安全的
集合
ArrayList 允许空,允许重复,有序,非线程安全(Collections.synchronizedList)
LinkedList 允许空,允许重复,有序,非线程安全
Vector 线程安全
HashSet底层是HashMap
List 是可重复集合,Set 是不可重复集合,这两个接口都实现了 Collection 父接口。
List 的实现类有 ArrayList,Vector 和 LinkedList:
ArrayList 和 Vector 内部是线性动态数组结构,在查询效率上会高很多,Vector 是线程安全的,相比 ArrayList 线程不安全的,性能会稍慢一些。
LinkedList:是双向链表的数据结构存储数据,在做查询时会按照序号索引数据进行前向或后向遍历,查询效率偏低,但插入数据时只需要记录本项的前后项即可,所以插入速度较快。
Set 的实现类有 HashSet 和 TreeSet;
HashSet:内部是由哈希表(实际上是一个 HashMap 实例)支持的。它不保证 set 元素的迭代顺序。
TreeSet:TreeSet 使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序。
List的特点:元素有放入顺序,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉(注意:元素虽然没有放入顺序,但是元素在set中的位置是有该元素的hashcode决定的,其位置其实是固定的,加入set的Object必须定义equals()方法,另外list支持for循环,也就是通过下标来进行遍历,也可以用迭代器,但是set只能迭代,因为它是无序的,无法用下标来取得想要的值)
HashSet类是如何实现添加元素保证不重复的---哈希码的原理
public boolean add(E e) return map.put(e, PRESENT)==null; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map用来匹配Map中后面的对象的一个虚拟值 private static final Object PRESENT = new Object();
public V put(K key, V value) if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; modCount++; addEntry(hash, key, value, i); return null;
可以看到for循环中,遍历table中的元素,
1,如果hash码值不相同,说明是一个新元素,存;
如果没有元素和传入对象(也就是add的元素)的hash值相等,那么就认为这个元素在table中不存在,将其添加进table;
2(1),如果hash码值相同,且equles判断相等,说明元素已经存在,不存;
2(2),如果hash码值相同,且equles判断不相等,说明元素不存在,存;
如果有元素和传入对象的hash值相等,那么,继续进行equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
hasCode
hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值
hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等;
(1)equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的;
(2)hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!
然而hashCode()和equal()一样都是基本类Object里的方法,而和equal()一样,Object里hashCode()里面只是返回当前对象的地址,如果是这样的话,那么我们相同的一个类,new两个对象,由于他们在内存里的地址不同,则他们的hashCode()不同,所以这显然不是我们想要的,所以我们必须重写我们类的hashCode()方法.
泛型
参数化类型
类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性
消除了代码中许多的强制类型转换,增强了代码的可读性
为较大的优化带来了可能
内部类
一旦编译成功,就会生成两个完全不同的.class文件了
内部类分为四种:成员内部类、局部内部类、匿名内部类、静态内部类
成员内部类:
Outer outer = new Outer(0);
Outer.PublicInner publicInner = outer.new PublicInner();
成员内部类是依附其外部类而存在的
局部内部类:
public static void main(String[] args) final int i = 0; class A public void print() System.out.println("AAA, i = " + i); A a = new A();
局部内部类是定义在一个方法或者特定作用域里面的类
局部内部类没有访问修饰符,另外局部内部类要访问外部的变量或者对象,该变量或对象的引用必须是用final修饰的
匿名内部类:
在多线程模块中的代码示例中大量使用,匿名内部类是唯一没有构造器的类
静态内部类:
Outer.staticInner os = new Outer.staticInner();
使用内部类的好处:
1、Java允许实现多个接口,但不允许继承多个类,使用成员内部类可以解决Java不允许继承多个类的问题。在一个类的内部写一个成员内部类,可以让这个成员内部类继承某个原有的类,这个成员内部类又可以直接访问其外部类中的所有属性与方法,是不是相当于多继承了呢?
2、成员内部类可以直接访问其外部类的private属性,而新起一个外部类则必须通过setter/getter访问类的private属性
3、有些类明明知道程序中除了某个固定地方都不会再有别的地方用这个类了,为这个只用一次的类定义一个外部类显然没必要,所以可以定义一个局部内部类或者成员内部类,写一段代码用用就好了
4、内部类某种程度上来说有效地对外隐藏了自己,比如我们常用的开发工具Eclipse、MyEclipse,看代码一般用的都是Packge这个导航器,Package下只有.java文件,我们是看不到定义的内部类的.java文件的
5、使用内部类可以让类与类之间的逻辑上的联系更加紧密
@RequestParam的要求
- 均支持POST,GET请求
- 只支持Content-Type: 为 application/x-www-form-urlencoded编码的内容。Http协议中,如果不指定Content-Type,则默认传递的参数就是application/x-www-form-urlencoded类型)
@RequestBody
不支持get请求,因为get请求没有HttpEntity
- 必须要在请求头中申明content-Type(如application/json).springMvc通过HandlerAdapter配置的HttpMessageConverters解析httpEntity的数据,并绑定到相应的bean上
- 只能一个@RequestBody。且不能与@RequestParam一起使用
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
JSONArray jsonArray = JSONArray.fromObject(tagList);
fastjson
JSON.toJSONString() ;
JSONObject.toJSON(dto);
JSONObject.parseObject(data);
JSONArray.parseArray(data, SingleShipmentReq.class);
注解
@Transactional和@Async传播有效性
自定义注解
注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
注解用来修饰,类、方法、变量、参数、包。
注解不会对所修饰的代码产生直接的影响。
设计模式
创建型(5):工厂、抽象工厂、建造者、原型、单例
结构型(7):适配器、装饰、代理、桥接、外观、享元、组合
行为型(11):迭代器、策略、模板、观察者、责任链、命令、备忘录、状态、访问者、中介者、解释器
简单工厂模式:线程池、ThreadFactory
单例模式:饿汉、懒汉、懒汉双检锁、应用(Runtime)
策略模式:jdk代理还是cgllib
装饰器模式:输入流InputStream
饿汉模式是安全的,懒汉模式
懒汉双检锁:
getInstance()
static volatile A instance = null; // volatile是为了指令重排序
if( instance != null )
synchronized(A.class)
if(instance != null)
instance = new A(); // 复杂操作
单例模式的好处:
控制资源的使用,通过线程同步来控制资源的并发访问
控制实例的产生,以达到节约资源的目的
控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信
之所以用单例,是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存;
之所以用多例,是为了防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;
用单例和多例的标准只有一个:
当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),则多例,否则单例;
Jdk
1.7 1.8
1. 接口的默认方
Java1.8以前,接口里的方法要求全部是抽象方法,java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可。
2. lambda表达式
它将允许我们将行为传到函数里。在Java 8之前,如果想将行为传入函数,仅有的选择就是匿名类,需要6行代码。而定义行为最重要的那行代码,却混在中间不够突出。Lambda表达式取代了匿名类,取消了模板,允许用函数式风格编写代码。这样有时可读性更好,表达更清晰。
3. 函数式接口
如果一个接口定义个唯一一个抽象方法,那么这个接口就成为函数式接口。一个函数式接口非常有价值的属性就是他们能够用lambdas来实例化。
4. 方法与构造函数引用
使用关键字来传递方法或者构造函数引用。
5. Lambda作用域
在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
6. 访问局部变量
可以直接在lambda表达式中访问外层的局部变量。
7. 访问对象字段与静态变量
和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的。
8. 访问接口的默认方法
JDK1.8 API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了注解以便能用在lambda上。
Java 8API同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自Google Guava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。
jdk1.8新特性知识点:
- Lambda表达式
- 函数式接口
- *方法引用和构造器调用
- Stream API
- 接口中的默认方法和静态方法
- 新时间日期API
Lmabda表达式的语法总结: () -> ();
前置 语法
无参数无返回值 () -> System.out.println(“Hello WOrld”)
有一个参数无返回值 (x) -> System.out.println(x)
有且只有一个参数无返回值 x -> System.out.println(x)
有多个参数,有返回值,有多条lambda体语句 (x,y) -> System.out.println(“xxx”);return xxxx;;
有多个参数,有返回值,只有一条lambda体语句 (x,y) -> xxxx
多线程
Thread类也是实现的Runnable接口
>>> interrupt()方法的作用:在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞状态。
换句话说,没有被阻塞的线程,调用interrupt()方法是不起作用的。
原理:通过无限轮询自己的中断标识位,中断了则打印、退出,否则一直运行
总结:中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
>>> join()方法会使调用join()方法的线程所在的线程无限阻塞,直到调用join()方法的线程销毁为止
join()方法内部使用的是wait(),因此会释放锁
CAS
无锁执行者CAS的核心算法原理然后分析Java执行CAS的实践者Unsafe类,该类中的方法都是native修饰的,因此我们会以说明方法作用为主介绍Unsafe类
执行函数:CAS(V,E,N)
其包含3个参数
- V表示要更新的变量
- E表示预期值
- N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。
CPU指令对CAS的支持
或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
volatile
加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。
可见性,禁止重排序,但是并不保证原子性
A、原子性 :对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
B、可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
ThreadLocal
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
方法:set(T value)、get()、remove()
1、每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象
实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2、每一个ThreadLocal对象都有一个循环计数器
3、ThreadLocal.get()取值,就是根据当前的线程,获取线程中自己的ThreadLocal.ThreadLocalMap,然后在这个Map中根据第二点中循环计数器取得一个特定value值
public T get() Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if(map != null) ThreadLocalMap.Entry e = map.getEntry(this); if(e != null) return (T)e.value; return setInitialValue; ThreadLocalMap getMap(Thread t) return t.threadLocals; //t.threadLocals实际上就是访问Thread类中的ThreadLocalMap这个成员变量
>>> 应用场景:
主要用途是为了保持线程自身对象和避免参数传递,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
比如数据库连接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>()
public Connection initialValue()
return DriverManager.getConnection(DB_URL);
;
public static Connection getConnection()
return connectionHolder.get();
>>> 存在问题:内存泄露
地址:https://www.jianshu.com/p/98b68c97df9b
线程池
ExecutorService extends Executor,ExecutorService底层是通过ThreadPoolExecutor实现;
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心池的大小。在创建了线程池之后,默认情况下,线程池中没有任何线程,而是等待有任务到来才创建线程去执行任务。默认情况下,在创建了线程池之后,线程池钟的线程数为0,当有任务到来后就会创建一个线程去执行任务
maximumPoolSize:池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量,当任务数量比corePoolSize大时,任务添加到workQueue,当workQueue满了,将继续创建线程以处理任务,maximumPoolSize表示的就是wordQueue满了,线程池中最多可以创建的线程数量
workQueue:存储还没来得及执行的任务
threadFactory:执行程序创建新线程时使用的工厂
handler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
>>> 四种线程池
newSingleThreadExecutos() 单线程线程池,线程池中运行的线程数肯定是1
newFixedThreadPool(int nThreads) 固定大小线程池
newCachedThreadPool() 无界线程池,采用了SynchronousQueue
newScheduleThreadPool()
>>> 拒绝策略
AbortPolicy直接抛出一个RejectedExecutionException,这也是JDK默认的拒绝策略
CallerRunsPolicy尝试直接运行被拒绝的任务,如果线程池已经被关闭了,任务就被丢弃了
DiscardOldestPolicy移除最晚的那个没有被处理的任务,然后执行被拒绝的任务。同样,如果线程池已经被关闭了,任务就被丢弃了
DiscardPolicy不能执行的任务将被删除
>>> qps
并发量
io线程数
计算线程数:一来服务器CPU核数有限,同时并发的线程数是有限的
IO 字节流 / 字符流
一个字节有8bit,一个字符有2个字节
File file = new File(location);
FileOutputStream out = new FileOutputStream(file);
out.write(byte [] );
FileInputStream in = new FileInputStream(file);
in.read(new byte [ file.length ] );
用户空间(常规进程,该部分执行的代码不能直接访问硬件设备)和内核空间(操作系统运行时所使用的程序调度/虚拟内存/连接硬件资源)
所有的内核都直接或者间接的通过内核空间,保证操作系统的稳定性和安全性。
每一次系统调用都会存在两个内存空间之间的相互切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户控件的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是牺牲了一部分的效率。
out.write(byte [] );
JUC并发包
BlockingQueue线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue
抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
1)add:把Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则招聘异常
2)offer:表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
3)put:把Object加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
CAS的缺点:
1??可以解决原子性问题,但是如果CAS失败,则会一直循环尝试,循环时间长开销很大;
2??只能保证一个共享变量的原子操作;
3??ABA问题,中途被其他线程改变过,但是又改回来了,使用要考虑是否有风险.
ReentrantReadWriteLock可重入,锁降级(允许写锁降级为读锁),中断锁,支持Condition
AQS的state表示写锁和读锁的个数,state的高16位表示读锁的个数,低16位表示写锁的个数
线程池、阻塞队列、CountDownLatch、Semaphore、Exchanger CyclicBarrier、Callable、Future、FutureTask
通过Semaphore控制并发并发数的方式和通过控制线程数来控制并发数的方式相比,粒度更小
锁
背景:对于Java开发者来说就可以很方便的使用这些锁及常用类。但是,随着锁的频繁使用及错用,随之而来的就是程序执行效率变低、应用变的缓慢。为了提高线程对共享数据访问的效率,HotSpot虚拟机从JDK1.5到JDK1.6做了重大改进,提供了很多锁优化技术,包括自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
自旋锁:当线程挂起或恢复执行的时,会从用户态转入内核态中完成,这种操作是很消耗时间的。自旋锁在JDK1.6中是默认开启的,默认自旋次数是10次,可以使用参数-XX:PreBlockSpin修改默认值。虽然,自旋锁避免了线程挂起和恢复的开销,但是它占用了处理器的执行时间,如果锁占用时间很短,自旋锁效果很好,否则会浪费处理器的执行时间,影响应用的整体性能。
自适应自旋锁:在JDK1.6中引入了自适应自旋锁,自旋的次数由上一次在同一个锁上自旋的时间和锁持有者的状态来决定。如果上一次同一个锁通过自旋刚刚被获取,并且持有锁的线程正在运行,那么虚拟机认为本次自旋也会成功,将会自旋相对长的时间获取锁。如果同一个锁很少通过自旋成功被获取,那么虚拟机认为本次自旋也会失败,不会执行自旋操作。
锁消除
一些使用了锁控制的代码,在虚拟机即时编译器运行时检测到不存在对共享数据的竞争访问,也就是代码只会被一个线程访问,此时会对锁进行消除,这项优化称为锁消除。锁消除的主要判断依据来源于逃逸分析(即分析对象的动态作用域,一个对象在方法内被定义后,在别的方法或线程中无法通过任何途径访问到这个对象,则可以进行一些优化操作)的数据支持。
锁粗化
大多数情况下,为了提高程序的执行效率,会缩小锁作用的范围。但是,对于一些连续操作都对同一个对象进行反复加锁、释放锁的情况来说,缩小锁的作用范围会消耗更多的资源,这种情况需要扩大锁的作用范围,这项优化称为锁粗化。
轻量级锁
在HotSpot虚拟机中,Java对象在内存中存储的布局分为3块区域:对象头、实例数据和对齐填充。对象头包含两部分,第一部分包含对象的HashCode、分代年龄、锁标志位、线程持有的锁、偏向线程ID等数据,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称为Mark World。
当代码执行到同步代码时,如果此时对象的锁未被锁定(锁标志位位01),虚拟机将在当前线程的栈帧中创建一个名为Lock Record空间,这个空间用于存储当前对象的Mark World拷贝,具体如下图所示。
接着,虚拟机使用CAS尝试将对象的对象头Mark Wolrd指向Lock Record,也就是在Mark Wolrd的30bit存储Lock Record的起始地址,具体如下图所示。如果上述操作执行成功,当前线程就持有了对象的锁,此时对象处于轻量级锁锁定状态,对应的锁标志位为00。
如果上述操作执行失败,首先会检查对象的对象头Mark World是否指向了当前线程栈帧中的Lock Record,如果指向了则表示当前线程已经持有了对象的锁,否则表示对象的锁已经被其它线程持有,锁膨胀为重量级锁,线程挂起等待。
轻量级锁的释放过程,通过CAS将Lock Record中存储的Mark Wolrd拷贝替换回对象的对象头Mark Wolrd中,替换成功则锁释放成功,否则表示有其它线程尝试获取过锁,释放锁的同时,唤醒挂起的线程,这里笔者的理解是此时锁膨胀为重量级锁,唤醒等待线程竞争。
偏向锁
锁对象第一次被线程持有的时候,虚拟机通过CAS把获取到这个锁的线程ID记录到对象头Mark World中,操作成功则成功获取偏向锁,对象头中的锁标志位设置为01。持有偏向锁的线程每次执行到这段同步代码时,不需要任何同步操作,这项优化称为偏向锁。
当有其它线程尝试获取对象的锁时,终止偏向模式,同时根据锁是否处于锁定状态,撤销偏向锁恢复到未锁定或轻量级锁状态。
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
- 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
- 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
- 重量级锁:有实际竞争,且锁竞争时间长。
synchronized
synchronized可用于修饰普通方法、静态方法和代码块,都可以实现对同步代码的并发安全控制。
synchronized修饰普通方法时,同步代码执行前,需要获取当前实例对象的锁(对象锁)。
synchronized修饰静态方法时,同步代码执行前,需要获取当前类的锁(类锁)。
第一种修饰代码块,对实例对象加锁,同步代码执行前,需要获取当前实例对象的锁(对象锁)。 synchronized (this)
第二种修饰代码块,对Class加锁,同步代码执行前,需要获取当前类的锁(类锁)。
1??对象锁
2??锁重入
3??异常自动释放锁
使用javap -c -v命令对class文件进行反编译
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
当执行到synchronized修饰的语句块时,通过monitorenter指令获取锁,成功获取锁之后,执行同步代码,同步代码正常执行完成后,通过monitorexit指令释放持有的锁。JVM为了保证同步代码执行非正常结束时也释放持有的锁,所以,在发生异常时,再次通过monitorexit指令释放持有的锁。
ReentrantLock
ReentrantLock持有的是对象监视器,但是和synchronized持有的对象监视器不是一个意思,虽然我也不清楚两个持有的对象监视器有什么区别,不过把methodB()方法用synchronized修饰,methodA()不变,两个方法还是异步运行的。
Maven
命令行创建工程
mvn archetype:create -DgroupId=com.leqee -DartifactId=report -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=Internal
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.leqee
-DartifactId=report -DpackageName=com.leqee -Dversion=1.0-SNAPSHOT -DinteractiveMode=false
mvn clean package依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7个阶段。
mvn clean install依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8个阶段。
mvn clean deploy依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9个阶段。
由上面的分析可知主要区别如下,
package命令完成了项目编译、单元测试、打包功能,但没有把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
install命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库,但没有布署到远程maven私服仓库
deploy命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
Log
总结这篇文章,我具有充分的理由的来选择SLF4J而不是直接选用Log4j, commons logging, logback 或者 java.util.logging。
1)在你的开源库或者私有库中使用SLF4J,可以使它独立于任何的日志实现,这就意味着不需要管理多个库和多个日志文件。你的客户端将会体会到这一点。
2)SLF4J提供了占位日志记录,通过移除对isDebugEnabled(), isInfoEnabled()等等的检查提高了代码的可读性。
3)通过使用日志记录方法,直到你使用到的时候,才会去构造日志信息(字符串),这就同时提高了内存和CPU的使用率。
4)做一个侧面的说明,越少的临时字符串,垃圾回收器就意味着越少的工作,这就意味着为你的应用程序提供更好的吞吐量和性能。
这些优势都只是冰山一角,当你开始使用SL4J并阅读它,你会学到更多的好处。我强烈建议,在java中任何新的代码开发,都应使用SLF4J而不是任何的日志API,包括log4J。
Linux
ps对进程进行监测和控制,显示进程的当前状态
ps -ef
ps -mp
cat -n X.log | grep ‘‘
more +n X.log
查看线程数
ps -ef | grep tomcat
watch ps -o nlwp 27004
368
tcp支持最大连接数
ulimit -n 查看
losf -i:8083
netstat -an | grep 8083
使用ps命令,具体用法是 ps -mp PID
这样可以看到指定的进程产生的线程数目
以上是关于JAVA基础的主要内容,如果未能解决你的问题,请参考以下文章