在非線程安全得狀況下,多個線程對同一個對象中得實例變量進行併發訪問時,產生得後果就是髒讀,也就是取到得數據實際上是被更改過得。java
非線程安全問題存在於"實例變量"中,若是是方法內部得私有變量,則不存在"非線程安全"的問題。安全
使用synchronized修飾方法時應注意使用同一個鎖對象,不然會致使synchronized失效。bash
public class ThreadTest {
public static void main(String[] args) {
Add add = new Add();
Add add1 = new Add();
ThreadAA threadAA = new ThreadAA(add);
threadAA.start();
ThreadBB threadBB = new ThreadBB(add1);
threadBB.start();
}
}
class ThreadAA extends Thread{
private Add a;
public ThreadAA(Add add){
this.a = add;
}
@Override
public void run(){
a.add("a");
}
}
class ThreadBB extends Thread{
private Add b;
public ThreadBB(Add add){
this.b = add;
}
@Override
public void run(){
b.add("b");
}
}
class Add{
private int num = 0;
//同步方法
synchronized public void add(String username){
try{
if (username.equals("a")){
num = 100;
System.out.println("add a end");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("add b end");
}
System.out.println(username + " name " + num);
}catch (Exception e){
e.printStackTrace();
}
}
}
複製代碼
打印結果多線程
add a end
add b end
b name 200
a name 100
複製代碼
從結果看出打印的順序不是同步的,而是交叉的,這是由於關鍵字synchronized取得的鎖都是對象鎖。因此上面的示例中,那個線程先執行帶synchronized關鍵字的方法,那個線程就持有該方法所屬對象的鎖,那麼其餘線程只能呈等待狀態,前提是多個線程訪問的是同一個對象。併發
驗證synchronized方法持有的鎖爲對象鎖異步
//將上面的ThreadTest類中的main方法進行修改
public class ThreadTest {
public static void main(String[] args) {
Add add = new Add();
// Add add1 = new Add();
ThreadAA threadAA = new ThreadAA(add);
threadAA.start();
ThreadBB threadBB = new ThreadBB(add);
threadBB.start();
}
}
複製代碼
運行結果jvm
add a end
a name 100
add b end
b name 200
複製代碼
此時看多的運行結果就是順序打印的。ide
上面講了同步方法,可是用synchronized聲明方法在某些狀況下是有弊端的,好比A線程調用同步方法執行一個長時間的任務,那麼其餘線程必須等待較長的時間。在這樣的狀況下,咱們可使用synchronized同步代碼塊來解決,使用synchronized同步代碼塊來包裹必需要同步執行的代碼部分。性能
public class ThreadFunction {
public static void main(String[] args) {
ObjFunction objFunction = new ObjFunction();
FunA funA = new FunA(objFunction);
funA.setName("a");
funA.start();
FunB funB = new FunB(objFunction);
funB.setName("b");
funB.start();
}
}
class FunB extends Thread{
private ObjFunction objFunction;
public FunB(ObjFunction objFunction){
this.objFunction = objFunction;
}
@Override
public void run(){
objFunction.objMethod();
}
}
class FunA extends Thread{
private ObjFunction objFunction;
public FunA(ObjFunction objFunction){
this.objFunction = objFunction;
}
@Override
public void run(){
objFunction.objMethod();
}
}
class ObjFunction{
public void objMethod(){
try{
System.out.println(Thread.currentThread().getName() + " start");
synchronized (this) {
System.out.println("start time = " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("end time = "+ System.currentTimeMillis());
}
System.out.println(Thread.currentThread().getName() + " end");
}catch (Exception e){
e.printStackTrace();
}
}
}
複製代碼
運行結果ui
a start
b start
start time = 1559033466082
end time = 1559033468083
a end
start time = 1559033468083
end time = 1559033470084
b end
複製代碼
能夠看出,同步代碼塊外的代碼是異步執行的,而同步代碼塊中的則是同步執行的。而且synchronized(this)的鎖對象也是當前對象。
除了以this
來做爲鎖對象,java還支持任意對象做爲鎖來實現同步功能,但須要注意的是做爲同步監視器的必須是同一對象,不然運行結果就是異步調用了。
關鍵字synchronized還能夠應用到static靜態方法上,這樣的話就是一當前的*.java文件對應的Class類做爲鎖對象。
靜態同步方法持有的鎖對象=synchronized(class)
public class ThreadTest {
public static void main(String[] args) {
ThreadAA threadAA = new ThreadAA();
threadAA.start();
ThreadBB threadBB = new ThreadBB();
threadBB.start();
}
}
class ThreadAA extends Thread{
@Override
public void run(){
Add.add("a");
}
}
class ThreadBB extends Thread{
@Override
public void run(){
Add.add("b");
}
}
class Add{
private static int num = 0;
//同步方法
synchronized static public void add(String username){
try{
if (username.equals("a")){
num = 100;
System.out.println("add a end");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("add b end");
}
System.out.println(username + " name " + num);
}catch (Exception e){
e.printStackTrace();
}
}
}
複製代碼
運行結果
add a end
a name 100
add b end
b name 200
複製代碼
使用關鍵字synchronized修飾一個類,那麼這個類中全部的方法都是同步方法,在編譯得時候會把全部方法自動加上synchronized。
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
複製代碼
synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程獲得一個對象鎖後,再次請求此對象鎖時是能夠再次獲得該對象的鎖的。也就是說synchronized方法/代碼塊的內部調用本類的其餘synchronized方法/代碼塊時,永遠能夠獲得所。
public class ThreadAgain {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new Service().service1();
}
}).start();
}
}
class Service{
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized private void service2() {
System.out.println("service2");
service3();
}
synchronized private void service3() {
System.out.println("service3");
}
}
複製代碼
運行結果
service1
service2
service3
複製代碼
關鍵字volatile的做用主要是使變量在多個線程間可見。
是強制從公共堆中取得變量的值,而不是從線程的私有數據棧中取得變量的值。在多線程中,棧與程序計數器是私有的,堆與全局變量是公有的。
先看代碼
public class MyVolatile {
public static void main(String[] args) {
try {
RunThread runThread = new RunThread();
runThread.start();
Thread.sleep(2000);
runThread.setRun(false);
System.out.println("爲runThread複製false");
}catch (Exception e){
e.printStackTrace();
}
}
}
class RunThread extends Thread{
private boolean isRun = true;
public boolean isRun() {
return isRun;
}
public void setRun(boolean run) {
isRun = run;
}
@Override
public void run(){
System.out.println("進入了run方法");
while (isRun == true){
}
System.out.println("退出run方法,線程中止");
}
}
複製代碼
從控制檯能夠看到,線程並無結束。這個問題就是私有堆棧中的值和工有堆棧中的值不一樣步形成的,想解決這樣的問題使用volatile關鍵字就能夠。
修改RunThread類中的代碼
class RunThread extends Thread{
volatile private boolean isRun = true;
public boolean isRun() {
return isRun;
}
public void setRun(boolean run) {
isRun = run;
}
@Override
public void run(){
System.out.println("進入了run方法");
while (isRun == true){
}
System.out.println("退出run方法,線程中止");
}
}
複製代碼
再次運行,線程正常結束了。
雖然volatile關鍵字可使實例變量在多線程之間可見,可是volatile有一個致命的缺點就是不支持原子性。
驗證volatile不支持原子性
public class IsAtomic {
public static void main(String[] args) {
MyAtomicRun[] myAtomicRuns = new MyAtomicRun[100];
for (int i = 0;i<100;i++){
myAtomicRuns[i] = new MyAtomicRun();
}
for (int i = 0;i<100;i++){
myAtomicRuns[i].start();
}
}
}
class MyAtomicRun extends Thread{
volatile public static int count;
private static void count(){
for (int i = 0;i<100;i++){
count++;
}
System.out.println("count: " + count);
}
@Override
public void run(){
count();
}
}
複製代碼
打印輸出
//篇幅較長,沒有所有粘貼
count: 5000
count: 4900
count: 4800
count: 4700
count: 4600
count: 4500
count: 4400
count: 4400
複製代碼
從輸出的結果看,並無輸出咱們理想狀態中的10000。
對代碼進行改進
class MyAtomicRun extends Thread{
volatile public static int count;
//須要使用同步靜態方法,這樣是以class爲鎖,才能達到同步效果
synchronized private static void count(){
for (int i = 0;i<100;i++){
count++;
}
System.out.println("count: " + count);
}
@Override
public void run(){
count();
}
}
複製代碼
打印輸出
count: 9300
count: 9400
count: 9500
count: 9600
count: 9700
count: 9800
count: 9900
count: 10000
複製代碼
這一次輸出的纔是正確的結果。
關鍵字volatile主要使用的場合是在多個線程中能夠感知實例變量被更改了,而且能夠獲取最新的值使用,也就是多線程讀取共享變量時能夠獲取最新的值。
像上面volatile關鍵字修飾的變量進行++運算這樣的操做其實並非一個原子操做,也就是非線程安全的。
i++操做步驟:
若是在第二步計算的時候另外一個線程也修改了i的值,那麼這個時候就會出現髒數據。
在多線程環境中,use和assign是屢次出現的,但這個操做並非原子性的,也就是讀取階段後,若是主內存中的變量值被修改,工做線程的內存由於已經加載過了,因此不會產生對應的變化,就形成了私有內存和公有內存中變量值不一樣步,計算出來的結果和預期就不同,出現非線程安全問題。
對於volatile關鍵字修飾的變量,jvm只保證從主內存加載到工做內存中的值是最新的。