CVTE Java後臺電話一面

目錄html

學習Java多久了java

使用Java作過什麼東西node

項目中遇到的問題,而後我說了幾個,他貌似不感興趣,而後問了我內存溢出遇到過沒有mysql

Servlet的生命週期c++

session和cookie的區別web

對Java的集合類瞭解哪一些,回答了Collection和Map這兩個以及他們的子類,扯到hashMap面試

在什麼狀況下使用過hashMap,對hashmap的底層結構能夠說一下嗎算法

hashMap是線程安全的嗎,回答不是,而後又扯到ConcurrentHashMap,而後問ConcurrentHashMap的細節sql

問數據庫,回答學的mysql,問有幾種引擎,回答有好幾種,主要就兩種數據庫

索引有了解嗎

數據庫的三大範式

線程能夠簡單講講嗎,而後問線程和線程之間如何進行通訊

JVM瞭解多少,把本身知道的都說了,不過他沒問細節


學習Java多久了

使用Java作過什麼東西

項目中遇到的問題,而後我說了幾個,他貌似不感興趣,而後問了我內存溢出遇到過沒有

Servlet的生命週期

Servlet生命週期

加載和實例化Servlet

咱們來看一下Tomcat是如何加載的:

     1. 若是已配置自動裝入選項,則在啓動時自動載入。

     2. 在服務器啓動時,客戶機首次向Servlet發出請求。

     3. 從新裝入Servlet時。

      當啓動Servlet容器時,容器首先查找一個配置文件web.xml,這個文件中記錄了能夠提供服務的Servlet。每一個Servlet被指定一個Servlet名,也就是這個Servlet實際對應的Java的完整class文件名。Servlet容器會爲每一個自動裝入選項的Servlet建立一個實例。因此,每一個Servlet類必須有一個公共的無參數的構造器。

初始化

      當Servlet被實例化後,Servlet容器將調用每一個Servlet的init方法來實例化每一個實例,執行完init方法以後,Servlet處於「已初始化」狀態。因此說,一旦Servlet被實例化,那麼必將調用init方法。經過Servlet在啓動後不當即初始化,而是收到請求後進行。在web.xml文件中用<load-on-statup> ...... </load-on-statup>對Servlet進行預先初始化。

      初始化失敗後,執行init()方法拋出ServletException異常,Servlet對象將會被垃圾回收器回收,當客戶端第一次訪問服務器時加載Servlet實現類,建立對象並執行初始化方法。

請求處理

      Servlet 被初始化之後,就處於能響應請求的就緒狀態。每一個對Servlet 的請求由一個Servlet Request 對象表明。Servlet 給客戶端的響應由一個Servlet Response對象表明。對於到達客戶機的請求,服務器建立特定於請求的一個「請求」對象和一個「響應」對象。調用service方法,這個方法能夠調用其餘方法來處理請求。

      Service方法會在服務器被訪問時調用,Servlet對象的生命週期中service方法可能被屢次調用,因爲web-server啓動後,服務器中公開的部分資源將處於網絡中,當網絡中的不一樣主機(客戶端)併發訪問服務器中的同一資源,服務器將開設多個線程處理不一樣的請求,多線程同時處理同一對象時,有可能出現數據併發訪問的錯誤。

      另外注意,多線程不免同時處理同一變量時(如:對同一文件進行寫操做),且有讀寫操做時,必須考慮是否加上同步,同步添加時,不要添加範圍過大,有可能使程序變爲純粹的單線程,大大削弱了系統性能;只須要作到多個線程安全的訪問相同的對象就能夠了。

卸載Servlet

      當服務器再也不須要Servlet實例或從新裝入時,會調用destroy方法,使用這個方法,Servlet能夠釋放掉全部在init方法申請的資源。一個Servlet實例一旦終止,就不容許再次被調用,只能等待被卸載。

      Servlet一旦終止,Servlet實例便可被垃圾回收,處於「卸載」狀態,若是Servlet容器被關閉,Servlet也會被卸載,一個Servlet實例只能初始化一次,但能夠建立多個相同的Servlet實例。如相同的Servlet能夠在根據不一樣的配置參數鏈接不一樣的數據庫時建立多個實例。

session和cookie的區別

兩者的定義:

當你在瀏覽網站的時候,WEB 服務器會先送一小小資料放在你的計算機上,Cookie 會幫你在網站上所打的文字或是一些選擇,都紀錄下來。當下次你再光臨同一個網站,WEB 服務器會先看看有沒有它上次留下的 Cookie 資料,有的話,就會依據 Cookie裏的內容來判斷使用者,送出特定的網頁內容給你。 Cookie 的使用很廣泛,許多有提供我的化服務的網站,都是利用 Cookie來辨認使用者,以方便送出使用者量身定作的內容,像是 Web 接口的免費 email 網站,都要用到 Cookie。

具體來講cookie機制採用的是在客戶端保持狀態的方案,而session機制採用的是在服務器端保持狀態的方案,同時咱們也看到,因爲採用服務器端保持狀態的方案在客戶端也須要保存一個標識,因此session機制可能須要藉助於cookie機制來達到保存標識的目的,但實際上它還有其餘選擇。

cookie機制。正統的cookie分發是經過擴展HTTP協議來實現的,服務器經過在HTTP的響應頭中加上一行特殊的指示以提示瀏覽器按照指示生成相應的cookie。然而純粹的客戶端腳本如JavaScript或者VBScript也能夠生成cookie。而cookie的使用是由瀏覽器按照必定的原則在後臺自動發送給服務器的。瀏覽器檢查全部存儲的cookie,若是某個cookie所聲明的做用範圍大於等於將要請求的資源所在的位置,則把該cookie附在請求資源的HTTP請求頭上發送給服務器。
 
cookie的內容主要包括:名字,值,過時時間,路徑和域。路徑與域一塊兒構成cookie的做用範圍。若不設置過時時間,則表示這個cookie的生命期爲瀏覽器會話期間,關閉瀏覽器窗口,cookie就消失。這種生命期爲瀏覽器會話期的cookie被稱爲會話cookie。 

會話cookie通常不存儲在硬盤上而是保存在內存裏,固然這種行爲並非規範規定的。若設置了過時時間,瀏覽器就會把cookie保存到硬盤上,關閉後再次打開瀏覽器,這些cookie仍然有效直到超過設定的過時時間。存儲在硬盤上的cookie能夠在不一樣的瀏覽器進程間共享,好比兩個IE窗口。而對於保存在內存裏的cookie,不一樣的瀏覽器有不一樣的處理方式。

session機制是一種服務器端的機制,服務器使用一種相似於散列表的結構(也可能就是使用散列表)來保存信息。 當程序須要爲某個客戶端的請求建立一個session時,服務器首先檢查這個客戶端的請求裏是否已包含了一個session標識(稱爲session id),若是已包含則說明之前已經爲此客戶端建立過session,服務器就按照session id把這個session檢索出來使用(檢索不到,會新建一個),若是客戶端請求不包含session id,則爲此客戶端建立一個session而且生成一個與此session相關聯的session id,session id的值應該是一個既不會重複,又不容易被找到規律以仿造的字符串,這個session id將被在本次響應中返回給客戶端保存。保存這個session id的方式能夠採用cookie,這樣在交互過程當中瀏覽器能夠自動的按照規則把這個標識發送給服務器。通常這個cookie的名字都是相似於SEEESIONID。但cookie能夠被人爲的禁止,則必須有其餘機制以便在cookie被禁止時仍然可以把session id傳遞迴服務器。

常常被使用的一種技術叫作URL重寫,就是把session id直接附加在URL路徑的後面。還有一種技術叫作表單隱藏字段。就是服務器會自動修改表單,添加一個隱藏字段,以便在表單提交時可以把session id傳遞迴服務器。好比: 
<form name="testform" action="/xxx"> 
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764"> 
<input type="text"> 
</form> 
實際上這種技術能夠簡單的用對action應用URL重寫來代替。

cookie 和session 的區別:

一、cookie數據存放在客戶的瀏覽器上,session數據放在服務器上。

二、cookie不是很安全,別人能夠分析存放在本地的COOKIE並進行COOKIE欺騙,考慮到安全應當使用session。

三、session會在必定時間內保存在服務器上。當訪問增多,會比較佔用你服務器的性能,   考慮到減輕服務器性能方面,應當使用COOKIE。

四、單個cookie保存的數據不能超過4K,不少瀏覽器都限制一個站點最多保存20個cookie。

五、因此我的建議:
   將登錄信息等重要信息存放爲SESSION
   其餘信息若是須要保留,能夠放在COOKIE中

對Java的集合類瞭解哪一些,回答了Collection和Map這兩個以及他們的子類,扯到hashMap

在什麼狀況下使用過hashMap,對hashmap的底層結構能夠說一下嗎

1. HashMap的數據結構

數據結構中有數組和鏈表來實現對數據的存儲,但這二者基本上是兩個極端。

數組

數組存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,爲O(1);數組的特色是:尋址容易,插入和刪除困難;

鏈表

鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特色是:尋址困難,插入和刪除容易。

哈希表

那麼咱們能不能綜合二者的特性,作出一種尋址容易,插入刪除也容易的數據結構?答案是確定的,這就是咱們要提起的哈希表。哈希表((Hash table)既知足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。

  哈希表有多種不一樣的實現方法,我接下來解釋的是最經常使用的一種方法—— 拉鍊法,咱們能夠理解爲「鏈表的數組」 ,如圖:

 

 

  從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。

  HashMap其實也是一個線性的數組實現的,因此能夠理解爲其存儲數據的容器就是一個線性數組。這可能讓咱們很不解,一個線性的數組怎麼實現按鍵值對來存取數據呢?這裏HashMap有作一些處理。

  首先HashMap裏面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。

    /**

     * The table, resized as necessary. Length MUST Always be a power of two.

     */

    transient Entry[] table;

2. HashMap的存取實現

     既然是線性數組,爲何能隨機存取?這裏HashMap用了一個小算法,大體是這樣實現:

// 存儲時:
int hash = key.hashCode(); // 這個hashCode方法這裏不詳述,只要理解每一個key的hash是一個固定的int值
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

 

1)put

 

疑問:若是兩個key經過hash%Entry[].length獲得的index相同,會不會有覆蓋的危險?

  這裏HashMap裏面用到鏈式數據結構的一個概念。上面咱們提到過Entry類裏面有一個next屬性,做用是指向下一個Entry。打個比方, 第一個鍵值對A進來,經過計算其key的hash獲得的index=0,記作:Entry[0] = A。一會後又進來一個鍵值對B,經過計算其index也等於0,如今怎麼辦?HashMap會這樣作:B.next = A,Entry[0] = B,若是又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣咱們發現index=0的地方其實存取了A,B,C三個鍵值對,他們經過next這個屬性連接在一塊兒。因此疑問不用擔憂。也就是說數組中存儲的是最後插入的元素。到這裏爲止,HashMap的大體實現,咱們應該已經清楚了。

 public V put(K key, V value) {

        if (key == null)

            return putForNullKey(value); //null老是放在數組的第一個鏈表中

        int hash = hash(key.hashCode());

        int i = indexFor(hash, table.length);

        //遍歷鏈表

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {

            Object k;

            //若是key在鏈表中已存在,則替換爲新value

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }

        }

 

        modCount++;

        addEntry(hash, key, value, i);

        return null;

    }

 

void addEntry(int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數e, 是Entry.next

    //若是size超過threshold,則擴充table大小。再散列

    if (size++ >= threshold)

            resize(2 * table.length);

}

  固然HashMap裏面也包含一些優化方面的實現,這裏也說一下。好比:Entry[]的長度必定後,隨着map裏面數據的愈來愈長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap裏面設置一個因子,隨着map的size愈來愈大,Entry[]會以必定的規則加長長度。

2)get

 public V get(Object key) {

        if (key == null)

            return getForNullKey();

        int hash = hash(key.hashCode());

        //先定位到數組元素,再遍歷該元素處的鏈表

        for (Entry<K,V> e = table[indexFor(hash, table.length)];

             e != null;

             e = e.next) {

            Object k;

            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

                return e.value;

        }

        return null;

}

 

3)null key的存取

null key老是存放在Entry[]數組的第一個元素。

   private V putForNullKey(V value) {

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {

            if (e.key == null) {

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }

        }

        modCount++;

        addEntry(0, null, value, 0);

        return null;

    }

 

    private V getForNullKey() {

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {

            if (e.key == null)

                return e.value;

        }

        return null;

    }

 

 

 

 

4)肯定數組index:hashcode % table.length取模

HashMap存取時,都須要計算當前key應該對應Entry[]數組哪一個元素,即計算數組下標;算法以下:

   /**

     * Returns index for hash code h.

     */

    static int indexFor(int h, int length) {

        return h & (length-1);

    }

 

按位取並,做用上至關於取模mod或者取餘%。

這意味着數組下標相同,並不表示hashCode相同。

 

5)table初始大小

 

  public HashMap(int initialCapacity, float loadFactor) {

        .....

 

        // Find a power of 2 >= initialCapacity

        int capacity = 1;

        while (capacity < initialCapacity)

            capacity <<= 1;

 

        this.loadFactor = loadFactor;

        threshold = (int)(capacity * loadFactor);

        table = new Entry[capacity];

        init();

    }

注意table初始大小並非構造函數中的initialCapacity!!

而是 >= initialCapacity的2的n次冪!!!!

————爲何這麼設計呢?——

3. 解決hash衝突的辦法

  1. 開放定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列)
  2. 再哈希法
  3. 鏈地址法
  4. 創建一個公共溢出區

Java中hashmap的解決辦法就是採用的鏈地址法。

 

4. 再散列rehash過程

當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那麼該方法就將容量調整到Integer.MAX_VALUE返回,這時,須要建立一張新表,將原表的映射到新表中。

   /**

     * Rehashes the contents of this map into a new array with a

     * larger capacity.  This method is called automatically when the

     * number of keys in this map reaches its threshold.

     *

     * If current capacity is MAXIMUM_CAPACITY, this method does not

     * resize the map, but sets threshold to Integer.MAX_VALUE.

     * This has the effect of preventing future calls.

     *

     * @param newCapacity the new capacity, MUST be a power of two;

     *        must be greater than current capacity unless current

     *        capacity is MAXIMUM_CAPACITY (in which case value

     *        is irrelevant).

     */

    void resize(int newCapacity) {

        Entry[] oldTable = table;

        int oldCapacity = oldTable.length;

        if (oldCapacity == MAXIMUM_CAPACITY) {

            threshold = Integer.MAX_VALUE;

            return;

        }

 

        Entry[] newTable = new Entry[newCapacity];

        transfer(newTable);

        table = newTable;

        threshold = (int)(newCapacity * loadFactor);

    }

 

    /**

     * Transfers all entries from current table to newTable.

     */

    void transfer(Entry[] newTable) {

        Entry[] src = table;

        int newCapacity = newTable.length;

        for (int j = 0; j < src.length; j++) {

            Entry<K,V> e = src[j];

            if (e != null) {

                src[j] = null;

                do {

                    Entry<K,V> next = e.next;

                    //從新計算index

                    int i = indexFor(e.hash, newCapacity);

                    e.next = newTable[i];

                    newTable[i] = e;

                    e = next;

                } while (e != null);

            }

        }

    }

hashMap是線程安全的嗎,回答不是,而後又扯到ConcurrentHashMap,而後問ConcurrentHashMap的細節

ConcurrentHashMap是Java 5中支持高併發、高吞吐量的線程安全HashMap實現。在這以前我對ConcurrentHashMap只有一些膚淺的理解,僅知道它採用了多個鎖,大概也足夠了。可是在通過一次慘痛的面試經歷以後,我以爲必須深刻研究它的實現。面試中被問到讀是否要加鎖,由於讀寫會發生衝突,我說必需要加鎖,我和麪試官也所以發生了衝突,結果可想而知。仍是閒話少說,經過仔細閱讀源代碼,如今總算理解ConcurrentHashMap實現機制了,其實現之精巧,使人歎服,與你們共享之。

實現原理 

鎖分離 (Lock Stripping)

ConcurrentHashMap容許多個修改操做併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不一樣部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不一樣的部分,每一個段其實就是一個小的hash table,它們有本身的鎖。只要多個修改操做發生在不一樣的段上,它們就能夠併發進行。

有些方法須要跨段,好比size()和containsValue(),它們可能須要鎖定整個表而而不只僅是某個段,這須要按順序鎖定全部段,操做完畢後,又按順序釋放全部段的鎖。這裏「按順序」是很重要的,不然極有可能出現死鎖,在ConcurrentHashMap內部,段數組是final的,而且其成員變量實際上也是final的,可是,僅僅是將數組聲明爲final的並不保證數組成員也是final的,這須要實現上的保證。這能夠確保不會出現死鎖,由於得到鎖的順序是固定的。不變性在多線程編程佔有很重要的地位,下面還要談到。

Java代碼  收藏代碼

  1. /** 
  2.  * The segments, each of which is a specialized hash table 
  3.  */  
  4. final Segment<K,V>[] segments;  

不變(Immutable)和易變(Volatile)

ConcurrentHashMap徹底容許多個讀操做併發進行,讀操做並不須要加鎖。若是使用傳統的技術,如HashMap中的實現,若是容許能夠在hash鏈的中間添加或刪除元素,讀操做不加鎖將獲得不一致的數據。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry表明每一個hash鏈中的一個節點,其結構以下所示:

Java代碼  收藏代碼

  1. static final class HashEntry<K,V> {  
  2.     final K key;  
  3.     final int hash;  
  4.     volatile V value;  
  5.     final HashEntry<K,V> next;  
  6. }  

能夠看到除了value不是final的,其它值都是final的,這意味着不能從hash鏈的中間或尾部添加或刪除節點,由於這須要修改next引用值,全部的節點的修改只能從頭部開始。對於put操做,能夠一概添加到Hash鏈的頭部。可是對於remove操做,可能須要從中間刪除一個節點,這就須要將要刪除節點的前面全部節點整個複製一遍,最後一個節點指向要刪除結點的下一個結點。這在講解刪除操做時還會詳述。爲了確保讀操做可以看到最新的值,將value設置成volatile,這避免了加鎖。


其它

爲了加快定位段以及段中hash槽的速度,每一個段hash槽的的個數都是2^n,這使得經過位運算就能夠定位段和段中hash槽的位置。當併發級別爲默認值16時,也就是段的個數,hash值的高4位決定分配在哪一個段中。可是咱們也不要忘記《算法導論》給咱們的教訓:hash槽的的個數不該該是2^n,這可能致使hash槽分配不均,這須要對hash值從新再hash一次。(這段彷佛有點多餘了 )

這是從新hash的算法,還比較複雜,我也懶得去理解了。

Java代碼  收藏代碼

  1. private static int hash(int h) {  
  2.     // Spread bits to regularize both segment and index locations,  
  3.     // using variant of single-word Wang/Jenkins hash.  
  4.     h += (h <<  15) ^ 0xffffcd7d;  
  5.     h ^= (h >>> 10);  
  6.     h += (h <<   3);  
  7.     h ^= (h >>>  6);  
  8.     h += (h <<   2) + (h << 14);  
  9.     return h ^ (h >>> 16);  
  10. }  

 

這是定位段的方法:

Java代碼  收藏代碼

  1. final Segment<K,V> segmentFor(int hash) {  
  2.     return segments[(hash >>> segmentShift) & segmentMask];  
  3. }  

數據結構

 

關於Hash表的基礎數據結構,這裏不想作過多的探討。Hash表的一個很重要方面就是如何解決hash衝突,ConcurrentHashMap和HashMap使用相同的方式,都是將hash值相同的節點放在一個hash鏈中。與HashMap不一樣的是,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)。下面是ConcurrentHashMap的數據成員:

 

Java代碼  收藏代碼

  1. public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
  2.         implements ConcurrentMap<K, V>, Serializable {  
  3.     /** 
  4.      * Mask value for indexing into segments. The upper bits of a 
  5.      * key's hash code are used to choose the segment. 
  6.      */  
  7.     final int segmentMask;  
  8.   
  9.     /** 
  10.      * Shift value for indexing within segments. 
  11.      */  
  12.     final int segmentShift;  
  13.   
  14.     /** 
  15.      * The segments, each of which is a specialized hash table 
  16.      */  
  17.     final Segment<K,V>[] segments;  
  18. }  

 

全部的成員都是final的,其中segmentMask和segmentShift主要是爲了定位段,參見上面的segmentFor方法。

每一個Segment至關於一個子Hash表,它的數據成員以下:

 

Java代碼  收藏代碼

  1.     static final class Segment<K,V> extends ReentrantLock implements Serializable {  
  2. private static final long serialVersionUID = 2249069246763182397L;  
  3.         /** 
  4.          * The number of elements in this segment's region. 
  5.          */  
  6.         transient volatile int count;  
  7.   
  8.         /** 
  9.          * Number of updates that alter the size of the table. This is 
  10.          * used during bulk-read methods to make sure they see a 
  11.          * consistent snapshot: If modCounts change during a traversal 
  12.          * of segments computing size or checking containsValue, then 
  13.          * we might have an inconsistent view of state so (usually) 
  14.          * must retry. 
  15.          */  
  16.         transient int modCount;  
  17.   
  18.         /** 
  19.          * The table is rehashed when its size exceeds this threshold. 
  20.          * (The value of this field is always <tt>(int)(capacity * 
  21.          * loadFactor)</tt>.) 
  22.          */  
  23.         transient int threshold;  
  24.   
  25.         /** 
  26.          * The per-segment table. 
  27.          */  
  28.         transient volatile HashEntry<K,V>[] table;  
  29.   
  30.         /** 
  31.          * The load factor for the hash table.  Even though this value 
  32.          * is same for all segments, it is replicated to avoid needing 
  33.          * links to outer object. 
  34.          * @serial 
  35.          */  
  36.         final float loadFactor;  
  37. }  

count用來統計該段數據的個數,它是volatile,它用來協調修改和讀取操做,以保證讀取操做可以讀取到幾乎最新的修改。協調方式是這樣的,每次修改操做作告終構上的改變,如增長/刪除節點(修改節點的值不算結構上的改變),都要寫count值,每次讀取操做開始都要讀取count的值。這利用了Java 5中對volatile語義的加強,對同一個volatile變量的寫和讀存在happens-before關係。modCount統計段結構改變的次數,主要是爲了檢測對多個段進行遍歷過程當中某個段是否發生改變,在講述跨段操做時會還會詳述。threashold用來表示須要進行rehash的界限值。table數組存儲段中節點,每一個數組元素是個hash鏈,用HashEntry表示。table也是volatile,這使得可以讀取到最新的table值而不須要同步。loadFactor表示負載因子。

實現細節

 

修改操做

 

先來看下刪除操做remove(key)。

Java代碼  收藏代碼

  1. public V remove(Object key) {  
  2.     hash = hash(key.hashCode());  
  3.     return segmentFor(hash).remove(key, hash, null);  
  4. }  

整個操做是先定位到段,而後委託給段的remove操做。當多個刪除操做併發進行時,只要它們所在的段不相同,它們就能夠同時進行。下面是Segment的remove方法實現:

Java代碼  收藏代碼

  1. V remove(Object key, int hash, Object value) {  
  2.     lock();  
  3.     try {  
  4.         int c = count - 1;  
  5.         HashEntry<K,V>[] tab = table;  
  6.         int index = hash & (tab.length - 1);  
  7.         HashEntry<K,V> first = tab[index];  
  8.         HashEntry<K,V> e = first;  
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  10.             e = e.next;  
  11.   
  12.         V oldValue = null;  
  13.         if (e != null) {  
  14.             V v = e.value;  
  15.             if (value == null || value.equals(v)) {  
  16.                 oldValue = v;  
  17.                 // All entries following removed node can stay  
  18.                 // in list, but all preceding ones need to be  
  19.                 // cloned.  
  20.                 ++modCount;  
  21.                 HashEntry<K,V> newFirst = e.next;  
  22.                 for (HashEntry<K,V> p = first; p != e; p = p.next)  
  23.                     newFirst = new HashEntry<K,V>(p.key, p.hash,  
  24.                                                   newFirst, p.value);  
  25.                 tab[index] = newFirst;  
  26.                 count = c; // write-volatile  
  27.             }  
  28.         }  
  29.         return oldValue;  
  30.     } finally {  
  31.         unlock();  
  32.     }  
  33. }  

 整個操做是在持有段鎖的狀況下執行的,空白行以前的行主要是定位到要刪除的節點e。接下來,若是不存在這個節點就直接返回null,不然就要將e前面的結點複製一遍,尾結點指向e的下一個結點。e後面的結點不須要複製,它們能夠重用。下面是個示意圖,我直接從這個網站 上覆制的(畫這樣的圖實在是太麻煩了,若是哪位有好的畫圖工具,能夠推薦一下)。

 

 

刪除元素以前:

 

a hash chain before an element is removed

 

 

刪除元素3以後:

the chain with element 3 removed

 

第二個圖其實有點問題,複製的結點中應該是值爲2的結點在前面,值爲1的結點在後面,也就是恰好和原來結點順序相反,還好這不影響咱們的討論。

 

整個remove實現並不複雜,可是須要注意以下幾點。第一,當要刪除的結點存在時,刪除的最後一步操做要將count的值減一。這必須是最後一步操做,不然讀取操做可能看不到以前對段所作的結構性修改。第二,remove執行的開始就將table賦給一個局部變量tab,這是由於table是volatile變量,讀寫volatile變量的開銷很大。編譯器也不能對volatile變量的讀寫作任何優化,直接屢次訪問非volatile實例變量沒有多大影響,編譯器會作相應優化。

 

 

接下來看put操做,一樣地put操做也是委託給段的put方法。下面是段的put方法:

Java代碼  收藏代碼

  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.     lock();  
  3.     try {  
  4.         int c = count;  
  5.         if (c++ > threshold) // ensure capacity  
  6.             rehash();  
  7.         HashEntry<K,V>[] tab = table;  
  8.         int index = hash & (tab.length - 1);  
  9.         HashEntry<K,V> first = tab[index];  
  10.         HashEntry<K,V> e = first;  
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  12.             e = e.next;  
  13.   
  14.         V oldValue;  
  15.         if (e != null) {  
  16.             oldValue = e.value;  
  17.             if (!onlyIfAbsent)  
  18.                 e.value = value;  
  19.         }  
  20.         else {  
  21.             oldValue = null;  
  22.             ++modCount;  
  23.             tab[index] = new HashEntry<K,V>(key, hash, first, value);  
  24.             count = c; // write-volatile  
  25.         }  
  26.         return oldValue;  
  27.     } finally {  
  28.         unlock();  
  29.     }  
  30. }  

該方法也是在持有段鎖的狀況下執行的,首先判斷是否須要rehash,須要就先rehash。接着是找是否存在一樣一個key的結點,若是存在就直接替換這個結點的值。不然建立一個新的結點並添加到hash鏈的頭部,這時必定要修改modCount和count的值,一樣修改count的值必定要放在最後一步。put方法調用了rehash方法,reash方法實現得也很精巧,主要利用了table的大小爲2^n,這裏就不介紹了。

 

修改操做還有putAll和replace。putAll就是屢次調用put方法,沒什麼好說的。replace甚至不用作結構上的更改,實現要比put和delete要簡單得多,理解了put和delete,理解replace就不在話下了,這裏也不介紹了。

 

 

獲取操做

 

首先看下get操做,一樣ConcurrentHashMap的get操做是直接委託給Segment的get方法,直接看Segment的get方法:

 

Java代碼  收藏代碼

  1. V get(Object key, int hash) {  
  2.     if (count != 0) { // read-volatile  
  3.         HashEntry<K,V> e = getFirst(hash);  
  4.         while (e != null) {  
  5.             if (e.hash == hash && key.equals(e.key)) {  
  6.                 V v = e.value;  
  7.                 if (v != null)  
  8.                     return v;  
  9.                 return readValueUnderLock(e); // recheck  
  10.             }  
  11.             e = e.next;  
  12.         }  
  13.     }  
  14.     return null;  
  15. }  

 

get操做不須要鎖。第一步是訪問count變量,這是一個volatile變量,因爲全部的修改操做在進行結構修改時都會在最後一步寫count變量,經過這種機制保證get操做可以獲得幾乎最新的結構更新。對於非結構更新,也就是結點值的改變,因爲HashEntry的value變量是volatile的,也能保證讀取到最新的值。接下來就是對hash鏈進行遍歷找到要獲取的結點,若是沒有找到,直接訪回null。對hash鏈進行遍歷不須要加鎖的緣由在於鏈指針next是final的。可是頭指針卻不是final的,這是經過getFirst(hash)方法返回,也就是存在table數組中的值。這使得getFirst(hash)可能返回過期的頭結點,例如,當執行get方法時,剛執行完getFirst(hash)以後,另外一個線程執行了刪除操做並更新頭結點,這就致使get方法中返回的頭結點不是最新的。這是能夠容許,經過對count變量的協調機制,get能讀取到幾乎最新的數據,雖然可能不是最新的。要獲得最新的數據,只有採用徹底的同步。

 

最後,若是找到了所求的結點,判斷它的值若是非空就直接返回,不然在有鎖的狀態下再讀一次。這彷佛有些費解,理論上結點的值不可能爲空,這是由於put的時候就進行了判斷,若是爲空就要拋NullPointerException。空值的惟一源頭就是HashEntry中的默認值,由於HashEntry中的value不是final的,非同步讀取有可能讀取到空值。仔細看下put操做的語句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在這條語句中,HashEntry構造函數中對value的賦值以及對tab[index]的賦值可能被從新排序,這就可能致使結點的值爲空。這種狀況應當很罕見,一旦發生這種狀況,ConcurrentHashMap採起的方式是在持有鎖的狀況下再讀一遍,這可以保證讀到最新的值,而且必定不會爲空值。

 

Java代碼  收藏代碼

  1. V readValueUnderLock(HashEntry<K,V> e) {  
  2.     lock();  
  3.     try {  
  4.         return e.value;  
  5.     } finally {  
  6.         unlock();  
  7.     }  
  8. }  

 

 

另外一個操做是containsKey,這個實現就要簡單得多了,由於它不須要讀取值:

Java代碼  收藏代碼

  1. boolean containsKey(Object key, int hash) {  
  2.     if (count != 0) { // read-volatile  
  3.         HashEntry<K,V> e = getFirst(hash);  
  4.         while (e != null) {  
  5.             if (e.hash == hash && key.equals(e.key))  
  6.                 return true;  
  7.             e = e.next;  
  8.         }  
  9.     }  
  10.     return false;  
  11. }  

 

 

跨段操做 

 

有些操做須要涉及到多個段,好比說size(), containsValaue()。先來看下size()方法:

 

Java代碼  收藏代碼

  1. public int size() {  
  2.     final Segment<K,V>[] segments = this.segments;  
  3.     long sum = 0;  
  4.     long check = 0;  
  5.     int[] mc = new int[segments.length];  
  6.     // Try a few times to get accurate count. On failure due to  
  7.     // continuous async changes in table, resort to locking.  
  8.     for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {  
  9.         check = 0;  
  10.         sum = 0;  
  11.         int mcsum = 0;  
  12.         for (int i = 0; i < segments.length; ++i) {  
  13.             sum += segments[i].count;  
  14.             mcsum += mc[i] = segments[i].modCount;  
  15.         }  
  16.         if (mcsum != 0) {  
  17.             for (int i = 0; i < segments.length; ++i) {  
  18.                 check += segments[i].count;  
  19.                 if (mc[i] != segments[i].modCount) {  
  20.                     check = -1; // force retry  
  21.                     break;  
  22.                 }  
  23.             }  
  24.         }  
  25.         if (check == sum)  
  26.             break;  
  27.     }  
  28.     if (check != sum) { // Resort to locking all segments  
  29.         sum = 0;  
  30.         for (int i = 0; i < segments.length; ++i)  
  31.             segments[i].lock();  
  32.         for (int i = 0; i < segments.length; ++i)  
  33.             sum += segments[i].count;  
  34.         for (int i = 0; i < segments.length; ++i)  
  35.             segments[i].unlock();  
  36.     }  
  37.     if (sum > Integer.MAX_VALUE)  
  38.         return Integer.MAX_VALUE;  
  39.     else  
  40.         return (int)sum;  
  41. }  

 

size方法主要思路是先在沒有鎖的狀況下對全部段大小求和,若是不能成功(這是由於遍歷過程當中可能有其它線程正在對已經遍歷過的段進行結構性更新),最多執行RETRIES_BEFORE_LOCK次,若是還不成功就在持有全部段鎖的狀況下再對全部段大小求和。在沒有鎖的狀況下主要是利用Segment中的modCount進行檢測,在遍歷過程當中保存每一個Segment的modCount,遍歷完成以後再檢測每一個Segment的modCount有沒有改變,若是有改變表示有其它線程正在對Segment進行結構性併發更新,須要從新計算。

 

 

其實這種方式是存在問題的,在第一個內層for循環中,在這兩條語句sum += segments[i].count; mcsum += mc[i] = segments[i].modCount;之間,其它線程可能正在對Segment進行結構性的修改,致使segments[i].count和segments[i].modCount讀取的數據並不一致。這可能使size()方法返回任什麼時候候都未曾存在的大小,很奇怪javadoc竟然沒有明確標出這一點,多是由於這個時間窗口過小了吧。size()的實現還有一點須要注意,必需要先segments[i].count,才能segments[i].modCount,這是由於segment[i].count是對volatile變量的訪問,接下來segments[i].modCount才能獲得幾乎最新的值(前面我已經說了爲何只是「幾乎」了)。這點在containsValue方法中獲得了淋漓盡致的展示:

 

 

Java代碼  收藏代碼

  1. public boolean containsValue(Object value) {  
  2.     if (value == null)  
  3.         throw new NullPointerException();  
  4.   
  5.     // See explanation of modCount use above  
  6.   
  7.     final Segment<K,V>[] segments = this.segments;  
  8.     int[] mc = new int[segments.length];  
  9.   
  10.     // Try a few times without locking  
  11.     for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {  
  12.         int sum = 0;  
  13.         int mcsum = 0;  
  14.         for (int i = 0; i < segments.length; ++i) {  
  15.             int c = segments[i].count;  
  16.             mcsum += mc[i] = segments[i].modCount;  
  17.             if (segments[i].containsValue(value))  
  18.                 return true;  
  19.         }  
  20.         boolean cleanSweep = true;  
  21.         if (mcsum != 0) {  
  22.             for (int i = 0; i < segments.length; ++i) {  
  23.                 int c = segments[i].count;  
  24.                 if (mc[i] != segments[i].modCount) {  
  25.                     cleanSweep = false;  
  26.                     break;  
  27.                 }  
  28.             }  
  29.         }  
  30.         if (cleanSweep)  
  31.             return false;  
  32.     }  
  33.     // Resort to locking all segments  
  34.     for (int i = 0; i < segments.length; ++i)  
  35.         segments[i].lock();  
  36.     boolean found = false;  
  37.     try {  
  38.         for (int i = 0; i < segments.length; ++i) {  
  39.             if (segments[i].containsValue(value)) {  
  40.                 found = true;  
  41.                 break;  
  42.             }  
  43.         }  
  44.     } finally {  
  45.         for (int i = 0; i < segments.length; ++i)  
  46.             segments[i].unlock();  
  47.     }  
  48.     return found;  
  49. }  

 

一樣注意內層的第一個for循環,裏面有語句int c = segments[i].count; 可是c卻歷來沒有被使用過,即便如此,編譯器也不能作優化將這條語句去掉,由於存在對volatile變量count的讀取,這條語句存在的惟一目的就是保證segments[i].modCount讀取到幾乎最新的值。關於containsValue方法的其它部分就不分析了,它和size方法差很少。

 

 

跨段方法中還有一個isEmpty()方法,其實現比size()方法還要簡單,也不介紹了。最後簡單地介紹下迭代方法,如keySet(), values(), entrySet()方法,這些方法都返回相應的迭代器,全部迭代器都繼承於Hash_Iterator類(提交時竟然提醒我不能包含sh It,只得加了下劃線),裏實現了主要的方法。其結構是:

 

Java代碼  收藏代碼

  1. abstract class Hash_Iterator{  
  2.     int nextSegmentIndex;  
  3.     int nextTableIndex;  
  4.     HashEntry<K,V>[] currentTable;  
  5.     HashEntry<K, V> nextEntry;  
  6.     HashEntry<K, V> lastReturned;  
  7. }  

 

 nextSegmentIndex是段的索引,nextTableIndex是nextSegmentIndex對應段中中hash鏈的索引,currentTable是nextSegmentIndex對應段的table。調用next方法時主要是調用了advance方法:

 

 

Java代碼  收藏代碼

  1. final void advance() {  
  2.     if (nextEntry != null && (nextEntry = nextEntry.next) != null)  
  3.         return;  
  4.   
  5.     while (nextTableIndex >= 0) {  
  6.         if ( (nextEntry = currentTable[nextTableIndex--]) != null)  
  7.             return;  
  8.     }  
  9.   
  10.     while (nextSegmentIndex >= 0) {  
  11.         Segment<K,V> seg = segments[nextSegmentIndex--];  
  12.         if (seg.count != 0) {  
  13.             currentTable = seg.table;  
  14.             for (int j = currentTable.length - 1; j >= 0; --j) {  
  15.                 if ( (nextEntry = currentTable[j]) != null) {  
  16.                     nextTableIndex = j - 1;  
  17.                     return;  
  18.                 }  
  19.             }  
  20.         }  
  21.     }  
  22. }  

不想再多介紹了,惟一須要注意的是跳到下一個段時,必定要先讀取下一個段的count變量。 

 

這種迭代方式的主要效果是不會拋出ConcurrentModificationException。一旦獲取到下一個段的table,也就意味着這個段的頭結點在迭代過程當中就肯定了,在迭代過程當中就不能反映對這個段節點併發的刪除和添加,對於節點的更新是可以反映的,由於節點的值是一個volatile變量。

問數據庫,回答學的mysql,問有幾種引擎,回答有好幾種,主要就兩種

(1):MyISAM存儲引擎
不支持事務、也不支持外鍵,優點是訪問速度快,對事務完整性沒有 要求或者以select,insert爲主的應用基本上能夠用這個引擎來建立表

支持3種不一樣的存儲格式,分別是:靜態表;動態表;壓縮表

靜態表:表中的字段都是非變長字段,這樣每一個記錄都是固定長度的,優勢存儲很是迅速,容易緩存,出現故障容易恢復;缺點是佔用的空間一般比動態表多(由於存儲時會按照列的寬度定義補足空格)ps:在取數據的時候,默認會把字段後面的空格去掉,若是不注意會把數據自己帶的空格也會忽略。

動態表:記錄不是固定長度的,這樣存儲的優勢是佔用的空間相對較少;缺點:頻繁的更新、刪除數據容易產生碎片,須要按期執行OPTIMIZE TABLE或者myisamchk-r命令來改善性能

壓縮表:由於每一個記錄是被單獨壓縮的,因此只有很是小的訪問開支

(2)InnoDB存儲引擎*

該存儲引擎提供了具備提交、回滾和崩潰恢復能力的事務安全。可是對比MyISAM引擎,寫的處理效率會差一些,而且會佔用更多的磁盤空間以保留數據和索引。 
InnoDB存儲引擎的特色:支持自動增加列,支持外鍵約束

(3):MEMORY存儲引擎

Memory存儲引擎使用存在於內存中的內容來建立表。每一個memory表只實際對應一個磁盤文件,格式是.frm。memory類型的表訪問很是的快,由於它的數據是放在內存中的,而且默認使用HASH索引,可是一旦服務關閉,表中的數據就會丟失掉。 
MEMORY存儲引擎的表能夠選擇使用BTREE索引或者HASH索引,兩種不一樣類型的索引有其不一樣的使用範圍

Hash索引優勢: 
Hash 索引結構的特殊性,其檢索效率很是高,索引的檢索能夠一次定位,不像B-Tree 索引須要從根節點到枝節點,最後才能訪問到頁節點這樣屢次的IO訪問,因此 Hash 索引的查詢效率要遠高於 B-Tree 索引。 
Hash索引缺點: 那麼不精確查找呢,也很明顯,由於hash算法是基於等值計算的,因此對於「like」等範圍查找hash索引無效,不支持;

Memory類型的存儲引擎主要用於哪些內容變化不頻繁的代碼表,或者做爲統計操做的中間結果表,便於高效地對中間結果進行分析並獲得最終的統計結果,。對存儲引擎爲memory的表進行更新操做要謹慎,由於數據並無實際寫入到磁盤中,因此必定要對下次從新啓動服務後如何得到這些修改後的數據有所考慮。

(4)MERGE存儲引擎

 

Merge存儲引擎是一組MyISAM表的組合,這些MyISAM表必須結構徹底相同,merge表自己並無數據,對merge類型的表能夠進行查詢,更新,刪除操做,這些操做其實是對內部的MyISAM表進行的。

索引有了解嗎

mysql索引須要瞭解的幾個注意

板子以前作過2年web開發培訓(入門?),得到挺多學生好評,這是蠻有成就感的一件事,準備花點時間根據當時的一些備課內容整理出一系列文章出來,但願能給更多人帶來幫助,這是系列文章的第一篇

注:科普文章一篇,大牛繞道

索引是作什麼的?

索引用於快速找出在某個列中有一特定值的行。不使用索引,MySQL必須從第1條記錄開始而後讀完整個表直到找出相關的行。
表越大,花費的時間越多。若是表中查詢的列有一個索引,MySQL能快速到達一個位置去搜尋到數據文件的中間,沒有必要看全部數據。

大多數MySQL索引(PRIMARY KEY、UNIQUE、INDEX和FULLTEXT)在B樹中存儲。只是空間列類型的索引使用R-樹,而且MEMORY表還支持hash索引。

索引好複雜,我該怎麼理解索引,有沒一個更形象點的例子?

有,想象一下,你面前有本詞典,數據就是書的正文內容,你就是那個cpu,而索引,則是書的目錄

索引越多越好?

大多數狀況下索引能大幅度提升查詢效率,但:

  • 數據的變動(增刪改)都須要維護索引,所以更多的索引意味着更多的維護成本
  • 更多的索引意味着也須要更多的空間 (一本100頁的書,卻有50頁目錄?)
  • 太小的表,建索引可能會更慢哦 :)  (讀個2頁的宣傳手冊,你還先去找目錄?)

索引的字段類型問題

  • text類型,也可建索引(需指定長度)
  • myisam存儲引擎索引鍵長度綜合不能超過1000字節
  • 用來篩選的值儘可能保持和索引列一樣的數據類型

 like 不能用索引?

  • 儘可能減小like,但不是絕對不可用,」xxxx%」 是能夠用到索引的,

想象一下,你在看一本成語詞典,目錄是按成語拼音順序創建,查詢需求是,你想找以 「一」字開頭的成語(」一%「),和你想找包含一字的成語(「%一%」)

  • 除了like,如下操做符也可用到索引:

<,<=,=,>,>=,BETWEEN,IN

<>,not in ,!=則不行

什麼樣的字段不適合建索引?

  • 通常來講,列的值惟一性過小(如性別,類型什麼的),不適合建索引(怎樣叫過小?一半說來,同值的數據超過表的百分之15,那就不必建索引了)
  • 太長的列,能夠選擇只創建部分索引,(如:只取前十位作索引)
  • 更新很是頻繁的數據不適宜建索引(怎樣叫很是?意會)

 一次查詢能用多個索引嗎?

不能

多列查詢該如何建索引?

一次查詢只能用到一個索引,因此 首先槍斃 a,b各建索引方案

a仍是b? 誰的區分度更高(同值的最少),建誰!

固然,聯合索引也是個不錯的方案,ab,仍是ba,則同上,區分度高者,在前

聯合索引的問題?

where a = 「xxx」 能夠使用 AB 聯合索引
where b = 「xxx」 則不可 (再想象一下,這是書的目錄?)

因此,大多數狀況下,有AB索引了,就能夠不用在去建一個A索引了

哪些常見狀況不能用索引?

  • like 「%xxx」
  • not in , !=
  • 對列進行函數運算的狀況(如 where md5(password) = 「xxxx」)
  • WHERE index=1 OR A=10
  • 存了數值的字符串類型字段(如手機號),查詢時記得不要丟掉值的引號,不然沒法用到該字段相關索引,反之則不要緊

也即

select * from test where mobile = 13711112222;

但是沒法用到mobile字段的索引的哦(若是mobile是char 或 varchar類型的話)

btw,千萬不要嘗試用int來存手機號(爲何?本身想!要不本身試試)

 

覆蓋索引(Covering Indexes)擁有更高效率

索引包含了所需的所有值的話,就只select 他們,換言之,只select 須要用到的字段,如無必要,可儘可能避免select *

NULL 的問題

NULL會致使索引形同虛設,因此在設計表結構時應避免NULL 的存在(用其餘方式表達你想表達的NULL,好比 -1?)

如何查看索引信息,如何分析是否正確用到索引?

show index from tablename;
explain select ……;

關於explain,改天能夠找個時間專門寫一篇入門帖,在此以前,能夠嘗試 google

瞭解本身的系統,不要過早優化!

過早優化,一直是個很是討厭而又時刻存在的問題,大多數時候就是由於不瞭解本身的系統,不知道本身系統真正的承載能力

好比:幾千條數據的新聞表,天天幾百幾千次的正文搜索,大多數時候咱們能夠放心的去like,而不要又去建一套全文搜索什麼的,畢竟cpu仍是比人腦厲害太多

分享個小案例:

曾經有個朋友找板子,說:大師幫看看,公司網站打不開

板子笑了笑:大師可不敢當啊,待我看看再說

板子花了10分鐘分析了下:中小型企業站,量不大(兩三萬pv天天),獨立服務器,數據量不大(100M不到),應該不至於太慢
某個外包團隊作的項目,年久失修,完全改造?不現實!

因而,板子花了20分鐘給能夠加索引的字段都加上了索引,因而,世界安靜了

朋友說:另一個哥們說,優化至少得2w外包費,你只用30分鐘,看來,大師你是當之無愧了,選個最好的餐館吧

板子:那就來點西餐吧,常熟路地鐵站肯德基等你!

數據庫的三大範式

第一範式

   一、每一列屬性都是不可再分的屬性值,確保每一列的原子性

 

   二、兩列的屬性相近或類似或同樣,儘可能合併屬性同樣的列,確保不產生冗餘數據。

 

 

 

若是需求知道那個省那個市並按其分類,那麼顯然第一個表格是不容易知足需求的,也不符合第一範式。

 

 

 

顯然第一個表結構不但不能知足足夠多物品的要求,還會在物品少時產生冗餘。也是不符合第一範式的。

 

第二範式

 

每一行的數據只能與其中一列相關,即一行數據只作一件事。只要數據列中出現數據重複,就要把表拆分開來。

 

 

一我的同時訂幾個房間,就會出來一個訂單號多條數據,這樣子聯繫人都是重複的,就會形成數據冗餘。咱們應該把他拆開來。

 

 

 

 

 

這樣便實現啦一條數據作一件事,不摻雜複雜的關係邏輯。同時對錶數據的更新維護也更易操做。

 

第三範式

 數據不能存在傳遞關係,即沒個屬性都跟主鍵有直接關係而不是間接關係。像:a-->b-->c  屬性之間含有這樣的關係,是不符合第三範式的。

好比Student表(學號,姓名,年齡,性別,所在院校,院校地址,院校電話)

這樣一個表結構,就存在上述關係。 學號--> 所在院校 --> (院校地址,院校電話)

這樣的表結構,咱們應該拆開來,以下。

(學號,姓名,年齡,性別,所在院校)--(所在院校,院校地址,院校電話)

 

最後:

三大範式只是通常設計數據庫的基本理念,能夠創建冗餘較小、結構合理的數據庫。若是有特殊狀況,固然要特殊對待,數據庫設計最重要的是看需求跟性能,需求>性能>表結構。因此不能一味的去追求範式創建數據庫。

線程能夠簡單講講嗎,而後問線程和線程之間如何進行通訊

一,介紹

本總結我對於JAVA多線程中線程之間的通訊方式的理解,主要以代碼結合文字的方式來討論線程間的通訊,故摘抄了書中的一些示例代碼。

 

二,線程間的通訊方式

①同步

這裏講的同步是指多個線程經過synchronized關鍵字這種方式來實現線程間的通訊。

參考示例:

複製代碼

public class MyObject {

    synchronized public void methodA() {
        //do something....
    }

    synchronized public void methodB() {
        //do some other thing
    }
}

public class ThreadA extends Thread {

    private MyObject object;
//省略構造方法
    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

public class ThreadB extends Thread {

    private MyObject object;
//省略構造方法
    @Override
    public void run() {
        super.run();
        object.methodB();
    }
}

public class Run {
    public static void main(String[] args) {
        MyObject object = new MyObject();

        //線程A與線程B 持有的是同一個對象:object
        ThreadA a = new ThreadA(object);
        ThreadB b = new ThreadB(object);
        a.start();
        b.start();
    }
}

複製代碼

因爲線程A和線程B持有同一個MyObject類的對象object,儘管這兩個線程須要調用不一樣的方法,可是它們是同步執行的,好比:線程B須要等待線程A執行完了methodA()方法以後,它才能執行methodB()方法。這樣,線程A和線程B就實現了 通訊。

這種方式,本質上就是「共享內存」式的通訊。多個線程須要訪問同一個共享變量,誰拿到了鎖(得到了訪問權限),誰就能夠執行。

 

②while輪詢的方式

代碼以下:

複製代碼

1 import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 public class MyList {
 5 
 6     private List<String> list = new ArrayList<String>();
 7     public void add() {
 8         list.add("elements");
 9     }
10     public int size() {
11         return list.size();
12     }
13 }
14 
15 import mylist.MyList;
16 
17 public class ThreadA extends Thread {
18 
19     private MyList list;
20 
21     public ThreadA(MyList list) {
22         super();
23         this.list = list;
24     }
25 
26     @Override
27     public void run() {
28         try {
29             for (int i = 0; i < 10; i++) {
30                 list.add();
31                 System.out.println("添加了" + (i + 1) + "個元素");
32                 Thread.sleep(1000);
33             }
34         } catch (InterruptedException e) {
35             e.printStackTrace();
36         }
37     }
38 }
39 
40 import mylist.MyList;
41 
42 public class ThreadB extends Thread {
43 
44     private MyList list;
45 
46     public ThreadB(MyList list) {
47         super();
48         this.list = list;
49     }
50 
51     @Override
52     public void run() {
53         try {
54             while (true) {
55                 if (list.size() == 5) {
56                     System.out.println("==5, 線程b準備退出了");
57                     throw new InterruptedException();
58                 }
59             }
60         } catch (InterruptedException e) {
61             e.printStackTrace();
62         }
63     }
64 }
65 
66 import mylist.MyList;
67 import extthread.ThreadA;
68 import extthread.ThreadB;
69 
70 public class Test {
71 
72     public static void main(String[] args) {
73         MyList service = new MyList();
74 
75         ThreadA a = new ThreadA(service);
76         a.setName("A");
77         a.start();
78 
79         ThreadB b = new ThreadB(service);
80         b.setName("B");
81         b.start();
82     }
83 }

複製代碼

在這種方式下,線程A不斷地改變條件,線程ThreadB不停地經過while語句檢測這個條件(list.size()==5)是否成立 ,從而實現了線程間的通訊。可是這種方式會浪費CPU資源。之因此說它浪費資源,是由於JVM調度器將CPU交給線程B執行時,它沒作啥「有用」的工做,只是在不斷地測試 某個條件是否成立。就相似於現實生活中,某我的一直看着手機屏幕是否有電話來了,而不是: 在幹別的事情,當有電話來時,響鈴通知TA電話來了。關於線程的輪詢的影響,可參考:JAVA多線程之當一個線程在執行死循環時會影響另一個線程嗎?

這種方式還存在另一個問題:

輪詢的條件的可見性問題,關於內存可見性問題,可參考:JAVA多線程之volatile 與 synchronized 的比較中的第一點「一,volatile關鍵字的可見性

線程都是先把變量讀取到本地線程棧空間,而後再去再去修改的本地變量。所以,若是線程B每次都在取本地的 條件變量,那麼儘管另一個線程已經改變了輪詢的條件,它也察覺不到,這樣也會形成死循環。

 

③wait/notify機制

代碼以下:

複製代碼

1 import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 public class MyList {
 5 
 6     private static List<String> list = new ArrayList<String>();
 7 
 8     public static void add() {
 9         list.add("anyString");
10     }
11 
12     public static int size() {
13         return list.size();
14     }
15 }
16 
17 
18 public class ThreadA extends Thread {
19 
20     private Object lock;
21 
22     public ThreadA(Object lock) {
23         super();
24         this.lock = lock;
25     }
26 
27     @Override
28     public void run() {
29         try {
30             synchronized (lock) {
31                 if (MyList.size() != 5) {
32                     System.out.println("wait begin "
33                             + System.currentTimeMillis());
34                     lock.wait();
35                     System.out.println("wait end  "
36                             + System.currentTimeMillis());
37                 }
38             }
39         } catch (InterruptedException e) {
40             e.printStackTrace();
41         }
42     }
43 }
44 
45 
46 public class ThreadB extends Thread {
47     private Object lock;
48 
49     public ThreadB(Object lock) {
50         super();
51         this.lock = lock;
52     }
53 
54     @Override
55     public void run() {
56         try {
57             synchronized (lock) {
58                 for (int i = 0; i < 10; i++) {
59                     MyList.add();
60                     if (MyList.size() == 5) {
61                         lock.notify();
62                         System.out.println("已經發出了通知");
63                     }
64                     System.out.println("添加了" + (i + 1) + "個元素!");
65                     Thread.sleep(1000);
66                 }
67             }
68         } catch (InterruptedException e) {
69             e.printStackTrace();
70         }
71     }
72 }
73 
74 public class Run {
75 
76     public static void main(String[] args) {
77 
78         try {
79             Object lock = new Object();
80 
81             ThreadA a = new ThreadA(lock);
82             a.start();
83 
84             Thread.sleep(50);
85 
86             ThreadB b = new ThreadB(lock);
87             b.start();
88         } catch (InterruptedException e) {
89             e.printStackTrace();
90         }
91     }
92 }

複製代碼

線程A要等待某個條件知足時(list.size()==5),才執行操做。線程B則向list中添加元素,改變list 的size。

A,B之間如何通訊的呢?也就是說,線程A如何知道 list.size() 已經爲5了呢?

這裏用到了Object類的 wait() 和 notify() 方法。

當條件未知足時(list.size() !=5),線程A調用wait() 放棄CPU,並進入阻塞狀態。---不像②while輪詢那樣佔用CPU

當條件知足時,線程B調用 notify()通知 線程A,所謂通知線程A,就是喚醒線程A,並讓它進入可運行狀態。

這種方式的一個好處就是CPU的利用率提升了。

可是也有一些缺點:好比,線程B先執行,一會兒添加了5個元素並調用了notify()發送了通知,而此時線程A還執行;當線程A執行並調用wait()時,那它永遠就不可能被喚醒了。由於,線程B已經發了通知了,之後再也不發通知了。這說明:通知過早,會打亂程序的執行邏輯。

 

④管道通訊就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通訊

具體就不介紹了。分佈式系統中說的兩種通訊機制:共享內存機制和消息通訊機制。感受前面的①中的synchronized關鍵字和②中的while輪詢 「屬於」 共享內存機制,因爲是輪詢的條件使用了volatile關鍵字修飾時,這就表示它們經過判斷這個「共享的條件變量「是否改變了,來實現進程間的交流。

而管道通訊,更像消息傳遞機制,也就是說:經過管道,將一個線程中的消息發送給另外一個。

JVM瞭解多少,把本身知道的都說了,不過他沒問細節

相關文章
相關標籤/搜索