最近學習並發編程,並發編程肯定和多線程、線程安全有關系,那麼下面是我總結了自己學習線程安全的筆記!!!
1.線程安全的概念
多個線程訪問某一個類(對象或方法)時,這個類始終都能保持正確的行為,那麼這個類(對象或方法是線程安全的)。
2.synchronized
可以在任意對象和方法上加鎖,而加鎖的這段代碼稱為“互斥區”或“臨界區”。
a.示例
package com.wp.test;
public class MyThread extends Thread {
private int count = 5;
public synchronized void run(){
count--;
System.out.println(this.currentThread().getName()+" count="+count);
}
public static void main(String args[]){
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
b.示例總結
如果run方法前沒有加關鍵字“synchronized”,那麼對於多個線程操作count變量,是不安全的。加上“synchronized”時,那麼線程是安全的;當多個線程訪問run方法時,以排隊的方式進行處理(此處的排隊是按照CPU分配的先後順序而定),一個線程想要執行這個synchronized修飾的run方法,首先要獲得鎖,如果獲取不到,便會一直等到獲取這把鎖為止,此時,如果有多個線程,那麼多個線程會競爭這把鎖,這時會有鎖競爭問題。鎖競爭問題很導致應用程序非常慢。
3.多個線程多個鎖
多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後執行鎖定的內容。此處有兩個概念:對象鎖和類鎖,示例總結將做解釋。
a.示例

package com.wp.test;
/**
* 關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當作鎖
* 所以代碼中哪個線程先執行synchronized對應的方法,該線程就持有該方法所屬對象的鎖(LOCK)
*
* 在靜態方法前加關鍵字synchronized,表示該方法的鎖是類級別的鎖。
*
*/
public class MultiThread {
private static int num = 0;
/* static **/
public static synchronized void printNum(String tag){
try{
if("a".equals(tag)){
num = 100;
System.out.println("tag a!set number over!");
Thread.sleep(1000);
}else{
num = 200;
System.out.println("tag b! set number over!");
}
System.out.println("num="+num);
}catch(Exception e){
e.printStackTrace();
}
}
//注意觀察run方法輸出順序
public static void main(String args[]){
//兩個不同的對象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
View Code
執行結果:
1. 無static: tag a!set number over! tag b! set number over! num=200 num=100 2.有static tag a!set number over! num=100 tag b! set number over!
b.示例總結
代碼中線程取得的鎖都是對象鎖,而不是把一段代碼(或方法)當作鎖,對象鎖的特點是不同的對象對應著不同的鎖,互不影響。在靜態方法上加上關鍵字synchronized,表示鎖定的是.class類,屬於類級別的鎖。
4.對象鎖的同步和異步
a.同步synchronized
同步的概念是共享,如果多個線程共享資源,那麼就沒有必要進行同步了。
b.異步asynchronized
異步的概念是獨立,多個線程相互之間不受任何制約。
c.同步的目的就是為了線程安全,對於線程安全,需要滿足兩個特性:1).原子性(同步) 2).可見性
d.示例

package com.wp.test;
/**
* 對象鎖的同步和異步問題
*/
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void method2(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String args[]){
final MyObject mo = new MyObject();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mo.method1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mo.method2();
}
},"t2");
t1.start();
t2.start();
}
}
View Code
示例總結:若t1線程先持有Object對象鎖,t2線程如果這個時候要調用對象中的同步(synchronized)方法則需要等待t1釋放鎖,才可以調用,也就說同步了;若t1線程先持有Object對象鎖,t2線程這個時候調用異步(非synchronized修飾)的方法,則會立即調用,t1和t2之間沒有影響,也就說是異步了。
4.髒讀
對於對象的同步和異步方法,我們在設計的時候一定要考慮問題的整體性,不然就會出現數據不一致的錯誤,很經典的一個錯誤就是髒讀。
a.示例

package com.wp.test;
public class DirtyRead {
private String uname = "sxt";
private String pwd = "123";
public synchronized void setValue(String uname,String pwd){
try {
this.uname = uname;
Thread.sleep(2000);
this.pwd = pwd;
System.out.println("setValue設置的值:uname="+uname+" pwd="+pwd);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void getValue(){
System.out.println("getValue的最終值:uname="+uname+" pwd="+pwd);
}
public static void main(String args[]) throws Exception{
final DirtyRead dr = new DirtyRead();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("wangping", "456");
}
});
t.start();
Thread.sleep(1000);
dr.getValue();
}
}
/**getValue的最終值:uname=wangping pwd=123
setValue設置的值:uname=wangping pwd=456*/
View Code
b.示例分析
pwd不一致,解決方法:getValue方法加關鍵字synchronized。加上對象鎖之後,等到鎖被釋放才可以獲得鎖。保持業務數據一致性。
c.示例總結
當我在給對象的方法加鎖的時候,一定要考慮業務的整體性,即示例中setValue和getValue方法都加synchronized鎖住,就保證了業務的原子性,保證業務不會出錯。
d.oracle關系型數據庫,一致性讀實現原理
案例描述:oracle,用戶A,9點查詢某條數據(100),9:10才可以查到所需數據。而用戶B在9:05執行了DML操作,那麼A所查的數據在數據庫已經變化(200)。那麼A在9:10查到的結果是100還是200?答案是:100。
原因:oracle數據庫有一致性讀的特性。B在update時,會將之前的數據保存到undo中做記錄。當A在9:00這一時刻查時,將這一動作保存到ITL中,當A在9:10查100數據時,會判斷ITL中對該數據的動作是否更新(是否在9:00這一刻之後發生變化),如果更新了,那麼會從100對應的undo中將之前的數據返回。所以,結果為100。
Undo的作用:提供一致性讀(Consistent Read)、回滾事務(Rollback Transaction)以及實例恢復(Instance Recovery)。
詳細解釋:http://www.educity.cn/shujuku/1121393.html
5.synchronized鎖重入
關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個對象得到對象鎖之後,不釋放鎖,還可以再次獲得該對象的鎖,稱為鎖重入。
a.示例1:方法鎖重入

package com.wp.test;
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1------------------");
method2();
}
public synchronized void method2(){
System.out.println("method2------------------");
method3();
}
public synchronized void method3(){
System.out.println("method3------------------");
}
public static void main(String args[]){
final SyncDubbo1 s = new SyncDubbo1();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
s.method1();
}
});
t.start();
}
}
/**結果:
* method1-----------------
* method2------------------
* method3------------------
*/
View Code
示例2:子類方法鎖重入

package com.wp.test;
public class SyncDubbo2 {
static class Main{
public int i = 10;
public synchronized void operationSup(){
try{
i--;
System.out.println("Main print i="+i);
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}
}
static class Sub extends Main{
public synchronized void operationSub(){
try{
while(i>0){
i--;
System.out.println("Sub print i="+i);
Thread.sleep(100);
this.operationSup();
}
}catch(Exception e){
e.printStackTrace();
}
}
}
public static void main(String args[]){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
Sub s = new Sub();
s.operationSub();
}
});
t.start();
}
}
View Code
結果:
Sub print i=9 Main print i=8 Sub print i=7 Main print i=6 Sub print i=5 Main print i=4 Sub print i=3 Main print i=2 Sub print i=1 Main print i=0
示例3:鎖中的內容出現異常
對於web應用程序,異常釋放鎖的情況,如果不特殊處理,那麼業務邏輯會出現很嚴重的錯,比如執行一個隊列任務,很對任務對象都在等待第一個對象正確執行完之後釋放鎖,但是執行第一個對象時出現了異常,導致剩余任務沒有執行,那麼業務邏輯就會出現嚴重錯誤,所以在設計代碼的時候一定要慎重考慮。
解決方式:出現異常時,捕獲異常後通過記錄異常信息到日志文件,然後剩余任務對象繼續執行,那麼整個任務執行完之後,再對出現異常的那個任務對象進行處理,從而不會影響其他任務對象的執行。

package com.wp.test;
public class SyncException {
private int i = 0;
public synchronized void operation(){
while(true){
try{
i++;
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+",i="+i);
if(i==3){
Integer.parseInt("a");//throw RuntimeException
}
}catch(Exception e){
e.printStackTrace();
System.out.println("log info i="+i);
}
}
}
public static void main(String args[]){
final SyncException se = new SyncException();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
});
t.start();
}
}
View Code
結果:執行方法的時候,出現異常,不會釋放鎖,業務繼續執行。執行完之後,對發生的異常進行處理。比如說存儲過程:當更新某張表的數據時,出現異常,那麼將異常信息保存到日志表中,稍後進行處理,而使得該表的數據繼續更新。
Thread-0,i=1
Thread-0,i=2
Thread-0,i=3
java.lang.NumberFormatException: For input string: "a"
log info i=3
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:492)
at java.lang.Integer.parseInt(Integer.java:527)
at com.wp.test.SyncException.operation(SyncException.java:11)
at com.wp.test.SyncException$1.run(SyncException.java:24)
at java.lang.Thread.run(Thread.java:745)
Thread-0,i=4
Thread-0,i=5
Thread-0,i=6
6.synchronized關鍵字
使用synchronized聲明的方法在某些情況下是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待同樣長的時間才能執行,這樣的情況下可以使用synchronized代碼塊去優化代碼執行時間,通常說,減小了鎖的粒度。
Synchronized可以使用任意的Object進行加鎖,用法比較靈活。
特別注意,就是不要使用String的常量加鎖,會出現死循環問題。
鎖對象改變問題:當使用一個對象進行加鎖的時候,要注意對象本身是否發生變化(地址),如果發生變化,那麼就會釋放鎖。例如,如果是字符串的鎖,當字符串改變後就會釋放鎖。但是,對象的屬性發生變化,不會有影響的。
a.示例1:synchronized代碼塊

package com.wp.test;
public class ObjectLock {
public void method1(){
synchronized(this){
try {
System.out.println("do method1...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){
synchronized(ObjectLock.class){
try {
System.out.println("do method2...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private Object lock = new Object();
public void method3(){
synchronized(lock){
try {
System.out.println("do method3...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
final ObjectLock ol = new ObjectLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ol.method1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ol.method2();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
ol.method3();
}
});
t1.start();
t2.start();
t3.start();
}
}
View Code
結果:
do method1... do method3... do method2...
b.示例2:字符串鎖改變

package com.wp.test;
public class ChangeLock {
private String lock = "lock";
public void method(){
synchronized(lock){
try {
System.out.println("當前線程:"+Thread.currentThread().getName()+"開始");
lock = "lock1";
Thread.sleep(2000);
System.out.println("當前線程: "+Thread.currentThread().getName()+"結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
final ChangeLock cl = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
cl.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
cl.method();
}
},"t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
View Code
結果:字符串改變之後,會釋放鎖。
示例3:

package com.wp.test;
public class ModifyLock {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void changeAttribute(String name,int age){
try {
System.out.println("當前線程:"+Thread.currentThread().getName()+"開始");
this.setName(name);
this.setAge(age);
System.out.println("當前線程:"+Thread.currentThread().getName()+"修改的內容為:"
+this.getName()+","+this.getAge());
Thread.sleep(2000);
System.out.println("當前線程:"+Thread.currentThread().getName()+"結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
final ModifyLock ml = new ModifyLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ml.changeAttribute("張三", 20);
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ml.changeAttribute("李四", 21);
}
},"t2");
t1.start();
t2.start();
}
}
View Code
結果分析:如果對象本身不發生變化,那麼依然同步,屬性變化,沒有影響,依然同步。
7.volatile關鍵字
當多個線程使用同一變量時,為了線程安全,通常我們會在訪問變量的地方加一把鎖,但是這樣效率比較低。但是Volatile的效率相對高點。
Volatile關鍵字的主要作用是使用變量在多個線程間可見。原理:強制線程到主內存裡去讀取變量,而不去線程工作內存區去讀,那麼當前線程使用的變量是最新的變量(不管其他線程有無修改),從而實現了多個線程間變量可見,也就是滿足了線程安全的可見性。如果不明白,下面的示例,運行一下,就明白了。

package com.wp.test;
public class RunThread extends Thread{
/**volatile*/
private volatile boolean isRunning = true;
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run(){
System.out.println("進入run方法。。");
while(isRunning == true){
//...
}
System.out.println("線程終止。。");
}
public static void main(String args[]) throws InterruptedException{
RunThread rt = new RunThread();
rt.start();
Thread.sleep(3000);
rt.setRunning(false);
System.out.println("isRunning的值已經設置成了false");
Thread.sleep(1000);
System.out.println(rt.isRunning);
}
}
View Code
結果分析:isRunning不用volatile修飾時,當主線程修改isRunning的值時,線程rt的內存中的isRunning副本不會變化;isRunning用volatile修飾時,當主線程修改isRunning的值時,會強制線程rt從內存中讀取isRunning的值,那麼rt內存裡的isRunning也就發生了修改。
Volatile關鍵字雖然擁有多個線程之間的可見性,但是卻不具備同步性(也就是原子性),可以算得上一個輕量級的synchronized,性能要比synchronized強很多,不會造成阻塞(在很多開源的框架裡,比如netty的底層代碼就是大量使用volatile,可見netty性能非常不錯。)這裡需要注意,一般volatile用於只針對於多個線程可見的變量操作,並不能代替synchronized的同步功能。具體來說,volatile關鍵字只有可見性,沒有原子性。要實現原子性,建議使用atomic類的系列對象,支持原子性操作(注意atomic類只保證本身方法原子性,並不保證多次操作的原子性)。用一個示例說明:
示例1:volatile關鍵字只有可見性,沒有原子性,建議使用atomic類的系列對象

package com.wp.test;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileNoAtomic extends Thread {
//private static volatile int count;
private static AtomicInteger count = new AtomicInteger();
private static void addCount(){
for(int i=0;i<1000;i++){
//count++;
count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String args[]){
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for(int i=0;i<10;i++){
arr[i] = new VolatileNoAtomic();
}
for(int i=0;i<10;i++){
arr[i].start();
}
}
}
View Code
結果分析:當使用volatile修飾時,由於沒有原子性,因此,(線程不安全)結果達不到10000;那麼使用AtomicInteger時,具有原子性,結果正確為10000。
示例2:atomic類只保證本身方法原子性,並不保證多次操作的原子性

package com.wp.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicUse {
private static AtomicInteger count = new AtomicInteger(0);
//多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性
/**synchronized*/
public synchronized int multiAdd(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4);
return count.get();
}
public static void main(String args[]){
final AtomicUse au = new AtomicUse();
List<Thread> ts = new ArrayList<Thread>();
for(int i=0;i<100;i++){
ts.add(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(au.multiAdd());
}
}));
}
for(Thread t : ts){
t.start();
}
}
}
View Code
結果分析:多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性。不加,每次加的值不是整10,加的話是整10。