月份: 2020 年 3 月

  • 三個月(敏捷)項目收穫

    三個月(敏捷)項目收穫

     

     

    項目背景

    客戶已有運行多年的官網老站(PC端),想在今年對老站進行一次UI全面更新、功能全部平移的升級,對接新的運營後端,然後建立官網小程序端且與官網PC端進行聯動,使得品牌自有渠道能夠更加全面化。

     

    挑戰

    • 時間緊。五月份進行Inception Workshop,確定項目交付範圍與架構方案。官網六月初開始開發,小程序八月份開始開發,整個項目九月中旬必須上線。
    • 系統集成和數據遷移。系統需要對接客戶的CRM,對接3個服務商,需要對老官網歷史數據(訂單、會員等)進行遷移。
    • 多團隊溝通。小程序設計稿由第三方提供,因此多出了溝通、確認的時間,以及把控第三方交付的時間,以避免交付進度的影響。

     

    迭代計劃

    Inception Workshop一結束,差不多就開始整理整個項目涉及的故事和技術卡,按照兩周一迭代進行迭代計劃安排並與客戶確認,每個迭代第一周周三安排跟客戶showcase上一周的預定的交付結果,得到反饋並安排進行改進。官網項目比較順利,改造自定義了一下SSR框架就能開始進行開發,並且因為歷史原因,還能享受到上一個項目遺留的一些福利,當然也少不了一些坑。

    小程序的時間比較緊,相當於整個複製了一遍官網的功能,主要是前端任務,後端可以復用官網後端,因此一開始就給團隊同學同步到整個項目的情況,讓大家有一個大概的心理準備。然後就是與官網類似的處理,整個交付內容進行迭代排期並與客戶確認,前期盡量能多做一些,避免後期怎麼努力都無法完成的囧鏡。

     

    項目進行時

    整個項目的過程中,PM會根據迭代完成情況靈活的找外援加入項目進行支援,以免交付延期。每日的站會(Standup Meeting)更新,讓團隊能對當前進度有一個大概的了解以及同步一些突發信息。定期的回顧會議(Retrospective Meeting)能暴露團隊內部問題,將風險扼殺於苗頭,鼓勵能為團隊帶來正向幫助的行為,及時停止不好的做法。

    迭代會議(IPM)能讓團隊對下一個迭代具體要做的事情有一個詳細的了解,進行大致的估點,以便check開發進度情況。技術人員定期的CodeReview成為一個大家交流的時段,發現風險,指出問題,互相提高,還可以幫助新人快速的融入團隊。

    根據團隊內部人員情況,可以定期進行一對一溝通,了解個人訴求或是給與近況反饋都是一個不錯的渠道。TL應考慮團隊內部人員提升自己的訴求,在一些安排上給與傾斜和鼓勵,發現問題也需要提前制止。

     

    不足之處

    • 後期對卡牆(Jira)的管理鬆懈。導致有些問題反覆修改,且丟失context
    • 項目對運營後台有一些的定製化配置,沒有提前準備運營需要了解的後台操作資料和培訓,導致後期花費大量精力幫助運營進行後台配置與更新
    • 人員(QA)變動頻繁。公司處於高速發展階段,項目經歷了4個QA,因此有些context可能丟失,測試不到位,導致項目上線出了一些低級問題。比如上線后發現部分瀏覽器有支付兼容問題
    • 甲乙方定位太角色化,不能站在專業角度評估客戶需求(項目做完感覺都一樣,客戶是爸爸)
    • 與第三方合作交付產物管控不到位,導致第三方設計稿的延遲影響到我們的交付計劃
    • 與客戶溝通的需求,後面有一些沒有進行郵件確認,導致交付驗收階段因一些需求上的問題產生不愉快(這個完全沒必要的)
    • 對第三方系統的了解不充分和集成系統的需求整理不清晰導致後續一系列的開發、測試都不到位,以致上線出了不可控的問題

     

    項目總結

    • 提前評估項目的風險點,且在項目進行過程中持續維護,後期安排足夠的時間進行調研與分析
    • 與第三方合作一定要有自己的規劃,並且將定好的規劃提前與第三方確認時間,然後派人提前專門細緻的了解第三方需求詳細點,確定好具體的業務場景,再來規劃己方與第三方的具體集成的點。此外,在進行的過程中,還應注意定時檢查合作進度,管控風險
    • 與客戶溝通的所有需求都要進行郵件的二次確認,一個是能夠對所有需求來源有所記錄,另一個是能避免後面的不必要的消耗
    • 開發管理不能鬆懈,盡量做到所有的改動都能有卡,能夠進行追溯
    • 對後期交付時所需要的資料提前準備,對需要進行培訓的人員提前約好時間進行溝通培訓

    在前端這塊的管理上,做的還不夠。前期經常codereview,然後效果都還不錯,讓我有了一些錯覺就是當前團隊趨於穩定,中後期即便是加班比較多,大家氣氛這塊我覺得都還好。不過在項目後期的時候,有些疏於管理,然後大家有些人也被分配到了其他項目,和同事們的交流不夠,沒有及時的顧及到一些個人情緒,這塊是可以加強的。

    作為一個Lead,不論同事是否還在一個項目都應該及時的去了解近況,給與自己力所能及的幫助,這樣才能產生向心力,以幫助一些比較迷茫的同學找到方向,看見燈塔。

    整個項目時間不長,得失還是挺多的,不論是管理還是技術上,都會有一些心得。然後項目的ROI還不錯,得到公司領導的肯定,最後客戶那邊的反饋也還不錯,算是對大家努力的一種認可。

    ps: 及時總結,靜心沉澱;如風少年,砥礪前行。

    如想了解更多,請移步

    歡迎關注我的公眾號 “和F君一起xx”

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 附009.Kubernetes永久存儲之GlusterFS獨立部署

    附009.Kubernetes永久存儲之GlusterFS獨立部署

    一 前期準備

    1.1 基礎知識


    Heketi提供了一個RESTful管理界面,可以用來管理GlusterFS卷的生命周期。Heketi會動態在集群內選擇bricks構建所需的volumes,從而確保數據的副本會分散到集群不同的故障域內。同時Heketi還支持任意數量的ClusterFS集群。

    提示:本實驗基於glusterfs和Kubernetes分開部署,heketi管理glusterfs,Kubernetes使用heketi提供的API,從而實現glusterfs的永久存儲,,而非Kubernetes部署glusterfs。

    1.2 架構示意






    提示:本實驗Heketi僅管理單zone的glusterfs集群。

    1.3 相關規劃

































    主機 IP 磁盤 備註
    servera 172.24.8.41 sdb glusterfs節點
    serverb 172.24.8.42 sdb glusterfs節點
    serverc 172.24.8.43 sdb glusterfs節點
    heketi 172.24.8.44 Heketi主機










































    servera serverb serverc
    PV sdb1 sdb1 sdb1
    VG vg0 vg0 vg0
    LV datalv datalv datalv
    bricks目錄 /bricks/data /bricks/data /bricks/data

    1.4 其他準備


    所有節點NTP配置;

    所有節點添加相應主機名解析:

    172.24.8.41 servera

    172.24.8.42 serverb

    172.24.8.43 serverc

    172.24.8.44 heketi

    注意:若非必要,建議關閉防火牆和SELinux。

    二 規劃相應存儲卷

    2.1 劃分LVM

      1 [root@servera ~]# fdisk /dev/sdb				#創建lvm的sdb1,過程略
      2 [root@servera ~]# pvcreate /dev/sdb1			#使用/dev/vdb1創建PV
      3 [root@servera ~]# vgcreate vg0 /dev/sdb1			#創建vg
      4 [root@servera ~]# lvcreate -L 15G -T vg0/thinpool		#創建支持thin的lv池
      5 [root@servera ~]# lvcreate -V 10G -T vg0/thinpool -n datalv	#創建相應brick的lv
      6 [root@servera ~]# vgdisplay					#驗證確認vg信息
      7 [root@servera ~]# pvdisplay					#驗證確認pv信息
      8 [root@servera ~]# lvdisplay					#驗證確認lv信息



    提示:serverb、serverc類似操作,根據規劃需求創建完所有基於LVM的brick。

    三 安裝glusterfs

    3.1 安裝相應RPM源

      1 [root@servera ~]# yum -y install centos-release-gluster


    提示:serverb、serverc、client類似操作,安裝相應glusterfs源;

    安裝相應源之後,會在/etc/yum.repos.d/目錄多出文件CentOS-Storage-common.repo,內容如下:

      1 # CentOS-Storage.repo
      2 #
      3 # Please see http://wiki.centos.org/SpecialInterestGroup/Storage for more
      4 # information
      5 
      6 [centos-storage-debuginfo]
      7 name=CentOS-$releasever - Storage SIG - debuginfo
      8 baseurl=http://debuginfo.centos.org/$contentdir/$releasever/storage/$basearch/
      9 gpgcheck=1
     10 enabled=0
     11 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-Storage


    3.2 安裝glusterfs

      1 [root@servera ~]# yum -y install glusterfs-server


    提示:serverb、serverc類似操作,安裝glusterfs服務端。

    3.3 啟動glusterfs

      1 [root@servera ~]# systemctl start glusterd
      2 [root@servera ~]# systemctl enable glusterd



    提示:serverb、serverc類似操作,所有節點啟動glusterfs服務端;

    安裝完glusterfs之後建議exit退出終端重新登錄,從而可以補全glusterfs相關命令。

    3.4 添加信任池

      1 [root@servera ~]# gluster peer probe serverb
      2 peer probe: success.
      3 [root@servera ~]# gluster peer probe serverc
      4 peer probe: success.
      5 [root@servera ~]# gluster peer status		#查看信任池狀態
      6 [root@servera ~]# gluster pool list			#查看信任池列表




    提示:加信任池的操作,只需要在servera、serverb、serverc所有集群節點主機中的任意一台上面執行添加其他三個節點的操作即可。

    提示:若未關閉防火牆,在添加信任池之前必須放通防火牆相應規則,操作如下:

      1 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=glusterfs
      2 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=nfs
      3 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=rpc­bind
      4 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=mountd
      5 [root@servera ~]# firewall­cmd ­­permanent ­­add­port=5666/tcp
      6 [root@servera ~]# firewall­cmd ­­reload


    四 部署Heketi

    4.1 安裝heketi服務

      1 [root@heketi ~]# yum -y install centos-release-gluster
      2 [root@heketi ~]# yum -y install heketi heketi-client


    4.2 配置heketi

      1 [root@heketi ~]# vi /etc/heketi/heketi.json
      2 {
      3   "_port_comment": "Heketi Server Port Number",
      4   "port": "8080",					#默認端口
      5 
      6   "_use_auth": "Enable JWT authorization. Please enable for deployment",
      7   "use_auth": true,					#基於安全考慮開啟認證
      8 
      9   "_jwt": "Private keys for access",
     10   "jwt": {
     11     "_admin": "Admin has access to all APIs",
     12     "admin": {
     13       "key": "admin123"					#管理員密碼
     14     },
     15     "_user": "User only has access to /volumes endpoint",
     16     "user": {
     17       "key": "xianghy"					#普通用戶
     18     }
     19   },
     20 
     21   "_glusterfs_comment": "GlusterFS Configuration",
     22   "glusterfs": {
     23     "_executor_comment": [
     24       "Execute plugin. Possible choices: mock, ssh",
     25       "mock: This setting is used for testing and development.",	#用於測試
     26       "      It will not send commands to any node.",
     27       "ssh:  This setting will notify Heketi to ssh to the nodes.",	#ssh方式
     28       "      It will need the values in sshexec to be configured.",
     29       "kubernetes: Communicate with GlusterFS containers over",		#在GlusterFS由kubernetes創建時採用
     30       "            Kubernetes exec api."
     31     ],
     32     "executor": "ssh",
     33 
     34     "_sshexec_comment": "SSH username and private key file information",
     35     "sshexec": {
     36       "keyfile": "/etc/heketi/heketi_key",
     37       "user": "root",
     38       "port": "22",
     39       "fstab": "/etc/fstab"
     40     },
     41 ……
     42 ……
     43     "loglevel" : "warning"
     44   }
     45 }


    4.3 配置免秘鑰

      1 [root@heketi ~]# ssh-keygen -t rsa -q -f /etc/heketi/heketi_key -N ""
      2 [root@heketi ~]# chown heketi:heketi /etc/heketi/heketi_key
      3 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@servera
      4 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@serverb
      5 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@serverc


    4.4 啟動heketi

      1 [root@heketi ~]# systemctl enable heketi.service
      2 [root@heketi ~]# systemctl start heketi.service
      3 [root@heketi ~]# systemctl status heketi.service
      4 [root@heketi ~]# curl http://localhost:8080/hello		#測試訪問
      5 Hello from Heketi


    4.5 配置Heketi拓撲


           拓撲信息用於讓Heketi確認可以使用的存儲節點、磁盤和集群,必須自行確定節點的故障域。故障域是賦予一組節點的整數值,這組節點共享相同的交換機、電源或其他任何會導致它們同時失效的組件。必須確認哪些節點構成一個集群,Heketi使用這些信息來確保跨故障域中創建副本,從而提供數據冗餘能力,Heketi支持多個Gluster存儲集群。

    配置Heketi拓撲注意以下幾點:

    • 可以通過topology.json文件定義組建的GlusterFS集群;
    • topology指定了層級關係:clusters –> nodes –> node/devices –> hostnames/zone;
    • node/hostnames字段的manage建議填寫主機ip,指管理通道,注意當heketi服務器不能通過hostname訪問GlusterFS節點時不能填寫hostname;
    • node/hostnames字段的storage建議填寫主機ip,指存儲數據通道,與manage可以不一樣,生產環境管理網絡和存儲網絡建議分離;
    • node/zone字段指定了node所處的故障域,heketi通過跨故障域創建副本,提高數據高可用性質,如可以通過rack的不同區分zone值,創建跨機架的故障域;
    • devices字段指定GlusterFS各節點的盤符(可以是多塊盤),必須是未創建文件系統的裸設備。

      1 [root@heketi ~]# vi /etc/heketi/topology.json
      2 {
      3     "clusters": [
      4         {
      5             "nodes": [
      6                 {
      7                     "node": {
      8                         "hostnames": {
      9                             "manage": [
     10                                 "172.24.8.41"
     11                             ],
     12                             "storage": [
     13                                 "172.24.8.41"
     14                             ]
     15                         },
     16                         "zone": 1
     17                     },
     18                     "devices": [
     19                         "/dev/mapper/vg0-datalv"
     20                     ]
     21                 },
     22                 {
     23                     "node": {
     24                         "hostnames": {
     25                             "manage": [
     26                                 "172.24.8.42"
     27                             ],
     28                             "storage": [
     29                                 "172.24.8.42"
     30                             ]
     31                         },
     32                         "zone": 1
     33                     },
     34                     "devices": [
     35                         "/dev/mapper/vg0-datalv"
     36                     ]
     37                 },
     38                 {
     39                     "node": {
     40                         "hostnames": {
     41                             "manage": [
     42                                 "172.24.8.43"
     43                             ],
     44                             "storage": [
     45                                 "172.24.8.43"
     46                             ]
     47                         },
     48                         "zone": 1
     49                     },
     50                     "devices": [
     51                         "/dev/mapper/vg0-datalv"
     52                     ]
     53                 }
     54             ]
     55         }
     56     ]
     57 }
     58 
     59 [root@heketi ~]# echo "export HEKETI_CLI_SERVER=http://heketi:8080" >> /etc/profile.d/heketi.sh
     60 [root@heketi ~]# echo "alias heketi-cli='heketi-cli --user admin --secret admin123'" >> .bashrc
     61 [root@heketi ~]# source /etc/profile.d/heketi.sh
     62 [root@heketi ~]# source .bashrc
     63 [root@heketi ~]# echo $HEKETI_CLI_SERVER
     64 http://heketi:8080
     65 [root@heketi ~]# heketi-cli --server $HEKETI_CLI_SERVER --user admin --secret admin123 topology load --json=/etc/heketi/topology.json




    4.6 集群管理

      1 [root@heketi ~]# heketi-cli cluster list					#集群列表
      2 [root@heketi ~]# heketi-cli cluster info aa83b0045fafa362bfc7a8bfee0c24ad	#集群詳細信息
      3 Cluster id: aa83b0045fafa362bfc7a8bfee0c24ad
      4 Nodes:
      5 189ee41572ebf0bf1e297de2302cfb39
      6 46429de5666fc4c6cc570da4b100465d
      7 be0209387384299db34aaf8377c3964c
      8 Volumes:
      9 
     10 Block: true
     11 
     12 File: true
     13 [root@heketi ~]# heketi-cli topology info aa83b0045fafa362bfc7a8bfee0c24ad	#查看拓撲信息




      1 [root@heketi ~]# heketi-cli node list						#卷信息
      2 Id:189ee41572ebf0bf1e297de2302cfb39     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      3 Id:46429de5666fc4c6cc570da4b100465d     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      4 Id:be0209387384299db34aaf8377c3964c     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      5 [root@heketi ~]# heketi-cli node info 189ee41572ebf0bf1e297de2302cfb39		#節點信息
      6 [root@heketi ~]# heketi-cli volume create --size=2 --replica=2			#默認為3副本的replica模式



      1 [root@heketi ~]# heketi-cli volume list						#卷信息
      2 [root@heketi ~]# heketi-cli volume info 7da55685ebeeaaca60708cd797a5e391
      3 [root@servera ~]# gluster volume info						#通過glusterfs節點查看


    4.7 測試驗證

      1 [root@heketi ~]# yum -y install centos-release-gluster
      2 [root@heketi ~]# yum -y install glusterfs-fuse					#安裝glusterfs-fuse
      3 [root@heketi ~]# mount -t glusterfs 172.24.8.41:vol_7da55685ebeeaaca60708cd797a5e391 /mnt



      1 [root@heketi ~]# umount /mnt
      2 [root@heketi ~]# heketi-cli volume delete 7da55685ebeeaaca60708cd797a5e391	#驗證完畢刪除



    參考:https://www.jianshu.com/p/1069ddaaea78

    https://www.cnblogs.com/panwenbin-logs/p/10231859.html

    五 Kubernetes動態掛載glusterfs

    5.1 StorageClass動態存儲


    kubernetes共享存儲provider模式:

    靜態模式(Static):集群管理員手工創建PV,在定義PV時設置後端存儲的特性;

    動態模式(Dynamic):集群管理員不需要手工創建PV,而是通過StorageClass的設置對後端存儲進行描述,標記為某種”類型(Class)”;此時要求PVC對存儲的類型進行說明,系統將自動完成PV的創建及與PVC的綁定;PVC可以聲明Class為””,說明PVC禁止使用動態模式。

    基於StorageClass的動態存儲供應整體過程如下圖所示:


    1. 集群管理員預先創建存儲類(StorageClass);
    2. 用戶創建使用存儲類的持久化存儲聲明(PVC:PersistentVolumeClaim);
    3. 存儲持久化聲明通知系統,它需要一個持久化存儲(PV: PersistentVolume);
    4. 系統讀取存儲類的信息;
    5. 系統基於存儲類的信息,在後台自動創建PVC需要的PV;
    6. 用戶創建一個使用PVC的Pod;
    7. Pod中的應用通過PVC進行數據的持久化;
    8. 而PVC使用PV進行數據的最終持久化處理。


    提示:關於Kubernetes的部署參考《附003.Kubeadm部署Kubernetes》。

    5.2 定義StorageClass


    關鍵字說明:

    • provisioner:表示存儲分配器,需要根據後端存儲的不同而變更;
    • reclaimPolicy: 默認即”Delete”,刪除pvc后,相應的pv及後端的volume,brick(lvm)等一起刪除;設置為”Retain”時則保留數據,若需刪除則需要手工處理;
    • resturl:heketi API服務提供的url;
    • restauthenabled:可選參數,默認值為”false”,heketi服務開啟認證時必須設置為”true”;
    • restuser:可選參數,開啟認證時設置相應用戶名;
    • secretNamespace:可選參數,開啟認證時可以設置為使用持久化存儲的namespace;
    • secretName:可選參數,開啟認證時,需要將heketi服務的認證密碼保存在secret資源中;
    • clusterid:可選參數,指定集群id,也可以是1個clusterid列表,格式為”id1,id2”;
    • volumetype:可選參數,設置卷類型及其參數,如果未分配卷類型,則有分配器決定卷類型;如”volumetype: replicate:3”表示3副本的replicate卷,”volumetype: disperse:4:2”表示disperse卷,其中‘4’是數據,’2’是冗餘校驗,”volumetype: none”表示distribute卷


    提示:關於glusterfs各種不同類型的卷見《004.RHGS-創建volume》。

      1 [root@k8smaster01 ~]# kubectl create ns heketi		#創建命名空間
      2 [root@k8smaster01 ~]# echo -n "admin123" | base64		#將密碼轉換為64位編碼
      3 YWRtaW4xMjM=
      4 [root@k8smaster01 ~]# mkdir -p heketi
      5 [root@k8smaster01 ~]# cd heketi/
      6 [root@k8smaster01 ~]# vi heketi-secret.yaml			#創建用於保存密碼的secret
      7 apiVersion: v1
      8 kind: Secret
      9 metadata:
     10   name: heketi-secret
     11   namespace: heketi
     12 data:
     13   # base64 encoded password. E.g.: echo -n "mypassword" | base64
     14   key: YWRtaW4xMjM=
     15 type: kubernetes.io/glusterfs
     16 [root@k8smaster01 heketi]# kubectl create -f heketi-secret.yaml	#創建heketi
     17 [root@k8smaster01 heketi]# kubectl get secrets -n heketi
     18 NAME                  TYPE                                  DATA   AGE
     19 default-token-5sn5d   kubernetes.io/service-account-token   3      43s
     20 heketi-secret         kubernetes.io/glusterfs               1      5s
     21 [root@kubenode1 heketi]# vim gluster-heketi-storageclass.yaml	#正式創建StorageClass
     22 apiVersion: storage.k8s.io/v1
     23 kind: StorageClass
     24 metadata:
     25   name: gluster-heketi-storageclass
     26 parameters:
     27   resturl: "http://172.24.8.44:8080"
     28   clusterid: "aa83b0045fafa362bfc7a8bfee0c24ad"
     29   restauthenabled: "true"					#若heketi開啟認證此處也必須開啟auth認證
     30   restuser: "admin"
     31   secretName: "heketi-secret"				#name/namespace與secret資源中定義一致
     32   secretNamespace: "heketi"
     33   volumetype: "replicate:3"
     34 provisioner: kubernetes.io/glusterfs
     35 reclaimPolicy: Delete
     36 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-storageclass.yaml



    注意:storageclass資源創建后不可變更,如修改只能刪除后重建。

      1 [root@k8smaster01 heketi]# kubectl get storageclasses		#查看確認
      2 NAME                          PROVISIONER               AGE
      3 gluster-heketi-storageclass   kubernetes.io/glusterfs   85s
      4 [root@k8smaster01 heketi]# kubectl describe storageclasses gluster-heketi-storageclass




    5.3 定義PVC

      1 [root@k8smaster01 heketi]# cat gluster-heketi-pvc.yaml
      2 apiVersion: v1
      3 metadata:
      4   name: gluster-heketi-pvc
      5   annotations:
      6     volume.beta.kubernetes.io/storage-class: gluster-heketi-storageclass
      7 spec:
      8   accessModes:
      9   - ReadWriteOnce
     10   resources:
     11     requests:
     12       storage: 1Gi



    注意:accessModes可有如下簡寫:

    • ReadWriteOnce:簡寫RWO,讀寫權限,且只能被單個node掛載;
    • ReadOnlyMany:簡寫ROX,只讀權限,允許被多個node掛載;
    • ReadWriteMany:簡寫RWX,讀寫權限,允許被多個node掛載。

      1 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-pvc.yaml
      2 [root@k8smaster01 heketi]# kubectl get pvc
      3 [root@k8smaster01 heketi]# kubectl describe pvc gluster-heketi-pvc
      4 [root@k8smaster01 heketi]# kubectl get pv
      5 [root@k8smaster01 heketi]# kubectl describe pv pvc-5f7420ef-082d-11ea-badf-000c29fa7a79




      1 [root@k8smaster01 heketi]# kubectl describe endpoints glusterfs-dynamic-5f7420ef-082d-11ea-badf-000c29fa7a79




    提示:由上可知:PVC狀態為Bound,Capacity為1G。查看PV詳細信息,除容量,引用storageclass信息,狀態,回收策略等外,同時可知GlusterFS的Endpoint與path。EndpointsName為固定格式:glusterfs-dynamic-PV_NAME,且endpoints資源中指定了掛載存儲時的具體地址。

    5.4 確認查看


    通過5.3所創建的信息:

    • volume與brick已經創建;
    • 主掛載點(通信)在172.24.8.41節點,其餘兩個節點備選;
    • 三副本的情況下,所有節點都會創建brick。

      1 [root@heketi ~]# heketi-cli topology info			#heketi主機查看
      2 [root@serverb ~]# lsblk						#glusterfs節點查看
      3 [root@serverb ~]# df -hT					#glusterfs節點查看
      4 [root@servera ~]# gluster volume list				#glusterfs節點查看
      5 [root@servera ~]# gluster volume info vol_e4c948687239df9833748d081ddb6fd5




    5.5 Pod掛載測試

      1 [root@xxx ~]# yum -y install centos-release-gluster
      2 [root@xxx ~]# yum -y install glusterfs-fuse					#安裝glusterfs-fuse



    提示:所有需要使用glusterfs volume的Kubernetes節點都必須安裝glusterfs-fuse以便於正常掛載,同時版本需要和glusterfs節點一致。

      1 [root@k8smaster01 heketi]# vi gluster-heketi-pod.yaml
      2 kind: Pod
      3 apiVersion: v1
      4 metadata:
      5   name: gluster-heketi-pod
      6 spec:
      7   containers:
      8   - name: gluster-heketi-container
      9     image: busybox
     10     command:
     11     - sleep
     12     - "3600"
     13     volumeMounts:
     14     - name: gluster-heketi-volume			#必須和volumes中name一致
     15       mountPath: "/pv-data"
     16       readOnly: false
     17   volumes:
     18   - name: gluster-heketi-volume
     19     persistentVolumeClaim:
     20       claimName: gluster-heketi-pvc			#必須和5.3創建的PVC中的name一致
     21 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-pod.yaml -n heketi		#創建Pod


    5.6 確認驗證

      1 [root@k8smaster01 heketi]# kubectl get pod -n heketi | grep gluster
      2 gluster-heketi-pod          1/1     Running   0          2m43s
      3 [root@k8smaster01 heketi]# kubectl exec -it gluster-heketi-pod /bin/sh		#進入Pod寫入測試文件
      4 / # cd /pv-data/
      5 /pv-data # echo "This is a file!" >> a.txt
      6 /pv-data # echo "This is b file!" >> b.txt
      7 /pv-data # ls
      8 a.txt  b.txt
      9 [root@servera ~]# df -hT					#在glusterfs節點查看Kubernetes節點的測試文件
     10 [root@servera ~]# cd /var/lib/heketi/mounts/vg_47c90d90e03de79696f90bd94cfccdde/brick_721243c3e0cf8a2372f05d5085a4338c/brick/
     11 [root@servera brick]# ls
     12 [root@servera brick]# cat a.txt
     13 [root@servera brick]# cat b.txt




    5.7 刪除資源

      1 [root@k8smaster01 heketi]# kubectl delete -f gluster-heketi-pod.yaml
      2 [root@k8smaster01 heketi]# kubectl delete -f gluster-heketi-pvc.yaml
      3 [root@k8smaster01 heketi]# kubectl get pvc
      4 [root@k8smaster01 heketi]# kubectl get pv
      5 [root@servera ~]# gluster volume list
      6 No volumes present in cluster



    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • 正則表達式 第五篇:C# 正則表達式

    正則表達式 第五篇:C# 正則表達式

    本文整理C#正則表達式的元字符,正則表達式是由字符構成的表達式,每個字符代表一個規則,表達式中的字符分為兩種類型:普通字符和元字符。普通字符是指字面含義不變的字符,按照完全匹配的方式匹配文本,而元字符具有特殊的含義,代表一類字符。

    把文本看作是字符流,每個字符放在一個位置上,例如,正則表達式 “Room\d\d\d”,前面四個字符Room是普通字符,後面的字符\是轉義字符,和後面的字符d組成一個元字符\d,表示該位置上有任意一個数字。

    用正則表達式的語言來描述是:正則表達式 “Room\d\d\d”共捕獲7個字符,表示“以Room開頭、以三個数字結尾”的一類字符串,我們把這一類字符串稱作一個模式(Pattern),也稱作是一個正則。

    一,轉義字符

    轉義字符是\,把普通字符轉義為具有特殊含義的元字符,常用的轉義字符有:

    • \t:水平製表符
    • \v:垂直製表符
    • \r:回車
    • \n:換行
    • \\:表示字符 \,也就說,把轉義字符 \ 轉義為普通的字符 \
    • \”:表示字符 “,在C#中,雙引號用於定義字符串,字符串包含的雙引號用 \” 來表示

    二,字符類

    在進行正則匹配時,把輸入文本看作是有順序的字符流,字符類元字符匹配的對象是字符,並會捕獲字符。所謂捕獲字符是指,一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    常用的字符類元字符:

    • [ char_group]:匹配字符組中的任意一個字符
    • [^char_group]:匹配除字符組之外的任意一個字符
    • [first-last]:匹配從first到last的字符範圍中的任意一個字符,字符範圍包括first和last。
    • .   :通配符,匹配除\n之外的任意一個字符
    • \w:匹配任意一個單詞(word)字符,單詞字符通常是指A-Z、a-z和0-9
    • \W:匹配任意一個非單詞字符,是指除A-Z、a-z和0-9之外的字符
    • \s:匹配任意一個空白字符
    • \S:匹配任意一個非空白字符
    • \d:匹配任意一個数字字符
    • \D:匹配任意一個非数字字符

    注意,轉義字符也屬於字符類元字符,在進行正則匹配時,也會捕獲字符。

    三,定位符

    定位符匹配(或捕獲)的對象是位置,它根據字符的位置來判斷模式匹配是否成功,定位符不會捕獲字符,是零寬的(寬度為0),常用的定位符有:

    • ^:默認情況下,匹配字符串的開始位置;在多行模式下,匹配每行的開始位置;
    • $:默認情況下,匹配字符串的結束位置,或 字符串結尾的\n之前的位置;在多行模式下,匹配每行結束之前的位置,或者每行結尾的\n之前的位置。
    • \A:匹配字符串的開始位置;
    • \Z:匹配字符串的結束位置,或 字符串結尾的\n之前的位置;
    • \z:匹配字符串的結束位置;
    • \G:匹配上一個匹配結束的位置;
    • \b:匹配一個單詞的開始或結束的位置;
    • \B:匹配一個單詞的中間位置;

     

    四,量詞、貪婪和懶惰

    量詞是指限定前面的一個正則出現的次數,量詞分為兩種模式:貪婪模式和懶惰模式,貪婪模式是指匹配盡可能多的字符,而懶惰模式是指匹配盡可能少的字符。默認情況下,量詞處於貪婪模式,在量詞的後面加上?來啟用懶惰模式。

    • *:出現0次或多次
    • +:出現1次或多次
    • ?:出現0次或1次
    • {n}:出現n次
    • {n,}:出現至少n次
    • {n,m}:出現n到m次

    注意,出現多次是指前面的元字符出現多次,例如,\d{2} 等價於 \d\d,只是出現兩個数字,並不要求兩個数字是相同的。要表示相同的兩個数字,必須使用分組來實現。

    五,分組和捕獲字符

    ()  括號不僅確定表達式的範圍,還創建分組,()內的表達式就是一個分組,引用分組表示兩個分組匹配的文本是完全相同的。定義一個分組的基本語法:

    (pattern)

    該類型的分組會捕獲字符,所謂捕獲字符是指:一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    1,分組編號和命名

    默認情況下,每個分組自動分配一個組號,規則是:從左向右,按分組左括號的出現順序進行編號,第一個分組的組號為1,第二個為2,以此類推。也可以為分組指定名稱,該分組稱作命名分組,命名分組也會被自動編號,編號從1開始,逐個加1,為分組指定名稱的語法是:

    (?< name > pattern)

    通常來說,分組分為命名分組和編號分組,引用分組的方式有:

    • 通過分組名稱來引用分組:\k<name>
    • 通過分組編號來引用分組:\number

    注意,分組只能後向引用,也就是說,從正則表達式文本的左邊開始,分組必須先定義,然後才能在定義之後面引用。

    在正則表達式里引用分組的語法為“\number”,比如“\1”代表與分組1 匹配的子串,“\2”代表與分組2 匹配的字串,以此類推。

    例如,對於 “<(.*?)>.*?</\1>” 可以匹配 <h2>valid</h2>,在引用分組時,分組對應的文本是完全相同的。

    2,分組構造器

    分組構造方法如下:

    • (pattern):捕獲匹配的子表達式,併為分組分配一個組號
    • (?< name > pattern):把匹配的子表達式捕獲到命名的分組中
    • (?:pattern):非捕獲的分組,並未分組分配一個組號
    • (?> pattern):貪婪分組

    3,貪婪分組

    貪婪分組也稱作非回溯分組,該分組禁用了回溯,正則表達式引擎將盡可能多地匹配輸入文本中的字符。如果無法進行進一步的匹配,則不會回溯嘗試進行其他模式匹配。

    (?> pattern )

    4,二選一

    | 的意思是或,匹配兩者中的任意一個,注意,|把左右兩邊的表達式分為兩部分。

    pattern1 | pattern2

    六,零寬斷言

    零寬是指寬度為0,匹配的是位置,所以匹配的子串不會出現在匹配結果中,而斷言是指判斷的結果,只有斷言為真,才算匹配成功。

    對於定位符,可以匹配一句話的開始、結束(^ $)或者匹配一個單詞的開始、結束(\b),這些元字符只匹配一個位置,指定這個位置滿足一定的條件,而不是匹配某些字符,因此,它們被成為 零寬斷言。所謂零寬,指的是它們不與任何字符相匹配,而匹配一個位置;所謂斷言,指的是一個判斷,正則表達式中只有當斷言為真時才會繼續進行匹配。零寬斷言可以精確的匹配一個位置,而不僅僅是簡單的指定句子或者單詞。

    正則表達式把文本看作從左向右的字符流,向右叫做後向(Look behind),向左叫做前向(Look ahead)。對於正則表達式,只有當匹配到指定的模式(Pattern)時,斷言為True,叫做肯定式,把不匹配模式為True,叫做否定式。

    按照匹配的方向和匹配的定性,把零寬斷言分為四種類型:

    • (?= pattern):前向、肯定斷言
    • (?! pattern):前向、否定斷言
    • (?<= pattern):後向、肯定斷言
    • (?<! pattern):後向、否定斷言

     1,前向肯定斷言

    前向肯定斷言定義一個模式必須存在於文本的末尾(或右側),但是該模式匹配的子串不會出現在匹配的結果中,前向斷言通常出現在正則表達式的右側,表示文本的右側必須滿足特定的模式:

     (?= subexpression )

    使用前向肯定斷言可以定一個模糊匹配,後綴必須包含特定的字符:

    \b\w+(?=\sis\b)

    對正則表達式進行分析:

    • \b:表示單詞的邊界
    •  \w+:表示單詞至少出現一次
    • (?=\sis\b):前向肯定斷言,\s 表示一個空白字符, is 是普通字符,完全匹配,\b 是單詞的邊界。

    從分析中,可以得出,匹配該正則表達式的文本中必須包含 is 單詞,is是一個單獨的單詞,不是某一個單詞的一個部分。舉個例子

    Sunday is a weekend day 匹配該正則,匹配的值是Sunday,而The island has beautiful birds 不匹配該正則。

    2,後向肯定斷言

    後向肯定斷言定義一個模式必須存在於文本的開始(或左側),但是該模式匹配的子串不會出現在匹配的結果中,後向斷言通常出現在正則表達式的左側,表示文本的左側必須滿足特定的模式:

    (?<= subexpression )

    使用後向肯定斷言可以定一個模糊匹配,前綴必須包含特定的字符:

    (?<=\b20)\d{2}\b

    對正則表達式進行分析:

    • (?<=\b20):後向斷言,\b表示單詞的開始,20是普通字符
    • \d{2}:表示兩個数字,数字不要求相同
    • \b:單詞的邊界

    該正則表達式匹配的文本具備的模式是:文本以20開頭、以兩個数字結尾。

    七,用正則從格式化的文本中扣值

    有如下的JSON格式的文本,從文本中扣出字段(CustomerId、CustomerName、CustomerIdSource和CustomerType)的值:

    {"CustomerDetails":"[{\"CustomerId\":\"57512f19\",\"CustomerName\":\"cust xyz\",\"CustomerIdSource\":\"AadTenantId\",\"CustomerType\":\"Enterprise\"}]"}

    注意,該文本轉換為C#中的字符時,需要對雙引號和轉義字符進行轉義。由於這四個字段提取規則相同,可以寫一個通用的模式來提取:

    public static string GetNestedItem(string txt, string key)
    {
        string pat = string.Format("(?<=\\\\\"{0}\\\\\":\\\\\").*?(?=\\\\\")", key);
        return Regex.Match(txt, pat, RegexOptions.IgnoreCase).Value;
    }

    正則表達式得解析:

    • (?<=\\\\\”{0}\\\\\”:\\\\\”):後向斷言,用於匹配字段名、雙引號和冒號
    • .*?:懶惰模式,匹配盡可能少的文本
    • (?=\\\\\”):前向斷言,用於匹配字段值得雙引號

     

     

    參考文檔:

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

  • 【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    本文為 SnailClimb 的原創,目前已經收錄自我開源的 中(61.5 k Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。歡迎 Star!)。

    另外推薦一篇原創:

    Java 併發基礎常見面試題總結

    1. 什麼是線程和進程?

    1.1. 何為進程?

    進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。

    在 Java 中,當我們啟動 main 函數時其實就是啟動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。

    如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。

    1.2. 何為線程?

    線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器虛擬機棧本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。

    Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下。

    public class MultiThread {
        public static void main(String[] args) {
            // 獲取 Java 線程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            // 不需要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
            ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
            // 遍歷線程信息,僅打印線程 ID 和線程名稱信息
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
            }
        }
    }

    上述程序輸出如下(輸出內容可能不同,不用太糾結下面每個線程的作用,只用知道 main 線程執行 main 方法即可):

    [5] Attach Listener //添加事件
    [4] Signal Dispatcher // 分發處理給 JVM 信號的線程
    [3] Finalizer //調用對象 finalize 方法的線程
    [2] Reference Handler //清除 reference 線程
    [1] main //main 線程,程序入口

    從上面的輸出內容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行

    2. 請簡要描述線程與進程的關係,區別及優缺點?

    從 JVM 角度說進程和線程之間的關係

    2.1. 圖解進程和線程的關係

    下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關係。如果你對 Java 內存區域 (運行時數據區) 這部分知識不太了解的話可以閱讀一下這篇文章:

    從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器虛擬機棧本地方法棧

    總結: 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反

    下面是該知識點的擴展內容!

    下面來思考這樣一個問題:為什麼程序計數器虛擬機棧本地方法棧是線程私有的呢?為什麼堆和方法區是線程共享的呢?

    2.2. 程序計數器為什麼是私有的?

    程序計數器主要有下面兩個作用:

    1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

    需要注意的是,如果執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的才是下一條指令的地址。

    所以,程序計數器私有主要是為了線程切換后能恢復到正確的執行位置

    2.3. 虛擬機棧和本地方法棧為什麼是私有的?

    • 虛擬機棧: 每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
    • 本地方法棧: 和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。

    所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

    2.4. 一句話簡單了解堆和方法區

    堆和方法區是所有線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新創建的對象 (所有對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。

    3. 說說併發與并行的區別?

    • 併發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);
    • 并行: 單位時間內,多個任務同時執行。

    4. 為什麼要使用多線程呢?

    先從總體上來說:

    • 從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
    • 從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。

    再深入到計算機底層來探討:

    • 單核時代: 在單核時代多線程主要是為了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閑;進行 IO 操作時,CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
    • 多核時代: 多核時代多線程主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

    5. 使用多線程可能帶來什麼問題?

    併發編程的目的就是為了能提高程序的執行效率提高程序運行速度,但是併發編程並不總是能提高程序運行速度的,而且併發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閑置問題。

    6. 說說線程的生命周期和狀態?

    Java 線程在運行的生命周期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。

    線程在生命周期中並不是固定處於某一個狀態而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 併發編程藝術》4.1.4 節):

    由上圖可以看出:線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法后開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)后就處於 RUNNING(運行) 狀態。

    操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源::),所以 Java 系統一般將這兩個狀態統稱為 RUNNABLE(運行中) 狀態 。

    當線程執行 wait()方法之後,線程進入 WAITING(等待) 狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。

    7. 什麼是上下文切換?

    多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 採取的策略是為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。

    概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

    上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

    Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

    8. 什麼是線程死鎖?如何避免死鎖?

    8.1. 認識線程死鎖

    多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

    如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

    下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源於《併發編程之美》):

    public class DeadLockDemo {
        private static Object resource1 = new Object();//資源 1
        private static Object resource2 = new Object();//資源 2
    
        public static void main(String[] args) {
            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 1").start();
    
            new Thread(() -> {
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource1");
                    synchronized (resource1) {
                        System.out.println(Thread.currentThread() + "get resource1");
                    }
                }
            }, "線程 2").start();
        }
    }

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 2,5,main]get resource2
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 2,5,main]waiting get resource1

    線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

    學過操作系統的朋友都知道產生死鎖必須具備以下四個條件:

    1. 互斥條件:該資源任意一個時刻只由一個線程佔用。
    2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
    3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
    4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

    8.2. 如何避免線程死鎖?

    我們只要破壞產生死鎖的四個條件中的其中一個就可以了。

    破壞互斥條件

    這個條件我們沒有辦法破壞,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

    破壞請求與保持條件

    一次性申請所有的資源。

    破壞不剝奪條件

    佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。

    破壞循環等待條件

    靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

    我們對線程 2 的代碼修改成下面這樣就不會產生死鎖了。

            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 2").start();

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 1,5,main]get resource2
    Thread[線程 2,5,main]get resource1
    Thread[線程 2,5,main]waiting get resource2
    Thread[線程 2,5,main]get resource2
    
    Process finished with exit code 0

    我們分析一下上面的代碼為什麼避免了死鎖的發生?

    線程 1 首先獲得到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。然後線程 1 再去獲取 resource2 的監視器鎖,可以獲取到。然後線程 1 釋放了對 resource1、resource2 的監視器鎖的佔用,線程 2 獲取到就可以執行了。這樣就破壞了破壞循環等待條件,因此避免了死鎖。

    9. 說說 sleep() 方法和 wait() 方法區別和共同點?

    • 兩者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖
    • 兩者都可以暫停線程的執行。
    • Wait 通常被用於線程間交互/通信,sleep 通常被用於暫停執行。
    • wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成后,線程會自動蘇醒。或者可以使用wait(long timeout)超時后線程會自動蘇醒。

    10. 為什麼我們調用 start() 方法時會執行 run() 方法,為什麼我們不能直接調用 run() 方法?

    這是另一個非常經典的 java 多線程面試問題,而且在面試中會經常被問到。很簡單,但是很多人都會答不上來!

    new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啟動一個線程並使線程進入了就緒狀態,當分配到時間片后就可以開始運行了。 start() 會執行線程的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多線程工作。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,所以這並不是多線程工作。

    總結: 調用 start 方法方可啟動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,還是在主線程里執行。

    開源項目推薦

    作者的其他開源項目推薦:

    1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
    2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
    3. : 我覺得技術人員應該有的一些好習慣!
    4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • [NLP] Adaptive Softmax

    [NLP] Adaptive Softmax

    1. Overview

    Adaptive softmax算法在鏈接1中的論文中提出,該算法目的是為了提高softmax函數的運算效率,適用於一些具有非常大詞彙量的神經網絡。

    在NLP的大部分任務中,都會用到softmax,但是對於詞彙量非常大的任務,每次進行完全的softmax會有非常大的計算量,很耗時(每次預測一個token都需要O(|V|)的時間複雜度)。

    所以paper中提出adaptive softmax來提升softmax的運算效率。

    1) 該算法的提出利用到了單詞分佈不均衡的特點(unbalanced word distribution)來形成clusters, 這樣在計算softmax時可以避免對詞彙量大小的線性依賴關係,降低計算時間;

    2) 另外通過結合現代架構和矩陣乘積操作的特點,通過使其更適合GPU單元的方式進一步加速計算。

    2. Introduction

    2.1 一般降低softmax計算複雜度的兩種方式

    1) 考慮原始分佈:近似原始概率分佈或近似原始概率分佈的子集

    2) 構造近似模型,但是產生準確的概率分佈。比如:hierarchical softmax.

    (上述兩個方法可以大致區分為:一是準確的模型產生近似概率分佈,二是用近似模型產生準確的概率分佈)

    2.2 本文貢獻點

    paper中主要採用的上述(2)的方式,主要借鑒的是hierarchical softmax和一些變型。與以往工作的不同在於,本paper結合了GPU的特點進行了計算加速。主要

    1. 定義了一種可以生成 近似層次模型 的策略,該策略同時考慮了矩陣乘積運算的計算時間。這個計算時間並非與矩陣的維數是簡單線性關係。

    2. 在近來的GPU上對該模型進行了實證分析。在所提出的優化算法中也包含了對實際計算時間模型的定義。

    3. 與一般的softmax相比,有2× 到 10× 的加速。這等價於在同等算力限制下提高了準確度。另外一點非常重要,該paper所提出的這種高效計算的方法與一些通過并行提高效率的方法相比,在給定一定量的training data下,對準確度並沒有損失。

    3. Adaptive Softmax 

    3.1 矩陣乘積運算的計算時間模型

    hidden states: (B × d); Word representation: (d × k); 這兩個矩陣的乘積運算時間: g(k, B)。其中B是batch size, d: hidden layer 的size, k vectors的數量。

    (1) 固定B,d, 探究k值大小與g(k)的關係:

    在K40、 M40兩種GPU model中進行實驗,發現k在大約為$k_0 \approx 50$的範圍之內,g(k)是常數級別的,在$k > k_0$后,呈線性依賴關係。其計算模型為:

    同樣的,關於B的計算時間的函數也呈現這樣的關係。這可以表示,矩陣相乘時,當其中一個維度很小時,矩陣乘法是低效的。

    如何理解呢?比如對於 $k_1 = 2^2$, $k_2 = 2^4$,它們均在同樣的常量時間內完成運算,那麼顯然對於$k1$量級有算力被浪費了。

    那麼這也說明,一些words的層次結構,其每個節點只具有少量的子節點(比如huffman 編碼的tree),是次優的。

    (2) 探究batch size B值大小與g(B)的關係:

    同樣的,當探究計算用時與batch size $B$,的關係時,發現在矩陣乘法運算中,其中一個維度很小時計算並不高效。因此會導致:

    (i) 在層次結構中,因為每個節點只有少數一些孩子節點 (比如Huffman 樹),其計算效率是次優的;

    (ii) 對於按照詞頻劃分clusters后,那些只包含稀有詞的cluster,被選擇的概率為p, 並且其batch size也縮減為 $p B$,也會出現以上所述的低效的矩陣乘積運算問題。

    (3) 本文的計算模型

    所以paper中綜合考慮了k與B,提出了下面的計算模型:

    對於Adaptive Softmax, 其核心思想是根據詞頻大小,將不同詞頻區間的詞分為不同的clusters,按照詞頻高的優先訪問的原則,來對每個cluster整體,進而對cluster中的每個詞進行訪問,進行softmax操作。

    那麼paper中首先以2個clusters為例,對該模型進行講解,之後推廣到多個clusters的一般情況。

    3.2 Two-Clusters Case

    根據Zipf定律,在Penn TreeBank中 20%的詞,可以覆蓋一般文檔中87%的出現過的詞。

    直觀地,可以將字典$V$中的詞分為$V_h$、$V_t$兩個部分。其中$V_h$表示高頻詞集合(head),$V_t$表示低頻詞集合(tail)。一般地,$|V_h| << |V_t|$,$P(V_h) >> P(V_t)$。

    (1) Clusters 的組織

    直觀地可以想到對於這兩個clusters有兩種組織方式:(i) 兩個clusters都是2層的樹結構,(ii) 將head部分用一個短列表保存,對tail部分用二層樹結構。何種方式更優可以就其計算效率和準確率進行比較:

    在準確率方面,(i) 較之 (ii) 一般有 5 ~ 10 %的下降。原因如下:

    對於屬於cluster $c$的每個詞$w$的概率計算為: 

    若採用(i):  $P(c | h) * P(w | c, h)$

    若採用(ii): 對於高頻詞head部分可以直接計算獲得 $P(w | h)$,其更為簡單直接。

    因此,除非(i)(ii)的計算時間有非常大的差別,否則選擇(ii)的組織方式。

    (2) 對計算時間的縮短

     

    圖2. Two clusters示意圖

    如圖2所示,$k_h = |V_h|, k_t = k – k_h, p_t = 1 – p_h$

    (i) 對於第一層:對於batch size 為B的輸入,在head中對$k_h$個高頻詞以及1個單位的二層cluster的向量(加陰影部分),共$k_h + 1$個向量,做softmax,

    這樣占文檔中$p_h$的詞可以基本確定下來,在head 的 short list 部分找到對應的單詞;

    而如果與陰影部分進行softmax值更大,表明待預測的詞,在第二層的低頻詞部分;

    第一層的計算開銷為:$g(k_h + 1, B)$

    (ii) 在short list 中確定了 $p_h × B$ 的內容后,還剩下 $p_t B$的輸入需要在tail中繼續進行softmax來確定所預測的詞。

    第二層中需要對$k_t$個向量做softmax;

    第二層的計算開銷為:$g(k_t, pB)$

    綜上,總的計算開銷為:$$C = g(k_h + 1, B) + g(k_t, p_t B)$$

    相比於最初的softmax,分母中需要對詞典中的每個詞的向量逐一進行計算;採用adaptive softmax可以使得該過程分為兩部分進行,每部分只需對該部分所包含的詞的向量進行計算即可,降低了計算用時。

    論文中的Figure 2显示了,對於$k_h$的合理劃分,最高可以實現相較於完全softmax的5x加速。

    (3) 不同cluster的分類器能力不同

    由於每個cluster基本是獨立計算的,它們並不要求有相同的能力(capacity)。

    一般可以為高頻詞的cluster予以更高的capacity,而可以適當降低詞頻詞的cluster的capacity。因為低頻詞在文檔中很少出現,因此對其capacity的適當降低並不會非常影響總體的性能。

    在本文中,採取了為不同的clusters共享隱層,通過加映射矩陣的方式降低分類器的輸入大小。一般的,tail部分的映射矩陣將其大小從$d$維縮小到$d_t = d / 4$維。

    3.3 一般性情況

    在3.2中以分為2個cluster為例,介紹了adaptive softmax如何組織並計算的,這裏將其拓展為一般情況,即可以分為多個clusters時的處理方式。

    假設將整個詞典分為一個head部分和J個詞頻詞cluster部分,那麼有:

    $$V = V_h \cup V_1 … V_j, V_i \cap V_j = \phi$$

    其中$V_h$在第一層,其餘均在第二層,如圖Figure 3所示。

    每個cluster中的詞向量的數目為$k_i = |V_i|$,單詞$w$屬於某個cluster的概率為:$p_i = \sum_{w \in V_i} p(w)$.

    那麼,

    計算head部分的時間開銷為:$C_h = g(J + k_h, B)$

    計算每個二層cluster的時間開銷為:$\forall_i, C_i = g(k_i, p_i B)$

    那麼總的時間開銷為:$C = g(J + k_h, B) + \sum_i g(k_i, p_i B) \tag8$

     

    回顧公式(7):

    $$g(k,B) = max(c + \lambda k_0 B_0, c + \lambda k B) \tag7$$

    這裡有兩部分,一個是常數部分$c + \lambda k_0 B_0$,一個是線性變換部分$c + \lambda k B$。

    通過3.1可知,在常數部分GPU的算力並未得到充分利用,因此應盡量避免落入該部分。那麼需要滿足:

    $kB \geq k_0B_0$,這樣在求max時,可以利用以上的第二部分。

    將(8)式代入(7)的第二部分得:

     

    那麼接下來的目標就是根據(10) 以最小化時間開銷C. 

    在(10)中,$J ,B$是固定的,那麼可以重點關注$\sum_i p_ik_i$與$k_h$。

    (1) $\sum_i p_ik_i$

    假設$p_{i + j} = p_i + p_j$, 那麼 $p_jk_j = (p_{i + j} – p_i) k_j$

    $p_ik_i + p_jk_j = p_i(k_i – k_j) + p_{i + j}k_j \tag{11}$  

    假設$k_i > k_j, p_{i + j}$, $k_j$均固定,那麼(11)中只有$p_i$可變,可以通過減小$p_i$的方式使(11)式減小。=> 也即$k_i > k_j$ 且 $p_i$盡量小,即包含越多詞的cluster,其概率越小。

    (2) $k_h$

    減小$k_h$可以使(10)式減小。 => 即為可以使高頻詞所在的cluster包含更少的詞。

    綜上,給定了cluster的個數J,與batch size B,可以通過給大的cluster分配以更小的概率降低時間開銷C.

    另外paper中講到可以用動態規劃的方法,在給定了J后,對$k_i$的大小進行劃分。

    cluster的劃分個數$J$。paper中實驗了採用不同的J所導致的不同計算時間。如圖Figure 4所示。雖然在$10 ~ 15$區間內效果最好,但$ > 5$之後效果沒有非常明顯的提升。所以文中建議採用2~5個clusters。

    實驗主要在text8, europarl, one billion word三個數據集上做的,通過比較ppl發現,adaptive softmax仍能保持低的ppl,並且相比於原模型有2x到10x的提速。

      

    參考鏈接:

    1. Efficient softmax approximation for GPUs: 

    Some of us get dipped in flat, some in satin, some in gloss. But every once in a while you find someone who’s iridescent, and when you do, nothing will ever compare. ~ Flipped

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

  • 微服務框架 – Jimu(積木) 升級 1.0.0 支持 .Net Core 3.0

    微服務框架 – Jimu(積木) 升級 1.0.0 支持 .Net Core 3.0

    如果不知道 Jimu(積木) 是啥,請移步
    這次升級除了支持 .Net Core 3.0 還新增部分功能,如 REST, 鏈路跟蹤等,以下為詳細;

    一、功能列表

    功能 說明 Jimu 1.0.0 Jimu 0.6.0
    平台 .Net Core 2.1
    .Net Core 3.0
    服務註冊與發現 consul
    網關 Asp.Net Core Web
    RPC DotNetty
    鑒權 JWT
    負載均衡 輪訓
    容錯策略 重試
    容器 docker
    路由配置 Attribute註解
    日誌記錄 log4net
    nlog
    文檔 swagger
    鏈路跟蹤 skywalking
    REST Attribute註解
    健康監測 心跳
    文件上存下載 多文件上存,單文件下載
    跳轉 在服務端跳轉到指定url
    ORM Dapper
    DDD MiniDDD

    二、建議用積木結合 docker 搭建分佈式架構

    三、swagger

    四、skywalking

    拓撲圖: user -> jimu_apigateway -> jimu_order -> jimu_user

    Trace 跟蹤

    五、網關

    服務器

    微服務

    微服務詳細

    六、源碼

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 類加載器 – ClassLoader詳解

    獲得ClassLoader的途徑

    • 獲得當前類的ClassLoader
      • clazz.getClassLoader()
    • 獲得當前線程上下文的ClassLoader
      • Thread.currentThread().getContextClassLoader();
    • 獲得系統的ClassLoader
      • ClassLoader.getSystemClassLoader()
    • 獲得調用者的ClassLoader
      • DriverManager.getCallerClassLoader

    ClassLoader源碼解析

    概述

    類加載器是用於加載類的對象,ClassLoader是一個抽象類。如果我們給定了一個類的二進制名稱,類加載器應嘗試去定位或生成構成定義類的數據。一種典型的策略是將給定的二進制名稱轉換為文件名,然後去文件系統中讀取這個文件名所對應的class文件。

    每個Class對象都會包含一個定義它的ClassLoader的一個引用。

    數組類的Class對象,不是由類加載器去創建的,而是在Java運行期JVM根據需要自動創建的。對於數組類的類加載器來說,是通過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是一樣的;如果數組當中的元素類型是一個原生類型,數組類是沒有類加載器的【代碼一】。

    應用實現了ClassLoader的子類是為了擴展JVM動態加載類的方式。

    類加載器典型情況下時可以被安全管理器所使用去標識安全域問題。

    ClassLoader類使用了委託模型來尋找類和資源,ClassLoader的每一個實例都會有一個與之關聯的父ClassLoader,當ClassLoader被要求尋找一個類或者資源的時候,ClassLoader實例在自身嘗試尋找類或者資源之前會委託它的父類加載器去完成。虛擬機內建的類加載器,稱之為啟動類加載器,是沒有父加載器的,但是可以作為一個類加載器的父類加載器【雙親委託機制】。

    支持併發類加載的類加載器叫做并行類加載器,要求在初始化期間通過ClassLoader.registerAsParallelCapable 方法註冊自身,ClassLoader類默認被註冊為可以并行,但是如果它的子類也是并行加載的話需要單獨去註冊子類。

    在委託模型不是嚴格的層次化的環境下,類加載器需要并行,否則類加載會導致死鎖,因為加載器的鎖在類加載過程中是一直被持有的。

    通常情況下,Java虛擬機以平台相關的形式從本地的文件系統中加載類,比如在UNIX系統,虛擬機從CLASSPATH環境所定義的目錄加載類。
    然而,有些類並不是來自於文件;它們是從其它來源得到的,比如網絡,或者是由應用本身構建【動態代理】。定義類(defineClass )方法會將字節數組轉換為Class的實例,這個新定義類的實例可以由Class.newInstance創建。

    由類加載器創建的對象的方法和構造方法可能引用其它的類,為了確定被引用的類,Java虛擬機會調用最初創建類的類加載器的loadClass方法。

    二進制名稱:以字符串參數的形式向CalssLoader提供的任意一個類名,必須是一個二進制的名稱,包含以下四種情況

    • “java.lang.String” 正常類
    • “javax.swing.JSpinner$DefaultEditor” 內部類
    • “java.security.KeyStore\(Builder\)FileBuilder$1″ KeyStore的內部類Builder的內部類FileBuilder的第一個匿名內部類
    • “java.net.URLClassLoader$3$1” URLClassLoader類的第三個匿名內部類的第一個匿名內部類

    代碼一:

    public class Test12 {
        public static void main(String[] args) {
            String[] strings = new String[6];
            System.out.println(strings.getClass().getClassLoader());
            // 運行結果:null
    
            Test12[] test12s = new Test12[1];
            System.out.println(test12s.getClass().getClassLoader());
            // 運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2
    
            int[] ints = new int[2];
            System.out.println(ints.getClass().getClassLoader());
            // 運行結果:null
        }
    }

    loadClass方法

    loadClass的源碼如下, loadClass方法加載擁有指定的二進制名稱的Class,默認按照如下順序尋找類:

    • 調用findLoadedClass(String)檢查這個類是否被加載
    • 調用父類加載器的loadClass方法,如果父類加載器為null,就會調用啟動類加載器
    • 調用findClass(String)方法尋找

    使用上述步驟如果類被找到且resolve為true,就會去調用resolveClass(Class)方法

    protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
    {
      synchronized (getClassLoadingLock(name)) {
          // First, check if the class has already been loaded
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      c = parent.loadClass(name, false);
                  } else {
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }
    
              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // to find the class.
                  long t1 = System.nanoTime();
                  c = findClass(name);
    
                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {
              resolveClass(c);
          }
          return c;
      }
    }

    findClass方法

    findClass的源碼如下,findClass尋找擁有指定二進制名稱的類,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委託機制,該方法會在檢查完父類加載器之後被loadClass方法調用,默認返回ClassNotFoundException異常。

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    defineClass方法

    defineClass的源碼如下,defineClass方法將一個字節數組轉換為Class的實例。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

    自定義類加載器

    /**
     * 繼承了ClassLoader,這是一個自定義的類加載器
     * @author 夜的那種黑丶
     */
    public class ClassLoaderTest extends ClassLoader {
        public static void main(String[] args) throws Exception {
            ClassLoaderTest loader = new ClassLoaderTest("loader");
           Class<?> clazz = loader.loadClass("classloader.Test01");
            Object object = clazz.newInstance();
            System.out.println(object);
            System.out.println(object.getClass().getClassLoader());
        }
        //------------------------------以上為測試代碼---------------------------------
    
        /**
         * 類加載器名稱,標識作用
         */
        private String classLoaderName;
    
        /**
         * 從磁盤讀物字節碼文件的擴展名
         */
        private String fileExtension = ".class";
    
        /**
         * 創建一個類加載器對象,將系統類加載器當做該類加載器的父加載器
         * @param classLoaderName 類加載器名稱
         */
        private ClassLoaderTest(String classLoaderName) {
            // 將系統類加載器當做該類加載器的父加載器
            super();
            this.classLoaderName = classLoaderName;
        }
    
        /**
         * 創建一個類加載器對象,显示指定該類加載器的父加載器
         * 前提是需要有一個類加載器作為父加載器
         * @param parent 父加載器
         * @param classLoaderName 類加載器名稱
         */
        private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
            // 显示指定該類加載器的父加載器
            super(parent);
            this.classLoaderName = classLoaderName;
        }
    
        /**
         * 尋找擁有指定二進制名稱的類,重寫ClassLoader類的同名方法,需要自定義加載器遵循雙親委託機制
         * 該方法會在檢查完父類加載器之後被loadClass方法調用
         * 默認返回ClassNotFoundException異常
         * @param className 類名
         * @return Class的實例
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            byte[] data = this.loadClassData(className);
            /*
             * 通過defineClass方法將字節數組轉換為Class
             * defineClass:將一個字節數組轉換為Class的實例,在使用這個Class之前必須要被解析
             */
            return this.defineClass(className, data, 0 , data.length);
        }
    
        /**
         * io操作,根據類名找到對應文件,返回class文件的二進制信息
         * @param className 類名
         * @return class文件的二進制信息
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        private byte[] loadClassData(String className) throws ClassNotFoundException {
            InputStream inputStream = null;
            byte[] data;
            ByteArrayOutputStream byteArrayOutputStream = null;
    
            try {
                this.classLoaderName = this.classLoaderName.replace(".", "/");
                inputStream = new FileInputStream(new File(className + this.fileExtension));
                byteArrayOutputStream = new ByteArrayOutputStream();
    
                int ch;
                while (-1 != (ch = inputStream.read())) {
                    byteArrayOutputStream.write(ch);
                }
    
                data = byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                throw new ClassNotFoundException();
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (byteArrayOutputStream != null) {
                        byteArrayOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    }

    以上是一段自定義類加載器的代碼,我們執行這段代碼

    classloader.Test01@7f31245a
    sun.misc.Launcher$AppClassLoader@18b4aac2

    可以看見,這段代碼中進行類加載的類加載器還是系統類加載器(AppClassLoader)。這是因為jvm的雙親委託機製造成的,private ClassLoaderTest(String classLoaderName)將系統類加載器當做我們自定義類加載器的父加載器,jvm的雙親委託機制使自定義類加載器委託系統類加載器完成加載。

    改造以下代碼,添加一個path屬性用來指定類加載位置:

    public class ClassLoaderTest extends ClassLoader {
        public static void main(String[] args) throws Exception {
            ClassLoaderTest loader = new ClassLoaderTest("loader");
            loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
            Class<?> clazz = loader.loadClass("classloader.Test01");
            System.out.println("class:" + clazz);
    
            Object object = clazz.newInstance();
            System.out.println(object);
            System.out.println(object.getClass().getClassLoader());
        }
        //------------------------------以上為測試代碼---------------------------------
    
        /**
         * 從指定路徑加載
         */
        private String path;
    
        ......
        
        /**
         * io操作,根據類名找到對應文件,返回class文件的二進制信息
         * @param className 類名
         * @return class文件的二進制信息
         * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
         */
        private byte[] loadClassData(String className) throws ClassNotFoundException {
            InputStream inputStream = null;
            byte[] data;
            ByteArrayOutputStream byteArrayOutputStream = null;
    
            className = className.replace(".", "/");
    
            try {
                this.classLoaderName = this.classLoaderName.replace(".", "/");
                inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
                byteArrayOutputStream = new ByteArrayOutputStream();
    
                int ch;
                while (-1 != (ch = inputStream.read())) {
                    byteArrayOutputStream.write(ch);
                }
    
                data = byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                throw new ClassNotFoundException();
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (byteArrayOutputStream != null) {
                        byteArrayOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return data;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    }

    運行一下

    class:class classloader.Test01
    classloader.Test01@7f31245a
    sun.misc.Launcher$AppClassLoader@18b4aac2

    修改一下測試代碼,並刪除工程下的Test01.class文件

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
       loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz);
    
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }

    運行一下

    class:class classloader.Test01
    classloader.Test01@135fbaa4
    classloader.ClassLoaderTest@7f31245a

    分析

    改造后的兩塊代碼,第一塊代碼中加載類的是系統類加載器AppClassLoader,第二塊代碼中加載類的是自定義類加載器ClassLoaderTest。是因為ClassLoaderTest會委託他的父加載器AppClassLoader加載class,第一塊代碼的path直接是工程下,AppClassLoader可以加載到,而第二塊代碼的path在桌面目錄下,所以AppClassLoader無法加載到,然後ClassLoaderTest自身嘗試加載並成功加載到。如果第二塊代碼工程目錄下的Test01.class文件沒有被刪除,那麼依然是AppClassLoader加載。

    再來測試一塊代碼

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest("loader");
        loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    結果顯而易見,類由系統類加載器加載,並且clazz和clazz2是相同的。

    class:2133927002
    sun.misc.Launcher$AppClassLoader@18b4aac2
    class:2133927002
    sun.misc.Launcher$AppClassLoader@18b4aac2

    在改造一下

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
        loader2.setPath("/home/fanxuan/桌面/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    運行結果

    class:325040804
    classloader.ClassLoaderTest@7f31245a
    class:621009875
    classloader.ClassLoaderTest@45ee12a7

    ClassLoaderTest是顯而易見,但是clazz和clazz2是不同的,這是因為類加載器的命名空間的原因。

    我們可以通過設置父類加載器來讓loader和loader2處於同一命名空間

    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/桌面/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz.hashCode());
    
        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    
        ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
        loader2.setPath("/home/fanxuan/桌面/");
        Class<?> clazz2 = loader2.loadClass("classloader.Test01");
        System.out.println("class:" + clazz2.hashCode());
    
        Object object2 = clazz2.newInstance();
        System.out.println(object2.getClass().getClassLoader());
    }

    運行結果

    class:325040804
    classloader.ClassLoaderTest@7f31245a
    class:325040804
    classloader.ClassLoaderTest@7f31245a

    擴展:命名空間

    • 每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成
    • 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
    • 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • 多線程編程(3)——synchronized原理以及使用

    多線程編程(3)——synchronized原理以及使用

    一、對象頭

      通常在java中一個對象主要包含三部分:

    • 對象頭 主要包含GC的狀態、、類型、類的模板信息(地址)、synchronization狀態等,在後面介紹。

    • 實例數據:程序代碼中定義的各種類型的字段內容。

    • 對齊數據:對象的大小必須是 8 字節的整數倍,此項根據情況而定,若對象頭和實例數據大小正好是8的倍數,則不需要對齊數據,否則大小就是8的差數。

    先看下面的實例、程序的輸出以及解釋。

    /*需提前引入jar包
    <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 解析java對象布局 -->
            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>
    ​
    */
    //Java對象以8個字節對其,不夠則使用對其數據
    public class Student {
        private int id;       // 4字節
        private boolean sex;  // 1字節
        public Student(int id, boolean sex){
            this.id = id;
            this.sex = sex;
        }
    }
    public class Test01 {
        public static void main(String[] args) {
            Student stu = new Student(6, true);
            //計算對象hash,底層是C++實現,不需要java去獲取,如果此處不調用,則後面的hash值不會去計算
            System.out.println("hashcode: " + stu.hashCode());  
            System.out.println(ClassLayout.parseInstance(stu).toPrintable());
        }
    }
    /* output
    hashcode: 523429237
    com.thread.synchronizeDemo.Student object internals:
    OFFSET SIZE TYPE DESCRIPTION        VALUE
     0     4    (object header)    01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
    4      4     (object header)    1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
    8      4     (object header)    43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
    12     4       int Student.id                                6
    16     1   boolean Student.sex                               true
    17     7           (loss due to the next object alignment)
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
    ​
    備註:上述代碼在64位的機器上運行,此時
    對象頭占  (4+4+4)*8 = 96 位(bit)
    實例數據  (4+1)*8 = 40 位(bit)
    對齊數據  7*8 = 56 位(bit) 因為Java對象以8個字節對其的方式,需補7byte去對齊
    */

       下面主要陳述對對象頭的解釋,內容從hotspot官網摘抄下來的信息:

    object header

      Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

    mark word

      The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

    klass pointer

      The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original objec

      由此可知,對象頭主要包含GC的狀態(用4位表示——表示範圍0-15,用來記錄GC年齡,這也就是為什麼對象在survivor中從from區到to區來迴轉換15次後轉入到老年代tenured區)、類型、類的模板信息(地址)、synchronization 狀態等,由兩個字組成mark word和klass pointer(類元素據信息地址,具體數據通常在堆的方法區中,即8字節,但有時候會有一些優化設置,會開啟指針壓縮,將代表klass pointer的8字節變成4字節大小,這也是為什麼在上述代碼中對象頭大小是(8+4)byte,而不是16byte。)。本節最主要介紹對象頭的mark word這部分。關於對象頭中每部分bit所代表的意義可以查看hotspot源碼中代碼的注,這段註釋是從openjdk中拷貝的。

    JVM和hotspot、openjdk的區別

    JVM是一種產品的規範定義,hotspot(Oracle公司)是對該規範實現的產品,還有遵循這些規範的其他產品,比如J9(IBM開發的一個高度模塊化的JVM)、Zing VM等。

    openjdk是一個hotspot項目的大部分源代碼(可以通過編譯后變成.exe文件),hotspot小部分代碼Oracle並未公布

    // openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\oops\markOop.hpp
    /*
    Bit-format of an object header (most significant first, big endian layout below):
    ​
    32 bits:
    --------
            hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
            JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
            size:32 ------------------------------------------>| (CMS free block)
            PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    ​
    64 bits:
    --------
    unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    size:64 ----------------------------------------------------->| (CMS free block)
    ​
    */

    可以看到在32位機器和64位機器中,對象的布局的差異還是很大的,本文主要 敘述64位機器下的布局,其實兩者無非是位數不同而已,大同小異。在64位機器用64位(8byte)表示Mark Word,首先前25位(0-25)是未被使用,接下來31位表示hash值,然後是對象分代年齡大小,最後Synchronized的鎖信息,分為兩部分,共3bit,如下錶,鎖的嚴格性依次是鎖、偏向鎖、輕量級鎖、重量級鎖。

     

    關於鎖的一些解釋

    無鎖

      無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。

    偏向鎖

      引入偏向鎖是為了在無多線程競爭的情況下,一段同步代碼一直被一個線程所訪問因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,當由另外的線程所訪問,偏向鎖就會升級為輕量級鎖。

    輕量級鎖 

      當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。

    重量級鎖

      依賴於操作系統Mutex Lock所實現的鎖,JDK中對Synchronized做的種種優化,其核心都是為了減少這種重量級鎖的使用。JDK1.6以後,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了“輕量級鎖”和“偏向鎖”。  

    GC

      這並不是鎖的狀態,而是GC標誌,等待GC回收。

    現在開始從程序層面分析前面程序的對象頭的布局信息,在此之前需要知道的是,在windows中對於數據的存儲採用的是小端存儲,所以要反過來讀

    大端模式——是指數據的高字節保存在內存的低地址中,而數據的低字節保存在內存的高地址中,這樣的存儲模式有點兒類似於把數據當作字符串順序處理:地址由小向大增加,而數據從高位往低位放;這和我們的閱讀習慣一致。

    小端模式——是指數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。 一般在網絡中用的大端;本地用的小端;

    運行程序如下,可以看到對應的hashcode值被打印出來:

    public static void main(String[] args) {
         Student stu = new Student(6, true);
        //Integer.toHexString()此方法返回的字符串表示的無符號整數參數所表示的值以十六進制
         System.out.println("hashcode: " + Integer.toHexString(stu.hashCode()));
         System.out.println(ClassLayout.parseInstance(stu).toPrintable());
    }
    /*
    hashcode: 1f32e575
    com.thread.synchronizeDemo.Student object internals:
     OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
          0     4           (object header)                           01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
          4     4           (object header)                           1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
          8     4           (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
         12     4       int Student.id                                6
         16     1   boolean Student.sex                               true
         17     7           (loss due to the next object alignment)
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

    //前8個字節反過來看,可以看出對象頭的hash是1f32e575,同時是無鎖的狀態00000001
    */

     二、Monitor

           可以把它理解為一個同步工具(數據結構),也可以描述為一種同步機制,通常被描述為一個對象。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處於鎖定狀態(每一個線程都有一個可用 monitor record 列表)[具體可以看參考資料5]。需要注意的是這種監視器鎖是發生在對象的內部鎖已經變成重量級鎖的時候。

    /*  openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
    // initialize the monitor, exception the semaphore, all other fields // are simple integers or pointers ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; } */

      Monitor的實現主要藉助三個結構去完成多線程的併發操作——_owner、_WaitSet 、_EntryList。當多個線程同時訪問由synchronized修飾的對象、類或一段同步代碼時,首先會進入_EntryList 集合,如果某個線程取得了_owner的所有權,該線程就可以去執行,如果該線程調用了wait()方法,就會放棄_owner的所有權,進入等待狀態,等下一次喚醒。如下圖(圖片摘自參考資料5)。

     三、synchronized的用法

         synchronized修飾方法和修飾一個代碼塊類似,只是作用範圍不一樣,修飾代碼塊是大括號括起來的範圍,而修飾方法範圍是整個函數。其中synchronized(this) 與synchronized(class) 之間的區別有以下五點要注意:

        1、對於靜態方法,由於此時對象還未生成,所以只能採用類鎖;

        2、只要採用類鎖,就會攔截所有線程,只能讓一個線程訪問。

        3、對於對象鎖(this),如果是同一個實例,就會按順序訪問,但是如果是不同實例,就可以同時訪問。

       4、如果對象鎖跟訪問的對象沒有關係,那麼就會都同時訪問。

       5、當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

    當然,Synchronized也可修飾一個靜態方法,而靜態方法是屬於類的而不屬於對象的,所以synchronized修飾的靜態方法鎖定的是這個類的所有對象。關於如下synchronized的用法,我們經常會碰到的案例:

    public class Thread5 implements Runnable {
        private static int count = 0;
        public synchronized static void add() {
            count++;
        }
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (Thread5.class){
                    count++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            ExecutorService es = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 20; i++) {
                es.execute(new Thread5());
            }
            es.shutdown();
            es.awaitTermination(6, TimeUnit.SECONDS);
            System.out.println(count);
        }
    }
    /* 類鎖
      20000000
      */

    而一旦換成對象鎖,不同實例,就可以同時訪問。則會出錯:

    public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (this){
                    count++;
                }
            }
    }
    /* 對象鎖
     10746948
    */
    

    這是因為靜態變量並不屬於某個實例對象,而是屬於類所有,所以對某個實例加鎖,並不會改變count變量臟讀和臟寫的情況,還是造成結果不正確。

     

    參考資料

    1. 對象布局的各部分介紹——

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 小程序雲開發:菜鳥也能全棧做產品

    小程序雲開發:菜鳥也能全棧做產品

    我想獨立實現一個全棧產品為什麼這麼難

    日常生活中,我們會使用很多軟件產品。在使用這些產品的時候,我們看得見的東西稱為“前端界面”如一個輸入框、一個按鈕,點擊按鈕之後發生的一切看不見的東西稱為“後端服務”。與之對應的創造者分別稱為“前端程序員”、“後端程序員”,然而,一個完整產品的開發不僅僅是只有前端和後端,還有設計師,架構師,運維等。有沒有可能這些所有的事情都一個人干呢?有可能,事實上如今就有很多的“全棧工程師”,他們身兼數職,是多面手。能獨立完成一個產品的方方面面。這種人固然十分了得,他們通常具有多年的經驗,涉獵廣泛,是老手,也是高手,當有一個產品想法的時候,他們可以用自己的全面專業技能,盡情的發揮去實現自己的想法。所以,從某種意義上講“全棧也是一種自由”,你可以自由的實現你的想法,這簡直太美妙了!

    然而,很多時候當我們有一個產品想法的時候,我們往往發現,前端寫完了,後端怎麼搞?數據庫怎麼搞?域名怎麼搞?域名還要備案?應用部署怎麼搞?我的買什麼樣的服務器啊?靜態資源 CDN 怎麼搞?文件上傳服務器怎麼搞?萬一訪問用戶多了能撐住嗎?等等……問題很多,導致你的一個個想法,都只是在腦海中曇花一現,從來都無法將她們實現,或者說你激情飽滿的實現了其中自己最擅長的一部分,當碰到其他難題的時候就止步了。於是仰天長嘯:我就想獨立做一個完整的產品為什麼這麼難?年輕人,這一切都不怪你……

    破局:小程序雲開發

    為什麼使用小程序雲開發來破局?

    為啥是用“小程序雲開發”來破局?首先,我們的目的是全棧實現一個產品。全棧可以有多種技術方案,你可用任何你能會的技能來達到全棧的目的。你可以開發安卓,IOS,或者 PC 站,然而小程序是最實際的!為啥?手機上能做的事情為啥要用 PC 版?OK,既然手機版比較好,那能不能再簡單一點?能,就是小程序,不需要開發IOS,安卓兩個版本。可以快速產出,快速試錯。

    其次,前面說到了,全棧實現一個產品並不容易,對很多人來說甚至是巨難!選擇了小程序已經是比較划算的方案。而再集成雲開發,全棧立馬就有了。這就是為什麼選擇“小程序雲開發”來破局。

    小程序雲開發是什麼?

    小程序雲開發是什麼?官方文檔是這麼說的:開發者可以使用雲開發開發微信小程序、小遊戲,無需搭建服務器,即可使用雲端能力。雲開發為開發者提供完整的原生雲端支持和微信服務支持,弱化後端和運維概念,無需搭建服務器,使用平台提供的 API 進行核心業務開發,即可實現快速上線和迭代,同時這一能力,同開發者已經使用的雲服務相互兼容,並不互斥。

    看完上面的描述,也許你仍然無法非常清楚的知道什麼是“小程序雲開發”,沒關係,你只需要注意加粗的部分,大概知道它“無需搭建服務器”,從傳統觀念將,這個似乎“毀三觀”咋可能沒服務器啊?是的,可以沒有傳統意義上的服務器,這種模式是 serveless 的。

    那麼,小程序雲開發提供了哪些東西來破局呢?且看下面的表格:

    能 力 作 用 說 明
    雲函數 無需自建服務器 在雲端運行的代碼,微信私有協議天然鑒權,開發者只需編寫自身業務邏輯代碼
    數據庫 無需自建數據庫 一個既可在小程序前端操作,也能在雲函數中讀寫的 JSON 數據庫
    存儲 無需自建存儲和 CDN 在小程序前端直接上傳/下載雲端文件,在雲開發控制台可視化管理
    雲調用 原生微信服務集成 基於雲函數免鑒權使用小程序開放接口的能力,包括服務端調用、獲取開放數據等能力

    上面的表格中提到了“雲開發”中的一些能力:“雲函數”,“數據庫”,“存儲”,“雲調用”,我們可以將這些詞帶入你曾經開發過的應用,看看它們分別代表了哪些部分。對於程序員來說,如果有疑問的話,沒有什麼是一個 helloword 解決不了的。

    實戰:獨立開發一個簡易的零售小程序

    哆嗦再多,不如實戰。下面我們就來使用小程序雲開發實現一個簡單的零售小程序。

    項目構思

    既然是一個零售小程序,那麼我們可以思考一下零售小程序的大致業務流程,以及粗略的梳理一下,其功能點。現根據自己的想法,大致畫一下草圖,如果沒有靈感可以參考一下別的 APP 是如何設計的。

    我根據自己的想法設計之後是這樣的:

    功能模塊:首頁,商品列表頁,購物車,確認訂單,個人中心,個人訂單,管你模塊(商品添加,分類添加)其中商品需要上傳圖片。

    梳理完功能之後,我們對於要實現的東西已經有個初步的概念了。接下來,我們需要大概畫一下頁面設計、及功能流轉。初次設計可能沒有太多經驗,沒關係,開始做就行了,做着做着就會想法越來越多,然後優化的越來越好。。我也是經過了多番修改調整,最終找到了一些思路。我的(拙劣)設計如下,圖片如果看不清楚可複製圖片鏈接在新窗口打開查看:

    說明,以上圖片是根據成品(我真的開發了一個雲小程序並上線使用了)截圖的,而實際我再設計的時候也是經過幾番修改才最終定成這樣。

    同時,補充說明一下,這裏前端頁面使用的是 vant-weapp控件,非常好用。推薦!如果你和我一樣是一個純後端程序員,建議使用 vant-weapp 來作為 ui,非常方便。否則自己寫頁面樣式的話可能就做不出來了。全棧不是那麼好乾的啊。選擇自己能駕馭的,能實現最終功能,就是一個合格的全棧。

    創建小程序雲開發項目

    我們先下載微信小程序開發工具,下載地址,安裝好了之後,新建項目,界面如下,APPID 需要你自己去註冊一個。然後注意,選擇“小程序雲開發”,如下圖所示:

    創建好了之後,項目目錄如下,先看 1 標註的地方:

    如果你曾經有過小程序的開發經驗,那麼miniprogram文件夾下面的結構你肯定熟悉了,miniprogram下面的子目錄分別是小程序對應的組件、圖片、頁面、樣式以及app.js,app.json,sitemap.json,其中components下面的vant-weapp就是上面提到的 ui 組件。

    最後一個比較重要的文件夾就是cloudfunctions,這個目錄是用來存放“雲函數的”,雲函數就是我們的後端。每一個雲函數提供一個服務。一個個的雲函數組成了我們整體的後端服務。雲函數可以看做是 FaaS(function as a service)。途中,2 標記的位置的“雲開發”按鈕,我們點進去,就可以看到“雲開發的控制台”,如下圖所示:

    如果上圖看不清楚,可以複製鏈接到新的瀏覽器窗口查看,如圖,小程序雲開發默認的免費套餐有一定的額度可供使用。首頁便是使用統計。然後我們能看到,有“數據庫”,“存儲”,“雲函數”。

    這裏的“數據庫”其實就是類似於一個 MongoDB,你可以點進去創建一個個的 collection(即:關係型數據庫中的table);這裏的“存儲”其實就是“文件夾”,我們可以通過微信提供的 api把圖片上傳到“存儲”中;這裏的“雲函數”就是我們需要實現的後端業務邏輯,他就是一個個的函數(函數由我們自己寫好後上傳)。一般開發過程中我們在開發者工具中的cloudfunctions目錄下創建雲函數(比方說是:user-add)開發完成之後在雲函數目錄點擊右鍵——上傳即可。然後就可以在小程序的代碼中調用這個user-add雲函數。

    雲開發之——3 分鐘實現文件上傳

    注意:在開始雲開發之前,我們現在 小程序代碼的 app.js 中加入wx.cloud.init,如下:

    App({
      onLaunch: function () {
        if (!wx.cloud) {
          console.error('請使用 2.2.3 或以上的基礎庫以使用雲能力')
        } else {
          wx.cloud.init({
            // env 參數說明:
            //   env 參數決定接下來小程序發起的雲開發調用(wx.cloud.xxx)會默認請求到哪個雲環境的資源
            //   此處請填入環境 ID, 環境 ID 可打開雲控制台查看
            //   如不填則使用默認環境(第一個創建的環境)
            env: 'your-env-id',
            traceUser: true,
          })
        }
        this.globalData = {}
      }
    })

    上面的圖中,我們已經看到了“商品添加”頁面的效果,它需要我們輸入商品名稱、價格、並上傳圖片,然後保存。傳統架構中,上傳圖片需要前端頁面擺一個控件,然後後端提供一個 api用來接收前端傳來的文件,通常來說這個後端 api 接收到圖片之後,會將圖片文件保存到自己的文件服務器或者是阿里雲存儲、或者是七牛雲存儲之類的。然後返回給你一個文件鏈接地址。非常麻煩,然而,小程序雲開發上傳文件超級簡單,上代碼:

    頁面代碼:
    <van-notice-bar
      scrollable="false"
      text="發布商品"
    />
      <van-field
        value="{{ productName }}"
        required
        clearable
        label="商品名稱"
        placeholder="請輸入商品名稱"
        bind:change="inputName"
      />
        <van-field
        value="{{ productPrice }}"
        required
        clearable
        label="價格"
        icon="question-o"
         bind:click-icon="onClickPhoneIcon"
        placeholder="請輸入價格"
        error-message="{{phoneerr}}"
        border="{{ false }}"
        bind:change="inputPrice"
      />
    
    <van-action-sheet
      required
      show="{{ showSelect }}"
      actions="{{ actions }}"
      close-on-click-overlay="true"
      bind:close="toggleSelect"
      bind:select="onSelect" cancel-text="取消"
    />
      <van-field
        value="{{ productCategory }}"
        center
        readonly
        label="商品分類"
        border="{{ false }}"
        use-button-slot
      >
        <van-button slot="button" size="small" plain type="primary"  
         bind:click="toggleSelect">選擇分類</van-button>
      </van-field>
      
      <van-button class="rightside" type="default" bind:click="uploadImage" >上傳商品圖片</van-button>
      <view class="imagePreview">
        <image src="{{productImg}}" />
      </view>
     <van-submit-bar
      price="{{ totalShow }}"
      button-text="提交"
      bind:submit="onSubmit"
      tip="{{ false }}"
     >
     </van-submit-bar> 
    <van-toast id="van-toast" />
    <van-dialog id="van-dialog" />

    這裡有個控件,綁定了uploadImage方法,其代碼為:

      uploadImage:function(){
        let that = this;
        wx.chooseImage({
          count: 1,
          sizeType: ['compressed'],
          sourceType: ['album', 'camera'],
          success(res) {
            wx.showLoading({
              title: '上傳中...',
            })
            const tempFilePath = res.tempFilePaths[0]
            const name = Math.random() * 1000000;
            const cloudPath = name + tempFilePath.match(/\.[^.]+?$/)[0]
            wx.cloud.uploadFile({
              cloudPath:cloudPath,//雲存儲圖片名字
              filePath: tempFilePath,//臨時路徑
              success: res => {
                let fileID = res.fileID;
                that.setData({
                  productImg: res.fileID,
                });
                wx.showToast({
                  title: '圖片上傳成功',
                })
              },
              fail: e =>{
                wx.showToast({
                  title: '上傳失敗',
                })
              },
              complete:()=>{
                wx.hideLoading();
              }
            });
          }
        })
      }

    這裏,wx.chooseImage用於調起手機選擇圖片(相冊/相機拍照),然後wx.cloud.uploadFile用於上傳圖片到上面說到的雲開發能力之一的“存儲”中。上傳圖片成功之後返回一個文件 ID,類似:

    cloud://release-0kj63.7265-release-0kj63-1300431985/100477.13363146288.jpg  

    這個鏈接可以直接在小程序頁面展示:

    <image src="cloud://release-0kj63.7265-release-0kj63-1300431985/100477.13363146288.jpg  " />

    也可以通過微信 api,裝換成 http 形式的圖片鏈接。

    雲開發之——操作數據庫,1 分鐘寫完保存商品到數據庫的代碼

    上面我們實現了商品圖片上傳,但是,商品圖片並沒有保存到數據庫。正常錄入商品的時候,我們會填好商品名稱,價格等,然後上傳圖片,最終點擊“保存”按鈕,將商品保存到數據庫。傳統模式下,前端仍然是需要調用一個後端接口,通過 post 提交數據,最終由後端服務(比如 java 服務)將數據保存到數據庫。小程序雲開發使得操作數據庫十分簡單,首先我們在雲開發控制台創建“商品表”,即一個 collection,取名為:products。然後我們就可以保存數據到數據庫了,代碼如下:

    onSubmit:function(){
        // 校驗代碼,略
        let product = {};
        product.imgId = this.data.productImg;
        product.name= this.data.productName;
        product.categoryId = this.data.productCategoryId;
        product.price = this.data.productPrice;
        // 其他賦值,略
        const db = wx.cloud.database();
        db.collection('products').add({
         data: product,
         success(res) {
           wx.showToast({
             title: '保存成功',
           })
         }
       });
      }

    以上就實現了數據入庫,就這點代碼,超簡單,1 分鐘寫完,誠不欺我。其中這裏的products就是我們的“商品表”,之前說過,類似 MongoDB 數據庫,這裏操作的是db.collection,這和 MongoDB 的語法差不多。

    雲開發之——使用雲函數完成後端業務邏輯,訂單創建

    小程序雲開發提供了幾大能力:“數據庫”,“存儲”,“雲函數”,前兩項我們已經有所體會了。下面我們能創建一個雲函數來實現訂單創建。這裏說明,雲函數其實就是 一段JavaScript 代碼,上傳至雲服務器之後,最終也是運行在 nodejs 環境的,只是這一切,我們不需要關心。我們只需要關心我們這個雲函數提供的功能是什麼就可以了。

    創建雲函數很簡單,直接在開發工具中右鍵“新建Node.js 雲函數”。然後以創建訂單為例,假設我們創建一個雲函數名為c-order-add,創建好了之後,目錄是這樣:

    雲函數的主要代碼在 index.js 中,其完整代碼是這樣:

    // 雲函數入口文件
    const cloud = require('wx-server-sdk')
    cloud.init({
      env: 'release-xxx'// your-env-id
    })
    const db = cloud.database()
    
    // 雲函數入口函數
    exports.main = async (event, context) => {
      const wxContext = cloud.getWXContext();
      console.log("雲函數 c-order-add : ")  
      // 這裡是一些邏輯處理...
      
      return await db.collection('uorder').add({
        data: {
          openid: event.userInfo.openId,
          address: event.address,
          userName: event.userName,
          phone: event.phone,
          shoppingInfo: event.shoppingInfo,
          totlePrice: event.totlePrice,
          shoppingStr: event.shoppingStr,
          remark:event.remark,
          createTime: now,
          // ...
        }
      });
    }

    這個雲函數寫好之後,需要上傳到服務器,直接在雲函數目錄點擊右鍵,然後點擊“上傳並部署”即可,這就相當於部署好了後端服務。前端小程序頁面調用的寫法是這樣的:

    let orderData={};
    orderData.userName = this.data.userName;
    orderData.phone = this.data.phone;
    orderData.address = this.data.address;
    // ....
    wx.cloud.callFunction({
          // 雲函數名稱
          name: 'c-order-add',
          // 傳給雲函數的參數
          data: orderData,
          complete: res => {
            Dialog.alert({
              title: '提交成功',
              message: '您的訂單成功,即將配送,請保持手機通暢。'
            }).then(() => {
              // ....
              wx.redirectTo({
                url: '../uorder/uorder'
              });
            });
          }
    })

    這裏,向程序前端,通過wx.cloud.callFunction完成了對雲函數的調用,也可以理解為對後端服務的調用。至此我們我們介紹完了,小程序雲開發的功能。雖然,我只貼出了少量的代碼,即保存商品,和提交訂單。由於時間和篇幅有限,我不可能把整個完整的程序代碼貼出來。但是你可以參照這個用法示例,將剩下的業務邏輯補充完整,最終完成“項目構思”一節中展示的成品截圖效果。

    小程序審核的一點經驗

    我開發的小程序審核在提交審核的時候遭遇了兩次退回,第一次是因為:“小程序具備電商性質,個人小程序號不支持”。所以,我只好申請了一個企業小程序號,使用的是超市的營業執照。服務類目的選擇也被打回了一次,最後選擇了食品還提交了食品經營許可證。第二次打回是因為:“用戶體驗問題”。其實就是“授權索取”的問題,微信不讓打開首頁就“要求授權”,同時不能強制用戶接受授權,得提供拒絕授權也能使用部分功能。

    上面兩條解決之後,更新新了好幾版,都沒有出現過被拒的情況。並且,有次我是夜晚 10 左右提價的審核,結果10 點多就提示審核通過,當時沒看具體時間,就是接盆水泡了個腳的時間審核通過了。所以,我推斷小程序審核初次審核會比較嚴,之後如果改動不大應該直接機審就過了。

    總結及對比

    這裏我們可以對小程序雲開發和傳統模式做一個對比:

    對比條目 傳統模式 雲開發
    是否需要後端服務 需要 (如一個java應用部署在 Tomcat 中) 不需要 只需要“雲函數”
    是否需要域名 需要 (還得在微信後台的把域名加入安全域名) 不需要
    是否需要購買服務器 需要 (你得部署後端 Java 應用,還得安裝數據庫) 不需要
    開通雲開發之後免費套餐夠用
    不夠的話購買套餐按調用量計費
    是否需要懂運維 需要
    (你得會折騰服務器,數據庫之類的
    還得配置好相關的用戶,端口,啟動服務)
    不需要
    圖片上傳及 CDN 麻煩 簡單
    獲取微信 openID 麻煩 超級簡單,雲函數中直接獲取
    ···

    就對比這麼多吧,總之,我非常喜歡小程序雲開發,小程序真的可以讓你輕鬆干全棧。或者咱們別動不動就提“全棧”,姑且說,小程序雲開發可以讓你更簡單、更快速、更便宜的實現你的產品落地。我自己開發的雲小程序上線之後,使用了一兩個月,沒出現任何問題。我也不用操心服務器什麼的。所以,我已經給身邊很多人安利了小程序雲開發了。這裏我就不貼出我的小程序碼了,因為已經正式給我同學的超市使用了,所以不方便讓別人去產生測試數據。如果你感興趣想看的話,可以聯繫我。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • 【面經系列】一線互聯網大廠前端面試技巧深入淺出總結

    【面經系列】一線互聯網大廠前端面試技巧深入淺出總結

    一二面(基礎面)

    1. 一面基礎面

    1.1 面試準備

    1.1.1 個人簡歷

    • 基本信息:姓名-年齡-手機-郵箱-籍貫
    • 工作經歷:時間-公司-崗位-職責-技術棧-業績(哪些成就)
    • 學歷: 博士 > 碩士 > 本科 > 大專
    • 工作經歷:時間-公司-崗位-職責-技術棧-業績
    • 開源項目:GitHub和說明

    1.2.2 自我陳述

    1.2.2.1 把我面試的溝通方向(別把自己帶到坑裡面)

    答:我平時喜歡研究一些網站,並對一些技術的原理和好玩的點感興趣,我自己也喜歡思考,也喜歡嘗試探索有沒有更好的方式和實現。(有所收留,不要全部說出來,稍微留一點懸念留作面試官來提問)

    1.2.2.2 豁達、自信的適度發揮

    答:適當自信,向自己擅長的方向上面來引路;要讓面試官來欣賞我,而不是來鄙視他。

    1.2.2.3 自如談興趣

    (豁達自信,適當收住),巧妙演示實例,適時討論疑問(不知道的問題請求指導一下,如何去解決,不要說不知道,或者不了解)

    1.2.2.4 節奏要適宜

    切忌小聰明(盡量把問題的所有實現方法都寫出來,表現出來的是熟練)

    1.2 面試實戰

    [!NOTE]
    > 1. 方向要對,過程要細(性能優化,過程詳細)
    > 2. 膽子要大、心態要和(算法題認真思考,認真使勁想;敢於承擔責任,不要輕易放棄)

    2. CSS相關

    2.1 頁面布局

    2.1.1 如何實現垂直居中布局呢?

    1.已知寬高

    /*v1*/
    .container {
        position: absolute;
        left: 50%;
        top: 50%;
        marigin-left: -width / 2;
        marigin-top: -width / 2;
    }
    
    /*v2*/
    .container {
        position: absolute;
        top: calc(50% - 5em);
        left: calc(50% - 9em);
    }
    

    2.未知寬高

    /*v1*/
    .container {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
    
    /*v2:flex+ auto*/
    .wrapper {
        dislay: flex;
    }
    .content {
        margin: auto;
    }
    
    /*v3. 父元素居中*/
    .wrapper {
        display: flex;
        /* 盒子橫軸的對齊方式 */
        justify-content: center;
        /* 盒子縱軸的對齊方式 */
        align-items: center;
    }
    
    /*v4.body內部居中*/
    .content {
         /* 1vh = 1% * 視口高度 */
          margin: 50vh auto;
          transform: translateY(-50%);
    }

    2.1.2 如何實現水平居中布局呢?

    1. 如果需要居中的元素為常規流中 inline / inline-block 元素,為父元素設置 text-align: center;
    2. 父元素上設置 text-align: center; 居中元素上margin 為 auto。
    3. 如果元素positon: absolute; 那麼
      • 0)設置父元素postion: relative
      • 1)為元素設置寬度,
      • 2)偏移量設置為 50%,
      • 3)偏移方向外邊距設置為元素寬度一半乘以-1

    2.1.3 如何實現三欄布局呢?

    1. left和right寫在center前面,並且分別左右浮動;
    2. 左右區域分別postion:absolute,固定到左右兩邊;中間的這個div因為是塊級元素,所以在水平方向上按照他的包容塊自動撐開。
    3. 父元素display: table;並且寬度為100%; 每一個子元素display: table-cell; 左右兩側添加寬度,中間不加寬度
    4. 包裹這個3個塊的父元素display: flex; 中間的元素flex: 1;
    5. 網格布局
    /* 網格布局 */
    .wrapper {
        display: grid;
        width: 100%;
        grid-template-columns: 300px 1fr 300px;
    }

    2.2 知道CSS動畫的實現嗎?

    [!NOTE]
    知道transition 過渡動畫和animation 關鍵幀動畫區別和具體實現。

    • 1.CSS動畫實現輪播圖
    • 2.CSS動畫實現旋轉的硬幣
    • 3.CSS動畫實現鐘擺效果

    2.3 CSS盒子模型

    2.3.1 說一下CSS的盒子模型?標準模型和IE模型的區別?CSS如何設置這兩種模型?

    • 標準盒子模型:width = content
    • IE盒子模型:width = content + pading + border

    • box-sizing : content-box
    • box-sizing : border-box

    2.4 CSS樣式獲取

    2.4.1 JS如何設置獲取盒子模型對應的寬度和高度?(面試重點)

    • dom.style.width/height : 只能取到內聯樣式的的屬性信息(拿不到外部引入的CSS樣式信息的)
    • dom.currentStyle.width/height : 會拿到瀏覽器渲染之後的屬性信息(IE瀏覽器)
    • window.getComputedStyle(dom).width/height : Chrome/Firefox 兼容, Firefox可以通過document.defaultView.getComputedStyle(dom)的方式來獲取
    • dom.getBoundingClientRect().width/height : 可以獲取距離viewport位置的寬度和高度

    2.5 BFC

    2.5.1 根據盒子模型解釋邊距額重疊問題?邊距重疊問題的解決方案?

    • 父子元素
    • 兄弟元素
    • 其他 ————————–計算方式:以參數的最大值來進行計算

    解決方案:對父級元素創建BFC

    2.5.2 BFC原理

    [!NOTE]
    BFC: 塊級格式化上下文,IFC(內聯格式化上下文)

    1. 在BFC的垂直邊距上面會發生重疊
    2. BFC的區域不會與浮動元素的BOX重疊
    3. BFC在頁面上是一個獨立的渲染區域,外部的元素不會影響到我,同時也不會影響到外部的元素
    4. 計算BFC的高度的時候,浮動元素也會參与運算

    2.5.3 如何創建BFC?

    1. float值不是none
    2. position值不是static或者relative
    3. display值為table, table-cell, inline-box1.
    4. overflow : auto/hidden

    2.5.4 BFC的使用場景?(重點理解)

    1. 解決邊距的重疊問題
    <section id="margin">
            <style>
                #margin {
                    background-color: #4eff35;
                    overflow: hidden;
                }
                #margin>p {
                    /*上 左右 下*/
                    margin: 5px auto 25px;
                    background-color: #ff255f;
                }
            </style>
            <p>1</p>
            <!--把一個元素放在一個容器裏面,為這個容器創建BFC即可解決邊距重疊問題-->
            <div style="overflow: hidden">
                <p>2</p>
            </div>
    
            <p>3</p>
    </section>
    1. BFC 不與float部分重疊的解決
    <section id="layout">
          <style>
              #layout {
                  background-color: #48adff;
              }
              #layout .left {
                  float: left;
                  height: 300px;
                  width: 200px;
                  background-color: #ff4344;
              }
              #layout .right {
                  height: 400px;
                  background-color: #ff255f;
                  /*給右邊的這個盒子容器創建一個BFC, 這個容器裏面的內容就會沿着垂直方向延伸*/
                  overflow: auto;
                  /*overflow: auto;*/
                  /*display: table;*/
                  /*float: left;*/
                  /*position: fixed;*/
              }
          </style>
          <div class="left">
              LEFT
          </div>
          <div class="right">
              RIGHT
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
              <p>111</p>
          </div>
      </section>
    1. BFC子元素即使是float元素也要參与運算
    <section id="float">
          <style>
              /*一個盒子內部的內容如果是浮動的話,那麼這個盒子的內容實際上是不參与父容器高度計算的*/
              #float {
                  background-color: red;
                  /*overflow: hidden;*/
                  float: left;
              }
              #float .float {
                  float: left;
                  font-size: 30px;
              }
          </style>
          <div class="float">
              我是浮動的元素
          </div>
    </section>

    3. 事件相關

    3.1 DOM事件

    3.1.1 DOM事件的級別有哪些?

    [!NOTE]
    DOM級別一共可以分為四個級別:DOM0級、DOM1級、DOM2級和DOM3級。而DOM事件分為3個級別:DOM0級事件處理,DOM2級事件處理和DOM3級事件處理。

    1. DOM0 : element.onclick = function(e) {}
      DOM1 :該標準中未涉及到事件綁定的相關東西
    2. DOM2 : element.addEventListener(‘click’, function(e){}, false), 一個DOM元素可以添加多個事件
    3. DOM3 : element.addEventListener(‘keyup’, function(e){}, false),在DOM2標準基礎上面增加了新的事件類型:鼠標事件,鍵盤事件,焦點事件

    3.1.2 DOM事件模型有哪些?

    1. 事件捕獲:從外向內, window -> document -> body -> button
    2. 事件冒泡:從內向外,button -> body -> document -> window

    3.1.3 DOM事件流?

    瀏覽器為當前的頁面與用戶進行交互的過程中,點擊鼠標後事件如何傳入和響應的呢?

      1. 捕獲階段:從外部容器開始向內
      1. 目標階段:事件通過捕獲到達目標階段
      1. 冒泡階段:從目標元素再上傳到window對象

    3.1.4 什麼事件可以代理?什麼事件不可以代理呢?

    什麼樣的事件可以用事件委託,什麼樣的事件不可以用呢?

    [!NOTE]

    1. 通常支持事件冒泡(Event Bubbling)的事件類型為鼠標事件和鍵盤事件,例如:mouseover, mouseout, click, keydown, keypress。
    2. 接口事件(指的是那些不一定與用戶操作有關的事件)則通常不支持事件冒泡(Event Bubbling),例如:load, change, submit, focus, blur。

    很明顯:focus 和 blur 都屬於不支持冒泡的接口事件。既然都不支持冒泡,那又如何實現事件代理呢?

    3.1.5 IE和DOM事件流的區別?

    IE採用冒泡型事件 Netscape使用捕獲型事件 DOM使用先捕獲后冒泡型事件

    1. 冒泡型事件模型: button -> div -> body (IE瀏覽器本身只支持Bubbling不支持Capturing)
    2. 捕獲型事件模型: body -> div-> button (Netscape事件流,網景瀏覽器公司)
    3. DOM事件模型: body -> div -> button -> button -> div -> body (先捕獲后冒泡,除了IE以外的其他瀏覽器都支持標準的DOM事件處理模型)

    [!NOTE]

    • 規範和瀏覽器實現的差別?
      • DOM2級事件規範的捕獲階段,事件從文檔節點document開始傳播,現代瀏覽器大多數都是從window對象開始傳播事件的;
      • DOM2級事件規範捕獲階段不涉及事件目標,現代瀏覽器大多數都在這個階段包含事件目標。

    3.1.6 事件對象event的屬性方法的差別?

            IE                    DOM
    cancelBubble = true    stopPropagation()    // 停止冒泡
    returnValue = false    preventDefault()     // 阻止元素默認事件
    srcEelement            target               // 事件目標

    3.1.7 描述DOM事件捕獲的具體流程?

    window -> document -> HTML標籤 -> body -> … -> 目標元素

    [!NOTE]
    關鍵點: 注意根節點是window這個對象的

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
    </head>
    <body>
      <div id="container">
          <style>
              #container {
                  width: 200px;
                  height: 200px;
                  background-color: #ff255f;
              }
          </style>
      </div>
      <script>
          // 事件捕獲機制
          window.addEventListener('click', function(){
              console.log('window capture');
          }, true)
          document.addEventListener('click', function () {
              console.log('document capture');
          }, true)
          document.documentElement.addEventListener('click', function () {
              console.log('HTML capture');
          }, true)
          document.body.addEventListener('click', function () {
              console.log('body capture');
          }, true)
          document.getElementById('container').addEventListener('click', function () {
              console.log('container capture');
          }, true)
    
          // 事件冒泡機制
          window.addEventListener('click', function(){
              console.log('window capture');
          })
          document.addEventListener('click', function () {
              console.log('document capture');
          })
          document.documentElement.addEventListener('click', function () {
              console.log('HTML capture');
          })
          document.body.addEventListener('click', function () {
              console.log('body capture');
          })
          document.getElementById('container').addEventListener('click', function () {
              console.log('container capture');
          })
    
          // 輸出結果
          window capture  --> document capture --> HTML capture --> body capture --> container capture --> container capture -->  body capture --> HTML capture --> document capture --> window capture
      </script>
    </body>
    </html>

    3.1.8 如何拿到HTML這個標籤節點元素呢?(加分項)

      var html = document.documentElement;

    3.1.9 描述Event對象的常見應用?

    1. e.preventDefault() : 阻止默認事件(如阻止a標籤的默認跳轉行為)
    2. e.stopPropagation() : 阻止事件冒泡的行為
    3. *** e.stopImmediatePropagation() : 事件響應的優先級的應用場景,如果一個元素綁定了多個事件,但是又不想讓其他的事件執行的時候使用該方法【也會阻止冒泡】
    4. e.currentTarget : 當前所綁定的事件對象
      document.documentElement.onclick = function(e) {
        console.log(e.currentTarget, e.target);       // <html><body>...</body></html>()給綁定事件的那個元素, 當前被點擊的那個元素
      }

    [!NOTE]
    e.target : 當前被點擊的元素,父元素使用事件代理的方式來實現,可以直接使用該屬性獲取被點擊的那個元素

    3.2 如何自定義事件?(重點))

    3.2.1 如何給一個按鈕綁定一個自己定義的事件呢?

      // v1. 使用Event對象來自定義事件
      // 開始創建一個自己定義的事件對象
      var eve = new Event('customEvent');
      // 使用dom2事件處理的方式來給這個元素綁定一個事件
      var dom = document.documentElement;
      dom.addEventListener('customEvent', function(e) {
        console.log('customEvent called!');
      });
      // 下面的這句話可以在適合的場景中來觸發一個自己定義的事件對象
      setTimeout(function(){
        // 在1s之後觸發這個事件
        dom.dispatchEvent(eve);
      }, 1000)
    
    
      // v2. 使用CustomEvent來實現自定義事件
      var dom = document.documentElement;
      // 使用CustomEvent的方式可以在事件觸發的時候傳遞一個參數,然後通過e.detail 的方式來獲取這個參數信息
      var myClick = new CustomEvent('myClick', {detail : {name : 'zhangsan', age : 24}});
      dom.addEventListener('myClick', function(e){
        console.log(e.detail, e.target)
      })
      dom.dispatchEvent(myClick);
    

    4. HTTP協議

    4.1 HTTP協議的主要特點?

    • 簡單快速
    • 靈活
    • 無連接
    • 無狀態

    4.2 HTTP報文的組成部分?

    • 請求報文
      請求行:請求方法 資源地址 HTTP版本
      請求頭: key : value
      空行 :
      請求體 : name=zhangsan&age=18
    • 響應報文 : HTTP版本 狀態碼
      狀態行
      響應頭
      空行
      響應體

    4.3 HTTP方法?

    • GET : 獲取資源
    • POST : 傳輸資源
    • PUT :更新資源
    • DELETE : 刪除資源
    • HEAD :獲取報文首部
    • OPTIONS : 允許客戶端查看服務器的性能。

    4.4 POST和GET的區別?

    1. GET請求在瀏覽器回退的時候是無害的,而POST會再次提交請求
    2. GET請求產生的URL地址可以被收藏,而POST不可以
    3. GET請求會被瀏覽器主動緩存,而POST不會,除非主動設置
    4. GET請求只能進行URL編碼,而POST支持多種編碼方式
    5. GET請求參數會被完整第保留在瀏覽器的歷史記錄裏面,而POST參數不會被保留
    6. GET請求愛URL中傳送的參數的長度是有限的(2KB),而POST沒有限制
    7. 對參數的數據類型,GET值接受ASCII字符,而POST沒有限制
    8. POST比GET更安全,GET參數直接暴露在URL上,所以不能用來傳遞敏感信息
      9. GET參數通過URL傳遞,POST參數直接放在了Request body中

    4.5 HTTP狀態碼?

    4.5.1 狀態碼的第一位

    • 1xx :指示信息-表示請求已接收,繼續處理(重點)
    • 2xx :成功-表示請求已被成功接收
    • 3xx :重定向-要完成請求必須進行更進一步的操作
    • 4xx :客戶端錯誤-請求有語法錯誤或請求無法實現
    • 5xx :服務器錯誤-服務器未能實現合法的請求

    4.5.2 狀態碼詳解

    • 200 OK : 客戶端請求成功
    • 206 Partial Content : 客戶端發送了一個帶有Range頭的GET請求(Video標籤或者audio標籤在請求數據的時候)
    • 301 Moved Permanently : 請求的頁面已經轉移到了新的URL
    • 302 Found : 所請求的頁面已經臨時轉移到了新的URL
    • 304 Not Modified :客戶端有緩衝的文檔併發出了一個條件下的請求,原來緩衝的文檔還可以繼續使用
    • 400 Bad Request : 客戶端請求有語法錯誤,不被服務器所理解
    • 401 Unauthorized : 請求未經授權,這個狀態碼必須和WWW-Authenticate報頭域一起使用
    • 403 Forbidden:對被請求頁面的訪問被禁止
    • 404 Not Found : 請求資源不存在
    • 500 Internal Server Error :服務器發生不可預期的錯誤,原來緩衝的文檔還可以繼續使用
    • 503 Service Unavailable : 請求未完成,服務器臨時過載或宕機,一段時間后可能恢復正常

    4.6 什麼是持久連接?

    [!NOTE]
    HTTP協議採用‘請求-應答’模式, HTTP1.1版本才支持的,使用Keep-alive字段可以建立一個長連接,從而不需要每次請求都去建立一個新的連接。

    4.7 什麼是管線化?

    4.7.1 基本概念

    • 在使用持久連接(Keep-alive)的情況下,某個連接上的消息的傳遞類似於:請求1 –> 響應1 –> 請求2 –> 響應2 –> 請求3 –> 響應3
    • 管線化的過程: 請求1 –> 請求2 –> 請求3 –> 響應1 –> 響應2 –> 響應3

    4.7.2 管線化的特點(特點)

    1. 管線化機制通過持久連接完成,僅在HTTP1.1版本之後支持
    2. 只有GET和HEAD請求可以進行管線化,POST有所限制的
    3. 初次創建連接的時候不應該啟動管線機制,因為對方(服務器)不一定支持HTTP1.1版本的協議
    4. 管線化不會影響到響應到來的順序,HTTP響應返回的順序並未改變
    5. HTTP1.1 要求服務器支持管線化,但並不要求服務器也對響應進行管線化處理,只是要求對於管線化的請求不失敗即可
    6. 由於上面提到的服務器端問題,開啟管線化很可能並不會帶來大幅度的性能提升,而且很多服務器和代理程序對管線化的支持並不好,因此現代的瀏覽器如Chrome和Firefox默認並沒有開啟管線化支持

    5. 原型鏈

    5.1 創建對象的幾種方法?

    // 1. 使用字面量的方式來創建
    var o1 = {name : 'zhangsan'};
    var o11 = new Object({name : 'zhangsan'});
    
    // 2. 使用普通構造函數的方式來創建
    var M = function(){
        this.name = 'zhangsan';
    }
    var o2 = new M();
    
    // 3. Object.create方法
    var p = {name : 'zhangsan'};
    var o3 = Object.create(p);

    5.2 原型、構造函數、實例、原型鏈?

    構造函數:使用new運算符來聲明一個實例(任何函數都是可以通過構造函數來使用的)

    原型鏈:通過原型鏈可以找到上一級別的原型對象

    原型對象:多個實例公用的數據和屬性或者方法

    5.3 instanceof的原理?

    [!NOTE]
    instanceof 檢測一個對象A是不是另一個對象B的實例的原理是:查看對象B的prototype指向的對象是否在對象A的[[prototype]]鏈上。如果在,則返回true,如果不在則返回false。不過有一個特殊的情況,當對象B的prototype為null將會報錯(類似於空指針異常)。

    // 2. 使用普通構造函數的方式來創建
    var M = function(){
      this.name = 'zhangsan';
    }
    var o2 = new M();
    undefined
    o2.__proto__ == M.prototype
    true
    o2.__proto__ == M.prototype
    true
    o2.__proto__.constructor === Object
    false
    o2.__proto__.constructor === M
    true

    5.4 new運算符的原理?

    1. 一個新對象被創建。它繼承於foo.prototype
    2. 構造函數foo被執行。執行的時候,相應的傳參會被傳入,同時上下文(this)會被指定為這個新實例,new foo等同於 new foo(),只能用在不傳遞任何參數的情況
    3. 如果構造函數返回了一個“對象”,那麼這個對象會取代整個new出來的結果。如果構造函數沒有返回對象,那麼new 出來的結果為步驟1創建的對象
    // new 一個對象的過程
    var _new = function (fn) {
      // 1. 創建一個對象,這個對象要繼承fn這個構造函數的原型對象
      var o = Object.create(fn.prototype);
      // 2. 執行構造函數
      var k = fn.call(o, arguments);
      // 3. 看下執行的這個函數的運行效果是不是函數
      if (typeof k === 'object'){
          return k;
      }
      else
      {
          return o;
      }
    }

    6. 面向對象

    6.1 類與繼承:如何實現繼承,繼承的幾種實現方式

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
    </head>
    <body>
    <script>
      // 類的聲明
      function Animal1() {
          this.name = 'name';
      }
      // ES6 中的class的聲明
      class Animal2 {
          constructor(){
              this.name = 'name';
          }
      }
    
      console.log(new Animal1(), new Animal2());
      ///////////////////////////////////////////////////////////////////////////////////////////
    
    
      // 如何實現類的繼承呢???-----------本質:原型鏈
      // v1. 藉助構造函數實現繼承
      function Parent1() {
          this.name = 'parent1'
      }
      Parent1.prototype.sayHello = function () {
          console.log('hello');
      }
      function Child1() {
          // 執行父親的構造函數:
          // 1. 實現原理:將父級函數的this指向了這個子類的實例上面去了
          // 2. 缺點:父親的原型鏈上面的方法或者屬性不能被繼承;只能實現部分繼承
          Parent1.call(this);
          this.type = 'child1';
      }
      // 沒有參數的時候,可以直接new + 函數名稱
      console.log(res = new Child1);
    
    
    
    
      // v2. 藉助原型鏈實現繼承
      function Parent2() {
          this.name = 'parent2';
          this.data = [1, 2, 3];
      }
      Parent2.prototype.sayHello = function () {
          console.log('hello');
      }
      function Child2() {
          this.type = 'child2';
      }
      // prototype 就是為了讓這個對象的實例可以訪問到原型鏈上的內容
      Child2.prototype = new Parent2();
      // new Child2().__proto__ === Child2.prototype  // true
      // new Child2().__proto__.name                  // parent2
      // 原型鏈繼承的缺點:
      // 1. 原理:通過修改原型鏈來實現對象的繼承關係
      // 2. 缺點:修改第一個對象上面的屬性,會直接修改第二個對象屬性數據(引用類型)
      var c1 = new Child2();
      var c2 = new Child2();
      c1.data.push(100, 200, 300);
    
      // v3. 組合繼承
      function Parent3() {
          this.name = 'parent3';
          this.data = [1, 2, 3];
      }
      function Child3() {
          // 1. 借用構造函數繼承
          Parent3.call(this);
          this.type = 'child3';
      }
      // 2. 原型鏈繼承
      // child3的原型對象是Parent3的一個實例對象,但是這個實例對象中是沒有constructor這個屬性的,因此尋找屬性的時候回沿着這個實例對象的原型鏈繼續向上尋找new Parent3().prototype 這個原型對象的,
      // 最終在Parent3.prototype這個原型對象中找到了這個屬性,new一個對象找的實際上是{Parent3.prototype.constructor : Parent3}
      Child3.prototype = new Parent3();
      var c1 = new Child3();
      var c2 = new Child3();
      c1.data.push(100, 200, 300);
      // 組合繼承的特點:
      // 1. 原理:結合借用構造函數繼承和原型鏈繼承的優點,摒棄二者的缺點
      // 2. 缺點:父類構造函數在創建實例的時候總共執行了兩次(new Parent3(), new Child3())
    
    
      // v4. 組合繼承的優化1
      function Parent4() {
          this.name = 'parent4';
          this.data = [1, 2, 3];
      }
      function Child4() {
          // 1. 借用構造函數繼承
          Parent4.call(this);
          this.type = 'child4';
      }
      // 讓子類的構造函數的原型對象和父類構造函數的原型對象執向同一個對象(都是同一個對象)
      Child4.prototype = Parent4.prototype;
      // 測試
      var c1 = new Child4();
      var c2 = new Child4();
      console.log(c1 instanceof Child4, c1 instanceof Parent4);
      console.log(c1.constructor)         // Parent4? 如何實現:c1.constructor(c1.__proto__.constructor) === Child4 呢?
      // 缺點:
      // 1. 無法通過原型對象的constructor屬性來獲取對象的屬性對應的構造函數了(子類和父類公用的是一個contructor)
      // 2. obj instanceof Child4 === true; obj instanceof Parent4 === true
      // 3. obj.__proto__.constructor === Child4; obj.__proto__.constructor === Parent4  ???
    
      // v5. 組合繼承的優化2【完美寫法】
      function Parent5() {
          this.name = 'parent5';
          this.data = [1, 2, 3, 4, 5];
      }
      function Child5(){
          Parent5.call(this);
          this.type = 'child5';
      }
    
      // 通過創建中間對象的方式來把兩個對象區分開
      // var obj = new Object(); obj.__proto__ = Constructor.prototype;
      // 1. Object.create創建的對象obj, 這個obj的原型對象就是參數
      // 2. Child5的原型對象是Child5.prototype
      // 3. Child5.prototype = obj,obj這個對象相當於就是一个中間的橋樑關係
      Child5.prototype = Object.create(Parent5.prototype);
      // 當前的方式還是會按照原型鏈一級一級向上尋找的, 給Child5的原型對象上面綁定一個自己定義的constructor屬性
      Child5.prototype.constructor = Child5;
    
      // var s1 = new Child5()
    
      // 上面的代碼等價於
      var obj = Object.create(Parent5.prototype);     // obj.prototype = Parent5.prototype
      Child5.prototype = obj;
      Child5.prototype.constructor = Child5;
      // 1. 對象之間就是通過__proto__ 屬性向上尋找的
      // 2. 尋找規則: child5 ---> Child5.prototype ---> obj(Object.create(Parent5.prototype)) ---> Parent5.prototype
    
    
      // 技巧:不要讓面試官問太多題目:拖拉時間【擠牙膏】,把一個問題盡量吃透
      // 消化這一塊內容
    </script>
    </body>
    </html>

    [!WARNING]
    面試技巧

    1. 不要讓面試官問太多題目:拖拉時間【擠牙膏】,把一個問題盡量吃透
    2. 知識深度

    7. 通信

    7.1 什麼是同源策略個限制?

    [!NOTE]
    同源策略限制是從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。(一個源的文檔或腳本是沒有權利直接操作另外一個源的文檔或腳本的)

    1. Cookie, LocalStorage和IndexDB無法讀取
    2. DOM無法獲得;(document.body是無法獲取的)
    3. Ajax請求不能發送

    7.2 前後端如何進行通信呢?

    1. Ajax(有同源策略限制);Fetch API則是XMLHttpRequest的最新替代技術, 它是W3C的正式標準
    2. WebSocket:支持跨域請求數據,沒有同源策略的限制
    3. CORS:新的協議通信標準;CORS則將導致跨域訪問的請求分為三種:Simple Request,Preflighted Request以及Requests with Credential;cors相對於jsonp而言的好處就是支持所有的請求方式,不止是get請求,還支持post,put請求等等,而它的缺點就很明顯,無法兼容所有的瀏覽器,對於要兼容到老式瀏覽器而言,還是使用jsonp好點

    7.3 如何創建Ajax呢?

    1. XMLHttpRequest對象的工作流程
    2. 瀏覽器的兼容性處理【重點】
    3. 事件的觸發條件
    4. 事件的觸發順序
      function ajax(params){
        // 1. 創建對象,考慮兼容性【重點】
        var xhr = XMLHTTPRequest ? new XMLHTTPRequest() : new window.ActiveXObject('Microsoft.XMLHTTP');      // *** 兼容性問題必須考慮
        // 2. 打開連接
        var type = params.type || 'GET',
            url = params.url || '',
            data = params.data || {},
            success = params.success,
            error = params.error,
            dataArr = [];
        for (var k in data) {
          dataArr.push(k + '=' + data[k]);
        }
        //帶上Cookie
        xhr.withCredentials = true;
        if (type.toUpperCase() === 'GET') {
          // get
          url += '?' + dataArr.join('&');
          // 問號結尾的話,直接替換為空字符串
          xhr.open(type, url.replace(/\?$/g, ''), true);
          // GET 請求的話,是不需要再send方法中帶上參數的
          xhr.send();
        }
        else {
          // POST
          xhr.open(type, url, true);
          xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
          // POST 請求需要把數據放在send方法裏面, data = name=zhangsna&age=18&sex=male
          xhr.send(dataArr.join('&'));
        }
        // 開始監聽變化
        xhr.onreadystatechange = function(){
          // 這裏需要考慮強緩存和協商緩存的話直接處理,206是媒體資源的創建方式
          if (xhr.readyState === 4 && xhr.status === 200 || xhr.status === 304) {
              var res;
              if (success instanceof Function) {
                res = xhr.responseText;
                if (typeof res === 'string') {
                  res = JSON.parse(res);
                  // 開始執行成功的回調函數
                  success.call(xhr, res);
                }
              } else {
                if (error instanceof Function) {
                  // 失敗的話直接返回這個responseText中的內容信息
                  error.call(xhr, res);
                }
              }
          }
        }
      }

    7.4 跨域通信的幾種方式?

    7.4.1 JSONP

      function jsonp(url, onsuccess, onerror, charset){
        // 1. 全局註冊一個callback
        var callbackName = 'callback' + Math.random() * 100;
        window[callbackName] = function(){
          if (onsuccess && typeof onsuccess === 'Function') {
            onsuccess(arguments[0]);
          }
        }
        // 2. 動態創建一個script標籤
        var script = document.createElement('script');
        script.setAttribute('type', 'text/javascript');
        charset && script.setAttribute('charset', charset);
        script.setAttribute('src', url);
        script.async = true;
        // 3. 開始監聽處理的過程
        script.onload = script.onreadystatechange = function(){
          if (!script.readyState || /loaded|complete/.test(script.readyState)) {
            // 4. 成功之後移除這個事件
            script.onload = script.onreadystatechange = null;
            // 刪除這個script的DOM對象(head.removeChild(script), 這個DOM節點的父節點相當於是head標籤這個父節點)
            script.parentNode && script.parentNode.removeChild(script);
            // 刪除函數或變量
            window[callbackName] = null;
          }
        }
        script.onerror = function(){
          if (onerror && typeof onerror === 'Function') {
            onerror();
          }
        }
        // 5. 開始發送這個請求(把這個標籤放在頁面中的head標籤中即可)
        document.getElementsByTagName('head')[0].appendChild(script);
      }

    7.4.2 Hash

    hash 改變后頁面不會刷新的

    [!NOTE]
    使用場景:當前的頁面A通過iframe或者frame嵌入了跨域的頁面

      // 1. A頁面中的代碼如下
      var B = document.getElementsByTagName('iframe');
      B.src = B.src + '#' + JSON.stringfy(data);
      // 2. B中的偽代碼如下
      window.onhashchange = function(){
        var data = window.location.hash;    // 接受數據
        data = JSON.parse(data);
      }

    7.4.3 postMessage(HTML5中新增)

    [!NOTE]
    使用場景: 可以實現窗口A(A.com)向窗口B(B.com)發送信息

      // 1. 窗口B中的代碼如下
      var BWindow = window;
      BWindow.postMessage(JSON.stringfy(data), 'http://www.A.com');   
      // 2. 窗口A中代碼
      var AWindow = window;
      AWindow.addEventListener('message', function(e){
          console.log(e.origin);                  // http://www.B.com
          console.log(e.source);                  // BWindow
    
          e.source.postMessage('已成功收到消息');
    
          console.log(JSON.parse(e.data));        // data
      }, false)
      // 父窗口給子窗口發信息,需要用iframe的contentWindow屬性作為調用主體
      // 子窗口給父窗口發的信息需要使用window.top,多層iframe使用window.frameElement

    7.4.4 . WebSocket

    [!NOTE]
    不受同源策略影響,可以直接使用

      var ws = new window.WebSocket('ws://echo.websocket.org');
    
      // 打開連接
      ws.onopen = function(e){
        console.log('Connection open ……');
        ws.send('Hello WebSocket!');
      }
    
      // 接受消息
      ws.onmessage = function(e){
        console.log('Received Message : ', e.data);
      }
    
      // 關閉連接
      ws.onclose = function(e){
        console.log('Connection closed');
      }

    7.4.5 CORS

    支持跨域通信版本的Ajax,是一種新的標準(Origin頭)【ajax的一個變種,適用於任何】

      fetch('/get/name', {
        method : 'get'
      }).then(function(response){
        console.log(response);
      }).catch(function(err){
        // 出錯了;等價於then的第二個參數
      });
      // 原因:瀏覽器默認會攔截ajax請求,會根據頭中的origin消息進行判斷處理消息;Origin字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。JSONP只支持GET請求,CORS支持所有類型的HTTP請求。JSONP的優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。
    7.4.5.1 CORS請求的基本流程
    1. 對於簡單請求,瀏覽器直接發出CORS請求。具體來說,就是在頭信息之中,增加一個Origin字段。
    2. Origin字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
    3. 如果Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。
    4. 如果Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。
      Access-Control-Allow-Origin: http://api.bob.com   // 必需的字段
      Access-Control-Allow-Credentials: true            // 可選字段: 是否允許發送cookie
      Access-Control-Expose-Headers: FooBar
      Content-Type: text/html; charset=utf-8
    1. 簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為”預檢”請求(preflight)。OPTIONS表示當前的這個請求是用來詢問的;服務器收到”預檢”請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以後,確認允許跨源請求,就可以做出回應。
    7.4.5.2 JSONP和CORS的區別?
    1. JSONP只支持GET請求,CORS支持所有類型的HTTP請求
    2. JSONP的優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。

    8. 安全

    8.1 CSRF

    8.1.1 基本概念和縮寫

    CSRF: 跨站請求偽造,Cross site request forgery

    8.1.2 CSRF 攻擊原理

    8.1.3 可以成功攻擊的條件?

    1. 目標網站存在CSRF漏洞的請求接口(一般為get請求)
    2. 目標用戶之前已經成功登錄過這個網站(留下了Cookie)

    8.1.4 如何防禦呢?

    1. Token驗證:訪問服務器接口的時候,會自動帶上這個token
    2. Referer驗證:驗證網站的頁面來源(只有我當前網站下的頁面才可以請求,對於來自其他網站的請求一律攔截)
    3. 隱藏令牌: 隱藏信息會放在header中(類似於Token)

    8.2 XSS

    8.2.1 基本概念和縮寫

    XSS: cross-site scripting, 跨站腳本攻擊

    8.2.2 XSS防禦

    攻擊原理: 注入JS腳本

    防禦措施: 讓JS代碼無法解析執行

    8.3 CSRF和XSS的區別呢?

    1. CSRF:網站本身存在漏洞的接口,依賴這些登錄過目標網站的用戶來實現信息的竊取;
    2. XSS:向頁面中注入JS執行,JS函數體內執行目標任務;

    [!NOTE]

    1. 一定要說出中文名稱,實現原理,防範措施都說出來
    2. 不要拖泥帶水,言簡意賅

    9. 算法

    [!NOTE]
    算法攻略:多刷題才是硬道理!!!

    二三面(知識深度面)

    10. 渲染機制

    10.1 什麼是DOCTYPE及作用?

    1. DTD(Document Type Definition):文檔類型定義,是一系列的語法規則,用來定義XML或者(X)HTML的文件類型。瀏覽器會使用它來判斷文檔的類型,決定使用哪一種協議來解析,以及切換瀏覽器模式;
    2. DOCTYPE: 是用來聲明文檔類型和DTD規範的,一個主要的用途是文件的合法性驗證;如果文件代碼不合法,那麼瀏覽器解析的時候就會出現一些出錯
    3. 總結:Doctype就是通知瀏覽器當前的文檔是屬於那種類型的,包含哪些DTD。
      <!--HTML5的寫法-->
      <DOCTYPE html>
      <!-- HTML 4.01  Strict
        1. 這個DTD 包含所有的HTML元素和屬性
        2. 但是不包含展示性的和棄用的元素(比如font)
      -->
      <DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" >
      <!-- HTML 4.0.1 Transitional
        1. 這個DTD 包含所有的HTML元素和屬性
        2. 也包含展示性的和棄用性的元素(比如font)
      -->
      <DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd" >

    [!NOTE]
    在W3C標準出來之前,不同的瀏覽器對頁面渲染有不同的標準,產生了一定的差異。這種渲染方式叫做混雜模式。在W3C標準出來之後,瀏覽器對頁面的渲染有了統一的標準,這種渲染方式叫做標準模式。<!DOCTYPE>不存在或者形式不正確會導致HTML或XHTML文檔以混雜模式呈現,就是把如何渲染html頁面的權利交給了瀏覽器,有多少種瀏覽器就有多少種展示方式。因此要提高瀏覽器兼容性就必須重視<!DOCTYPE>

    10.2 嚴格模式和混雜模式

    [!NOTE]
    嚴格模式和混雜模式都是瀏覽器的呈現模式,瀏覽器究竟使用混雜模式還是嚴格模式呈現頁面與網頁中的DTD(文件類型定義)有關,DTD裡面包含了文檔的規則。比如:loose.dtd

    • 嚴格模式:又稱標準模式,是指瀏覽器按照W3C標準來解析代碼,呈現頁面
    • 混雜模式:又稱為怪異模式或者兼容模式,是指瀏覽器按照自己的方式來解析代碼,使用一種比較寬鬆的向後兼容的方式來显示頁面。

    10.3 瀏覽器的渲染過程?

    10.3.1 開始進行DOM解析,渲染DOM Tree

    10.3.2 開始進行CSS解析,渲染CSSOM Tree

    10.3.3 DOM樹和CSS樹的結合,最後會轉換為Render Tree

    10.3.4 Layout的過程,計算每一個DOM元素的位置、寬度、高度等信息,最終渲染並显示頁面到瀏覽器

    10.4 何時會觸發Reflow?

    [!NOTE]
    定義:DOM結構中每個元素都有自己的盒子模型,這些都是需要根據各種樣式來計算並根據計算結果將元素放在它該出現的位置,這個過程就是reflow;

    1. 當你增加、刪除、修改DOM節點的時候,會導致Reflow或Repaint
    2. 當你移動DOM的位置,或者設置動畫的時候
    3. 當你修改CSS樣式的時候
    4. 當你Resize窗口的時候(移動端沒有這個問題,與瀏覽器有關),或者在滾動窗口的時候
    5. 當你修改網頁的默認的字體的時候

    10.5 何時回觸發Repaint?

    [!NOTE]
    定義:當各種盒子的位置、大小以及其他屬性,例如顏色、字體大小都確定下來以後,瀏覽器於是便按照元素各自的特性繪製了一遍,於是頁面的內容出現了,這個過程就是repaint

    1. DOM改動
    2. CSS改動

    10.6 如何最大程度上的減少瀏覽器的重繪Repaint過程(頻率)呢?

    10.6.1 避免在document上直接進行頻繁的DOM操作,如果確實需要可以採用off-document的方式進行

      1. 先將元素從document中刪除,完成修改之後然後再把元素放回原來的位置
      1. 將元素的display設置為none, 然後完成修改之後再把元素的display屬性修改為原來的值
      1. 如果需要創建多個DOM節點,可以使用DocumentFragment創建完畢之後一次性地加入document中去
      var frag = document.createDocumentFragment();
      frag.appendChild(dom);    /*每次創建的節點先放入DocumentFragment中*/

    10.6.2 集中修改樣式

    1. 盡可能少的修改元素style上的屬性
    2. 盡量通過修改className來修改樣式(一次性修改)
    3. 通過cssText屬性來設置樣式值
      document.getElementById("d1").style.cssText = "color:red; font-size:13px;";

    10.6.3 緩存Layout的屬性值

    [!NOTE]
    對於Layout屬性中非引用類型的值(数字型),如果需要多次訪問則可以在一次訪問時先存儲到局部變量中,之後都使用局部變量,這樣可以避免每次讀取屬性時造成瀏覽器的渲染。

      var width = el.offsetWidth;
      var scrollLeft = el.scrollLeft;

    10.6.4 設置元素的position為absolute或fixed

    [!NOTE]
    在元素的position為static和relative時,元素處於DOM樹結構當中,當對元素的某個操作需要重新渲染時,瀏覽器會渲染整個頁面。將元素的position設置為absolute和fixed可以使元素從DOM樹結構中脫離出來獨立的存在,而瀏覽器在需要渲染時只需要渲染該元素以及位於該元素下方的元素,從而在某種程度上縮短瀏覽器渲染時間。

    11. 布局Layout?

    Layout屬性包括

    1. offsetLeft、offsetTop、offsetHeight、offsetWidth: 相對於父對象的邊距信息,且返回值為数字;left獲取或設置相對於具有定位屬性(position定義為relative)的父對象的邊距信息,返回值為字符串10px
    2. scrollTop/Left/Width/Height:滾動條在各個方向上拉動的距離,返回值為数字
    3. clientTop/Left/Width/Height:瀏覽器的可視區域的大小
    4. getComputedStyle()、currentStyle(in IE):瀏覽器渲染DOM元素之後的寬度和高度等樣式信息

    12. JS運行機制

    12.1 如何理解JS的單線程?

    看代碼,寫結果?

      // 同步任務
      console.log(1);
      // 異步任務要掛起
      setTimeout(function(){
        console.log(2)
      }, 0);
      console.log(3)
      // out : 1 3 2
      console.log('A');
      setTimeout(function(){
        console.log('B')
      }, 0);
      while (true) {
    
      }
    
      // out : A

    12.2 什麼是任務隊列?

      for (var i = 0; i < 4; i++) {
        // setTimeout , setInterval 只有在時間到了的時候,才會把這個事件放在異步隊列中去
        setTimeout(function(){
          console.log(i);
        }, 1000);
      }
      // out : 4 4 4 4

    12.3 什麼是Event Loop?

    [!NOTE]
    JS是單線程的,瀏覽器引擎會先來執行同步任務,遇到異步任務之後,會把當前的這個異步任務放在time模塊中,等到主線程中的所有的同步任務全部執行完畢之後;然後當前的這個異步任務只有時間到了之後,才會把這個任務(回調函數)放在一個異步隊列中;噹噹前的任務棧中的任務全部執行完畢了之後,會先去執行微任務隊列中的任務(Promise),然後等到微任務隊列中的所有任務全部執行完畢之後,再去執行process.nextTick()這個函數,等到這個函數執行完畢之後,本次的事件輪訓結束;
    開啟新的執行棧,從宏任務隊列中依次取出異步任務,開始執行;每個宏任務執行都會重新開啟一個新的任務執行棧

    12.3.1 3個關鍵點

    1. 執行棧執行的是同步任務;
    2. 什麼時候去異步隊列中取這個任務;
    3. 什麼時候向這個任務隊列中放入新的異步任務

      12.3.2 異步任務的分類

    • setTimeout, setInterval;
    • DOM事件(點擊按鈕的時候也會先去執行同步任務);
    • Promise

    13. 知識點總結

    1. 理解JS的單線程的概念
    2. 理解任務隊列
    3. 理解Event Loop
    4. 理解哪些語句會翻入到異步任務隊列
    5. 理解與放入到異步任務隊列的時機

      13.1 頁面性能

      13.1.1 提升頁面性能的方法有哪些?

    6. 資源壓縮合併,減少HTTP請求;
    7. 非核心代碼的異步加載 —> 異步加載的方式有哪些? —> 異步加載的區別?
    8. 利用瀏覽器的緩存 —> 緩存的分類 —> 緩存的原理
    9. 使用CDN加速
    10. 預解析DNS:DNS Prefetch 是一種DNS 預解析技術,當你瀏覽網頁時,瀏覽器會在加載網頁時對網頁中的域名進行解析緩存,這樣在你單擊當前網頁中的連接時就無需進行DNS的解析,減少用戶等待時間,提高用戶體驗。(提前解析域名,而不是點擊鏈接的時候才去進行DNS域名解析,可以節省DNS解析需要耗費的20-120毫秒時間)

      <!-- https協議的網站,默認是關閉了DNS的預解析的,可以使用下面的語句開啟  -->
      <meta http-equiv="x-dns-prefetch-control" content="on">
      <!-- 開始配置需要進行DNS預解析的域名 -->
      <link rel="dns-prefetch" href="//www.zhix.net">                               <!--支持http和HTTPS-->
      <link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />               <!--支持http的協議-->
      <link rel="dns-prefetch" href="http://nsclick.baidu.com" />
      <link rel="dns-prefetch" href="http://hm.baidu.com" />
      <link rel="dns-prefetch" href="http://eiv.baidu.com" />

    14. 異步加載的方式

    14.1 動態腳本的加載

      var script = document.createElement('script');
      document.getElementsByTagName('head')[0].appendChild(script);
    
      // 沒有 defer 或 async,瀏覽器會立即加載並執行指定的腳本,“立即”指的是在渲染該 script 標籤之下的文檔元素之前,也就是說不等待後續載入的文檔元素,讀到就加載並執行。
      <script src="script.js"></script>

    14.2 defer

    <!-- 有 defer,加載後續文檔元素的過程將和 script.js 的加載并行進行(異步),但是 script.js 的執行要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。 -->
    <script defer src="myscript.js"></script>

    14.3 async

      <!-- 有 async,加載和渲染後續文檔元素的過程將和 script.js 的加載與執行并行進行(異步)。 -->
      <script async src="script.js"></script>

    14.4 異步加載的區別?

    [!NOTE]

    1. defer是在HTML解析完成之後(DOMContentLoaded事件執行之後)才會執行,如果是多個,會按照加載的順序依次執行(按照順序執行)
    2. async是在加載完之後立即執行,如果是多個,執行順序和加載順序無關(與順序無關)

    15. 說一下瀏覽器的緩存機制吧?

    15.1 緩存的分類

    [!NOTE]
    緩存目的就是為了提升頁面的性能

    15.1.1 強緩存

    直接從本地讀取,不發送請求

        Response Headers
        cache-control: max-age=315360000(相對時間,優先級比expires高)
        expires: Sat, 10 Mar 2029 04:01:39 GMT(絕對時間)

    15.1.2 協商緩存

    問一下服務器,這個文件有沒有過期,然後再使用這個文件

        Response Headers
        last-modified: Tue, 12 Mar 2019 06:22:34 GMT(絕對時間)
        etag: "52-583dfb6f4de80"

    向服務器請求資源的時候,帶上if-Modified-Since或者if-None-Match這個請求頭,去詢問服務器:

        Request Headers
        if-Modified-Since: Tue, 12 Mar 2019 06:22:34 GMT
        if-None-Match: "52-583dfb6f4de80"

    16. 錯誤監控/如何保證前端產品的上線質量?

    16.1 前端錯誤的分類?

    1. 即時運行錯誤:代碼錯誤
    2. 資源加載錯誤:圖片/css/js文件加載失敗

    16.2 錯誤的捕獲方式?

    16.2.1 即時運行錯誤的捕獲方式

      // 方法一:使用try catch捕獲
      try {
        // ...
      } catch (e) {
        // error
      } finally {
        // handle error
      }
    
      // 方法二:使用window.onerror 捕獲錯誤
      // 無法捕獲到資源加載錯誤
      window.onerror = function(msg, url, line, col, error){
        // ...
      }  
      window.addEventListener('error', function(msg, url, line, col, error){
        // ...
      })

    16.2.2 資源加載錯誤(不會向上冒泡)

      // 方法一: 直接在script, img這些DOM標籤上面直接加上onerror事件
      Object.onerror = function(e){
          // ...
      }
    
      // 方法二:window.performace.getEntries(間接獲取資源加載錯誤的數量)
      var loadedResources = window.performance.getEntries();           // 1. 獲取瀏覽器中已經加載的所有資源(包括各個階段的詳細加載時間)
      var loaderImgs = loadedResources.filter(item => {
          return /\.jpg|png|gif|svg/.test(item.name)
      });
      var imgs = document.getElementsByTagName('img');                // 2. 獲取頁面中所有的img集合
      var len = imgs.length - loaderImgs.length;                      // 3. 加載失敗的圖片數量
      console.log('圖片加載失敗數量:', len, '條');
    
    
      // 方法三: 使用事件捕獲的方式來實現Error事件捕獲
      // 使用事件捕獲的方式來實現資源加載錯誤的事件的捕獲:window ---> document --> html --- > body ---> div ---...
      window.addEventListener('error', function (msg) {
          console.log(msg);
      }, true);

    16.2.3 補充的方法

          // 使用事件捕獲的方式來實現
         window.addEventListener('error', function (msg) {
             console.log('資源加載異常成功捕獲:', msg);
         }, true);
         // 使用事件冒泡的方式是只能捕獲到運行的時候的一些異常
         window.addEventListener('error', function (e) {
             console.log('運行異常成功捕獲1:', e.message, e.filename, e.lineno, e.colno, e.error);
         }, false);
    
         // 這種方式是可以按照參數的方式來接受相關的參數信息
         window.onerror = function (msg, url, line, col, error) {
             console.log('運行異常成功捕獲2:', msg, url, line, col, error);
         }

    16.2.4 問題的延伸:跨域的js運行錯誤可以捕獲嗎,錯誤提示是什麼?應該怎麼處理呢?

    16.2.4.1 錯誤信息

    errorinfo :
    Script0 error
    0 row
    0 col
    16.2.4.2 處理方法
    1. 第一步:在script標籤上增加crossorigin屬性
      <!-- script 表情添加crossorigin屬性 -->
      <!-- 除了 script,所有能引入跨域資源的標籤包括 link 和 img 之類,都有一樣的屬性 -->
      <script crossorigin  src="http://www.lmj.com/demo/crossoriginAttribute/error.js"></script>
    1. 第二步:設置js資源響應頭’Access-Control-Allow-Origin: * ‘,服務器端需要開啟
      // 服務器可以直接設置一個響應頭信息
      res.setResponseHeader('Access-Control-Allow-Origin', 'www.lmj.com');

    16.3 上報錯誤的基本原理?

    1. 採用Ajax通信的方式來上報
    2. 利用Image對象進行上報(cnzz)[重點理解掌握]
      // 下面的兩種方式都是可以實現錯誤信息的上報功能的
      (new Image).src = 'http://www.baidu.com?name=zhangsna&age=18&sex=male'
      (new Image()).src = 'https://www.baidu.com?name=zhangsan'

    17. 如何使用JS獲取客戶端的硬件信息呢?

      // IE 瀏覽器提供的獲取電腦硬件的API
      var locator = new ActiveXObject ("WbemScripting.SWbemLocator");
      var service = locator.ConnectServer(".");
      var properties = service.ExecQuery("SELECT * FROM Win32_Processor");

    18. 使用window.performace 來實現用戶體驗的數據記錄呢?

    [!NOTE]
    可以參考性能優化章節-performance性能監控一文內容。

    三四面(業務項目面)

    [!NOTE]

    • 知識面要廣
    • 理解要深刻
    • 內心要誠實:沒了解過,問面試官有哪些資料可以學習
    • 態度要謙虛
    • 回答要靈活:把握一個度,不要和面試官爭執對錯
      • 要學會讚美:被問住了可以回答,適當讚美(沒面試官理解的那麼深,虛心請教)

    19.介紹一下你做過的項目?

    19.1 項目介紹模板(業務能力體現)

    1. 我做過什麼業務?
    2. 負責的業務有什麼業績?
    3. 使用了什麼技術方案?
    4. 突破了什麼技術難點?
    5. 遇到了什麼問題?
    6. 最大的收穫是什麼?

    19.2 團隊協作能力

    19.3 事務推動能力

    19.4 帶人能力

    終面(HR面)

    20. 技術終面或HR面試要點

    [!NOTE]
    主要考察點:樂觀积極、主動溝通、邏輯順暢、上進有責任心、有主張,做事果斷、職業競爭力、職業規劃

    20.1 職業競爭力

    1. 業務能力:可以做到行業第一

    2. 思考能力:對同一件事可以從不同角度去思考,找到最優解

    3. 學習能力:不斷學習新的業務,沉澱、總結

    4. 無上限的付出:對於無法解決的問題可以熬夜、加班

    20.2 職業規劃

    1. 目標是什麼:在業務上成為專家,在技術上成為行業大牛

    2. 近階段的目標:不斷的學習積累各方面地經驗,以學習為主

    3. 長期目標:做幾件有價值的事情,如開源作品、技術框架等

    4. 方式方法:先完成業務上的主要問題,做到極致,然後逐步向目標靠攏

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整