線程自己因爲建立和切換的開銷,採用多線程不會提升程序的執行速度,反而會下降速度,可是對於頻繁IO操做的程序,多線程能夠有效的併發。 對於包含不一樣任務的程序,能夠考慮每一個任務使用一個線程。這樣的程序在設計上相對於單線程作全部事的程序來講,更爲清晰明瞭,若是是單純的計算操做,多線程並無單線程的計算效率高,可是對於一些刻意分散使用計算機系統資源的操做,則適合使用多線程。 在實際的開發中對於性能優化的問題須要考慮到具體的場景來考慮是否使用多線程技術。通常來講一個程序是運行在一個進程中的,進程是具備必定獨立功能的程序、它是計算機系統進行資源分配和調度的一個獨立單位。而線程是進程的一個實體,是CPU調度和分派的基本單位,他是比進程更小的能獨立運行的基本單位。html
在JMM中,線程能夠把變量保存在本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另外一個線程還在繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致,這樣就會致使線程不安全,下面介紹幾種Java中常見的線程同步的方式。java
關於線程不安全的緣由是由於JMM定義了主內存跟工做內存,形成多個線程同事訪問同一個資源時致使的不一致問題,那麼要想解決這個問題其實也很簡單,也是從JMM入手,主要有如下3種方式, 程序員
採用synchronized修飾符實現的同步機制叫作互斥鎖機制,它所得到的鎖叫作互斥鎖。每一個對象都有一個鎖標記,當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池,互斥鎖分兩種一種是類鎖,一種是對象鎖。 類鎖:用於類的靜態方法或者一個類的class,一個對象只有一個 對象鎖:用於實例化的對象的普通方法,能夠有多個數組
下面仍是用程序員改bug這個例子來示範一下synchronized的使用方式緩存
Bug類安全
public class Bug {
private static Integer bugNumber = 0;
public static int getBugNumber() {
return bugNumber;
}
//普通同步方法
public synchronized void addNormal() {
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
}
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
//同步代碼塊
public synchronized void addBlock() {
synchronized (bugNumber) {
this.bugNumber = ++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
}
複製代碼
Runnable性能優化
public class BugRunnable implements Runnable {
private Bug mBug=new Bug();
@Override
public void run() {
mBug.addNormal();//普通方法同步
// mBug.addBlock();//同步代碼塊
// Bug.addStatic();//靜態方法同步
}
}
複製代碼
測試代碼bash
public static void main(String[] args) {
BugRunnable bugRunnable = new BugRunnable();
for (int i = 0; i < 6; i++) {
new Thread(bugRunnable).start();
}
}
複製代碼
//同步代碼塊
public synchronized void addBlock() {
synchronized (bugNumber) {
this.bugNumber = ++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
複製代碼
測試結果多線程
blockSynchronized--->1
blockSynchronized--->2
blockSynchronized--->3
blockSynchronized--->4
blockSynchronized--->5
blockSynchronized--->6
複製代碼
//普通同步方法
public synchronized void addNormal() {
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
}
複製代碼
測試結果併發
normalSynchronized--->1
normalSynchronized--->2
normalSynchronized--->3
normalSynchronized--->4
normalSynchronized--->5
normalSynchronized--->6
複製代碼
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
複製代碼
測試結果
staticSynchronized--->1
staticSynchronized--->2
staticSynchronized--->3
staticSynchronized--->4
staticSynchronized--->5
staticSynchronized--->6
複製代碼
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
//用同步代碼塊
public static void changeStatic() {
synchronized (Bug.class) {
++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
複製代碼
下面具體來總結一下三者的區別
除了synchronized這個關鍵字外,咱們還能經過concurrent包下的Lock接口來實現這種效果,ReentrantLock是lock的一個實現類,能夠在任何你想要的地方進行加鎖,比synchronized關鍵字更加靈活,下面看一下使用方式 使用方式
//ReentrantLock同步
public void addReentrantLock() {
mReentrantLock.lock();//上鎖
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
mReentrantLock.unlock();//解鎖
}
複製代碼
運行測試
ReentrantLock--->1
ReentrantLock--->2
ReentrantLock--->3
ReentrantLock--->4
ReentrantLock--->5
ReentrantLock--->6
複製代碼
咱們發現也是能夠達到同步的目的,看一下ReentrantLock的繼承關係
ReentrantLock實現了lock接口,而lock接口只是定義了一些方法,因此至關於說ReentrantLock本身實現了一套加鎖機制,下面簡單分析一下ReentrantLock的同步機制,在分析前,須要知道幾個概念:
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;//同步器
複製代碼
成員變量除了序列化ID以外,只有一個Sync,那就看一看具體是什麼
Sync有兩個實現類,一個是FairSync,一個是NonfairSync,從名字能夠大體推斷出一個是公平鎖,一個是非公平鎖,FairSync(公平鎖) lock方法:
final void lock() {
acquire(1);
}
複製代碼
ReentrantLock是獨佔鎖,1表示的是鎖的狀態state。對於獨佔鎖而言,若是所處於可獲取狀態,其狀態爲0,當鎖初次被線程獲取時狀態變成1,acquire最終調用的是tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 當c==0表示鎖沒有被任何線程佔用
(hasQueuedPredecessors),
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//鎖已經被線程佔用
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製代碼
tryAcquire主要是去嘗試獲取鎖,獲取成功則設置鎖狀態並返回true,不然返回false
NonfairSync(非公平鎖) 非公平鎖NonfairSync的lock()與公平鎖的lock()在獲取鎖的流程上是一直的,可是因爲它是非公平的,因此獲取鎖機制仍是有點不一樣。經過前面咱們瞭解到公平鎖在獲取鎖時採用的是公平策略(CLH隊列),而非公平鎖則採用非公平策略它無視等待隊列,直接嘗試獲取。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
複製代碼
lock()經過compareAndSetState嘗試設置鎖的狀態,若成功直接將鎖的擁有者設置爲當前線程(簡單粗暴),不然調用acquire()嘗試獲取鎖,對比一下,公平鎖跟非公平鎖的區別在於tryAcquire中
//NonfairSync
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//FairSync
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
複製代碼
公平鎖中要經過hasQueuedPredecessors()來判斷該線程是否位於CLH隊列頭部,是則獲取鎖;而非公平鎖則無論你在哪一個位置都直接獲取鎖。
public void unlock() {
sync.release(1);//釋放鎖
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
複製代碼
synchronized:線程A跟線程B同時競爭同一把鎖,若是線程A得到鎖以後不釋放,那麼線程B會一直等待下去,並不會釋放。
ReentrantLock:能夠在線程等待了很長時間以後進行中斷,不須要一直等待。
公平鎖:是指多個線程在等待同一個鎖時,必須按照申請的時間順序來依次得到鎖;非公平鎖:在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖;
因爲多個線程方法同一個變量,致使了線程安全問題,主要緣由是由於線程的工做副本的變量跟主內存的不一致,若是可以解決這個問題就能夠保證線程同步,而Java提供了volatile關鍵字,能夠幫助咱們保證內存可見性,當咱們聲明瞭一個volatile關鍵字,實際上有兩層含義;
volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操做,也就不會執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。
在使用volatile關鍵字的時候,會多出一個lock前綴指令,lock前綴指令實際上至關於一個內存屏障實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
2)它會強制將對緩存的修改操做當即寫入主存;
3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
這裏須要強調一點,volatile關鍵字並不必定能保證線程同步,若是非要採用volatile關鍵字來保證線程同步,則須要知足如下條件:
其實看了一些書跟博客,都是這麼寫的,按照個人理解實際上就是隻有當volatile修飾的對象是原子性操做,纔可以保證線程同步,爲何呢。
測試代碼:
class Volatile {
volatile static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Volatile.add();
}
}).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count--->" + ++count);
}
private static void add() {
count++;
}
}
複製代碼
運行結果
count--->1001
複製代碼
理論上是1000纔對,可是輸出的值是1001,爲何呢,這個其實在以前的JMM中已經分析過了,下面再貼一張圖
跟以前同樣,咱們每次從主內存中獲取到的count確實是最新的,可是因爲對count的操做不是原子性操做,假如如今有兩個線程,線程1跟線程2,若是線程1讀取到了count值是5,而後read--->load進內存了,而後如今被線程2搶佔了CPU,那麼線程2就開始read--->load,而且完成了工做副本的賦值操做,而且將count 的值回寫到主內存中,因爲線程1已經進行了load操做,因此不會再去主內存中讀取,會接着進行本身的操做,這樣的話就出現了線程不安全,因此volatile必須是原子性操做才能保證線程安全。 基於以上考慮,volatile主要用來作一些標記位的處理:
volatile boolean flag = false;
//線程1
while(!flag){
doSomething();
}
//線程2
public void setFlag() {
flag = true;
}
複製代碼
當有多個線程進行訪問的時候,只要有一個線程改變了flag的狀態,那麼這個狀態會被刷新到主內存,就會對全部線程可見,那麼就能夠保證線程安全。
automatic是JDK1.5以後Java新增的concurrent包中的一個類,雖然volatile能夠保證內存可見性,大部分操做都不是原子性操做,那麼volatile的使用場景就比較單一,而後Java提供了automatic這個包,能夠幫助咱們來保證一些操做是原子性的。
替換以前的volatile代碼
public static AtomicInteger atomicInteger = new AtomicInteger(0);
private static void add() {
atomicInteger.getAndIncrement();
}
複製代碼
測試一下:
AtomicInteger: 1000
複製代碼
AtomicInteger既保證了volatile保證不了的原子性,同時也實現了可見性,那麼它是如何作到的呢?
成員變量
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
複製代碼
運算方式
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
int compare_and_swap(int reg, int oldval, int newval) {
ATOMIC();
int old_reg_val = reg;
if (old_reg_val == oldval)
reg = newval;
END_ATOMIC();
return old_reg_val;
}
複製代碼
分析以前須要知道兩個概念:
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。
compare_and_swap這個纔是核心方法,也就是上面提到的CAS,由於CAS是基於樂觀鎖的,也就是說當寫入的時候,若是寄存器舊值已經不等於現值,說明有其餘CPU在修改,那就繼續嘗試。因此這就保證了操做的原子性。
這種方式實際上指的就是ThreadLocal,翻譯過來是線程本地變量,ThreadLocal會爲每一個使用該變量的線程提供獨立的變量副本,可是這個副本並非從主內存中進行讀取的,而是本身建立的,每一個副本相互之間獨立,互不影響。相對於syncronized的以時間換空間,ThreadLocal恰好相反,能夠減小線程併發的複雜度。
class ThreadLocalDemo {
public static ThreadLocal<String> local = new ThreadLocal<>();//聲明靜態的threadlocal變量
public static void main(String[] args) {
local.set("Android");
for (int i = 0; i < 5; i++) {
SetThread localThread = new SetThread();//建立5個線程
new Thread(localThread).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(local.get());
}
static class SetThread implements Runnable {
@Override
public void run() {
local.set(Thread.currentThread().getName());
}
}
}
複製代碼
進行 測試
Android
複製代碼
雖然我用for循環建立了好幾個線程,可是並無改變ThreadLocal中的值,依然是個人大Android,這個就可以說明我賦的值是跟個人線程綁定的,每一個線程有特定的值。
private final int threadLocalHashCode = nextHashCode();//當前線程的hash值
private static AtomicInteger nextHashCode =//下一個線程的hash值
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;//hash增加因子
複製代碼
public ThreadLocal() {
}
複製代碼
空實現。。。。
public void set(T value) {
Thread t = Thread.currentThread();//獲取到當前線程
ThreadLocalMap map = getMap(t);//獲取一個map
if (map != null)
//map不爲空,直接進行賦值
map.set(this, value);
else
//map爲空,建立一個Map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
上面建立的Map其實是一個ThreadLocalMap,也便是用來保存跟線程綁定的數據的,之間看過HashMap的源碼,既然也叫Map,那麼其實應該是差很少的
private static final int INITIAL_CAPACITY = 16;//初始容量,2的冪
private Entry[] table;//用來存放entry的數組
private int size = 0;//數組長度
private int threshold; // 閾值
//Entry繼承了WeakReference,說明key弱引用,便於內存回收
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製代碼
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table數組
table = new Entry[INITIAL_CAPACITY];
// 經過hash值來計算存放的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 建立entry節點
table[i] = new Entry(firstKey, firstValue);
// 數組長度由0到1
size = 1;
// 將閾值設置成爲初始容量
setThreshold(INITIAL_CAPACITY);
}
複製代碼
還有一個構造方法是傳一個Map,跟傳key-value大同小異就不解釋了
private Entry getEntry(ThreadLocal<?> key) {
//經過key來計算數組下標
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//遍歷到直接返回
return e;
else
//沒有遍歷到就會調用getEntryAfterMiss,繼續遍歷
return getEntryAfterMiss(key, i, e);
}
複製代碼
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;//拿到table數組
int len = tab.length;//獲取table的長度
int i = key.threadLocalHashCode & (len-1);//計算下標
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//如便利到相同的能夠,那麼取而代之
e.value = value;
return;
}
if (k == null) {
//替換key值爲空的entry
replaceStaleEntry(key, value, i);//
return;
}
}
tab[i] = new Entry(key, value);//進行賦值
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//遍歷下標尋找i
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);//清理指定的key
return;
}
}
}
複製代碼
基本上分析到這裏已經將ThreadLocal分析清楚了,它的核心是一個ThreadLocalMap,存放了一個entry數組,期中key是ThreadLocal的weakreference,value就是set的值,而後每次set跟get都會對已有的entry進行清理,加商weakreference就能夠最大限度的放置內存泄露。
死鎖:是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些進程都將沒法向前推動。
下面舉一個死鎖的例子
public class DeadLock implements Runnable {
public int flag = 1;
//靜態對象是類的全部對象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都處於可執行狀態,但JVM線程調度先執行哪一個線程是不肯定的。
//td2的run()可能在td1的run()以前運行
new Thread(td1).start();
new Thread(td2).start();
}
}
複製代碼
無論哪一個線程先啓動,啓動的線程都會先sleep500ms,讓另一個線程得到CPU的使用權,這樣一來就保證了線程td1獲取到了O1的對象鎖,在競爭O2的對象鎖,td2獲取到了O2的對象鎖,在競爭O1的對象鎖,呵呵,這就尷尬了,而後互不想讓,就卡死了,形成了死鎖。