好用的开发助手

属实好用

https://www.baidufe.com/ 官网下载地址


RGB在线提色工具 https://link.fobshanghai.com/rgbcolor.htm

用例图在线画 https://online.visual-paradigm.com/cn/diagrams/features/use-case-diagram-software/

unicode转中文 https://www.bejson.com/convert/unicode_chinese/

在线截代码工具 https://www.dute.org/code-snapshot

CURL<=>请求体,爬虫好助手 :https://curlconverter.com/

好多好多工具:http://tools.jb51.net/

后端好助手:https://www.bejson.com/

常用工具库:https://www.matools.com/

图片处理、二维码转换:https://www.gaitubao.com/qrcode-decoder

语雀: 超好用的编辑器 http://www.yuque.com

还有些插件也好用,后续推荐

服务器内存不够,宝塔便捷配置虚拟内存

我的服务器是学生机便宜的低配云服务器,内存才2GB,多跑几个项目后,会明显感觉内存不够,有段时间还常常内存不够,导致数据库直接停止服务,尝试过调整数据库配置参数和JVM的调参,但效果不太明显。

意外发现宝塔管理页面的Linux工具箱的Swap虚拟内存,我Swap设置的2GB大小(我实际硬盘存储40GB才用了10不到哈哈,用它2G没问题),现在明显运行稳定,数据库也没挂过。

如果你内存不够,也可以试试哦!

Python爬虫B站视频信息+Quick BI数据可视化分析

点击链接去看看 --> https://cstweb.top/py-data-work/python_bili_video_data_analysis.html

爬虫实战-随机爬取B站2021年视频信息

这学期通过学习python数据分析,写了个爬虫Python脚本,刚好作为这门课期末作业交了,我爬取的是B站2021年视频信息数据。

备注:爬取的数据仅为了学习爬虫技术,无盈利无传播

使用技术

开发环境:python3 + Pycharm
数据分析库包

  • python网络爬虫:requests 、pymysql、requests、pyquery
  • 数据分析与存储:MySQL、NumPy、Pandas、re
  • 数据可视化:Quick BI

爬虫思路

  • 先去B站寻找所需的视频,F12打开调试,找到相应的数据接口或页面,分析其接口的参数信息,以下三接口是本项目的爬取核心接口

  • # 爬取B站视频信息(含播放量、点赞数等)的接口
    bili_video_info_URL = "https://api.bilibili.com/x/web-interface/archive/stat?aid={}" 
    # 爬取B站视频的类别标签的接口
    bili_video_tag_URL = "https://api.bilibili.com/x/web-interface/view/detail/tag?aid={}"  
    # 爬取B站视频的HTML 文本信息,用于获取视频标题和视频发表时间
    bili_video_name_URL = "https://www.bilibili.com/video/{}"
    例如下方是 爬取B站视频信息(含播放量、点赞数等)的接口,返回的JSON数据
    {
      "code": 0,
      "message": "0",
      "ttl": 1,
      "data": {
          "aid": 2,
          "bvid": "BV1xx411c7mD",
          "view": 3394733,
          "danmaku": 103196,
          "reply": 75021,
          "favorite": 84651,
          "coin": 29730,
          "share": 13916,
          "like": 185117,
          "now_rank": 0,
          "his_rank": 0,
          "no_reprint": 0,
          "copyright": 2,
          "argue_msg": "",
          "evaluation": ""
      }
    }
    当前节点:JSON.data
  • 如果碰到接口返回的不是JSON数据,而是直接渲染在页面上,此时需要将爬取网页的HTML文本,通过pyquery的JQuery语法提取页面标签中的内容

    text = requests.get(bili_video_name_URL.format(data["bvid"]), headers=headers).text
    q = pyquery.PyQuery(text)
    q("h1[title]").text(),  # HTML中提取 视频标题
    q("span:eq(11)").text()  # HTML中提取 视频发布时间
    
  • 通过分析,调取这些接口时,发起Get请求时,需要传入bvid或aid的参数,这也就是B站存视频的ID号,且我发现这些ID都是有规律的,视频ID号都是递增的方式,这里我猜测B站存视频信息的ID号是使用数据库中常见的主键ID自增长的形式,所有我用多个不同范围内的数字去尝试,得出了个结果B站2021年的视频的ID号 范围在(203432111 ~~ 219813111 之间),所以爬取2021年的视频数据,传给对应参数的aid只要在这范围内,就能爬取到数据,如果爬到的是空数据,则跳过当条,期间还解决了一些aid转bvid的事项,爬数据时传入的aid我使用 python的随机数,确定好随机数的范围就行。

    "https://api.bilibili.com/x/web-interface/archive/stat?aid={}"
    .format(random.randint(203432111, 219813111)
  • 数据存储,爬下来的数据我存在MySQL数据库

    def save_db():
      # 将爬取的数据保存至服务器上的数据库
      global result, cur, conn, total
      sql = "replace into bili_video values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,now()) ;"
      try:
          cur.executemany(sql, result)
      except Exception as e:
          print("数据回滚:{}", e)
          conn.rollback()
      conn.commit()
      print("本次数据保存成功,当前进度 {}/10000 ---- {} %".format(total, total / hopeTotal * 100))
      result.clear()
    
    #创建数据表   
    cur.execute(
          """create table if not exists bili_video
                     (v_aid int primary key,
                      v_bvid varchar(50),
                      v_view int,
                      v_danmaku int,
                      v_reply int,
                      v_favorite int,
                      v_coin int,
                      v_share int,
                      v_like int,
                      v_name text,
                      v_tags text,
                      v_pubtime datetime,
                      create_time datetime)
                      """
    )
    
    #为了提升效率,我们插入数据到数据库时,可以使用批量插入,不要每爬一次,就插入一次数据,这样效率不高哦
    total += 1
    # 每爬取200条数据,批量保存一次到数据库
    if total % 200 == 0:
      save_db()
      result.clear()

    注意点:SQL语句我使用replace而不是insert,因为在爬取的过程中,因为是随机的,所以在爬取过程中是有可能碰到重复的,所以使用replace,当碰到重复的数据,替换它即可,避免了没必要的异常出现。

  • 数据爬取方式和遇到的问题爬取数据

    爬取数据的核心代码

    time.sleep(1)  # 每次请求,间隔1秒,避免太快 IP被封
    req = requests.get(url, headers=headers, timeout=10).json()
    data = req["data"]
    if data["view"] != "--" and data["aid"] != 0:
    req_tag = requests.get(bili_video_tag_URL.format(data["aid"]), headers=headers, timeout=10).json()
      tag_list = req_tag["data"]
      tags = ",".join([tag["tag_name"] for tag in tag_list])
      text = requests.get(bili_video_name_URL.format(data["bvid"]), headers=headers).text
      q = pyquery.PyQuery(text)
          video = (
              data["aid"],  # 视频编号
              data["bvid"],  # 视频bvid编号
              data["view"],  # 播放量
              data["danmaku"],  # 弹幕数
              data["reply"],  # 评论数
              data["favorite"],  # 收藏数
              data["coin"],  # 硬币数
              data["share"],  # 分享数
              data["like"],  # 点赞数
              q("h1[title]").text(),  # 视频标题
              tags,  # 视频的分类标签
              q("span:eq(11)").text()  # 视频发布时间
          )
    
    print("正在爬取,{}".format(video))

    怎么保证高效的爬取?如果只在本地爬取,那么电脑要一直开着,不方便,所以我把这python脚本放在了个人的云服务器上跑,配置好爬虫所需的环境后,最好将print语句换成logger日志输出方式方便实时查看,然后即可24小时不间断的进行爬取。
    不过也遇到了问题,当爬取速度过快时,会存在IP被限制、被封的情况,这种时候就无法继续爬取了,往往要等一段时间才能解封。我的解决方式是;每爬取一条数据时,程序睡眠1秒再进行,这样有效解决了IP被封的情况,但有时爬的速度慢B站也可能会封禁,这时候我在程序中通过得到接口返回的状态码,判断状态码的参数,即可得知IP是否被限制,如果被限制了,则让程序休眠十分钟,再继续尝试,使用这样的解决方式后,爬虫的效率显著提高了。

    req = requests.get(url, headers=headers, timeout=10).json()
       data = req["data"]
       if req["code"] == -412:
         print("IP被封了, 休眠10分钟后继续尝试...")
         time.sleep(600)
         pass
  • 数据清洗分析和可视化

    Python爬虫是语言优势,工具封装生态完善、效率高
    使用NumPy、Pandas、阿里的Quick BI 可视化工具,分析数据库中的,过滤和筛选所需的数据,配置对应的类别轴/维度、值轴/度量、进度指示/度量、扇区标签/维度等参数,生成相应的图表。
    ----源码后续整理好可能会开源

    Quick-BI

总览图

本次

近期生活

感叹时间过得太快,大四上快结束啦,这也相当于快毕业了。

这学期过得蛮充实的~

每周都有三件事:

  • 星期一在学校上课;
  • 星期二 ~ 星期五去实习;
  • 双休玩起来;

还有做毕设!

希望写的代码没Bug!加油学习!

OkHttp 同步异步请求

OkHttp 安卓开发中请求图片的Demo

开启网络权限

 <uses-permission android:name="android.permission.INTERNET" />

gradle新增okhttp依赖

 implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.10.0'

MainActivity.java代码

package com.example.okhttpdemo;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 *  OkHttp 加载图片 异步或同步请求
 * @author cst
 */
public class MainActivity extends AppCompatActivity {
    private ImageView imageView;
    private TextView title;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        imageView = (ImageView) findViewById(R.id.image);
        title = findViewById(R.id.title);
        Button getDefaultImageBtn = findViewById(R.id.get_btn);
        Button getSynImageBtn = findViewById(R.id.get_syn_btn);
        Button getImageBtn = findViewById(R.id.image_url_btn);

        //初始化按钮单击监听器
        getDefaultImageBtn.setOnClickListener(v -> showImage("https://www.cstweb.top/wp-content/uploads/2021/09/WBVEM0EQSQEXVDIDDUJU-1-768x810.jpg"));
        getSynImageBtn.setOnClickListener(v -> showImageBySyn("https://www.cstweb.top/wp-content/uploads/2021/09/java-think-721x1024.png"));
        getImageBtn.setOnClickListener(v -> showInputDialog());
    }

    /**
     *  加载图片(异步请求)
     * @param imageURL
     */
    public void showImage(String imageURL){
        Handler handler=new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == 1){
                    byte[]result= (byte[]) msg.obj;
                    Bitmap bitmap=BitmapFactory.decodeByteArray(result,0,result.length);
                    imageView.setImageBitmap(bitmap);
                    title.setText("加载完成");
                }
                return false;
            }
        });

        Request request = null;
        OkHttpClient client=new OkHttpClient();
        try {
            request = new Request.Builder().url( imageURL ).build();
        } catch (IllegalArgumentException e) {
            Toast.makeText(MainActivity.this, "无效的URL地址", Toast.LENGTH_SHORT).show();
            return;
        }
        imageView.setImageDrawable(null);
        title.setText("加载中.... ");

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Message message=handler.obtainMessage();
                if (response.isSuccessful()){
                    message.what=1;
                    message.obj=response.body().bytes();
                    handler.sendMessage(message);
                }else{
                    handler.sendEmptyMessage(0);
                }
            }
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(() -> {
                    title.setText("加载失败...");
                    imageView.setImageDrawable(null);
                });
            }
        });
    }

    /**
     * 加载图片(同步请求)
     * @param imageURL
     */
    public void showImageBySyn(String imageURL){
        imageView.setImageDrawable(null);
        new Thread(new Runnable() {
            @Override
            public void run() {
                OkHttpClient client = new OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).build();
                Request request = null;
                try {
                    request = new Request.Builder().url( imageURL ).build();
                } catch (IllegalArgumentException e) {
                    Toast.makeText(MainActivity.this, "无效的URL地址", Toast.LENGTH_SHORT).show();
                    return;
                }
                runOnUiThread(() -> title.setText("同步方式, 加载中..."));
                Call call = client.newCall(request);
                try {
                    Response response = call.execute();
                    byte[] result = response.body().bytes();
                    Bitmap bitmap=BitmapFactory.decodeByteArray(result,0,result.length);
                    runOnUiThread(() -> {
                        imageView.setImageBitmap(bitmap);
                        title.setText("加载完成");
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * URL输入对话框
     * 通过输入URL显示相应的图片
     **/
    public void showInputDialog() {
        View view = LayoutInflater.from(this).inflate(R.layout.dialog_input_url, null, false);
        final AlertDialog dialog = new AlertDialog.Builder(this).setView(view).create();
        Button getImage = view.findViewById(R.id.get_image);
        Button cancelBtn = view.findViewById(R.id.cancel);
        getImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText contentText = view.findViewById(R.id.url_text);
                String URL = contentText.getText().toString().trim();
                showImage(URL);
                dialog.dismiss();
            }
        });
        cancelBtn.setOnClickListener(v -> dialog.dismiss());
        dialog.show();
    }
}

实现效果图

JVM的学习笔记

JVM知识点

下载 JVM知识点.md (11.4 KB)

Java源代码 -> 编译成.class字节码 ->由JVM解释字节码 -> 底层执行机器码

JVM内存模型

线程私有:{
    虚拟机栈:每个方法在执行的同时都会创建一个 栈帧{局部变量表、操作数栈、动态链接、方法出口}
    本地方法栈:native方法,非java实现的底层代码,如c/c++
    程序计数器(PC寄存器):是当前线程所执行字节码的行号指示器,控制程序执行顺序、循环等
}

线程共享:{
    堆:{
       创建的对象和数组都保存在这,也是垃圾收集器主要回收的区域。
       目前采用分代收集算法,从GC的角度分析还可细分为:(新生代、老年代,占堆的比例1:3)
       新生代:{
            (Eden区、From Survivor,To Survivor区)存储比例8:1:1,小数据,生存周期短
             Eden区:Java新对象的出生地,当此区域内存不足时会触发MinorGC,对新生代进行垃圾回收
             From区:上一次GC 的幸存者,作为这一次的GC的被扫描者
             To区  : 保留了一次 MinorGC 过程中的幸存者。
             MinorGC采用复制算法:
             1.将Eden和From区域中存活的对象复制到To区域,同时把这些对象的年龄+1,如果年龄达到了(15)
               老年代的标准,则放入老年代。
             2.清空eden和From区
             3.to和from互换,to成为下次GC的from区

       }
       老年代:{
            较大的数据对象,生命周期长,老年代对象比较稳定,采用MajorGC且不会频繁执行
            采用标记清除算法:首先扫描一次所有老年代,标记处存活的对象,然后回收没有标记的对象
            如果老年待业装满了装不下的时候,会抛出OOM异常
       }  
    }

    方法区:{
       用于存储JVM加载的类信息、常量、静态变量、即时编译后的代码等数据
       运行时的常量池是方法区的一部分,用于存储编译期间生成的各种字面量和符号引用,类加载后放入。
       永久代:指内存永久保存的区域,主要存放Class和Meta元数据,GC不会在程序运行期间对永久代进行清理,
       所以当加载的class越来越多时,可能抛出OOM,在Java8中,永久代已被出,被“元数据区”所代替,元空间不       在虚拟机中,而是使用本地内存,因此,默认情况下元空间的大小受本地内存限制,类的元数据放入本地存储,
       字符串和类的静态变量方法堆中,有效减少OOM情况。
    }

}

垃圾回收算法

如何确定一个垃圾?{(重点)
    1.引用计数法:Java中引用和对象都有关联的,如果要操作对象则必须用引用进行,所以可用个计数器判断一个
      对象是否可用回收,如果计数为0,则回收,有循环引用,造成无法回收的弊端。
    2.可达性分析:为了解决循环引用问题,通过“GC roots”对象作用起点搜索,如果在root 和一个对象之间没有
      可达的路径,则表示此对象可被回收,不过要经历两次标记过程才能回收。
}

标记清除算法(Mark-Sweep):{
    最基础的垃圾回收算法,分为两阶段,标记和清除,标记处所有需要回收的对象,再进行清除;
    缺点:效率偏低、内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题
    适用场景:适合在老年代进行垃圾回收,比如CMS收集器就是采用该算法进行回收的;
}

复制算法:{
    可解决内存碎片化严重的情况,将可用内存分为两块,每次只是用其中的一块,当这块内存用完了,将还存活的对象
    复制到另一块中去,将当前使用过的内存一次性清理掉,效率较高。
    缺点:可用内存被压缩到了原来的一半,且存活对象增多的话,效率会降低;
    适用场景:
        适合新生代区,新生代中只有少量对象存活只要付出少量复制成本即可收集完成,serial new,parallel 
        new和scanvage收 集器采用此算法。
}

标记整理算法(Mark-Compact):{
    分为标记和整理两阶段,综合以上两种算法,设计的更精妙的算法,标记阶段和Mark-sweep算法相同,但标记后不    清除,而是将存活的对象移动至内存的一端,然后清除端边界的对象。
    适用于老年代,GC 和serial old收集器采用此算法,因为对象的存活率高,不必进行内存复制,直接腾出空闲内存
}

分代收集:{
    根据不同的对象生存周期,将内存划分为几块,然后对其采用合适的收集算法。
    分为新生代和老年代,新生代特点是每次垃圾回收都需要回收大量的垃圾,老年代则只有少量的垃圾需要回收。
    新生代:采用复制算法
    老年代:采用标记整理算法
}

Java中四种引用类型

强引用:{
    把一个对象赋值给一个引用变量(Test str = new Test();),这就是强引用,它处于可达状态,它是不可能
    被回收的,即使永远不会被用到也不回收,这也内存泄漏的主要原因之一。
}

软引用:需要用SoftReference 类实现,当系统内存足够时它不会被回收,不足则回收。
弱引用:需要用WeakReference 类实现,它比软引用生存期更短,只要垃圾回收机制一运行,就会回收它。
虚引用:用PhantomReference类实现,不能单独使用,必须和引用队列联合使用,常用于跟踪垃圾回收状态。

垃圾回收器

Serial:{
    最基本的垃圾收集器,使用复制算法,单线程,简单高效,垃圾收集的同时,必须暂停其它所用的工作现场,直到
    收集结束,对于单个CPU环境来说,没有线程交互的开销,可获得更高的效率,是java虚拟机在Client模式下默
    认的新生代收集器。
}

ParNew:{
    是Serial收集器的多线程版本,也使用复制算法,使用多线程进行回收,收集过程中通用也要暂停其它工作线程
    是很多java虚拟机在Server模式下的新生代默认垃圾收集器。
}

Parallel Scavenge:{
    多线程,使用复制算法、高效,也是也新生代收集器,高吞吐量提交CPU利用率。使用与后台运算而不需要太多交
    互的任务,尽快的完成程序的运算任务。
}

Serial Old:{
    Serial 的老年代版本,使用标记整理算法,作为老年代使用CMS收集齐的备用方案。
}

Parallel Old:{
    Parallel Scavenge 的老年代版本,使用多线程的标记整理算法,为了在老年代同样提供吞吐量优先的垃圾
    收集器,如系统要求吞吐量较高,可使用新生代Parallel Scavenge和Parallel Old 搭配使用。
}

CMS收集器(重点):{
    Concurrent mark sweep(CMS)是一种老年代收集器,其最主要的目标是获取最短的垃圾回收停顿时间,和其它
    年老代回收器不同,它使用多线程的标记-清楚算法,最短的垃圾收集停顿时间的程序可提高用户体验。
    CMS工作四阶段:{
        初始标记:标记下GC Roots能直接关联的对象,速度很快
        并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,无需暂停工作线程
        重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
        并发清除:
               清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
               由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,
               所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行
    }
}

G1收集器(重点):{
    Garbage first是目前垃圾回收器理论发展的最前言成果,相比CMS收集器,G1收集器两个最突出的改进是:
    1.基于标记-整理算法,不产生内存碎片。
    2.精准控制回收停顿时间,在不牺牲吞吐量的前提下,实现高效垃圾回收。
     G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域
     的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾
     最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收
     集效率
}

JVM类加载机制

JVM类加载,分为五部分:加载、验证、准备、解析、初始化;

加载:这个阶段会在内存中生成一个代表这个类的class对象。

验证:是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
     public static int a = 123; 这是在准备阶段是赋值是0而不是123;
     public static final int b = 123; 这个会被赋值成123;

解析:段是指虚拟机将常量池中的符号引用替换为直接引用的过程,符号引用就是 class 文件中的CONSTANT

初始化:初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载
       器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
注意以下几种情况不会执行类初始化:
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4. 通过类名获取 Class 对象,不会触发类的初始化。
5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这    个参数是告诉虚拟机,是否要对类进行初始化。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

类加载器

三种加载器:
启动类加载器(Bootstrap ClassLoader)
    负责加载 JAVA_HOME\lib 目录中的xx.jar

扩展类加载器(Extension ClassLoader)
    负责加载 JAVA_HOME\lib\ext 目录中的

应用程序类加载器(Application ClassLoader):{
    负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,
    当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
}
双亲委派是什么?
    当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父
类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,
只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的
Class),子类加载器才会尝试自己去加载。
    采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载
器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载
器最终得到的都是同样一个 Object 对象。

Java多线程、并发常见知识点

Java线程的状态

1.新建(New)
2.运行(Runnable)
3.阻塞(Blocked)
4.等待(Waiting)
5.超时等待(Timed_Waiting)
6.终止(Terminated)

Java线程中将 就绪(ready)和运行中(running)合并为 “运行(Runnable)”

创建线程的方式:

1.继承Thread类
2.实现Runnable接口
3.实现Callable接口(支持泛型返回值,执行后获取Future,再调用get方法获取返回值)
4.基于线程池的方式(用Executors创建)

 为什么要使用多线程呢?
 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。
 多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能,提高 CPU 和 IO 设备的综合利用率。

 线程和进程有什么区别?
 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
 开销:线程开销比进程小,是个包含关系,进程中至少要有1个及以上线程。
 内存方面:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

常用方法

start() : 开始执行线程,拿到时间片后自动调用run方法
run(): 普通的执行方法
sleep(): 静态方法,使线程睡眠,可指定时间,不释放锁
yield(): 线程让步,让出当前执行权进入就绪态,有可能下一轮还会抢到时间片继续运行
join(): 使当前线程等待另一个线程执行完毕后再执行,不释放锁,有点插队的意思
wait(): Object的方法,进入等待队列。依靠唤醒或者timeout时间到自动唤醒,会释放锁
currentThread(): 返回当前线程

sleep与wait区别? 
1.sleep方法属于Thread,wait方法属于Object
2.调用sleep方法不会释放锁;wait会释放锁,使得其他线程可以使用同步控制块或者方法
3.wait只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
4.sleep必须捕获异常,而wait不需要捕获异常

Java锁

乐观锁:
是一种乐观思想,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但在更新的时候回判断一下在此期间是否有别人修改了这个数据,如果一样则更新,不一样则重复执行 读-比较-写的操作;java中的乐观锁基本都是通过CAS(Compare and swap)操作实现的,有个缺点就是会导致ABA问题,解决方式是加版本号。乐观锁适合读多写少的场景。

悲观锁:
是一种悲观思想,每次拿数据都认为别人会修改,认为别人会它竞争,所以每次在读写的时候都会上锁,这样别人想读就会被阻塞,java中的悲观锁典型的就是synchronized,但AQS(Abstract Queue Synchronized)的锁则是先尝试CAS乐观锁去获取锁,如获取不到才会转化为悲观锁,如RetreenLock;

自旋锁:重复执行操作,可减少线程的阻塞,适用于锁的竞争不激烈且锁占用时间非常短的代码块,可提升性能,如果长时间获取不到锁,等于一直在做无用功,例如CAS的失败重复尝试
Synchronized 同步锁,非公平锁,保证原子性、可见性、有序性=
作用于普通方法时,锁住的是对象的实例(this);
作用于静态方法锁住的是类;
作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

Jdk1.6后的锁升级
分为:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
随着锁的竞争程度,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,是中锁优化策略;

在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
ReentrantLock和synchronized区别?
1.使用synchronized,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
2.synchronized是非公平锁,ReentrantLock可以设置为公平锁,构造函数传入ture。
3.ReentrantLock 是 API 代码级别的,synchronized 是 JVM 级别的
4.ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
5.ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
6.ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
7.ReentrantLock的功能更多
volatile和synchronized的区别是什么?
1.volatile 只能使用在单个变量上;而synchronized可以在类,变量,方法和代码块上。
2.volatile 保证可见性、禁止指令重排;synchronized保证原子性、可见性、有序性。
3.volatile 禁用指令重排序;synchronized不会。
4.volatile 不会造成阻塞;synchronized会。
5.volatile 不保证原子性,所以线程不安全;

为啥synchronized无法禁止指令重排,但可以保证有序性?
加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的
什么是CAS?
CAS全称 Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS 在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock 内部的 AQS 和原子类内部都使用了 CAS。

CAS算法涉及到三个操作数:
需要读写的内存值 V。
进行比较的值 A。
要写入的新值 B。
只有当 V 的值等于 A 时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。

线程死锁

什么是死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁的四个必要条件:互斥、占有且等待、不可剥夺、循环等待
避免死锁的方法,破坏其中一个条件即可(但互斥是不能破坏的):
一次性申请所有资源,破坏“占有且等待”
占有部分资源的线程进一步申请其它资源时,如果申请不到,主动释放它的占有的资源,破坏“不可剥夺”
按顺序申请资源,破坏“循环等待”条件

ThreadLocal

线程本地变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,内部是ThreadLocalMap,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。
 适用场景:
 线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
 每个线程需要有自己单独的实例,且需要在多个方法共享实例,即同时满足实例在线程间的隔离与方法间的共享。比如Java web应用中,每个线程有自己单独的 Session 实例,就可以使用ThreadLocal来实现,例如JDBC的数据库连接池;

 ThreadLocal内存泄漏的原因?
每个Thread都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。GC的时候会⾃动回收key,而value不会,数据量大后,value就有可能越来越多且无法释放,最终导致内存泄漏。
解决⽅法:每次使⽤完ThreadLocal就调⽤它的remove()⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。

线程池

线程池参数
corePoolSize 核心线程数
maximumPoolSize 最大线程数
workQueue 存储等待运行的任务,缓冲队列
keepAliveTime 非核心线程空闲时的存活时间
TimeUnit 时间单位
ThreadFactory 线程的创建工厂
RejectedExecutionHandler 拒绝策略,当队列和线程池都满
    AbortPolicy:默认的策略,直接抛出异常
    DiscardPolicy: 不处理,直接丢弃
    DiscardOldestPolicy: 将等待队列的首任务丢弃并执行当前任务
    CallerRunsPolicy: 由调用线程处理该任务
使用Executors创建线程的弊端:任务很多时可能会导致OOM
4种线程池
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程。适用场景:快速处理大量耗时较短的任务

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newScheduledThreadPool 创建一个周期线程池,支持定时及周期性任务执行,定时任务等

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
AQS组件

Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

看书啦,代码人的书单,PDF版!

疫情期间,待在家中或宿舍上网课?学校的图书馆闭馆了?游戏打麻了感觉无聊? 看视频学的慢? 想找些电子版的书看看?

那么我们来看看书吧,pdf版的书籍,手机电脑平板都可方便看,不过纸质的书看着确实更舒服,但是要去图书馆借或者说 书店花钱买,所以目前随时随地看免费的电子版是个不错的选择^.^


那我来推荐些pdf版书籍吧, 哈哈哈 ,Gitee上发现了个良心的宝藏“书库”!覆盖了大部分的计算机理论基础知识、Java编程技术类等书籍,全部都是免费!

https://gitee.com/tysondai/java-books (因政策管控暂时失效)

https://github.com/Tyson0314/java-books#%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0(可访问,但是外网)

所以上github吧,访问不了的,你懂的自己想办法哈哈哈哈

↑ 点击上方链接,找到你想看的书,下载好后, 解压压缩包即可,解压密码是 1010

有这些类别的书 ↓