面试准备2

328 阅读57分钟

final,finally,finalize 的区别

  • final:变量、类、方法的修饰符,被 final 修饰的类不能被继承,变量或方法被 final 修饰 则不能被修改和重写。
  • finally:异常处理时提供 finally 块来执行清除操作,不管有没有异常抛出,此处代码都会 被执行。如果 try 语句块中包含 return 语句,finally 语句块是在 return 之后运行;
  • finalize:Object 类中定义的方法,若子类覆盖了 finalize()方法,在在垃圾收集器将对象 从内存中清除前,会执行该方法,确定对象是否会被回收。

序列化 Serializable 和 Parcelable 的区别

  • 序列化:将一个对象转换成可存储或可传输的状态,序列化后的对象可以在网络上传输,也 可以存储到本地,或实现跨进程传输;
  • Parcelable:与 Serializable 实现的效果相同,也是将一个对象转换成可传输的状态,但它 的实现原理是将一个完整的对象进行分解,分解后的每一部分都是 Intent 所支持的数据类 型,这样实现传递对象的功能。

区别:Serializable 在序列化时会产生大量临时变量,引起频繁 GC。Serializable 本质上使 用了反射,序列化过程慢。Parcelable 不能将数据存储在磁盘上,在外界变化时,它不能 很好的保证数据的持续性。

选择原则:若仅在内存中使用,如 activity\service 间传递对象,优先使用 Parcelable,它 性能高。若是持久化操作,优先使用 Serializable

SharedPreference的commit ,apply区别,SharedPreference是线程安全的吗?

  • apply没有返回值而commit返回boolean表明修改是否提交成功。
  • apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。

代码中可以看到读写操作时都有大量的synchronized,因此它是线程安全的。

listView和recycleView区别

ListView 和 RecyclerView 是 Android 开发中常用的两个控件,它们都可以用来展示列表数据,但在一些方面存在着区别:

  1. 布局方式:ListView 只能在垂直方向上滚动,而 RecyclerView 支持垂直和水平方向的滚动,并且可以通过设置布局管理器来实现不同的布局效果,如线性布局、网格布局和瀑布流布局等;
  2. 缓存机制:RecyclerView 具有四级缓存机制,而 ListView 只有两级缓存。这使得 RecyclerView 在处理大量数据时具有更好的性能,可以更高效地回收和复用视图;
  3. 更新数据:RecyclerView 支持局部刷新,而 ListView 通常需要刷新整个列表。局部刷新可以提高性能,尤其是在数据更新频繁的情况下;
  4. 扩展性:RecyclerView 提供了更多的扩展性,开发者可以自定义布局管理器、ViewHolder 和动画等,以满足特定的需求。而 ListView 的扩展性相对较弱;
  5. ViewHolder 复用:在 ListView 中,需要开发者手动判断 convertView 是否为空来复用 ViewHolder。而在 RecyclerView 中,ViewHolder 的复用是自动处理的,并且可以通过设置不同的标识来区分不同类型的ViewHolder;
  6. 性能优化:RecyclerView 在性能优化方面提供了更多的灵活性,例如可以设置预取数量、延迟加载等。而 ListView 的性能优化相对较为简单。

在实际开发中,选择使用 ListView 还是 RecyclerView 取决于具体的需求和场景。如果需要简单的垂直列表展示,并且对性能要求不是很高,可以选择使用 ListView。如果需要更复杂的布局、更好的性能和扩展性,则可以选择使用 RecyclerView。

kotlin中?和!! 区别

"?"加在变量名后,系统在任何情况不会报它的空指针异常。
"!!"加在变量名后,如果对象为null,那么系统一定会报异常!

retrofit用到了哪些设计模式:

1:builder设计模式

Retrofit retrofit =new Retrofit.Builder()
                    .baseUrl(server1.url("/"))
                    .build();

2: 外观模式 要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。

Retrofit的门面就是retrofit.create()

3: 动态代理


Proxy.newProxyInstance(
    service.getClassLoader(),
    new Class<?>[] {service},
    new InvocationHandler() {
      private final Platform platform = Platform.get();
      private final Object[] emptyArgs = new Object[0];

      @Override
      public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
          throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
          return method.invoke(this, args);
        }
        args = args != null ? args : emptyArgs;
        return platform.isDefaultMethod(method)
            ? platform.invokeDefaultMethod(method, service, proxy, args)
            : loadServiceMethod(method).invoke(args);
      }
    });

4: 装饰模式 装饰模式又称包装模式,即动态地给一个对象添加一些额外的职责。


static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;
    final Call<T> delegate;

    ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

    @Override public void enqueue(final Callback<T> callback) {
      Objects.requireNonNull(callback, "callback == null");

      delegate.enqueue(new Callback<T>() {...}
      }
   }

你可以将ExecutorCallbackCall当作是Wrapper,而真正去执行请求的源Source是OkHttpCall。之所以要有个Wrapper类,是希望在源Source操作时去做一些额外操作。这里的操作就是线程转换,将子线程切换到主线程上去。

enqueue()方法是异步的,也就是说,当你调用OkHttpCall的enqueue方法,回调的callback是在子线程中的,如果你希望在主线程接受回调,那需要通过Handler转换到主线程上去。ExecutorCallbackCall就是用来干这个事。当然以上是原生retrofit使用的切换线程方式。如果你用rxjava,那就不会用到这个ExecutorCallbackCall而是RxJava的Call了

5:适配器模式 适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起的两个类能够在一起工作

public interface CallAdapter<R, T> {

  /**
   * Returns an instance of {@code T} which delegates to {@code call}.
   *
   * <p>For example, given an instance for a hypothetical utility, {@code Async}, this instance
   * would return a new {@code Async<R>} which invoked {@code call} when run.
   *
   * <pre><code>
   * &#64;Override
   * public &lt;R&gt; Async&lt;R&gt; adapt(final Call&lt;R&gt; call) {
   *   return Async.create(new Callable&lt;Response&lt;R&gt;&gt;() {
   *     &#64;Override
   *     public Response&lt;R&gt; call() throws Exception {
   *       return call.execute();
   *     }
   *   });
   * }
   * </code></pre>
   */
  T adapt(Call<R> call);

6:策略模式

策略模式定义了一系列算法,并将每一个算法封装起来,而且使他们还可以互相替换。策略模式让算法独立于使用它的客户而独立变化。

- private CallAdapter<Observable<?>> getCallAdapter(Type returnType, Scheduler scheduler) { Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType); Class<?> rawObservableType = getRawType(observableType); if (rawObservableType == Response.class) { if (!(observableType instanceof ParameterizedType)) { throw new IllegalStateException("Response must be parameterized" + " as Response<Foo> or Response<? extends Foo>"); } Type responseType=getParameterUpperBound(0,(ParameterizedType)observableType); return new ResponseCallAdapter(responseType, scheduler); } if (rawObservableType == Result.class) { if (!(observableType instanceof ParameterizedType)) { throw new IllegalStateException("Result must be parameterized" + " as Result<Foo> or Result<? extends Foo>"); } Type responseType = getParameterUpperBound(0, (ParameterizedType)observableType); return new ResultCallAdapter(responseType, scheduler); } return new SimpleCallAdapter(observableType, scheduler); }


7:工厂模式

juejin.cn/post/687932…

数组扩容

  • 通过构造一个新的数组,新数组的长度是目标size,然后通过数组复制将数据转移
  • 数组拷贝:Arrays.copyOf(array,2*array.length);
  • 转化为ArrayList,ArrayList在添加数据的时候有扩容机制。

扩展函数的原理

Kotlin 的扩展函数并没有修改原有的 String 类,而是在自己的类中生成了一个静态的方法,当我们在 Kotlin 中调用扩展函数时,编译器将会调用自动生成的函数并且把当前的对象(String)传入。

www.jianshu.com/p/bab988f56…

anr原理

ANR(Application Not Responding)的监测原理本质上是消息机制,设定一个delay消息,超时未被移除则触发ANR。具体逻辑处理都在system server端,包括发送超时消息,移除超时消息,处理超时消息以及ANR弹框展示等;对于app而言,触发ANR的条件是主线程阻塞。

以Service为例,在启动Service时, realStartServiceLocked 方法中会调用 bumpServiceExecutingLocked 方法,进而调用 scheduleServiceTimeoutLocked 方法发送一个延时消息,这个延时消息就相当于一个“炸弹”。如果在延时时间内(前台服务为20秒,后台服务为200秒),Service的 onCreate 方法执行完毕并调用了 serviceDoneExecuting 方法,就会移除这个延时消息,相当于“拆除炸弹”。如果延时时间到了,延时消息还没有被移除,就会触发ANR,系统会收集相关信息并弹出ANR对话框。

除了Service,Broadcast、ContentProvider和Input也可能会触发ANR,它们的原理与Service类似,都是通过发送延时消息来检测是否超时

messagequenue的数据结构

链表

kt对比java的优势

  • 1: kotlin更安全
  • 2: 函数式支持
  • 3: 扩展函数
  • 4: 内联函数
  • 5: 集合操作语法糖
  • 6: 代码更简洁

databinding的原理

  • 1: 程序在编译的时候,xml布局的layout标签会消失,会在布局的根布局里也加了一个tag,名字是"layout/xxxx_xx",对应view的绑定的属性也变成了一个tag,以binding_开头,后面接一个数字,如android:tag="binding_3",
  • 2: DataBindingUtil.setContentView方法将xml中的各个View赋值给ViewDataBinding,完成findviewbyid的任务
  • 3:ViewDataBinding的setVariable方法建立了ViewDataBinding与VM之间的联系,也就搭建了一个可以互相通信的桥梁
  • 4: 当VM层调用notifyPropertyChanged方法时,最终在ViewDataBinding的executeBindings方法中处理逻辑

juejin.cn/post/684490…

listview缓存

ListView的渲染过程最重要的是onLayout过程,其中第一次渲染由于缓存列表中没有缓存View,所以第一次调用的getView是新生成的,后续通过滑动ListView,RecyclerBin生成了缓存列表,之后可以从缓存列表中获取View,避免了重复创建View的过程,从而提供了性能。下面用一张图来总结一下ListView的缓存原理。

image.png

juejin.cn/post/684490…

recycleView缓存

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerviewAdapter.MyViewHolder> {

    private Context context;
    private List<String> data;

    public MyRecyclerViewAdapter(Context context,List<String> data){
        this.context = context;
        this.data = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.recycler_item_my, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
        holder.name.setText(data.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("这里是点击每一行item的响应事件",""+position+item);
            }
        });

    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    public class MyViewHolder extends RecyclerView.ViewHolder{
        TextView name;

        public MyViewHolder(View itemView) {
            super(itemView);
            name = itemView.findViewById(R.id.name);
        }
    }
}
  • mAttachedScrapmChangedScrap
    一级缓存,同ListViewActionViews,在layout发生前将屏幕上面的ViewHolder保存起来,供layout中进行复用

  • mCachedViews
    二级缓存,默认大小保持在DEFAULT_CACHE_SIZE = 2,可以通过RecyclerView.setItemViewCacheSize(int)方法进行设置 mCachedViews数量如果超出限制,会根据索引将里面旧的移动到RecyclerViewPool

  • ViewCacheExtension
    三级缓存,开发者可以自定义的缓存

  • RecyclerViewPool
    四级缓存,刚才说了Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的数据会被全部重置,相当于一个新的ViewHolder,而且Cache是根据position来获取ViewHolder

作者:肖邦kaka
链接:www.jianshu.com/p/3e9aa4bda…
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

blog.yorek.xyz/android/oth… www.jianshu.com/p/3e9aa4bda…

动画

  • View Animation
  • 帧动画
  • 属性动画 ValueAnimation ObjectAnimation
  • 插值器
  • Evaluator

为什么要有单例?

一个类Class只有一个实例在,使用单例模式好处在于可以节省内存,节约资源,对于一般频繁创建和销毁对象的可以使用单例模式。

单例双重锁为啥要有两次判空?

  • 第一次判断是为了验证是否创建对象,避免多线程访问时每个线程都加锁,提升效率
  • 第二次判断是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象。

单例对像使用场景

  • 有状态的工具类对象。例如logger日志打印工具
  • 频繁访问数据库或文件的对象。
  • 频繁访问网络的对象。
  • 访问缓存的对象,mmkv和sharedPreference
  • 获取公共配置

ArrayList和LinkedList的区别、优缺点以及应用场景

区别:

  • ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。

  • 对于随机访问的getset方法查询元素,ArrayList要优于LinkedList,因为LinkedList循环链表寻找元素。

  • 对于新增和删除操作addremoveLinkedList比较高效,因为ArrayList要移动数据

优缺点:

  • ArrayListLinkedList而言,在末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。

  • ArrayList集合中添加或者删除一个元素时,当前的列表移动元素后面所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。

  • LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。

  • ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

应用场景:

ArrayList使用在查询比较多,但是插入和删除比较少的情况,而LinkedList用在查询比较少而插入删除比较多的情况

juejin.cn/post/684490…

线程通信的方式

1: Handler机制.

private void one() {
        handler=new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what){
                    case 123:
                        tv.setText(""+msg.obj);
                        break;
                }
            }
        };
        new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i=0;i<3;i++){
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Message message=new Message();
                message.what=123;
                message.obj="通过Handler机制";
                handler.sendMessage(message);
            }
        }.run();
    }

2: runOnUiThread方法

private void two(){
        new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i=0;i<3;i++){
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText("通过runOnUiThread方法");
                    }
                });
            }
        }.run();
    }

3: View.post(Runnable r)

private void three(){
        new Thread(){
            @Override
            public void run() {
                super.run();
                for (int i=0;i<3;i++){
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                tv.post(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText("通过View.post(Runnable r) 方法");
                    }
                });
            }
        }.run();
    }

4: AsyncTask

private void four(){
        new MyAsyncTask().execute("通过AsyncTask方法");
    }
private class MyAsyncTask extends AsyncTask{
        @Override
        protected Object doInBackground(Object[] objects) {
            for (int i=0;i<3;i++){
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return objects[0].toString();
        }
        @Override
        protected void onPostExecute(Object o) {
            super.onPostExecute(o);
            tv.setText(o.toString());
        }
    }

5: eventbus 6:rxjava 7:协程

cpu过高怎么优化

1:合理的使用线程 —— 线程池

  • 1.CPU密集型线程池

用来处理 CPU 类型任务,如计算,逻辑操作,UI 渲染等。例如:newFixedThreadPool、newWorkStealingPool

它的核心线程数量一般为 CPU 的核数既然最大线程数就是核心线程数,那 keepAliveTime 这个非核心线程数的存活时间就是零了。

然后是workQueue缓存队列。CPU 线程池中统一使用 LinkedBlockingDeque,这是一个可以设置容量并支持并发的队列。

  • 2: IO密集型线程池:

用来处理 IO 类型任务,如拉取网络数据,往本地磁盘、数据读写数据等。例如:newCachedThreadPool

首先是corePoolSize(核线程数)一般为0,maximumPoolSize (最大线程数)Integer.MAX_VALUE 然后是workQueue缓存队列。我们把它设置为 SynchronousQueue队列,它是一个容量为0的队列

2 养成良好的代码习惯使和使用更优的算法或数据结构

  • 使用合适的数据结构和算法

  • 避免重复计算:如果某个计算量较大的操作结果在多个地方都被使用到,可以考虑将结果缓存起来,避免重复计算

  • 使用缓存或缓存策略:对于一些需要经过复杂计算和频繁读取的数据,可以考虑使用缓存来避免重复的访问或计算

  • 减少不必要的对象创建:避免在循环或频繁执行的代码块中创建不必要的对象,尽量重用已有对象。例如:for循环中操作string类,尽量使用stringBuild

  • 多使用资源引用:当需要使用应用程序资源(例如字符串、图标等)时,使用资源引用而不是硬编码的值,这样的代码习惯不仅可以提高代码的可维护性,而且还能减少指令数。

减少 CPU 等待——锁优化和使用协程

  1. 无锁比有锁好:除了不加锁,还有线程本地存储,偏向锁等方案,都属于无锁优化。
  2. 合理细化锁的粒度:比如将 Synchronize 锁住整个方法细化成只锁住方法内可能会产生线程安全的代码块。
  3. 协程本质上是一个轻量级线程。

if else 用的太多优化?

状态模式


public interface State {
    void handle();
}
public class ConcreteStateA implements State {

    @Override
 public void handle() {
     // 对应状态A的行为
   }

}

public class ConcreteStateB implements State {

    @Override
 public void handle() {
     // 对应状态B的行为
   }

}
public class Context {
 private State state;
 public void setState(State state) {
  this.state = state;
  }
 public void request() {
   state.handle();
    }
}

策略模式+工厂方法消除if else

假设需求为,根据不同勋章类型,处理相对应的勋章服务, 首先,我们把每个条件逻辑代码块,抽象成一个公共的接口,可以得出以下代码:

//勋章接口
public interface IMedalService {
    void showMedal();
}

我们根据每个逻辑条件,定义相对应的策略实现类,可得以下代码:

//守护勋章策略实现类
public class GuardMedalServiceImpl implements IMedalService {
    @Override
    public void showMedal() {
        System.out.println("展示守护勋章");
    }
}
//嘉宾勋章策略实现类
public class GuestMedalServiceImpl implements IMedalService {
    @Override
    public void showMedal() {
        System.out.println("嘉宾勋章");
    }
}
//VIP勋章策略实现类
public class VipMedalServiceImpl implements IMedalService {
    @Override
    public void showMedal() {
        System.out.println("会员勋章");
    }
}

接下来,我们再定义策略工厂类,用来管理这些勋章实现策略类,如下:

//勋章服务工产类
public class MedalServicesFactory {

    private static final Map<String, IMedalService> map = new HashMap<>();
    static {
        map.put("guard", new GuardMedalServiceImpl());
        map.put("vip", new VipMedalServiceImpl());
        map.put("guest", new GuestMedalServiceImpl());
    }
    public static IMedalService getMedalService(String medalType) {
        return map.get(medalType);
    }
}

使用了策略+工厂模式之后,代码变得简洁多了,如下:

public class Test {
    public static void main(String[] args) {
        String medalType = "guest";
        IMedalService medalService = MedalServicesFactory.getMedalService(medalType);
        medalService.showMedal();
    }
}

职责链模式 优化前:

public void handle(request) {
    if (handlerA.canHandle(request)) {
        handlerA.handleRequest(request);
    } else if (handlerB.canHandle(request)) {
        handlerB.handleRequest(request);
    } else if (handlerC.canHandle(request)) {
        handlerC.handleRequest(request);
    }
}

优化后:

public void handle(request) {
  handlerA.handleRequest(request);
}

public abstract class Handler {
  protected Handler next;
  public abstract void handleRequest(Request request);
  public void setNext(Handler next) { this.next = next; }
}

public class HandlerA extends Handler {
  public void handleRequest(Request request) {
    if (canHandle(request)) doHandle(request);
    else if (next != null) next.handleRequest(request);
  }
}

多态性 注解 合并条件 如果多个 if-else 分支中的条件存在逻辑上的相似性,可以尝试合并条件,减少分支数量。

什么是死锁

由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

规避死锁

1: 粗锁法 粗锁法即使用粗粒度的锁来代替多个锁。“占用并等待资源”这个条件隐含的情况即:线程在持有一个锁的同时还去申请另一个锁。那么,只要采用一个粒度较粗的锁来替代原先粒度较细的锁,使得涉及的资源都只需要申请一个锁就可以获得,那么就可以避免死锁

对应上诉的哲学家就餐问题,只要将 Philosopher 拿左手边筷子和拿右手边筷子的行为统一放到同个锁内,就可以消除“占用并等待资源”和“循环等待资源”这两个条件了 锁排序法 锁排序法的思路是:对所有锁按照一定规则进行排序,所有线程在申请锁之前均按照先后顺序进行申请,以此来消除“循环等待资源”这个条件,从而来规避死锁

资源限时申请 避免死锁的另一种方法是在申请资源时设定一个超时时间,避免无限制地等待资源,从而消除“占用并等待资源”这种情况。当等待时间超出既定的限制时,释放已持有的资源(哲学家放下左手边的筷子转而去继续思考)先给其它线程使用,待后续再重新申请资源

juejin.cn/post/690045…

协程

1:概念

协程就像轻量级的线程,为什么是轻量的?因为协程是依赖于线程,一个线程中可以创建N个协程,很重要的一点就是协程挂起时不会阻塞线程,几乎是无代价的。

2: 作用

  • 1.协程可以让异步代码同步化
  • 2.协程可以降低异步程序的设计复杂度

3: 特点

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。

  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。

  • 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。

  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

这就是kotlin最有名的【非阻塞式挂起】,使用同步的方式完成异步任务,而且很简洁,这是Kotlin协程的魅力所在。

image.png

协程构建

协程需要运行在协程上下文环境中(即协程作用域,下面会讲解到),在非协程环境中launch有两种方式创建协程:GlobalScope.launch()跟CoroutineScope.launch()

  • GlobalScope.launch() 在应用范围内启动一个新协程,不会阻塞调用线程,协程的生命周期与应用程序一致
fun launchTest() {
    print("start")
    //创建一个全局作用域协程,不会阻塞当前线程,生命周期与应用程序一致
    GlobalScope.launch {
        //在这1000毫秒内该协程所处的线程不会阻塞
        //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
        delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
        print("GlobalScope.launch")
    }
    print("end")//主线程继续,而协程被延迟
}

  • runBlocking 创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T,一般在项目中不会使用,主要是为main函数和测试设计的。
fun runBloTest() {
    print("start")
    //context上下文使用默认值,阻塞当前线程,直到代码块中的逻辑完成
    runBlocking {
        //这里是协程体
        delay(1000)//挂起函数,延迟1000毫秒
        print("runBlocking")
    }
    print("end")
}

  • launch 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job对象。这是最常用的用于启动协程的方式。 1:
fun launchTest2() {
    print("start")
    //开启一个IO模式的协程,通过协程上下文创建一个CoroutineScope对象,需要一个类型为CoroutineContext的参数
    val job = CoroutineScope(Dispatchers.IO).launch {
        delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
        print("CoroutineScope.launch")
    }
    print("end")//主线程继续,而协程被延迟
}

2: 通过launch在一个协程中启动子协程,可以根据业务需求创建一个或多个子协程:

fun launchTest3() {
    print("start")
    GlobalScope.launch {
        delay(1000)
        print("CoroutineScope.launch")

        //在协程内创建子协程
        launch {
            delay(1500)//1.5秒无阻塞延迟(默认单位为毫秒)
            print("launch 子协程")
        }
    }
    print("end")
}

  • async 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。
//获取返回值
fun asyncTest1() {
    print("start")
    GlobalScope.launch {
        val deferred: Deferred<String> = async {
            //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
            delay(2000)//2秒无阻塞延迟(默认单位为毫秒)
            print("asyncOne")
            "HelloWord"//这里返回值为HelloWord
        }

        //等待async执行完成获取返回值,此处并不会阻塞线程,而是挂起,将线程的执行权交出去
        //等到async的协程体执行完毕后,会恢复协程继续往下执行
        val result = deferred.await()
        print("result == $result")
    }
    print("end")
}

Job & Deferred

Job Job 是协程的句柄。如果把门和门把手比作协程和Job之间的关系,那么协程就是这扇门,Job就是门把手。意思就是可以通过Job实现对协程的控制和管理。

public interface Job : CoroutineContext.Element {
    //活跃的,是否仍在执行
    public val isActive: Boolean

    //启动协程,如果启动了协程,则为true;如果协程已经启动或完成,则为false
    public fun start(): Boolean
    
    //取消Job,可通过传入Exception说明具体原因
    public fun cancel(cause: CancellationException? = null)
    
    //挂起协程直到此Job完成
    public suspend fun join()
    
    //取消任务并等待任务完成,结合了[cancel]和[join]的调用
    public suspend fun Job.cancelAndJoin() 

    //给Job设置一个完成通知,当Job执行完成的时候会同步执行这个函数
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
}

Thread相比,Job同样有join(),调用时会挂起(线程的join()则会阻塞线程),直到协程完成;它的cancel()可以类比Threadinterrupt(),用于取消协程;isActive则是可以类比ThreadisAlive(),用于查询协程是否仍在执行

调度器

CoroutineDispatcher调度器指定指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行

image.png

withContext

在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个withContext顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)来创建一个在IO线程池中运行的块。

这个函数会使用新指定的上下文的dispatcher,将block的执行转移到指定的线程中。它会返回结果, 可以和当前协程的父协程存在交互关系, 主要作用为了来回切换调度器

GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
    val result: User = withContext(Dispatchers.IO) {//网络请求(IO 线程)
        userApi.getUserSuspend("FollowExcellence")
    }
    tv_title.text = result.name //更新 UI(主线程)
}

协程上下文

协程上下文主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。它有很多作用,包括携带参数,拦截协程执行等等。如何运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。

//协程的持久上下文。它是[Element]实例的索引集,这个集合中的每个元素都有一个唯一的[Key]。
public interface CoroutineContext {
    //从这个上下文中返回带有给定[key]的元素或null。
    public operator fun <E : Element> get(key: Key<E>): E?

    //从[initial]值开始累加该上下文的项,并从左到右应用[operation]到当前累加器值和该上下文的每个元素。
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    //返回一个上下文,包含来自这个上下文的元素和来自其他[context]的元素。
    public operator fun plus(context: CoroutineContext): CoroutineContext

    //返回一个包含来自该上下文的元素的上下文,但不包含指定的[key]元素。
    public fun minusKey(key: Key<*>): CoroutineContext

    //[CoroutineContext]元素的键。[E]是带有这个键的元素类型。
    public interface Key<E : Element>

    //[CoroutineContext]的一个元素。协程上下文的一个元素本身就是一个单例上下文。
    public interface Element : CoroutineContext {
        //这个协程上下文元素的key
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E?
    }
}

启动模式

image.png

suspend 挂起函数

协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。协程在执行到有suspend标记的函数时,当前函数会被挂起(暂停),直到该挂起函数内部逻辑完成,才会在挂起的地方resume恢复继续执行。

协程异常

1: try..catch

2: CoroutineExceptionHandler 异常如果需要被捕获,则需要满足下面两个条件:

  • 这个异常是被自动抛出异常的协程所抛出的 (是launch,而不是async);

  • CoroutineExceptionHandler设置在CoroutineScope的上下文中或者在一个根协程 (CoroutineScope或者supervisorScope的直接子协程) 中

3:SupervisorJob

的一个子协程的运行失败或取消不会导致自己失败,也不会影响到其他子协程。SupervisorJob不会取消它自己和它的子协程,也不会传播异常并传递给它的父级,它会让子协程自己处理异常。

juejin.cn/post/698772…

WebView秒开的一些优化可以怎么做

  • 1.文件下载耗时:包括html、css、js、图片等
  • 2.页面渲染耗时:页面渲染,解析js、css文件等
  • 3.WebView创建耗时:首次创建WebView耗时大约需要500ms左右,第二次创建耗时大约需要20ms左右

Bitmap Drawable View 三者之间的联系和区别

  • bitmap: 仅仅就是一个位图 你可以理解为一张图片在内存中的映射。 就这么简单。这个很多人都知道
  • view: 这个就是android的核心了,你看到的一切东西都是view 这个很多人也知道。 但是这个理解成都还不够,view最大的作用是2个 一个是draw 也就是canvas的draw方法,还有一个作用 就是测量大小。 要想明白这点。
  • drawable: 他其实本身和bitmap没有关系, 你可以把他理解为是一个绘制工具,和view的第一个作用是一摸一样的,你能用view的canvas 画出来的东西 你用drawable 一样可以画出来, 不一样的是drawable 仅仅能绘制,但是不能测量自己的大小,但是view可以。换句话说 drawable 承担了view的一半作用。

Handler实现两个子线程(Thrae)通信

子线程中创出Handler 需要先调用Looper.prepare()获取消息队列在之后调用Looper.loop();开启Looper的循环(主线程中默认启动looper,子线程需要自己手动开启)

两种线程:Runable与Thread区别详解

(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想

(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。

(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。

sendmessage和postmessage的区别

本质上没有区别

哪些情况下的对象会被垃圾回收机制处理掉?

利用可达性分析算法,虚拟机会将一些对象定义为 GC Roots,从 GC Roots 出发沿着引用链 向下寻找,如果某个对象不能通过 GC Roots 寻找到,虚拟机就认为该对象可以被回收掉。

1.1 哪些对象可以被看做是 GC Roots 呢?

  • 1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 2)方法区中的类静态属性引用的对象,常量引用的对象;
  • 3)本地方法栈中 JNI(Native 方法)引用的对象;

1.2 对象不可达,一定会被垃圾收集器回收么? 即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行 finalize() 方法,对象必须重写 finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个 队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。

blog.csdn.net/weixin_3800…

Java 的异常体系

Java 中 Throwable 是所有异常和错误的超类,两个直接子类是 Error(错误)和 Exception(异 常): 1)Error 是程序无法处理的错误,由 JVM 产生和抛出,如 OOM、ThreadDeath 等。这些异常 发生时,JVM 一般会选择终止程序。

2 )Exception 是 程 序 本 身 可 以 处 理 的 异 常 ,又 分 为 运 行 时 异 常 ( RuntimeException ( 也 叫 CheckedEception)和非运行时异常(不检查异常 Unchecked Exception)。运行时异常有 NullPointerException\IndexOutOfBoundsException 等,这些异常一般是由程序逻辑错误引起 的,应尽可能避免。非运行时异常有 IOException\SQLException\FileNotFoundException 以及 由用户自定义的 Exception 异常等。

说说你对 Java 反射的理解

反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只 对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方 法。

说说你对 Java 注解的理解

注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@ 为保证注解正常工作: 需要使用元注解 @Retention @Documented @Target @Inherited @Repeatable 五种

  • @Retention 说明注解的存活时间

    • RetentionPolicy.SOURCE 注解只在源码阶段保留,
    • RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会 被 加 载 到 J V M 中
    • R e t e n t i o n P o l i c y . R U N T I M E 可 以 留 到 程 序 运 行 的 时 候 ,它 会 被 加 载 进 入 到 J V M 中,所以在程序运行时可以获取到它们。
  • @Documented 注解中的元素包含到 javadoc 中去

  • @Target 限定注解的应用场景

    • ElementType.FIELD 给属性进行注解
    • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
    • ElementType.METHOD 可以给方法 进行注解
    • ElementType.PACKAGE 可以给一个包进行注解
    • ElementType.TYPE 可以给一个类型 进行注解,如类、接口、枚举
  • @Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用 的话,该子类就可继承超类的注解;

泛型中 extends 和 super 的区别

  • extends 限定参数类型的上界,参数类型必须是 T 或 T 的子类型,但对于 List<? extends T>,不能通过 add()来加入元素,因为不知道<? extends T>是 T 的哪一种子类;
  • super 限定参数类型的下界,参数类型必须是 T 或 T 的父类型,不能能过 get()获取

kotlin系列之let、with、run、apply、also函数的使用

image.png

cloud.tencent.com/developer/a… youkmi.blog.csdn.net/article/det…

kotlin中var、val、const val 中新增关键字vararg区别

  • var是变量,可对其进行读取和修改(读写)
  • val是常量,只能对其进行读取,而不能修改(读)
  • const val定义的【常量 】 const只能用来修饰val,并且Const 修饰的val只能定义在top level或者在objects中。top level指的是在最外面的类的外面,表示该常量属于整个文件,而非某个类。object可以指最外面的object和companion object。
  • 中新增关键字vararg,例如:"vararg args:String?" 替代Java中的写法"String...args"

kotlin-高阶函数

由于函数可以当做特殊变量,如果把A函数作为B函数的输入参数,此时,因为B函数的输入参数内嵌了A函数,故而B函数被称作为高阶函数,对应的A函数则为高阶函数的函数参数,又称函数变量。

        //4.3.5 高阶函数
        /**
         * 由于函数可以当做特殊变量,如果把A函数作为B函数的输入参数,
         * 此时,因为B函数的输入参数内嵌了A函数,故而B函数被称作为高阶函数
         */
        //maxCustom()是高阶函数,这里的greater函数就像是个变量
        //greater函数有两个输入参数,返回布尔类型的输出参数
        //如果第一个参数大于第二个参数,就认为greater返回true,否则返回false
        fun <T> maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? {
            var max: T? = null
            for (item in array) {
                if (max == null || greater(item, max))
                    max = item
            }
            return max
        }

   //调用
        var string_array: Array<String> = arrayOf("Hao", "do", "you", "do", "I'm", "fine")
        btn_default_fun8.setOnClickListener {
            btn_default_fun8.text = when (count % 4) {
                //string_array.max()返回的时you
                0 -> "字符串数组的默认最大值为${string_array.max()}"
                //string_array.max()对应的高阶函数是maxCustom同时也是泛型函数,所以要在函数名称后面加上<String>
                1 -> "字符串数组按长度比较的最大值为${maxCustom<String>(string_array, { a, b -> a.length > b.length })}"
                //因为系统可以根据string_array判断泛型函数采用了String类型,故而函数名称后面的<String>也可以省略掉
                2 -> "字符串数组的默认最大值(使用高阶函数)为${maxCustom(string_array, { a, b -> a > b })}"
                else -> "字符串数组按去掉空格在比较长度的最大值为${maxCustom(string_array, { a, b -> a.trim().length > b.trim().length })}"
            }
            count++
        }

kotlin-扩展函数

扩展函数允许开发者给系统类补写新的方法,而无须另外编写额外的工具类。

  /**
         * 概念:系统自带的类提供的方法无法满足日常开发需求,于是乎开发者往往编写了很多工具类,由于工具类繁多,难以管理。
         * Kotlin推出了扩展函数的概念,扩展函数允许开发者给系统类补写新的方法,而无需编写额外的工具类。
         */
        //例如:给数组Array新增交换的方法。
        fun <T> Array<T>.swap(pos1: Int, pos2: Int) {
            //this表示数组变量自身
            val temp = this[pos1]
            this[pos1] = this[pos2]
            this[pos2] = temp

        }

        val array: Array<Double> = arrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
        btn_default_fun9.setOnClickListener {
            //下标为0和3的两个数组元素进行交换
            array.swap(0, 3)
            btn_default_fun9.text = setArrayStr<Double>(array)
        }

kotlin-扩展高阶函数

  /**
     * 扩展高阶函数
     * 扩展:扩展类的方法
     * 高阶:将函数作为变量传递
     */
    fun <T> Array<T>.maxCustomize(greater: (T, T) -> Boolean): T? {
        var max: T? = null
        for (item in this) {
            if (max == null || greater(item, max)) {
                max = item
            }
        }
        return max
    }

       //扩展高阶函数
        //扩展函数+高阶函数

        btn_default_fun10.setOnClickListener {
            btn_default_fun10.text = when (count % 4) {
                0 -> "字符串数组的默认最大值为${string_array.max()}"
                1 -> "字符串数组按长度比较的最大值为${string_array.maxCustomize<String>({ a, b -> a.length > b.length })}"
                2 -> "字符串数组的默认最大值(使用高阶函数)为${string_array.maxCustomize({ a, b -> a > b })}"
                else -> "字符串数组按去掉空格在比较长度的最大值为${string_array.maxCustomize({ a, b -> a.trim().length > b.trim().length })}"
            }
            count++
        }

kotlin-内联函数

通过关键字inline修饰都函数 优点:

  • 优化Lambda开销 通过inline修饰的函数,其函数体代码被调用的Lambda代码都粘贴到了相应调用的位置
fun main() {
    payFoo {
        println("write kotlin...")
    }
}
fun payFoo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

--------inline修改后----------
fun main() {
    payFoo {
        println("write kotlin...")
    }
}
inline fun payFoo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}
  • 非局部返回 Kotlin中内联函数除了优化Lambda开销之外,还带来了非局部返回和具体化参数类型。

首先看常见的局部返回的例子

fun main() {
    payFoo()
}
 
fun localReturn() {
    return
}
 
fun payFoo() {
    println("before local return")
    localReturn()
    println("after local return")
    return
}
 
before local return
after local return

从上面代码可以发现,localReturn执行之后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。

通过内联函数修改

fun main() {
    payFoo { return }
}
 
inline fun payFoo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
 
before local return

Lambda的return执行之后直接让foo函数退出了执行。因为内联函数payFoo的函数体及参数Lambda会直接替代具体的调用,所以实际产生的代码中,return相当于是直接暴露在main函数中的,所以returning之后的代码就不会执行了,这就是非局部返回。

  • 具体化参数类型 内联函数可以帮助我们实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦除,我们不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这时候反而可以获得参数的具体类型。 使用reified修饰符来实现这一效果。
inline fun <reified T : Activity> Activity.startActivity() {
    startActivity(Intent(this, T::class.java))
}

内联函数不是万能的,以下情况避免使用内联函数:

1.由于JVM对普通函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。

2.尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。

3.一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把它们声明为internal。

kotlin-伴生对象

Kotlin取消了关键字static,也就无法直接声明静态成员。为了弥补这方面的功能缺陷,Kotlin引入 了伴生对象的概念.

class WildAnimalCompanion(var name: String, val sex: Int = 0) {
    var sexName: String

    init {
        sexName = if (sex == 0) "公" else "母"
    }

    fun getDesc(tag: String): String {
        return "欢迎来到$tag:这只${name}是${sexName}的"
    }
    //在类加载的时候就运行伴生对象的代码块,其作用相当于Java里面的static{...}代码块

    companion object WildAnimal {
        fun judgeSex(sexName: String): Int {
            var sex: Int = when (sexName) {
                "公", "雄" -> 0
                "母", "雌" -> 1
                else -> -1
            }
            return sex
        }
    }
}


--------

        val sexArray: Array<String> = arrayOf("公", "母", "雄", "雌")
        btn_simple_class7.setOnClickListener {
            var sexName: String = sexArray[count++ % 4]
            //伴生对象的WildAnimal名称可以省略
//            btn_simple_class7.text="${sexName} 对应的类型是${WildAnimalCompanion.WildAnimal.judgeSex(sexName)}"
            btn_simple_class7.text = "$sexName 对应的类型是${WildAnimalCompanion.judgeSex(sexName)}"
        }

kotlin--类的修饰符

  • open作为修饰符 表示该类可以被继承
  • public ->对所有人开放。kotlin的类,函数、变量不加开放性修饰的话默认就是public类型
  • internal ->只对本模块内部开放,这是kotlin新增的关键字。对于app开发来说,本模块便是指app自身
  • protected ->只对自己跟子类开放
  • private ->只对自己开放,即私有

注意:open 跟private 是对立的关系 不能同时出现

  • 抽象类用abstract关键字修饰,abstract方法默认就是open类型

kotlin--几种特殊的类

嵌套类

class Tree(var treeName: String) {


    //在类的内部再定一个类,这个新类称作嵌套类
    //不能访问外部类成员,如treename

    class Flower(var flowerName: String) {
        fun getName(): String {
            return "这是一朵${flowerName}"
        }
    }
}

注意:

  • 调用嵌套类时,得在嵌套类的类名前面添加外部类的类名,相当于把这个嵌套类作为外部类的静态对象使用
  • 不能访问外部类成员

内部类 既然Kotlin限制了嵌套类不能访问外部类的成员,Kotin另外增加了关键字inner表示内部,把inner加在嵌套类的class前面,这个内部类比起嵌套类的好处是能够访问外部类的成员。

class Tree(var treeName: String) {
    //嵌套类加上inner前缀,就成为内部类
    inner class Fruit(var fruitName: String) {
        fun getName(): String {
            return "这是${treeName}长出的$fruitName"
        }
    }
}

泛型类

  • 定义格式跟Java相似,一样在类名后面补充形如,<A,B>这样的表达式
  • 外部调用模板类构造函数的时候,要在类名后面补充“<参数类型>”,从而动态指定实际的参数类型
class River<T>(var name: String, var length: T) {
    fun getInfo(): String {
        var unit: String = when (length) {
            is String -> "米"
            is Number -> "m"
            else -> ""
        }
        return "${name}的长度是$length$unit"
    }
}
--------
btn_simple_class19.setOnClickListener {
            var river = when (count++ % 4) {
                //泛型类声明对象时,要在模板类的后面加上“<参数类型>”
                0 -> River<Int>("小溪", 100)
                //如果编译器根据输入参数就能知晓参数类型,也可以直接省略“<参数类型>"
                1 -> River("瀑布", 99.9f)
                1 -> River<Double>("瀑布", 55.5)
                else -> River("大河", "一千")
            }
            btn_simple_class19.text = river.getInfo()
        }

泛型擦除

  • 如果泛型没有设置上界约束,那么将泛型转化成 Object 类型
  • 如果泛型设置了上界约束,那么将泛型转化成该上界约束

协变/逆变

  • 协变 带 extends 限定了上界的通配符类型使得泛型参数类型是协变的,即如果 A 是 B 的子类,那么 Generic 就是 Generic的子类型
    private static <T> void copyAll(List<T> to, List<? extends T> from) {
        to.addAll(from);
    }

    private static <T> void copyAll(List<? super T> to, List<T> from) {
        to.addAll(from);
    }

out & in

fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) {
    to.addAll(from)
}

fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) {
    to.addAll(from)
}

  • in 关键字就相当于 Java 中的<? super T>,其作用就是限制了 to 只能用于接收值而不能向其取值,这样就避免了从 to 取出值然后向 from 赋值这种不安全的行为了,即实现了泛型逆变

  • out 关键字就相当于 Java 中的<? extends T>,其作用就是限制了 from 不能用于接收值而只能向其取值,这样就避免了从 to 取出值然后向 from 赋值这种不安全的行为了,即实现了泛型协变

reified & inline 上文讲了,由于类型擦除,Java 和 Kotlin 的泛型类型实参都会在编译阶段被擦除。而在 Kotlin 中存在一个额外手段可以来避免这个问题,即内联函数

用关键字 inline 标记的函数就称为内联函数,再用 reified 关键字修饰内联函数中的泛型形参,编译器在进行编译的时候便会将内联函数的字节码插入到每一个调用的地方,当中就包括泛型的类型实参。而内联函数的类型形参能够被实化,就意味着我们可以在运行时引用实际的类型实参了

例如,我们可以写出以下这样的一个内联函数,用于判断一个对象是否是指定类型

fun main() {
    println(1.isInstanceOf<String>())
    println("string".isInstanceOf<Int>())
}

inline fun <reified T> Any.isInstanceOf(): Boolean {
    return this is T
}

单例模式(懒汉式和饿汉式区别)

  • 饿汉氏 简单来说就是空间换时间,因为上来就实例化一个对象,占用了内存,天生线程安全

class Singleton{
	//私有的构造函数,保证外类不能实例化本类
	private Singleton(){}
	//自己创建一个类的实例化
	private static Singleton singleton = new Singleton();
	//创建一个get方法,返回一个实例s
	public static Singleton getInstance(){
		return singleton;
	}
  • 懒汉氏 简单的来说就是时间换空间,与饿汉式正好相反 线程不安全

浏览器输入URL会发生什么?

  • 首先先解析URL得到里面的参数,将域名和请求的资源分开,然后再将这些信息封装成一段HTTP请求报文发送出去。
  • DNS域名解析获取IP地址
  • 通过DNS解析拿到目标IP地址后,就可以将HTTP的传输工作委托给操作系统的协议栈了。
  • 进行三次握手建立连接,保证双方的一个可靠传输。
  • 使用ARP协议找到MAC地址实现两点传输
  • 服务器响应请求

HTTP与HTTPS有什么区别?

HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份 认证,保护交换数据的隐私与完整性。

HTPPS和HTTP的概念:

HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 它是一个URI scheme(抽象标识符体系),句法类同http:体系。用于安全的HTTP数据传输。https:URL表明它使用了HTTP,但HTTPS存在不同于HTTP的默认端口及一个加密/身份验证层(在HTTP与TCP之间)。这个系统的最初研发由网景公司进行,提供了身份验证与加密通讯方法,现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。

超文本传输协议 (HTTP-Hypertext transfer protocol) 是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

https协议需要到ca申请证书,一般免费证书很少,需要交费。http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。http的连接很简单,是无状态的HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全HTTPS解决的问题:1 . 信任主机的问题. 采用https 的server 必须从CA 申请一个用于证明服务器用途类型的证书. 改证书只有用于对应的server 的时候,客户度才信任次主机2 . 防止通讯过程中的数据的泄密和被窜改

如下图所示,可以很明显的看出两个的区别:

image.png

https 为什么是安全的

对称加密 就是客户端和服务器共用同一个密钥,该密钥可以用于加密一段内容,同时也可以用于解密这段内容。对称加密的优点是加解密效率高,但是在安全性方面可能存在一些问题,因为密钥存放在客户端有被窃取的风险。对称加密的代表算法有:AES、DES 等。

非对称加密 公钥和私钥。公钥通常存放在客户端,私钥通常存放在服务器。使用公钥加密的数据只有用私钥才能解密,反过来使用私钥加密的数据也只有用公钥才能解密。非对称加密的优点是安全性更高, 非对称加密的代表算法有:RSA、ElGamal 等。

对称加密的优点是加解密效率高,而我们在网络上传输数据是非常讲究效率的,因此https这里很明显应该使用对称加密。我们需要将非对称加密引入进来,协助解决无法安全创建对称加密密钥的问题‘

CA 机构。

首先,我们作为一个网站的管理员需要向CA机构进行申请,将自己的公钥提交给CA机构。CA机构则会使用我们提交的公钥,再加上一系列其他的信息,如网站域名、有效时长等,来制作证书。

证书制作完成后,CA机构会使用自己的私钥对其加密,并将加密后的数据返回给我们,我们只需要将获得的加密数据配置到网站服务器上即可。 然后,每当有浏览器请求我们的网站时,首先会将这段加密数据返回给浏览器,此时浏览器会用CA机构的公钥来对这段数据解密。 如果能解密成功,就可以得到 CA 机构给我们网站颁发的证书了,其中当然也包括了我们网站的公钥。

原文链接:blog.csdn.net/guolin_blog…

https是怎么抓包的

  • 下载证书
  • 配置https安全文件 Android 7.0系统中才做了这项安全升级。默认情况下,我们无法对各个App的https请求进行抓包,如果你是想要对自己App的https请求抓包的话,那么可以这样做。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="user"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

https协议确实是非常安全的,但同时,用https协议传输的数据也确实是可以被抓包的,它们两者之间并不冲突。https协议是结合了非对称加密和对称加密一起工作,从而保证数据传输的安全性的。

为什么抓包工具能截取 HTTPS 数据?

抓包工具 Fiddler 之所以可以明文看到 HTTPS 数据,工作原理与中间人一致的。

对于 HTTPS 连接来说,中间人要满足以下两点,才能实现真正的明文代理:

  1. 中间人,作为客户端与真实服务端建立连接这一步不会有问题,因为服务端不会校验客户端的身份;
  2. 中间人,作为服务端与真实客户端建立连接,这里会有客户端信任服务端的问题,也就是服务端必须有对应域名的私钥;

中间人要拿到私钥只能通过如下方式:

  1. 去网站服务端拿到私钥;
  2. 去CA处拿域名签发私钥;
  3. 自己签发证书,且被浏览器信任;

不用解释,抓包工具只能使用第三种方式取得中间人的身份。

使用抓包工具进行 HTTPS 抓包的时候,需要在客户端安装 Fiddler 的根证书,这里实际上起认证中心(CA)的作用。

Fiddler 能够抓包的关键是客户端会往系统受信任的根证书列表中导入 Fiddler 生成的证书,而这个证书会被浏览器信任,也就是 Fiddler 给自己创建了一个认证中心 CA。

客户端拿着中间人签发的证书去中间人自己的 CA 去认证,当然认为这个证书是有效的。

www.cnblogs.com/xiaolincodi…

如何避免被中间人抓取数据?

我们要保证自己电脑的安全,不要被病毒乘虚而入,而且也不要点击任何证书非法的网站,这样 HTTPS 数据就不会被中间人截取到了。

当然,我们还可以通过 HTTPS 双向认证来避免这种问题。

一般我们的 HTTPS 是单向认证,客户端只会验证了服务端的身份,但是服务端并不会验证客户端的身份。

如果用了双向认证方式,不仅客户端会验证服务端的身份,而且服务端也会验证客户端的身份。

服务端一旦验证到请求自己的客户端为不可信任的,服务端就拒绝继续通信,客户端如果发现服务端为不可信任的,那么也中止通信。

Http的request和response的协议组成

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:

请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

image.png

请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本。

Get请求例子

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host    img.mukewang.com
User-Agent  Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept  image/webp,image/*,*/*;q=0.8
Referer http://www.imooc.com/
Accept-Encoding gzip, deflate, sdch
Accept-Language zh-CN,zh;q=0.8

第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本. GET说明请求类型为GET,[/562f25980001b1b106000338.jpg]为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。 第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息 从第二行起为请求头部,HOST将指出请求的目的地.User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础.该信息由你的浏览器来定义,并且在每个请求中自动发送等等 第三部分:空行,请求头部后面的空行是必须的 即使第四部分的请求数据为空,也必须有空行。 第四部分:请求数据也叫主体,可以添加任意的其他数据。 这个例子的请求数据为空。

POST请求例子

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive

name=Professional%20Ajax&publisher=Wiley

第一部分:请求行,第一行明了是post请求,以及http1.1版本。

第二部分:请求头部,第二行至第六行。

第三部分:空行,第七行的空行。

第四部分:请求数据,第八行。

Request组成

一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。 HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

image.png 第一部分:状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。

第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为(ok)

第二部分:消息报头,用来说明客户端要使用的一些附加信息

第二行和第三行为消息报头, Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8

第三部分:空行,消息报头后面的空行是必须的

第四部分:响应正文,服务器返回给客户端的文本信息。

空行后面的html部分为响应正文

https 缓存

HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应式从缓存中取,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示

image.png

http三次握手跟四次挥手

  1. 三次握手:
  • 第一次握手 客户端向服务器发送一个连接请求的报文
  • 第二次握手 服务器接收到了报文段后,若同意建立连接,则向客户端发回连接确认的报文
  • 第三次握手 客户端收到确认报文段后,向服务器再次发出连接确认报文段

为什么TCP建立连接需三次握手?

防止服务器端因接收了早已失效的连接请求报文,从而一直等待客户端请求,最终导致形成死锁、浪费资源

  1. 4次挥手
  • 第一次挥手 客户端向服务器发送一个连接释放的报文
  • 第二次挥手 服务器接收到了释放报文段后,则向客户端发送表示同意释报文
  • 第三次挥手 无服务器无需在向客户端发送数据,则发出释放连接的报文
  • 第三次挥手 客户端收到连接释放报文后,则向服务器发送连接释放确认的报文

为什么TCP释放连接需四次挥手? 为了保证通信双方都能通知对方 需释放 & 断开连接

Http长连接。

Http1.0是短连接,HTTP1.1默认是长连接,也就是默认Connection的值就是keep-alive。但是长连接实质是指的TCP连接,而不是HTTP连接。TCP连接是一个双向的通道,它是可以保持一段时间不关闭的,因此TCP连接才有真正的长连接和短连接这一说。

Http1.1为什么要用使用tcp长连接?

长连接是指的TCP连接,也就是说复用的是TCP连接。即长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。

此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。

UDP和TCP的区别是什么?

  • TCP是面向有连接型,UDP是面向无连接型;

  • TCP是一对一传输,UDP支持一对一、一对多、多对一和多对多的交互通信;

  • TCP是面向字节流的,即把应用层传来的报文看成字节流,将字节流拆分成大小不等的数据块,并添加TCP首部;

  • UDP是面向报文的,对应用层传下来的报文不拆分也不合并,仅添加UDP首部;

  • TCP支持传输可靠性的多种措施,包括保证包的传输顺序、重发机制、流量控制和拥塞控制;

  • UDP仅提供最基本的数据传输能力。

image.png

有哪些响应码,分别都代表什么意思?

1** 信息,服务器收到请求,需要请求者继续执行操作

2** 成功,操作被成功接收并处理

3** 重定向,需要进一步的操作以完成请求

4** 客户端错误,请求包含语法错误或无法完成请求

5** 服务器错误,服务器在处理请求的过程中发生了错误

socket断线重连怎么实现,心跳机制又是怎样实现?

建立socket连接

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

  • 服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
  • 客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端- - 套接字的地址和端口号,然后就向服务器端套接字提出连接请求。 连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发 给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
SOCKET连接与TCP连接

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

Socket连接与HTTP连接

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网 络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数 据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求, 不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。TCP(Transmission Control Protocol) 传输控制协议

socket断线重连实现

正常连接断开客户端会给服务端发送一个fin包,服务端收到fin包后才会知道连接断开。 而断网断电时客户端无法发送fin包给服务端,所以服务端没办法检测到客户端已经短线。 为了缓解这个问题,服务端需要有个心跳逻辑,就是服务端检测到某个客户端多久没发送任何数据过来就认为客户端已经断开, 这需要客户端定时向服务端发送心跳数据维持连接。

心跳机制实现

长连接的实现:心跳机制,应用层协议大多都有HeartBeat机制,通常是客户端每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线。并传输一些可能必要的数据。使用心跳包的典型协议是IM,比如QQ/MSN/飞信等协议

1、在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。 系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。 而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。通过使用TCP的KeepAlive机制(修改那个time参数),可以让连接每隔一小段时间就产生一些ack包,以降低被踢掉的风险,当然,这样的代价是额外的网络和CPU负担。

2、应用层心跳机制实现。

Cookie与Session的作用和原理。

  • Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。
  • Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
Session:

由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的。

具体到Web中的Session指的就是用户在浏览某个网站时,从进入网站到浏览器关闭所经过的这段时间,也就是用户浏览这个网站所花费的时间。因此从上述的定义中我们可以看到,Session实际上是一个特定的时间概念。

当客户端访问服务器时,服务器根据需求设置Session,将会话信息保存在服务器上,同时将标示Session的SessionId传递给客户端浏览器,

浏览器将这个SessionId保存在内存中,我们称之为无过期时间的Cookie。浏览器关闭后,这个Cookie就会被清掉,它不会存在于用户的Cookie临时文件。

以后浏览器每次请求都会额外加上这个参数值,服务器会根据这个SessionId,就能取得客户端的数据信息。

如果客户端浏览器意外关闭,服务器保存的Session数据不是立即释放,此时数据还会存在,只要我们知道那个SessionId,就可以继续通过请求获得此Session的信息,因为此时后台的Session还存在,当然我们可以设置一个Session超时时间,一旦超过规定时间没有客户端请求时,服务器就会清除对应SessionId的Session信息。

Cookie

Cookie是由服务器端生成,发送给User-Agent(一般是web浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用Cookie)。Cookie名称和值可以由服务器端开发自己定义,对于JSP而言也可以直接写入Sessionid,这样服务器可以知道该用户是否合法用户以及是否需要重新登录等。

Android 各个版本适配问题

10,11版本 作用域存储: 那么到底什么是作用域存储呢?简单来讲,就是Android系统对SD卡的使用做了很大的限制。从Android 10开始,每个应用程序只能有权在自己的外置存储空间关联目录下读取和创建文件,获取该关联目录的代码是:context.getExternalFilesDir()。关联目录对应的路径大致如下:

/storage/emulated/0/Android/data/<包名>/files

需要适配的点场景:

  • 获取相册中的图片 :们只能借助MediaStore API获取到图片的Uri
  • 将图片添加到相册 :
  • 下载文件到Download目录:那么该如何解决呢?主要有以下两种方式。
    • 就是更改文件的下载目录
    • 适配10MediaStore中新增了一种Downloads集合,专门用于执行文件下载操作。
  • 使用文件选择器: 如果我们要读取SD卡上非图片、音频、视频类的文件,比如说打开一个PDF文件,这个时候就不能再使用MediaStore API了,而是要使用文件选择器。
const val PICK_FILE = 1

private fun pickFile() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "*/*"
    startActivityForResult(intent, PICK_FILE)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
        PICK_FILE -> {
            if (resultCode == Activity.RESULT_OK && data != null) {
                val uri = data.data
                if (uri != null) {
                    val inputStream = contentResolver.openInputStream(uri)
					// 执行文件读取操作
                }
            }
        }
    }
}

  • 第三方SDK不支持作用域存储怎么办?
    • 在那之前我们先不要将targetSdkVersion指定到29,或者先在AndroidManifest文件中配置一下requestLegacyExternalStorage属性。
    • 就是我们自己编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了

9.0版本适配 问题原因: Android P 限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉 解决方案: 1:在资源文件新建xml目录,新建文件network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

2:清单文件配置:

<application
    android:networkSecurityConfig="@xml/network_security_config">
    <!--Android 9.0加的-->
    <uses-library
        android:name="org.apache.http.legacy"
        android:required="false" />
</application>

8.0版本适配 Android 8.0中,为了更好的管制通知的提醒,不想一些不重要的通知打扰用户,新增了通知渠道,用户可以根据渠道来屏蔽一些不想要的通知。

7.0适配 应用间共享文件

6.0适配 权限适配

volatile关键字在Android中到底有什么用?

  • volatile这个关键字的其中一个重要作用就是解决可见性问题,即保证当一个线程修改了某个变量之后,该变量对于另外一个线程是立即可见的。

  • volatile关键字还有另外一个重要的作用,就是禁止指令重排

ReentrantLock与synchronized的区别

  1. 锁的获取方式不同

synchronized锁的获取是隐式的,即在进入[同步代码块]或方法时自动获取锁,退出时自动释放锁;

Reentrant[Lock锁]的获取是显式的,需要手动去调用lock()方法获取锁,unlock()方法释放锁。

  1. 锁的公平性不同

synchronized是非公平锁,不能保证等待时间最长的线程最先获取锁;

ReentrantLock可以是公平锁,可以保证等待时间最长的线程最先获取锁。

  1. 锁的灵活性不同

ReentrantLock提供了很多synchronized不具备的功能,例如可以设置超时时间,可以判断锁是否被其他线程持有,可以使用Condition类实现线程等待/通知机制等

image.png

sychronized关键字

三种应用方式

  • 修饰实例方法 作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法 作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized底层语义原理 理解Java对象头与Monitor

  • 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
    • Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成
    • Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    • Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

synchronized代码块底层原理

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

锁的分类4个

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

Java虚拟机对synchronized的优化

  • 偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁

  • 轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁

  • 自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

  • 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

volatile关键字

  • volatile这个关键字的其中一个重要作用就是解决可见性问题,即保证当一个线程修改了某个变量之后,该变量对于另外一个线程是立即可见的。
  • volatile关键字还有另外一个重要的作用,就是禁止指令重排, CPU在执行代码时,其实并不一定会严格按照我们编写的顺序去执行,而是可能会考虑一些效率方面的原因,对那些先后顺序无关紧要的代码进行重新排序,这个操作就被称为指令重排。
  • android 上的应用 对于多线程场景的变量基本会使用volatile关键字

guolin.blog.csdn.net/article/det…

ConcurrentHashMap源码分析

ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素。相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高。不允许key,value为null

JDK1.8之前的 ConcurrentHashMap

ConcurrentHashMap 类所采用的是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图:

image.png

并发读写的几种情况?

Case1:不同Segment的并发写入(可以并发执行)

image.png

Case2:同一Segment的一写一读(可以并发执行)

image.png

Case3:同一Segment的并发写入
image.png

Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。

接下来,我们就来看下ConcurrentHashMap读写的过程。

get方法

  1. 为输入的Key做Hash运算,得到hash值。
  2. 通过hash值,定位到对应的Segment对象
  3. 再次通过hash值,定位到Segment当中数组的具体位置。

put方法

  1. 为输入的Key做Hash运算,得到hash值。

  2. 通过hash值,定位到对应的Segment对象

  3. 获取可重入锁

  4. 再次通过hash值,定位到Segment当中数组的具体位置。

  5. 插入或覆盖HashEntry对象。

  6. 释放锁

    JDK1.8 中的 ConcurrentHashMap

    在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

    JDK1.8 中的ConcurrentHashMap 相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性

乐观锁采用的 CAS 算法全称是 Compare And Swap(比较与交换) ,是一种无锁算法,在不使用锁(所以也不会导致线程被阻塞)的情况下实现在多线程之间的变量同步

CAS 算法涉及到三个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 要写入的新值 B

当且仅当 V 的值等于 A 时,CAS 才会用新值 B 来更新 V 的值,且保证了**“比较+更新”**这整个操作的原子性。当 V 的值不等于 A 时则不会执行任何操作。一般情况下,“更新”是一个会不断重试的操作

put方法

  • 首先会判断 key、value是否为空,如果为空就抛异常;

  • 接着会判断容器数组是否为空,如果为空就初始化数组;

  • 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;

  • 在接着判断f.hash == -1是否成立,如果成立,说明当前fForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;

  • 其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;

  • 节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;

  • 最后,插入完成之后,进行扩容判断

get方法

  • 第1步,判断数组是否为空,通过key定位到数组下标是否为空;

  • 第2步,判断node节点第一个元素是不是要找到,如果是直接返回;

  • 第3步,如果是红黑树结构,就从红黑树里面查询;

  • 第4步,如果是链表结构,循环遍历判断。

juejin.cn/post/706406…

红黑树

它或者是一个空数,或者是具有以下性质的二叉查找数 特性

  • 节点非红即黑
  • 根结点是黑色
  • 所有null结点称为叶子节点,且默认是黑色
  • 所有红节点的子节点都为黑色
  • 从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点

image.png

对平衡树的改进.任意一个节点,他的左右子树的层次最多不超过一倍

AVL树(平衡二叉树)

平衡二叉树是一种二叉排序树,其中每一个节点的左子数和右子数的高度差不多等于1

image.png

平衡因子

二叉树上节点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)