- java相关概念
- ==和equals
- volatile
- static
- 正确理解protected关键字
- 异常
- 配置文件profile用法
- 简单测试springIOC
- 深拷贝与浅拷贝
- 程序的堆和栈
- [git cherry-pick为什么会有冲突?](#git cherry-pick为什么会有冲突?)
- 设计模式
- 正则
- 多线程
==和equals
老生常谈的话题。
基本数据类型重写了Object的equals方法,而引用类型需要自定义equals方法,默认使用Objec的equals方法,而Object的equals方法实际上就是==。因此:
对于==来说,如果比较基本类型,比较的是值,如果是引用类型,比较的是地址。
对于equals来说,默认比较地址,具体逻辑需要自定义实现。需要注意的是对应基本类型如Byte(字节型)、short(短整型)、char(字符型) 、int(整型)、float(单精度型/浮点型)、long(长整型)、double(双精度型) 和boolean(布尔类型)等不能使用equals,因为基本数据类型不是对象,没有方法。
Hashset和Hashmap的存取都是通过key的hash值存取的,如果key是一个对象,则需要重写hashcode方法。原因如下:
当对象的equals方法返回的值相同时,hashcode应该返回相同的值,当equals返回值不同,不一定要求hashcode返回不同的值,但返回不同值可以增加散列性能。当hashcode不同时,equals一定不同。
先调用hashCode,唯一则存储,不唯一则再调用equals,结果相同则不再存储,结果不同则散列到其他位置。因为hashCode效率更高(仅为一个int值),比较起来更快。
volatile
java指令重排:当现有的java源码编译成class文件之后,一句java语句会被翻译成若干句汇编语句, 例如:
INSTANCE = new myCat(); 可以被翻译成三句:
1.在内存中为myCat分配一块内存空间
2.把这块空间进行初始化,初始化的意思是为这个空间中的属性赋默认值,例如int赋值为0,引用类型赋值为空
3.把myCat这块空间对应的引用赋值给INSTANCE
指令重排可能导致三句语句不按顺序执行,例如先执行第三句把空间引用给INSTANCE之后再执行第二句初始化,导致INSTANCE == null判断不为空,但实际上得到的是不正确的初始化的对象(cpu可能在等待耗时指令(向内存进行IO)执行完毕时执行下一条指令)
因此volatile的作用有两个:
1.线程间可见,一个线程改了某个值,另一个线程立刻可以看到
2.防止指令重排,如果不加volatile,可能会出现一个“半初始化”的对象(但概率极低)
3.用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。
使用volatile修饰主存变量的意义:
jvm运行时刻内存的分配,其中有一个内存区域是jvm虚拟机的栈,每一个线程运行的时候都有一个线程栈,线程栈保存了线程运行时候的上下信息,当线程访问某一个对象的值的时候,首先通过对象引用找到对应在堆内存变量的值,然后把堆内存变量的具体指load到线程的本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系。而是直接修改副本的值,在修改完之后某一时刻,自动把线程变量副本的值写回对象在堆内存中变量,这样在堆中的对象的值就产生变化了。所以在线程运行时如果修改堆内存变量的值,线程中的副本是无法被同步的,如果想让线程中的副本每次都和堆中变量的值保持一致,就需要用volatile修饰变量。强制他每次都去主存中取值。
jvm运行时刻内存的分配,其中有一个内存区域是jvm虚拟机的栈,每一个线程运行的时候都有一个线程栈,线程栈保存了线程运行时候的上下信息,当线程访问某一个对象的值的时候,首先通过对象引用找到对应在堆内存变量的值,然后把堆内存变量的具体指load到线程的本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系。而是直接修改副本的值,在修改完之后某一时刻,自动把线程变量副本的值写回对象在堆内存中变量,这样在堆中的对象的值就产生变化了。所以在线程运行时如果修改堆内存变量的值,线程中的副本是无法被同步的,如果想让线程中的副本每次都和堆中变量的值保持一致,就需要用volatile修饰变量。强制他每次都去主存中取值。
volatile如何解决指令重排?
在两个指令中间加一道墙,称为内存屏障,不允许越过这道墙。JSR内存屏障有四种:loadload,storeload,loadstore,storestore。屏蔽的两条指令分别为:读指令-读指令,写指令-读指令,读指令-写指令,写指令-写指令。
static
当static修饰类:只存在一种情况,静态内部类。
修饰成员变量:先加载父类静态成员,然后是子类静态成员,只加载一次。静态变量存放在方法区中,并且是被所有线程所共享的。
修饰方法:不依赖任何对象即可访问,静态方法不能访问非静态成员和方法,但非静态方法中可以访问静态方法
修饰代码块:先加载父类静态代码块,然后是子类静态代码块。只加载一次。
正确理解protected关键字
其实就是子类只能在自己的作用域中,使用自己继承自父类的protected方法,而不能使用兄弟类继承自父类的protected方法,
转载:https://blog.csdn.net/qq_37150783/article/details/78734652?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-7.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-7.control
异常
异常有两种:Error和Exception,均继承自Throwable
Error指的是程序不能依靠自己处理的严重错误,非代码逻辑出问题导致的错误,一般表示运行时JVM出了问题,比如virtualmachineerror,noclassdeffound,outofmemory等。此类错误发生时jvm会终止线程。
Exception:是程序代码本身出问题导致的错误,是可以捕获并处理的异常。Exception又分为两种,运行时异常(runtimeexception)和检查异常(checked)
运行时异常:由代码逻辑问题引发的异常,编译器不会检查,并不要求处理。如空指针nullpointexception,数组下标越界arrayindexoutbound
检查异常:编译器会检查的异常,必须用trycatch捕获并处理,或throws抛出,否则编译不通过。
配置文件profile用法
在配置文件中配置一些属性,通过java反射机制读取属性并生成类,可以在不修改代码的情况下灵活指定属性,只需要修改配置文件即可。
**反射:**java程序运行起来有很多类从磁盘被加载到内存,所以每个程序有很多class被load到内存,站在面向对象的角度这是一类事物,即class类,java中class这个类封装了这一类事物的一些共同属性和方法,这种方式叫反射。无需源码,通过分析字节码即可得到类的属性方法等信息
java反射和spring反射不同,java反射很慢,spring反射课直接操纵字节码。
首先在idea的resources文件夹下生成配置文件
#java配置文件
initTankCount=8
goodFs = com.ziqi.tank.FourDirFireStrategy
badFs = com.ziqi.tank.DefaultFireStrategy
读取
import java.util.Properties;
public class PropertyMgr {
static Properties properties = new Properties();
static {
try {
properties.load(PropertyMgr.class.getClassLoader().getResourceAsStream("config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static Object get(String key){
if(properties == null) return null;
return properties.get(key);
}
public static void main(String[] args){
System.out.println(PropertyMgr.get("initTankCount"));
//根据配置动态生成实例
String goodFsName = (String) PropertyMgr.get("goodFs");
fs = (FourDirFireStrategy)Class.forName(goodFsName).getDeclaredConstructor().newInstance();
}
}
简单测试springIOC
IOC即控制反转,把创建各种bean组件的控制权交给spring这个大工厂,控制反转通过依赖注入的方式实现。spring配置文件的三种格式:xml,annotation,java的config
在gradle中添加spring context依赖:
compile group: 'org.springframework', name: 'spring-context', version: '5.1.6.RELEASE'
idea resources目录下添加配置文件app.xml
<bean id="d" class="com.ziqi.Driver"></bean>
<bean id="tank" class="com.ziqi.Tank">
<property name="driver" ref="d"></property>
</bean>
调用spring生成bean
public class Tank {
public void setDriver(Driver driver) {
this.driver = driver;
}
Driver driver;
public void move(){
System.out.println("moving...");
}
}
public static void main(String[] args) {
// 想象成一个spring的大工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("app.xml");
Tank t = (Tank)applicationContext.getBean("tank");
t.move();
}
深拷贝与浅拷贝
浅克隆:只拷贝对象,如果对象里面持有其他类型的引用,这个引用指向的对象并不会被拷贝
深克隆:实现Cloneable接口,然后把对象中持有的引用指向的对象也克隆一份
PS:string不需要深克隆,string和int一样都是放在常量池中的对象,克隆出来的string值被改变时会创建一个新的string指向新的string,不会直接修改原来的string。另外new出来的堆中string还是指向常量池中的string
程序的堆和栈
java中堆栈内存分析:https://blog.csdn.net/fox_bert/article/details/88367002?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-4.control&spm=1001.2101.3001.4242
内存堆与内存栈:https://blog.csdn.net/qq_27825451/article/details/102572795
简单来说,java堆栈由java虚拟机管理,java引用类型,基本类型的变量数据放在栈中,引用类型指向的实际内存地址(例如new出来的对象)放在堆中。
堆区:
1、存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2、jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2、每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3、栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。、
方法区:
1、又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。常量池。
2、方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
git cherry-pick为什么会有冲突?
有一条主分支master,进行过两次提交(m0和m1)。此时,新开了一个分支develop做开发,进行了三次提交(d0、d1和d2)。如果只想将d2这次提交合并到主分支master,会产生一个冲突,需要用户手动去编辑。为什么?
答:git是根据文件的变化而不是保存每个提交的完整文件,所以你的git cherry-pick d2的哈希码只是把develop分支上d1->d2的改动应用到了master分支上,显然m1-d1的变化没有先应用上,所以导致冲突。
单例
很多类不需要多个实例,各种各样的manager,factory等等,java进程会开启一个jvm虚拟机,需要保证在jvm中只存在一个类的实例。
第一种:class加载时就完成实例化,饿汉式(最简单实用)
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {
}
public static Mgr01 getInstance(){return INSTANCE;}
public static void main(String[] args) {
Mgr01 mgr01 = Mgr01.getInstance();
Mgr01 mgr011 = Mgr01.getInstance();
System.out.println(mgr01 == mgr011);
}
}
第二种:什么时候用到这个实例什么时候初始化,但不能保证new出的是同一个实例
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03(){}
public static synchronized Mgr03 getInstance(){
if(INSTANCE == null){
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr03.getInstance().hashCode());
}).start();
//上面用了lambda表达式打印实例的hashcode,括号代指接口对象,花括号代指重写的方法。等价于下面的写法
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println("...");
// }
// }).start();
}
}
}
第三种:双重检查+synchronized加锁(比较完美的写法,不会产生new出多个实例的情况)
public class Mgr06 {
private static volatile Mgr06 INSTANCE;//JIT优化的时候可能会有问题,所以需要加volatile
private Mgr06(){}
public static synchronized Mgr06 getInstance(){
if(INSTANCE == null){//第一个判断的必要性:可以省很多事,很多线程判断不为空就不用等着加锁了
// 双重检查
synchronized (Mgr06.class){
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
System.out.println(a);
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 1; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
第四种:静态内部类,更加完美,如果只加载Mgr07那么这个静态内部类是不会被加载的,只有在调用getInstance方法时才会加载该实例。由jvm帮我们保证他只加载一次。
public class Mgr07 {
private Mgr07(){}
private static class Mgr07Holder{
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance(){
return Mgr07Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
第五种:枚举+静态单例,最完美写法,保证单实例并且可以防止反序列化。java的反序列化可以把class文件load到内存,然后再new一个实例出来,所以挡不住用反射的方式new出实例,因此最完美的是用枚举,因为枚举没有构造方法。
public enum Mgr08 {
INSTANCE;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
多数情况下由spring的bean工厂保证单例。
策略
java1.8之后接口里必须有方法实现,因为要支持lambda表达式。所以查java接口时重点查java的抽象方法,即实现该接口时必须实现的方法。
方法一:如果需要比较Cat类,Cat类自己实现一个CompareTo(Cat c)方法,在需要具体比较的语句处调用该方法,比较实例和另一个Cat的值,返回值为1,0,-1
方法二:策略模式
首先定义比较器接口
@FunctionalInterface //使用该注解确定是一个函数式接口,配合lambda表达式使用
public interface Comparator<T> {
int compare(T o1, T o2);
// 1.8以后接口可以实现方法
default void m(){
System.out.println("m");
}
}
然后自定义比较器,实现该接口(灵活定制具体的比较策略)
public class CatWeightComparartor implements Comparator<cat> {
@Override
public int compare(cat o1, cat o2) {
if(o1.weight < o2.weight) return -1;
else if (o1.weight > o2.weight) return 1;
else return 0;
}
}
定义排序类
public class sorter<T> {
// 一个选择排序
public void sort(T[] arr, Comparator<T> comparator) {
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;
for(int j=i+1; j<arr .length; j++) {
minPos = comparator.compare(arr[j],arr[minPos]) == -1 ? j : minPos;
}
swap(arr,i,minPos);
}
}
void swap(T[] arr, int i, int j) {
T temp = arr[i] ;
arr[i] = arr[j] ;
arr[j] = temp;
}
}
使用比较器(策略)进行排序
public class main {
public static void main(String[] args) {
cat[] a = {new cat(3,3),new cat (5,5),new cat(1,1)};
sorter<cat> sorter1 = new sorter();
System.out.println(Arrays.toString(a));
}
}
策略模式把策略本身提取出来,将策略和该策略针对的对象传入执行器中,实现在不修改执行器的情况下灵活增加新策略。
工厂
为什么可以new,却依然需要工厂?
需要进行一些定制后的实例,如权限,修饰,日志等等。
工厂方法:什么类用什么工厂生产,是在产品维度拓展
抽象工厂:抽象工厂生产抽象产品,具体工厂生产具体产品。具体工厂继承自抽象工厂,具体产品继承自具体产品。用interface也可实现,但一般名词用抽象类,形容词用接口。 抽象工厂可以在产品族进行拓展。但该方法依然有局限,虽可以灵活定制工厂,但当工厂要添加新生产的东西时,所有工厂都需要加代码
例:定义抽象工厂
public abstract class abstractfactory {
abstract gas inputgas();
abstract veihcal creatv();
abstract ticket buyt();
}
定义具体工厂:
public class planefactory extends abstractfactory {
@Override
gas inputgas() {
return new planegas();
}
@Override
veihcal creatv() {
return new plane();
}
@Override
ticket buyt() {
return new planeticket();
}
}
public class carfactory extends abstractfactory {
@Override
gas inputgas() {
return new cargas();
}
@Override
veihcal creatv() {
return new car();
}
@Override
ticket buyt() {
return new carticket();
}
}
定义抽象产品:
public abstract class gas {
abstract void input();
}
public abstract class ticket {
abstract void buy();
}
public abstract class veihcal {
abstract void creat();
}
定义具体产品:每种产品对应一个工厂
public class car extends veihcal {
@Override
void creat() {}
}
public class cargas extends gas{
@Override
void input() {}
}
public class carticket extends ticket{
@Override
void buy() {}
}
调用具体工厂生产产品族:
public static void main(String[] args) {
abstractfactory abstractfactory = new carfactory();
veihcal car = abstractfactory.creatv();
gas cargas = abstractfactory.inputgas();
ticket cartic = abstractfactory.buyt();
}
调停者mediator
对外类似facade模式:代码过于复杂时,如果要再添加功能,创建一个门面管家,管家将这些功能都封装起来。他一对外提供功能。:代码过于复杂时,如果要再添加功能,创建一个门面管家,管家将这些功能都封装起来。他一对外提供功能。
对内为Mediator调停者模式(降低内部耦合),同上,只不不过是统一对系统内部各个功能提供功能。
现实中应用实例:消息中间件mq
装饰器
类似桥梁模式bridge:装饰器强调装饰,桥梁模式强调两个分支发展,写法相似语义不同
例:
GameObject <- bullet,
<-GodDecorator
<- RectDecorator
<- TailDecorator
bullet 和GodDecorator装饰器继承自GameObject,RectDecorator 和TailDecorator 继承自GodDecorator,任何继承自GameObject的类都可以和装饰器聚合在一起,甚至包括两个装饰器之间 RectDecorator 和TailDecorator也可以聚合在一起,如果需要对 bullet使用两个装饰器装饰,先把 bullet聚合到 RectDecorator中,再把RectDecorator 聚合到TailDecorator中。
典型应用:javaIO,Reader中聚合了InputStream,Writer聚合OutputStream
责任链
将许多对某个对象的处理器穿成一个链条,让对象一次通过这个链条,实现对他的处理
import java.util.ArrayList;
import java.util.List;
public class main {
public static void main(String[] args) {
Msg msg = new Msg();
msg.setMsg("<scripts> 999999911199999");
filterChain filters = new filterChain(); //第一个链
filters.add(new HtmlFilter()).add(new SensitiveFilter());
filterChain filterChain = new filterChain(); //第二个链
filterChain.add(new HtmlFilter()).add(new SensitiveFilter());
filters.add(filterChain);// 把链条整体看成一个filter
filters.doFilter(msg);
}
}
class Msg{
String name;
String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
return "Msg{" +
"msg='" + msg + '\'' +
'}';
}
}
interface filter{
boolean doFilter(Msg msg);
}
class HtmlFilter implements filter{
@Override
public boolean doFilter(Msg msg) {
String r = msg.getMsg();
r = r.replace('<','[');
r = r.replace('>',']');
msg.setMsg(r);
return true;
}
}
class SensitiveFilter implements filter{
@Override
public boolean doFilter(Msg msg) {
String r = msg.getMsg();
r = r.replace("111","999");
msg.setMsg(r);
return true;
}
}
class filterChain implements filter{
List<filter> filters = new ArrayList<>();
public filterChain add(filter filter){
filters.add(filter);
return this;
}
public boolean doFilter(Msg msg){
for (filter f: filters){
if(!f.doFilter(msg)) return false;// 如果返回false就中止责任链的传递
}
return true;
}
}
进阶版:filterChain同时过滤request和response,实现处理request时从左到右,处理response时从右到左。(servlet的filter就是这么实现的)
思路:filterchain如何调用request时正向调用,调用response时逆向调用?两个关键点:
1.filterchain中记录index,指向list中当前具体的filter,执行他的dofilter之后index++,
2.具体的filter的dofilter应该包含三个步骤,先处理request,再调用filterchain中的dofilter,最后处理reponse,这样第一个filter只有在所有的filter都被处理后才处理response,保证response被逆序处理,这种写法类似递归。
重写filter接口和他的各种实现类:
interface filter{
boolean doFilter(Msg requset, Msg response, filterChain filterChain);
}
class HtmlFilter implements filter{
@Override
public boolean doFilter(Msg requset, Msg response, filterChain filterChain) {
// 处理request
filterChain.doFilter(requset, response, filterChain);
// 处理response
return true;
}
}
class SensitiveFilter implements filter{
@Override
public boolean doFilter(Msg requset, Msg response, filterChain filterChain) {
// 处理request
filterChain.doFilter(requset, response, filterChain);
// 处理response
return true;
}
}
class filterChain implements filter{
List<filter> filters = new ArrayList<>();
int index = 0;
public filterChain add(filter filter){
filters.add(filter);
return this;
}
public boolean doFilter(Msg requset, Msg response, filterChain filterChain){
// 原来是自动从头执行到尾,现在是需要其他人调用才执行
if(index == filters.size()) return false;
filter f = filters.get(index);
index++;
return f.doFilter(requset, response, this);
}
observer观察者
观察者模式常用于处理事件:观察者(类似责任链),又名hook,callback,listener。事件处理经常使用observer+责任链。
PS:相当于钩子函数,或callback,其本质就是observer。因为js,c++等可以把函数做参数传入方法中的,例如java的lambda表达式就是钩子函数:
observers.add((e) ->{//向事件发起者添加被触发后要执行的代码块
System.out.println("wake up");
});
事件处理流程:事件->事件源对象(发出者)->事件接受者(接收者)->事件。
观察者可监听多个事件源,只和事件打交道不和事件源打交道,以便和事件源解耦合
更多:https://cloud.tencent.com/developer/article/1755745
例:child为事件发出者,cat,dog为事件接受者(观察者),通过wakeUpEvent通知观察者。另外在事件中包含事件发出者的引用,方便观察者进行一些处理(有些操作需要判断事件源对象类型)。
public interface Observer {
void actionwakeup(wakeUpEvent wakeUpEvent);
}
public class wakeUpEvent { //用来在事件发出者和观察者中间传递消息的事件
long timestamps;
String loc;
Child source;
public wakeUpEvent(long timestamps, String loc, Child source) {
this.timestamps = timestamps;
this.loc = loc;
this.source = source;
}
}
public class Dog implements Observer{
@Override
public void actionwakeup(wakeUpEvent wakeUpEvent) {
//被事件发出者通知后进行相关处理
}
}
public class Cat implements Observer{
@Override
public void actionwakeup(wakeUpEvent wakeUpEvent) {
//被事件发出者通知后进行相关处理
}
}
public class Child {
private boolean cry = false;
private List<Observer> observers = new ArrayList<>();
{
observers.add(new Cat());
observers.add(new Dog());
observers.add((e) ->{
System.out.println("wake up");
});
}
public boolean isCry(){return cry;}
public void wakeup(){
cry = true;
wakeUpEvent event = new wakeUpEvent(System.currentTimeMillis(), "bed",this);
for (Observer observer:
observers) {
observer.actionwakeup(event);
}
}
}
组合模式:用于表示树状结构。递归本身就是压栈的过程,任何递归都可以改成栈式遍历
享元模式:
共享元数据,例如字符处理时,每输入一个a字符就产生一个对象的话,产生的小对象会特别的多,因此使用一个池子把他们都放进去共享,用的时候直接去池子里拿。
实际应用的例子:连接池,线程池等。;另外java 的String即为享元,所有字符串放在常量池中,只有new字符串才去堆里新建,否则所有字符串的引用均指向同一个(PS:堆里新建的字符串也是指向常量池已有的字符串)
测试:
String s1 = "abc";
String s2 = "abc";
String s3 = new Str ing("abc") ;
String s4 = new Str ing("abc") ;
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //false
System.out.println(s3 == s4);
System.out.println(s3.intern() == s1);
System.out.println(s3.intern() == s4.intern());
代理
java的ClassLoader使用双亲委派机制可以看成代理。
静态代理:类似聚合,多个代理和被代理的都实现一个接口,在接口的方法中自定义自己的实现,new多个代理一层一层去嵌套。
public interface Movable {
void move();
}
public class Tank implements Movable {
@Override
public void move() {
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}}
}
public class TankLogProxy implements Movable {
Movable tank;
public TankLogProxy(Movable tank) {
this.tank = tank;
}
@Override
public void move() {
System.out.println("start");
tank.move();
System.out.println("end");
}
}
public class TankTimeProxy implements Movable {
Movable tank;
public TankTimeProxy(Movable tank) {
this.tank = tank;
}
@Override
public void move() {
long start = System.currentTimeMillis();
tank.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
public static void main(String[] args) {
Movable mv = new TankTimeProxy(new TankLogProxy(new Tank()));
mv.move();
}
动态代理:(反射,instruments,cglib三种实现方式),动态代理就是代理类不是我们手写的,而是动态生成的,他没有实体类。用到了反射,getClassLoader,getClass,这些方法会通过对象找到对应的class对象,以及是谁把这个class加载到内存的。
public static void main(String[] args) {
Tank tank = new Tank();
//用反射的方式动态生成代理对象,动态生成一个代理类,Proxy.newProxyInstance(用哪个ClassLoader,实现哪些接口数组,代理的具体操作类)
Movable m = (Movable) Proxy.newProxyInstance(Tank.class.getClassLoader(),
//采用哪个classloader进行反射
new Class[]{Movable.class},
//一个接口的数组,生成的代理对象需要实现哪些方法
new InvocationHandler() {
//被代理的方法在被调用时,需要进行哪些操作
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("method" + method.getName() + "start");
Object o1 = method.invoke(tank, args); //对用被代理类自己的方法
System.out.println("method" + method.getName() + "end");
return o1;
}
});
m.move(); //调用代理类,move方法会去调用invoke方法
//该语句可以保存临时动态生成的代理类的字节码,读该源码可以理解jdk的动态代理
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");
}
classloader,即类加载器,像所有对象都继承自object一样,类加载器也是一个树形结构,顶层的是classloader。
动态代理的底层机制是用asm实现,可以直接去内存操作二进制字节码,asm机制让java可以被成为动态语言,意思是在执行的时候修改类的属性和方法。asm很强大,甚至可以代理final,因为可以直接删除final对应的二进制字节。
asm和反射的区别:asm可以改属性和方法,而反射只能读出其中的方法和属性却不能改。
一个有意思的地方是:任何语言,只要能编译成class字节码,就可以在jvm上运行。scala和kotlin编译完了都是class文件可在jvm上运行。
注意:jdk反射生成代理必须面向接口,这是由proxy内部实现决定的,例如代理tank的move方法,必须让tank实现一个包含move方法的接口,为了让代理类知道我应该代理哪些方法。asm替换class文件内容时,java内存中的class文件可以被替换,也就是说java支持热部署。
Spring AOP:我们只需写好被切入的类和待切入的类,spring会帮我们通过配置文件自动配置进去,不想用了把配置文件删掉就搞定。说白了就是把我们需要的代码块帮我们自动插入到某个方法的前面后面。
例:在resources目录下的app.xml中配置:
<!-- 被切入的对象-->
<bean id="tank" class="com.ziqi.Tank">
<property name="driver" ref="d"></property>
</bean>
<!-- 切入的对象-->
<bean id="timeproxy" class="com.ziqi.TimeProxy"></bean>
<aop:config>
<aop:aspect id="time" ref="timeproxy">
<!-- 定义切点onmove-->
<aop:pointcut id="onmove" expression="execution(void com.ziqi.Tank.move())"/> <!-- 切入点是move方法-->
<!-- <aop:pointcut id="onmove" expression="execution(void com.ziqi.Tank.*)"/>切入点是Tank中的所有类-->
<!-- <aop:pointcut id="onmove" expression="execution(void com.ziqi.*.*)"/>切入点是ziqi包下所有类的所有方法-->
<!-- 切点之前的操作-->
<aop:before method="before" pointcut-ref="onmove"></aop:before>
<!-- 切点之后的操作-->
<aop:after method="after" pointcut-ref="onmove"></aop:after>
</aop:aspect>
</aop:config>
调用:
//被切入的类
public class Tank {
public void setDriver(Driver driver) {
this.driver = driver;
}
Driver driver;
public void move(){
System.out.println("moving...");
}
}
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//切入的类
//@Aspect
public class TimeProxy {
// @Before("execution(void com.ziqi.Tank.move())")
public void before(){
System.out.println("before");
}
// @After("execution(void com.ziqi.Tank.move())")
public void after(){
System.out.println("after");
}
}
IOC+AOP:bean工厂+动态装配+动态行为拼接。
迭代器
不同的容器遍历的方式不同,只有容器自己才知道自己该怎么遍历,所以每个容器统一实现迭代器接口,然后提供给外界hasnext和next接口。jdk迭代器实现方式。
定义容器和迭代器接口:
public interface collection <E> {
void add(E o);
int size();
Iterator_ iterator();
}
public interface Iterator_ <E>{
boolean hasnext();
E next();
}
定义具体容器
public class ArrayList_<E> implements collection <E>{
E[] objects = (E[])new Object[10];
// objects中下一个空的位置在哪,或者说容器目前有多少元素
private int index = 0;
@Override
public void add(E o){
if(index == objects.length){
E[] newobjects = (E[])new Object[objects.length*2];
System.arraycopy(objects, 0, newobjects, 0, objects.length);
objects = newobjects;
}
objects[index]=o;
index++;
}
@Override
public int size(){return index;}
@Override
public Iterator_<E> iterator() {
return new ArrayListIterator();
}
class ArrayListIterator<E> implements Iterator_<E>{
private int currentindex = 0;
@Override
public boolean hasnext() {
if(currentindex >= index)return false;
return true;
}
@Override
public E next() {
E o = (E) objects[currentindex];
currentindex++;
return o;
}
}
}
调用迭代器
public static void main(String[] args) {
collection<String> collections = new ArrayList_();
Iterator_<String> i = collections.iterator();
while (i.hasnext()){
Object o = i.hasnext();
System.out.println(o);
}
}
此外还可以根据接口实现Linklist,hashset等
常用规则
普通字符:
[aeiou] 匹配串中所有的 e o u a 字母
[^aeiou] 匹配串中除了 e o u a 字母的所有字母
[A-Z] 匹配所有大写字母
[a-z] 表示所有小写字母
. 匹配换行符(\n、\r)之外任何单个字符,相等于 [^\n\r]
\s 是匹配所有空白符,包括换行
\S 非空白符,不包括换行
\w 匹配字母、数字、下划线。等价于 [A-Za-z0-9_]
限定符:
+ 前面字符出现1次或多次
* 前面字符出现0次或多次
?前面字符出现1次或0次
'o{n}',n 是一个非负整数。匹配确定的 n 次
'o{n,}'至少匹配n 次, 'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'
"o{n,m}",最少匹配 n 次且最多匹配 m次,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'
贪婪匹配:
/<.*>/ 匹配 <h1></h1>
/<.*?>/ 只匹配第一个 <h1> ,等价于 /<\w+?>/
定位符:能够将正则表达式固定到行首或行尾,串开始,结尾的位置。边界和非边界等
选择:用圆括号 () 将所有选择项括起来,() 表示捕获分组
修饰符:i ignore - 不区分大小写,g global - 全局匹配,m multi line - 多行匹配,s 特殊字符圆点 . 中包含换行符 \n
基本模式
字符簇
一些其他模式
visitor:主要用于编译器,访问抽象语法树
adapter模式:
例:
FileInputStream fis = new FileInputStream("c:/test.text") ;
InputStreamReader isr = new InputStreamReader(fis);
Buf feredReader br =new BufferedReader(isr);
这里的InputStreamReader即为FileInputStream和BufferedReader中间的转接器
builder模式:对象的属性太多,构造复杂,使用builder可以灵活的构造对象,选择哪些部分初始化哪些不初始化.
state状态模式:抽象出不同的state,让不同的state自己去实现相应操作
template:模板方法就是钩子函数,定义好一个事物中函数调用顺序,每个函数具体实现交给子类
command模式+momento = transaction回滚
command模式+责任链 = 多次undo
comman模式+composite = 宏命令
prototype:一个对象已经被初始化,的各个属性都已经被制定,一种方法是new新的,然后再把他的属性制定,如果现在需要一个新的对象和原来对象的属性差不多,就把原来对象掉Object.clone出来,无需挨个指定属性。实现原型模式实现cloneable接口并重写clone方法。
什么是对象序列化:对象是存在内存中的一个个零和一,因此可以直接写到磁盘上。需要序列化的对象实现serializable
例:
//写对象
objectOutputStream oos = new objectOutputStream(new FileOutputStream(f));
o0s.writeObject (myTank);
oos.writeObject(objects);
//读对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
myTank = (Tank)ois.readobject();
objects = (List)ois.readobject();|
面向对象思想:可维护,修改时的改动少。可复用,重复使用,甚至作为类库。可拓展,添加功能无需修改代码,接口可以灵活调用,支持多种调用方式。
几大原则:
对修改关闭,对拓展开放
类别太大,负责单一职责,高内聚低耦合(核心是多态)
里式替换:使用父类对象的地方,替换成子类对象,剩余代码不用改也能用
面向接口编程,替换具体实现代码不用换
线程的历史
单进程人工切换:纸带机
多进程批处理:多个任务批量执行
多进程并行处理:把程序写在不同的内存位置上来回切换,等待进程A进行io的时候处理进程B,等待进程B进行io的时候处理进程C等等。
多线程:一个程序内部不同任务来回切换(有的线程在等待网络IO,有的线程在刷新UI,有的线程在和DB进行读写)。例如select和epoll
纤程/协程:绿色线程,用户管理的(而不是OS管理的)线程
计算机组成如下图:
操作系统会为每个进程分配资源,例如最基本的内存空间,文件描述符,IO端口号等等。
进程:操作系统资源分配的基本单位(静态概念)。一个程序可以在内存里面存放好多份,每一份都是一个进程。程序加载的时候会把程序load到内存,为他开辟一块独立的内存空间。
线程:操作系统调度执行的基本单位(动态概念)。程序执行的时候我们是从上向下,我们在if语句里做个分支,然后在while语句里做个循环,但不管怎么走他总是只有一个路径走到结束。如果程序里没有同时运行的路径,那就是单线程,也叫主线程。所以所线程说白了就是多个分支。真正执行的时候执行的是线程,java中就是main方法。找到main方法第一句话扔给cpu执行。然后执行线程切换(context switch)执行其他线程
线程有IO密集型和CPU密集型。
什么是三高:
高并发:保证系统可以同时并行处理很多请求,指标包括,响应时间,吞吐量,秒查询率,并发用户数。
高性能:程序处理速度快,占用内存和CPU少。高性能指标常和高并发指标紧密相关,性能优化时计算密集型和IO密集型要分开考虑、常见思路:避免因为IO阻塞让CPU闲置,避免多线程之间过多加锁导致并行系统串行化,避免线程数过多增大切换开销。
高可用:在突发状态下能减少系统停工时间,例如使用集群。
同步和异步的概念:
同步指一组操作在一个序列中按照顺序依次执行,是有顺序的,异步则不保证这一组操作直接的前后顺序。通常异步就是从主线程发射一个子线程来完成任务(IO等)。但异步创建的子线程会与主线程失去联系,无法同步,如果子线程结束后需要处理一些操作就无法合并到主线程中,因此需要一些机制来保证能够控制子线程的最终结果,例如回调函数(类似观察者模式和模板模式)。
在一些情况下单线程也可以进行异步操作,这里引用一段话:
回调并行在nodejs、python async、erlang中之所以可以并行,是因为解释器/运行时做了相应的工作;回调实现的并行是通过代码控制的并行;多线程、多进程的并行是OS+CPU调度的并行,不需要加额外的东西也能表现出并行化;回调、协程之类的并行,是代码管理的伪并行化,是需要手动切换的(至少指明可以切换的地方),你若不给机会,它就切不动。
java线程状态:
java线程有6中状态:
new:线程被创建,无权限无时间片
runnable:该状态有两个子状态,ready和running。running执行态,有权限有时间片,被挂起或调用 Thread.yield后回到就绪态。就绪态,进入待执行队列,有权限无时间片,被调度器选中后进入执行态
timewaiting:隔一段时间自动唤醒
waiting:等待被唤醒
blocked:被阻塞,等待锁
ready到running中间还有三种状态如何切换:
ready -> timewaiting -> running 等待制定时间时进入timewaiting态 sleep, wait(time) join(time) locksupport.parkutil等
ready -> waiting -> running 等待时进入waiting态,o.wait t.join Locksupport.park等方法
ready -> blocked -> running 等待进入同步代码块,获取锁(synchronize)时进入阻塞态
running -> ready调用Thread.yield
测试:
//打印new,runnable,terminated状态
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getState());
for(int i = 0; i < 3; i++){
SleepHelper.sleepSeconds(1);
}
})
System.out.println(t1.getState());
t1.start()
t1.join()
System.out.println(t1.getState());
//打印waiting和timewaiting状态
Thread t2 = new Thread(()->{
try{
LockSupport.park();
System.out.println("t2 go on");
TimeUnit.SECONDS.sleep(5);
}catch(InterruptedException e){
e.printStackTrace();
}
})
t2.start();
TimeUnit.SECONDS.sleep(1);
System.out.println(t2.getState());
LockSupport.unpark(t2);
TimeUnit.SECONDS.sleep(1);
System.out.println(t2.getState());
//打印出阻塞状态
final Object o = new Object();
Thread t3 = new Thread(()->{
synchronized (o) {
System.out.println("加锁")
}
})
new Thread(() -> {
synchronized (o) {
SleepHeler.sleepSeconds(5);
}
}).start()
SleepHelper.sleepSeconds(1);
t3.start();
SleepHelper.sleepSeconds(1);
System.out.println(t3.getState());
线程状态在Lock和Synchronize的区别
lock使用的是可重入锁,用的是JUC的锁,JUC的锁使用CAS来实现。而cas实现是一种忙等待,即进入waiting状态而不是blocked状态。记住,只有synchronized才会进入blocked状态。其他情况下不会进入blocked状态。因为synchronize是经过操作系统的调度的,经过操作系统调度才会出现synchronized状态
//使用lock阻塞线程并查看其状态:
final Lock lock = new ReentrantLock();
Thread t4 = new Thread(()->{
lock.lock();
System.out.println("获取锁");
lock.unlock();
})
new Thread(()->{
lock.lock();
SleepHelper.sleepSeconds(5);
lock.unlock();
}).start();
SleepHelper.sleepSeconds(1);
t4.start();
SleepHelper.sleepSeconds(1);
System.out.println(t4.getState()); //t4是waiting状态
//使用park阻塞线程并查看其状态
Thread t5 = new Thread(()->{
LockSupport.park();
});
t5.start();
SleepHelper.SECONDS(1);
System.out.println(t5.getState());
LockSupport.unpark(); //t5也是waiting状态
线程打断interrupt
和线程打断有关的有三个方法:
interrupt(): 打断某个线程(实际上是去标志位把中断标志位设置为true,至于应该做什么对应处理由线程自己本身决定)
isinterrupted(): 查询某线程是否被打断过(查询标志位)
static interrupted(): 查询当前线程是否被打断过,并重置打断标志
public static void maint(String[] args){
Thread t = new Thread(()->{
for(;;){
if(Thread.interrupted()){
System.out.println();
System.out.println(Thread.interrupted());
}
}
})
t.start();
SleepHelper.sleepSeconds(1);
t.interrupt();
}
当线程处于sleep,join,或者wait状态的时候,如果给他们设置标志位(调用interrupted),他们会抛出异常。有时在服务器程序的循环中,如果有sleep方法,如果有wait方法,等等,要想用interrupt方法让他停止,必须catch他抛出的异常并作出正常的响应,至于是退出还是忽略继续执行,这个灵活度交给程序员处理。
public static void main(String[] args){
Thread t = new Thread(){
try{
Thread.sleep(10000);
}catch(InterruptedException e){
System.out.println();
System.out.println(Thread.currented().isinterrupted()); //打印为false,因为sleep的时候被打断线程会帮你把标记为重置。
}}
}
t.start();
SleepHelper.SECONDS(5);
t.interrupt(); //重置标记位位true
}
注:锁竞争的过程(synchronized和lock)不会被interrupt干扰,因为interrupt只是设置一个标记位而已。
但使用reentrantlock可以在等待锁的时候被打断
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
lock.lock();
try{
SleepHelper.SECONDS(10);
}finally{
lock.unlock();
}
System.out.println("t1 end");
});
t1.start();
SleepHelper.sleepSeconds(1);
Thread t2 = new Thread(()->}{
System.out.println("t2 start!");
try{
lock.lockInterruptibly(); //使用该方式加锁,在锁竞争等待的时候可以被打断。
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
System.out.println("t2 end");
});
t2.start();
SleepHelper.sleepSeconds(1);
t2.interrupt();
}
BIO,NIO
IO当前已成为系统性能提升的瓶颈,java的网络通信模型包括NIO和BIO,BIO是同步阻塞IO,假设当前有10个socket连接,则系统创建10个线程一一对应为其服务,每个线程都是阻塞的,在服务完自己当前的连接之前不会被释放,由于系统最大线程数有限因此很容易遇到性能瓶颈。NIO是同步非阻塞模型,只使用单线程,但性能却更高,引入了selector,selector是一个大管家,负责接收当前一个循环内到达的所有请求,当一个请求到达时,其并不会阻塞线程,而是将其交给selector,selector不断的遍历所有的请求,当发现一个请求的socket建立完成时,通知线程对其进行处理,处理接收后返回客户端。可以看出从请求到达到socket建立完成的时间内,NIO并不会阻塞线程,这样线程就可以利用这段时间处理其他已经建立好的socket,从而提升处理连接的能力。
Java并发编程模型 : https://blog.csdn.net/weixin_48892608/article/details/107188042
netty简单的client-server例子
客户端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.GlobalEventExecutor;
public class server {
// 每当一个client连接到服务器的时候,就把他报寸到通道组里面,发送时操作通道组即可实现群发
public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);// 用一个单例默认线程处理通道组上的事件
public static void main(String[] args) {
EventLoopGroup bossgroup = new NioEventLoopGroup();
EventLoopGroup workgroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
ChannelFuture f = b.group(bossgroup, workgroup)
// bossgroup是用来接收,并建立连接的线程池,workgroup是用来具体处理连接建立后,具体请求内容的线程池
.channel(NioServerSocketChannel.class)
// 连接建立后执行下面
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println(ch);
ChannelPipeline pl = ch.pipeline();
pl.addLast(new ServerChildHandler());
}
})
.bind(8888)
// 等待启动
.sync();
System.out.println("server started");
// 等待关闭着对他进行关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workgroup.shutdownGracefully();
bossgroup.shutdownGracefully();
}
}
}
class ServerChildHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
server.clients.add(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception{
ByteBuf buf = null;
try {
buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), bytes);
System.out.println(new String(bytes));
// 写回给客户端
// 不需要执行finally中的release,该方法会自动释放
// ctx.writeAndFlush(msg);
// 写回给所有客户端
// 不需要执行finally中的release,该方法会自动释放
server.clients.writeAndFlush(msg);
}finally {
// if(buf!=null) ReferenceCountUtil.release(buf);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
服务端
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledDirectByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.ReferenceCountUtil;
public class client {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
try {
ChannelFuture f = b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
System.out.println(ch);
ch.pipeline()
// .addLast(new MsgEncoder())
// .addLast(new MsgDecoder())
.addLast(new ClientHandler());
}
})
.connect("localhost", 8888);
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(!future.isSuccess()){
System.out.println("not connected");
}else {
System.out.println("connected");
}
}
});
// 等待连接建立
f.sync();
System.out.println("...");
// 等待关闭着把它关闭
f.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// channel第一次连上可用,写出一个字符串Direct Memory
ByteBuf buf = Unpooled.copiedBuffer("hello".getBytes());
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 处理server给他发回去的数据
ByteBuf buf = null;
try {
buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), bytes);
System.out.println(new String(bytes));
}finally {
if(buf!=null) ReferenceCountUtil.release(buf);
}
}
}
gradle中引入依赖:
implementation group: 'io.netty', name: 'netty-all', version: '4.1.9.Final'
计算最佳线程数
最佳线程数 = cpu个数 x cpu期望利用率 x (1 + w/c)
其中w为线程等待时间,c为线程计算时间。
java的性能工具:Jprofiler:
使用性能分析工具进行分析,确定多长时间做计算,多长时间做网络IO,系统的瓶颈在哪里等等。
进行远程的性能分析可以使用阿里的Arthas:
创建线程的方法
方法一:创建线程类继承Thread,重写run方法,调用该类的start进行启动
static class myThread extends Thread{
@Override
public void run(){};
}
public static void main(String[] args){
new myThread().start();
}
方法二:实现Runnable接口。重写run方法,把这个runnble类扔给一个Thread并start
static class myRun implements Runnable{
@Override
public void run(){};
}
public static void main(String[] args){
new Thread(new myRun()).start();
}
方法三:使用lambda表达式:
public static void main(){
new Thread(() -> {System.out.println("this is a thread")}).start()
}
面试题:使用继承thread的方式和实现runnable接口的方式哪个更合适?
答:使用runnable接口的方式更合适。因为实现了runnable接口还可以继承其他类,但继承了thread就不能继承其他类了。所以实现runnable接口的方式更灵活。
方法四:用线程池启动
线程池中是已经被创建好的许多线程,在需要使用的时候直接取出来使用。
ExecutorService services = Executors.newCachedThreadPool();
service.execute(()->{
System.out.println("nihao") //用lambda表达式传入待执行的方法
})
service.shutdown();
线程池除了接收实现Runnable接口并重写run方法的类,还可以接收实现Callable接口的类,Callable接口和Runnable接口相比优点在于能用泛型指定返回类型。
方法五:线程池+callable+future实现获取线程返回值
static class myCall implements Callable<String>{ //通过泛型执行返回类型为String
@Override
public String call(){
System.out.println("a callable interface");
return "success";
}
}
public static void main(String[] args){
ExecutorService service = Executors.newCachedThreadPool();
Future<String> f = service.submit(new mycall()); //submit除了接收Callable类型之外还可以接收Runnable类型.
//使用future接收返回值。future是异步的,程序不会阻塞在这里而是继续执行,callable执行结束之后会把结果存入f中。
String s = f.get(); //获取返回值,这个方法是阻塞的,等到callable执行完并拿到返回值,才会继续执行
}
使用FutureTask来生产一个会产生返回值的方法。
FutureTask实现了RunnableFuture<>接口,RunnableFuture<>方法继承了Runnable接口和Future<>接口。
因此FutureTask既能具备Runnable可以扔给线程执行的特点,也能自己获取执行结束的返回值。
方法六:thread+futuretask获取线程的返回值
FutureTask<String> task = new FutureTask<>(new Mycall());
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
以上方法本质都是new一个thread对象,调用他的start方法。
优雅的结束线程
让线程自然而然的结束时最优雅的,但这种方式现实中很难做到,例如不间断运行的服务器,中间出问题了需要中断服务器,但此时很多人正登录在上面,保存了很多数据和状态信息,不能强行停止。不能让他执行完了再停止,因为服务器线程是死循环,那么如何优雅的结束线程,并且不丢失中间状态信息?
例如:客户端上传一个大文件,或进行费时的计算,怎么才能结束这个线程?
方法一:stop方法(已被弃用)
t.stop();
不建议使用,因为可能产生数据不一致问题。如果当前线程持有一把锁,得到锁后操作某个事物,stop方法会释放所有的锁并且不会善后,可能导致事务被中断从而产生数据不一致。
suspend和resume方法也被弃用了,原因同上。
方法二:volatile
public class aa{
private static volatile boolean running = true;
public static void main(String[] args){
Thread t = new Thread(() -> {
long i = 0L;
while(running){
i++;
}
System.out.println("end and i = " + i);
});
}
t.start();
SleepHelper.sleepSeconds(1);
running = false;
}
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。 这里使用volatile修饰running的原因是因为volatile可以保证线程可见性。
不适合的场景:
修改标志位的时候线程正在被阻塞,没办法循环回去。
打断时间不精确,比如阻塞一个容器,计划容量达到5时结束生产者。但volatile同步线程标志为时间控制的不精确,可能生产者还会生产一段时间。
方法三:interrupt事件标志位
public class bb{
public static void main(String[] args){
Thread t = new Thread(() -> {
while(!Thread.isiterrupted()){
//sleep wait
}
System.out.println("t1 end");
});
t.start();
SleepHelper.sleepSeconds(3);
t.interrupt();
}
}
如果说不依赖于中间的,精确地次数或者时间,interrupt或者volatile都好使
总结:想精确控制在某个点,或者循环多少次,或者到某个位置时停止,则必须业务线程和外部线程的配合。这时候需要用到锁进行精确控制。
并发编程的三大特性
有序性 visibility
有序性 ordering
原子性 atomicity
可见性:一个线程修改了一个被其他线程使用的值,使用这个值的其他线程是看不见这个修改的。是线程本地缓存和主存保持数据一致性的一种机制。
jvm运行时刻内存的分配,其中有一个内存区域是jvm虚拟机的栈,每一个线程运行的时候都有一个线程栈,线程栈保存了线程运行时候的上下信息,当线程访问某一个对象的值的时候,首先通过对象引用找到对应在堆内存变量的值,然后把堆内存变量的具体指load到线程的本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系。而是直接修改副本的值,在修改完之后某一时刻,自动把线程变量副本的值写回对象在堆内存中变量,这样在堆中的对象的值就产生变化了。所以在线程运行时如果修改堆内存变量的值,线程中的副本是无法被同步的,
使用volatile保持可见性:如果想让线程中的副本每次都和堆中变量的值保持一致,就需要用volatile修饰变量。强制他每次都去主存中取值。并且其他线程回写这个值的时候同步更新主存中的值。
某些语句可以触发内存缓存同步刷新:synchronized
看下面的例子:
public class aa {
private static boolean running = true;
public static void m(){
System.out.println("start");
while(running){
System.out.println("hello"); //该语句触发了线程栈内存和主存之间的变量同步
}
System.out.println("end"); //这句话会被打印
}
public static void main(String[] args){
Thread t = new Thread(aa::m, "t1");
t.start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
System.out.println(“hello”); 实现机制中用到了synchonized关键字,会使得栈内存和主存之间进行变量同步。
volatile修饰引用类型不能使引用类型中的成员变得线程可见:
例:
public class aa {
private static class A{
boolean running = true;
void m(){
System.out.println("start");
while(running){};
System.out.println("end");
}
}
private volatile static A a = new A(); //给a加volatile只能保证a这个引用线程可见,就是说如果a被赋予一块其他内存区域时,其他线程可以看到,但a中的属性并不会也变得线程可见。
public static void main(String[] args){
new Thread(a::m,"t1").start();
SleepHelper.SleepSeconds(1);
a.running = false; //无效
}
}
有序性
Thread one = new Thread(new Runnable(){
public void run(){
a = 1;
x = b;
latch.countDown();
}
});
Thread other = new Thread(new Runnable(){
public void run(){
b = 1;
y = a;
latch.countDown();
}
});
one.start();
other.start();
上述代码可能出现x和y均为零的的结果,因为可能会产生指令重排,每条代码会被翻译成若干汇编代码,这些代码执行的顺序有可能被重排,将x=b放到a=1前面,将y=a放到b=1前面。产生乱序。那么为什么会有乱序?
答:为了提高效率,只有前后两条指令没有依赖关系(不影响线程的最终一致性)才能重排指令。
但java中的指令重排并不是两条指令间没有影响就可以重排。
this的溢出问题也可以用指令重排解释,先看对象的创建过程:
对象创建的汇编码可以拆解成三步:
1.申请内存,成员变量赋默认值,int类型就是赋零,这是半初始化状态
2.调用构造方法,成员变量设为初始值,初始化完成
3.astore建立关联,和我们局部变量的引用建立关联
再看下面代码,可能出现构造函数中的线程在打印局部变量num的时候,此时对象为半初始化状态,打印的结果为零。
public class ThisEscape{
private int num = 8;
public void ThisEscape(){
new Thread(()->{
System.out.println("hi");
}).start();
}
}
public static void main(String[] args){
new ThisEscape();
System.in.read();
}
解决方法是把线程的启动提取出来,放到类的其他函数中,让对象构造完成之后再启动线程。
原子性
看一个例子
public static plustest{
private static long n = 0L;
public static void main(String[] args) throws Exception{
Thread[] threads = new Thread[100];
for(int i = 0; i < 100; i++){
threads[i] = new Thread(()->{
for(int j = 0; j < 1000; j++){n++;}
});
latch.countDown();
}
for(Thread t: threads){
t.start();
}
latch.await();
System.out.println(n);
}
}
应该输出100,000但是并没有,因为n++(不是原子性,可以被其他线程打断)可以看成三步操作,从内存取值到cpu,进行++操作,从cpu写回内存。这三条语句还可能被进一步翻译成本地的汇编语言,intel的amd的,所以更加有可能被其他线程打断。同时多个线程修改n导致出现数据一致性问题。
如何保证原子性,避免数据不一致(并发之下出现不期望出现的结果)?线程同步(把线程执行的顺序安排好)
加锁synchronized,另外synchronized还可以保证线程可见性,在synchronzed代码块执行完毕之后一定会和主内存做同步。
上述代码进行修改,对plustest类进行加锁:synchronized被当做一个整体不可被其他线程打断
for(int j = 0; j < 1000; j++){
synchronized(plustest.class){
n++;
}
}
上锁的本质就是把并发编程序列化。把原来的并发执行操作变成了序列化操作。
首先了解管程的概念:(monitor)通俗的说就是我们要上的那把锁
critical section:临界区,临界区的代码同一时刻只允许一个线程执行,如果临界区执行时间长,语句多,叫做锁的粒度比较粗,反之就是粒度比较细。
保持原子性的具体操作(atomicity):
1.悲观的认为这个操作会被别的线程打断(synchronized),悲观锁
2.乐观的认为这个操作不会被别的线程打断cas,乐观锁 ,自旋锁,无锁(cas = compared and set/swap/exchange)
CAS:
用cas对一个变量加锁,读取该变量,记录修改前的值,并修改,在写回的时候看看这个变量有没有被其他线程修改过,如果没有,则直接写回,如果已被其他线程修改,则放弃本次修改,重新读取最新的值再次修改,查看这个过程中有没有被其他线程修改,若没有则写回,否则重复上述过程。直到修改成功。
CAS的问题:ABA(次零非彼零)
虽然在写回的时候发现变量并没有被改变,但其实已经被其他多个线程修改过了,只不过有被改回了原来那个值。大多数情况下没有问题,但有些场景下会有问题,例如该变量是个引用变量,引用变量里的属性被改了但引用本身没有被改。
解决方法:给变量加版本。(时间戳或者布尔类型的版本标记)
CAS本身必须保证原子性:因为在写回变量的时候,判断变量有没有被修改过,如果没有被修改过,在正准备写回的这个节骨眼上被其他线程打断并更新了变量值,这时候你再写回就会导致数据不一致。
举例:AtomicInteger使用的就是乐观锁,而不是synchronized。利用该类型替代integer类型,他自带了increment方法可以保证自增操作是原子性的。底层用的是incrementAndget -> unsafe,其最底层使用的unsafe是用c++实现的。最终发现调用的cpu原语是cmpxchg,发现cpu本身就有一条指令支持CAS。如果是多核的cpu就是lock cmpxchg,防止在回写的过程中被其他线程打断。
可以发现cas的底层实现还是用到了悲观锁。
乐观锁和悲观锁消耗的系统资源谁多的问题(为什么有了自旋锁还需要重量级锁?):
悲观锁采用队列实现,后续参加所竞争的线程都排在队列(waitset)里由操作系统调度。乐观锁采用循环的方式实现,所有线程不断询问持有锁的线程,此时线程是激活的,所以消耗系统资源较多。一般来说等待时间长且参与竞争的线程多的临界区用悲观锁,等待时间短且线程少的用乐观锁,实战中一般用synchronized即可,如果有更细致的要求就需要用两种方式分别实现并进行压测。
深入理解synchronized
了解synchronized锁升级过程,需要了解用户态和内核态,为了保障操作系统健壮性,操作系统把指令分为不同的级别,有些指令用户无权访问,想使用只能通过操作系统。内核态可能访问所有指令,用户态只能访问用户态指令。
intel支持四种级别的指令分布,ring0级到ring3级,linux内核工作在ring0级,有权限访问所有指令,用户空间的程序工作在ring3级,有些指令是不能访问的。
在jdk早期,synchronized叫重量级锁,因为JVM也仅仅是工作在用户态,而锁这个资源需要经过操作系统kernal申请才能获取,经过一个用户态到内核态的系统调用,著名的0x80,再从内核态返回到用户态,经过一个中断的调用。现在synchronized做了一些优化,在上锁的时候不需要向操作系统申请,在用户空间就可以解决问题。
无锁状态的对象头
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000001 00000000 00000000 00000000) (5)
什么是偏向锁?
以stringbuffer为例,虽然他里面是加锁的,但很多时候我们并不会在多线程的环境下使用stringbuffer,大部分情况都是在单线程的情况下,而他又调synchronized锁,既然大多情况下我们只有单线程,就没必要每次都向操作系统申请锁,只需要哪个线程先来,我偏向它即可(把当前线程指针扔进markwrod标记位)。在这种情况下没有必要设计竞争机制。
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
什么是轻量级锁(自旋锁)?
两个线程竞争一把锁,撤销原来的偏向锁,两个线程谁能先把自己的LockerRecord(锁记录)指针贴到对象的markword标记位中,谁就能竞争成功。另一个线程此时进行CAS自旋,继续竞争锁,他并不去操作系统那里申请重量级锁,而只是在用户空间自旋。
重量锁的升级():
向操作系统申请资源,linux mutex,cpu从3级到0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
synchronized锁升级:通过修改对象的markword标记位实现
1.当我们new一个普通对象的时候,一旦被加synchronized关键字,它会升级为偏向锁。-> 偏向锁。指的是把markword的线程ID改为自己的线程ID的过程。
2.此时如果竞争激烈,会变为轻量级锁。俗称自旋锁(CAS)-> 轻量级锁。线程在自己的线程栈生成LockRecord,用cas操作将markword设置为指向自己的线程的指针,设置成功者得到锁。
3.竞争再加剧,耗时过长,wait等,自旋锁会变为重量级锁 -> 重量级锁。具体地,有线程超过10次自旋(次数可通过-XX:PreBlockSpin设置),或者自旋线程数超过cpu的一半,1.6之后加入自适应自旋Adaptive Self Spining,JVM自己控制
其中,偏向锁和轻量级锁工作在用户空间中,不需要向操作系统打交道就可以完成申请。
4.普通对象在确定有多线程竞争的情况下可以不打开偏向锁(因为撤销偏向锁要消耗资源)直接使用自旋锁
5.new一个对象出来如果直接通过 XX:BiasedLockingStartupDelay = 0 开启偏向锁,此时为匿名偏向锁,因为对象的markword里不存在线程指针。
6.偏向锁可以直接升级为重量级锁(wait方法)
锁的可重入:
当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
synchronized是可重入锁,加偏向锁的时候会把markword的内容替换成线程指针。线程指针指向上一个markword的内容,这个内容被存在线程栈里。偏向锁每重入一次就在生成一个LR,加入线程栈。
重入的次数必须记录,因为要解锁几次必须的对应
偏向锁 自旋锁 -> 线程栈 -> LR + 1
重量级锁 -> ?ObjectMonitor字段上
偏向锁是否一定比自旋锁的效率高?
不一定。在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销(要耗资源),这时候直接使用自旋锁。例如:jvm的启动过程,会有很多线程竞争(明确),所以默认情况启动时b不打开偏向锁,过一段时间再打开,可以通过-XX:BiasedLockingStartupDelay = 0 设置启动jvm之后等待几秒钟开启偏向锁
认识缓存
cpu读取缓存的的过程如下:
去寄存器register中查找,没有 -> 去l1缓存查找,没有 -> 去l2缓存查找,没有 -> 去l3缓存查找,没有 -> 去主存中查找 -> 向l3中拷贝一份 -> 向l2中拷贝一份 -> 向l1中拷贝一份 -> 传入寄存器。
l1和l2是cpu每个核都有的,l3是一颗cpu里有一个,多个核共享。
什么是缓存行?
首先理解局部性原理,当我们读到某个数据时,相邻的数据一般也会很快被读到,这是空间局部性原理,所以在读取的时候把周边的部分也一起读进去,按块读取,而不是用到哪个变量只读这个变量。发挥总线cpu针脚等一次性读取更多数据的能力。时间局部性原理就是指当我读取某个指令进行执行的时候,很快我也会用到他相邻的指令,所以在读取指令的时候也是一次性读取多条指令到缓存或者内存中。
那么这一块数据到底有多大呢。这读取的一块数据就是所说的缓存行cache line,大小是64kb(太大局部性空间效率高,读取时间慢,太小局部性空间效率低,读取快)。
通过程序认识缓存一致性:
public class aa{
public static long count = 10_0000_0000L;
private static class T{
//private long p1,p2,p3,p4,p5,p6,p7;
public long x = 0L;
//private long p9,p10,p11,p12,p13,p14,p15;
}
public static T[] arr = new T[2];
static{
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args){
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(()->{
for(long i = 0; i < count; i++){
arr[0].x = i;
}
latch.countDown();
})
Thread t2 = new Thread(()->{
for(long i = 0; i < count; i++){
arr[1].x = i;
}
latch.countDown();
})
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start) / 100_10000);
}
}
long类型占8个字节,一个缓存行占64字节,8个long可填满一个缓存。假设上面代码中注释了p1到p15,那么数组arr中只有两个元素,这两个元素大概率在同一个cache line中,t1和t2两个线程在修改这个数组的时候需要把这两个元素都load到自己的缓存中,t1改第一个,t2改第二个,但他们修改完自己修改的变量后需要进行同步,这里用到了缓存一致性协议进行同步,来通知另一个cpu去取最新的值。因为这种机制的存在,当我们用多线程修改的不同变量位于同一缓存行的时候,反而会互相干扰。这个时候耗费的时间很大。
打开p1到p15的注释,可以发现执行效率提升了,因为这时候数组的两个元素,必然不再位于同一个缓存行中,因此效率大大提升。例如MQ框架disrupter就使用了七个line进行填充。
注意:volatile和缓存行之间没有关系。
为了保证数据和数据之间不在同一个缓存行,jdk1.8提供了一个Contended注解(只有1.8能用),上述代码可修改成如下:
private static class T{
@Contended
public long x = 0L;
}
然后在jvm运行参数中添加下面参数:-XX:-RestrictContended 即可使注解生效
一道美团的面试题
请解释一下对象的创建过程? (半初始化)
加问DCL(例如单例中的双重检查)与volatile问题? (指令重排)
对象在内存中的存储布局? (对象与数组的存储不同)
对象头具体包括什么? ( markword klas spointer)
synchronized锁信息
对象怎么定位? (直接间接)
对象怎么分配? (栈上-线程本地- Eden-0ld )
Object 0 = new Object( )在内存中占用多少字节?
1.java和c++初始化对象时,一个给新变量初值设为0,一个不管,因此可能读出别的程序的值,所以不安全
2.线代cpu指令有乱序执行机制,导致看似有顺序的指令实则无序
3.单例的双重检查,到底需不需要volatile? 由于cpu的指令重拍,必须要加
4.加锁可以保证:可见性(被加锁的内存不可见),原子性,但无法保证 有序性。 加锁和不加锁的代码可以互相看见中间状态。
5.对象在内存中的存储布局:包含四部分:
1.对象头:markword 8byte + classpointer类型指针 4byte (属于哪个class,比如Person.class)
3.instancedata实例数据
4.对齐padding,补到8的倍数个字节(补到能被8整除个字节)
其中markword包含三部分:锁信息(synchronized),GC垃圾回收信息 hashcode,(identityhashcode独一无二的不是重写的那个)
T t = new T();
System.out . println(Class Layout. parseInstance(t) . toPrintable( ) :
用哪个工具查看对象布局?
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
6.java中一个引用占四个字节,例如一个string name这个name引用为四个字节,所以他在他所在的对象中仅占四个字节。布尔引用占个字节,内部补齐到四个字节
8.32/64位的含义:指针宽度,存地址的变量有多宽。java的指针压缩:大于32g不压缩,小于32g压缩指针。
正常情况下32位系统的指针长度是4个字节,寻址只能寻到4g内存,但可以通过把8bit的内存映射到一个指针位上的方式,使得4字节的指针寻址范围增加到32g。
CPU主要组成
CPU的主要组成:
指令计数器PC:
作用:保存下一跳指令的地址,CPU在运行的时候会根据指令寄存器中保存的地址从内存中获取数据,获取完后回保存到CPU的寄存器中。
寄存器 Registers:
作用:用来保存从内存中读取过来的数据
运算单元ALU:
作用:根据根据寄存器中保存的数据做运算,算完后再写入到内存中
高速缓存 Cache:
作用:用来缓存内存中的数据,避免直接从内存中获取,提升CPU的运算周期效率。
多线程(二)线程&锁
目前后端开发对能力的要求体现在两个方向:
上天:解决问题的技能,高并发,缓存,大流量,大数据量
入地:面试,JVM,OS,算法,线程,IO
public class test{
private static class T1 extends Thread{
@Override
public void run(){
for(int i = 0; i < 10 ;i++){
try{
TimeUnit.MICROSECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(1);
}}
}
public static void main(String[] args){
new T1().run(); //这样调用还是单线程
new T1().start(); //这样才是多线程
for(int i=0; i< 10;i++){
try{
TimeUnit.MICROSECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(2);
}
}
}
启动线程三种方式:
1.继承Thread
2.实现Runnable
3.Executors.newCachedThread 线程池
线程等待类型:
Thread.sleep(); //主动睡若干毫秒
Thread.yield(); //谦让地退出,让出CPU,返回就绪态
t1,t2两个线程,如果t1调用t2的join()方法,就是等待t2运行结束
线程被挂起,指的是操作系统为一个线程分配的时间片到期,于是将其挂起并将时间片分配给下一个线程。
synchronized锁的是对象不是代码:不要用String Integer long等基础数据类型
public class T{
private int count = 10;
public synchronized void m(){//等价于synchronized(this) 锁定当前对象
}
public synchronized static void mm(){//这里等同于synchronized(T.class),锁的是class对象
}
}
synchronized:保证原子性,可见性
volatile:保证可见性,有序性
**一个需要注意的点:**如果线程运行过程中产生了异常,这个异常会导致线程释放它拥有的锁,此时别的锁竞争的线程就可以拿到这把锁,如果不想让线程释放锁就必须catch产生异常的代码。
多线程(三)
volatile不能保证原子性的例子:
public class T{
volatile int count = 0;
void m(){for(int i=0; i < 10000;i++) count++;}
public static void main(String[] args){
T t = new T();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i< 10; i++){
threads.add(new Thread(t::m, "thread" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try{
o.join();
}catch(InterruptedException e){
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
注意一个细节,如果锁的对象发生改变,会导致本来应该等待的,竞争这把锁的线程得到执行的机会。这会导致一些问题,所以多个线程竞争的那个加锁对象应该加final关键字
final Object o = new Object();
AtomicInteger的用法:可以达到和加synchronized一样的效果(所有的Atomic开头的类,内部都是通过unsafe类里面的compareandset和compareandswap这一类的原子cas操作实现的,unsafe基本等同于c和c++中的指针)
public class AtomicIntegerTest{
AtomicInteger count = new AtomicInteger(0);
public void m(){
for (int i =0;i< 10000l ;i++){
count.incrementAndget();
}
}
public static void main(String [] args){
AtomicIntegerTest at = new AtomicIntegerTest();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i< 10;i++){
threads.add(new Thread(at::m, "thread-"+i);
}
threads.forEach((o)->start());
threads.forEach((o)->{
try{
o.join();
}catch(InterruptedException e){
e.printStackTrace();
}
});
}
}
多线程(四)
Atomic里面的任何操作都是用cas来实现,
public class a{
static long count2 = 0L;
static AtomicLong count1 = new AtomicLong(0L);
static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for(int i =0; i< threads.length; i++){
threads[i] = new Thread(()->{
for(int j = 0; j<100000;j++) count1.incrementAndGet();
});
}
long start = System.currentTimeMillis();
for(Thread t : threads) t.start();
for(Thread t : threads) t.join();
long end = System.currentTimeMillis();
System.out.println("AtomicLong : " + count1.get() + "time : " + (end - start));
Object lockk = new Object();
for(int i = 0; i< threads.length; i++){
threads[i] = new Thread(() -> {
for(int j = 0; j< 100000; j++){
synchronized(lockk){
count2++;
}
}
});
}
start = System.currentTimeMillis();
for(Thread t : threads) t.start();
for(Thread t : threads) t.join();
end = System.currentTimeMillis();
System.out.println("synchronized : " + count2 + "time : " + (end - start));
for(int i =0;i<threads.length;i++){
threads[i] = new Thread(()->{
for(int j=0;j<100000;j++){
count3.increment();
}
});
}
start = System.currentTimeMillis();
for(Thread t : threads) t.start();
for(Thread t: threads) t.join();
end = System.currentTimeMillis();
System.out.println("longadder : "+ count3.longValue()+"time:" + (end - start));
}
}
使用AtomicLong,synchronized加锁,和LongAdder的三种方式保证count自增操作的原子性。值得注意的是LongAdder的执行效率最高,但LongAdder在线程数较少,且自增次数变少的时候LongAdder未必有优势,所以需要根据实际情况需求选择使用的类型。
为什么synchronized效率比Atomic效率底?
因为Atomic不加锁,synchronized要加锁,可能会向操作系统申请重量级锁。
为什么LongAdder要比其他两个都要快?(用了分段锁也是cas操作)
longadder会把这个要递增的值拆分到一个数组中,一开始数组中每个值都是零。例如现在有1000个线程,每250个线程操对数组中的一个元素进行递增,最后得到结果之后把所有的数加在一起,四个数一加算一个总数 ,可以看出线程数特别多的时候LongAdder是有优势的。线程数少的时候就没有优势。
基于CAS类型的新的锁
可重入锁:ReentrantLock
synchronized本身就是可重入锁,线程自己多次申请同一把锁,就是可重入。
synchronized和Reentrantlock的区别?
1.需要使用try catch包围代码块
synchronized只需要把他包围的代码块执行完,就会自动释放锁
lock.lock必须手动调用unlock,并且必须用try finally包围unlock起来才行
Lock lock = new ReentrantLock();
try{
locl.lock();
for(int i = 0; i < 10; i++){
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
2.可以使用lock.trylock,在指定的一段时间内如果得到了这把锁,就加锁,否则自己做出相应处理
public class TestRL{
Lock lock = new ReentrantLock();
void m1(){
try{
lock.lock();
for(int i = 0; i < 10; i++){
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
void m2(){
boolean locked = false;
try{
locked = lock.tryLock(5,TimeUnit.SECONDS);
System.out.println("m2 " + locked);
}catch(InterruptedException e){
e.printStackTrace();
}finally{
if(locked) lock.unlock();
}
}
public static void main(String[] args){
TestRL rl = new TestRL();
new Thread(rl::m1).start();
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
3.synchronized一旦wait了之后,必须让别人notify才能醒来,否则是醒不过来的。Reentantlock可以使用lockInterruptibly加锁,就是可以被打断的加锁。(使用 t2.interrupt() 进行打断)
4.Reentrantlock有公平锁,公平锁是指线程申请加锁需要在一个队列中排队等待,后进入的线程需要检查这个队列中有没有线程在等待,如果有则让先来的线程先获得锁,如果没有则直接参与锁竞争,谁抢到算谁的。
public class ReentrantlockTest extends Thread{
private static ReentrantLock lock = new ReentrantLock(true); //设置为true表示公平锁
public void run(){
for(int i = 0; i< 100; i++){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args){
ReentrantlockTest rl = new ReentrantlockTest();
Thread t1 = new Thread(rl);
Thread t2 = new Thread(rl);
t1.start();
t2.start();
}
}
总结:Reentrantlock vs synchronized
cas vs synchronized锁升级
trylock
lockinterruptibly
公平和非公平锁 vs 非公平锁
CountDownLatch:
门栓的概念,用来等待所有线程的结束。一个线程中就可以countdown到零然后让线程往前执行,比使用join更灵活一些
private static void usingCountDownLatch(){
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for(int i = 0; i< threads.length; i++){
threads[i] = new Thread(()->{
int result = 0;
for(int j = 0; j<10000;j++)result +=j ;
System.out.println(result);
latch.countDown();
});
}
for(int i = 0; i<threads.length; i++){
threads[i].start();
}
try{
latch.await();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("end latch");
}
private static void usingJoin(){
Thread[] threads = new Thread[100];
for(int i=0;i<threads.length;i++){
threads[i] = new Thread(()->{
int result = 0;
for(int j = 0; j<10000;j++)result +=j ;
System.out.println(result);
});
}
for(int i =0; i<threads.length; i++){
threads[i].start();
}
for(int i = 0; i < threads.length;i++){
try{
threads[i].join();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("end join");
}
public static void main(String[] args){
usingCountDownLatch();
usingJoin();
}
CyclicBarrier: 循环栅栏
等待没20个线程执行完毕,就放行这20个线程继续执行。最终打印出来5个“满人发车”
public static void main(String[] args){
// CyclicBarrier barrier = new CyclicBarrier(20);
// CyclicBarrier barrier = new CyclicBarrier(20,()->System.out.println("满人发车"));
CyclicBarrier barrier = new CyclicBarrier(20, new Runnable(){
@Override
public void run(){
System.out.println("满人,发车");
}
});
for(int i = 0; i<100;i++){
new Thread(()->{
try{
barrier.await();
}catch(InterruptedException e){
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
}).start();
}
}
使用场景:需要等待其他若干个线程执行完成之后,才放行继续执行,可用于线程同步,例如一个操作需要同时访问数据库,网络,文件,则可以使用三个线程分别进行处理然后用CyclicBarriar等待他们一起结束。和限流的不同之处在于,流量太大的时候如果直接打到数据库或者其他服务上会崩,所以允许通过的线程数最大值为设置的限流值,但CyclicBarrier会等线程数等达到阈值才集体放行。
**Phaser:**可以看做CyclicBarriar的升级版,不仅可以控制栅栏的个数还可以控制栅栏上面等待线程的的数量。
ReadWriterLock:
读写锁的概念其实就是共享锁和排他锁,使用ReentrantReadWriteLock替换ReentrantLock,可以让读线程之间不必互斥,写锁是排他的,只有读写和写写之间互斥,提高执行效率。测试二者区别:
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock rwlock = new ReentrantReadWriteLock();
static Lock readLock = rwlock.readLock();
static Lock writeLock = rwlock.writeLock();
public static void read(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
System.out.println("read over");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public static void write(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
System.out.println("write over");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public static void main(String[] args){
// Runnable rdR = ()->read(lock);
Runnable rdR = ()->read(readLock);
// Runnable wtR = ()->write(lock);
Runnable wtR = ()->write(writeLock);
for(int i = 0;i<10;i++) new Thread(rdR).start();
for(int i = 0;i<2;i++) new Thread(wtR).start();
}
ReentrantLock执行10个读线程用时10秒,ReentrantReadWriteLock执行10个读线程用时1秒。
**semaphore:**可用于限流,最多允许多少个线程同时执行
Semaphore s = new Semaphore(10);
Semaphore s = new Semaphore(10,true); //可以设置为true公平竞争
new Thread(()-{
try{
s.acquire();
...
}catch(){
e.printStackTrace();
}finally{
s.release();
}
});
**exchanger:**交换器,用于交换数据
static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args){
new Thread(()->{
String s = "T1";
try{
s = exchanger.exchange(s);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" "+s);
},"T1").start();
new Thread(()->{
String s = "T2";
try{
s = exchanger.exchange(s);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" "+s);
},"T2").start();
}
**LockSupport:**用于手动阻塞和唤醒线程:
public static void main(String[] args){
Thread t = new Thread(()->{
for(int i = 0; i<10;i++){
System.out.println(i);
if(i==5)LockSupport.park();
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
try{
TimeUnit.SECONDS.sleep(8);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("结束");
LockSupport.unpark(t);
}
几道面试题:
一个线程向list中加入元素,第二个线程观察这个list,当加入5个元素时做出一些操作,如何实现?
做法:
1.尽量不要使用volatile修饰引用类型,用volatile修饰简单类型,越简单越好。
2.使用synchronized的 wait和 notify方法(wait释放锁,同时不再参与锁竞争。notify通知所有正在wait的线程参与锁竞争)
public static void main(String[] args){
List c = new ArrayList();
final Object lock = new Object();
new Thread(()->{
synchronized(lock){
System.out.println("t2启动");
if(c.size()!=5){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("t2结束");
lock.notify();
}
},"t2").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
new Thread(()->{
System.out.println("t1启动");
synchronized(lock){
for(int i=0;i<10;i++){
c.add(new Object());
System.out.println("add" + i);
if(c.size()==5){
lock.notify();
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
},"t1").start();
}
3.使用CountDownLatch
public static void main(String[] args){
List c = new ArrayList();
CountDownLatch cl = new CountDownLatch(1);
CountDownLatch cl2 = new CountDownLatch(1);
new Thread(()->{
System.out.println("t2启动");
if(c.size()!=5){
try {
cl.await();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("t2结束");
cl2.countDown();
},"t2").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
new Thread(()->{
System.out.println("t1启动");
for(int i=0;i<10;i++){
c.add(new Object());
System.out.println("add" + i);
if(c.size()==5){
cl.countDown();
try {
cl2.await();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
},"t1").start();
}
4.使用LockSupport进行同步
static Thread t1 =null, t2 = null;
public static void main(String[] args){
List c = new ArrayList();
t2= new Thread(()->{
System.out.println("t2启动");
if(c.size()!=5){
LockSupport.park();
}
System.out.println("t2结束");
LockSupport.unpark(t1);
},"t2");
t1 = new Thread(()->{
System.out.println("t1启动");
for(int i=0;i<10;i++){
c.add(new Object());
System.out.println("add" + i);
if(c.size()==5){
LockSupport.unpark(t2);
LockSupport.park();
}
}
},"t1");
t2.start();
t1.start();
}
5.使用semaphore
static Thread t1 =null, t2 = null;
public static void main(String[] args){
List c = new ArrayList();
Semaphore s = new Semaphore(1);
t2= new Thread(()->{
System.out.println("t2启动");
try {
s.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2结束");
s.release();
},"t2");
t1 = new Thread(()->{
try {
s.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1启动");
for(int i=0;i<10;i++){
c.add(new Object());
System.out.println("add" + i);
if(c.size()==5){
s.release();
t2.start();
try {
t2.join();
s.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1");
t1.start();
}
一个固定容量同步容器拥有put和get方法,以及getcount方法,能够支持2个生产者线程和10个消费者线程阻塞调用,使用wait和notify以及notifyall来实现
public class mycontainer<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int max = 10;
private int count = 0;
public synchronized void put(T t) { //可以用cas替代synchronized来实现
while (lists.size() == max) { //想想为什么用while不用if?反复确认队列是否是满的,生产者线程有可能叫醒其他生产者
try {
this.wait(); //this对这个容器加锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll(); //通知所有消费者线程消费
}
public synchronized T get() {
while (lists.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = lists.removeFirst();
count--;
this.notifyAll();
return t;
}
public static void main(String[] args) {
mycontainer<String> c = new mycontainer<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) System.out.println(c.get());
}).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) c.put(Thread.currentThread().getName() + j);
},i+"").start();
}
}
}
上述代码可以使用ReentrantLock的Condition来实现:condition 的本质就是等待队列
public class mycontainer<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int max = 10;
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
try{
lock.lock();
while (lists.size() == max) { //想想为什么用while不用if?反复确认队列是否是满的,生产者线程有可能叫醒其他生产者
try {
producer.wait(); //当前线程进入producer队列等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
consumer.signalAll(); //叫醒consumer队列中的线程
}finally{
lock.unlock();
}
}
public T get() {
T t;
try{
lock.lock();
while (lists.size() == 0) {
try {
consumer.wait(); //当前线程进入consumer队列等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count--;
producer.signalAll(); //叫醒producer队列中的线程
}finally{
lock.unlock();
}
return t;
}
public static void main(String[] args) {
mycontainer<String> c = new mycontainer<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) System.out.println(c.get());
}).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) c.put(Thread.currentThread().getName() + j);
},i+"").start();
}
}
}
关于读源码
之前介绍了许多锁,那么他们内部是怎么实现的?只能去读源码。
要求:
1.一定的数据结构基础,里面会用到很多数据结构,小顶堆大顶堆,不了解这些看半天也看不明白
2.注重理解主要思路,不要过多注重细节
3.设计模式,例如spring,mybatis,netty源码等
原则:
1.跑不起来不读(跑起来用打断点的方式跟进,事半功倍)
2.解决问题就好(例如需要在别人代码上添加功能,没有必要从头到尾全部读完,解决问题就好)
3.一条线索到底(一个方法切入不断深入,不要每个都读)
4.无关细节略过(很多小细节例如这里为什么是n+1为什么是n+2,先不管这些,先领会整体思路)
5.