前記:師夷長技以自強
1.問題背景
在上一篇文章中我們已經討論了線程具有異步運行的特性,因此當多線程同時訪問同一個實例變量時就會引發髒讀的問題。而這顯然不是我們願意看到的,解決辦法也很簡單,就是給訪問該變量的程序部分加鎖。多線程併發在一些追求效率的系統中常存在變量不可見的問題,由於變量的不可見也會導致程序運行的結果不是我們想要的。一句話,同步性和可見性問題是多線程中的兩大重點內容,他們分別對應於synchronized和volitle關鍵字的使用。本文主要圍繞了在各種情況下如何使用這兩個關鍵字而展開的。
2.synchronized同步方法
2.1方法內變量是線程安全的
ex1:
class HasSelfPrivateNum{
public void addI(String username){
try {
int num = 0;
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=100
可以看到b的寫覆蓋對a是完全沒有影響的。
2.2實例變量非線程安全
把上面的num變量改爲實例變量
ex2:
class HasSelfPrivateNum{
private int num = 0;
public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=200
當然,解決辦法也就是在addI函數前加synchronized,這裏不再演示。
2.3給非靜態方法加對象鎖
如果同步方法屬於多個對象,則每個方法屬於不同的鎖,因此其運行也是異步的。synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。
ex3:
class HasSelfPrivateNum{
private int num = 0;
synchronized public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRefA = new HasSelfPrivateNum();
HasSelfPrivateNum numRefB = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRefA);
threadA.start();
ThreadB threadB = new ThreadB(numRefB);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=100
可見,addI被兩個線程調用運行時都是異步交叉運行的。
2.4synchronized鎖可重入
也就是說,當一個線程得到一個對象鎖後,再次請求此對象時是可以再次得到該對象的鎖的。因此,一個synchronized方法/塊的內部調用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。
ex4:
class Service{
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
service3();
}
synchronized public void service3(){
System.out.println("service3");
}
}
class MyThread extends Thread{
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
public class Test{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
output:
service1
service2
service3
假如不允許鎖重入,那麼程序將會進入死鎖。
除此之外,子類的同步函數也可以重入從父類繼承的函數。如下:
ex5:
class Main {
public int i = 10;
synchronized public void operateIMainMethod() {
try {
i--;
System.out.println("main print i=" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Sub extends Main {
synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println("sub print i=" + i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2.4異常自動釋放鎖
線程即使獲得了鎖,但在運行的過程中如果遇到異常就會自動釋放鎖。如下:
ex6:
class Service{
synchronized public void testMethod(){
if(Thread.currentThread().getName().equals("a")){
System.out.println("ThreadName = "+Thread.currentThread().getName()+" run beginTime="+ System.currentTimeMillis());
int i = 1;
while (i==1){
if((""+Math.random()).substring(0,8).equals("0.123456")){
System.out.println("ThreadName="+Thread.currentThread().getName()+" run exceptionTime="+System.currentTimeMillis());
Integer.parseInt("a");
}
}
}else {
System.out.println("Thread B run Time="+System.currentTimeMillis());
}
}
}
class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
public class Test {
public static void main(String[] args) {
try {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
Thread.sleep(500);
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
output:
ThreadName = a run beginTime=1591535415742
ThreadName=a run exceptionTime=1591535416623
Exception in thread “a” Thread B run Time=1591535416624
java.lang.NumberFormatException: For input string: “a”
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Service.testMethod(Test.java:9)
at ThreadA.run(Test.java:27)
可以看到,在線程a最先獲得了對象鎖,但因爲異常而釋放鎖,由線程b獲得。
3.synchronized同步代碼塊
當同步方法執行耗時很長時,其他線程都要等待其運行完畢,因此總體的運行時間將會很長。這個時候就要同步代碼塊來解決了。同步代碼塊是作用於一個語句塊而不是整個方法,同步代碼塊提供多線程間的互斥訪問。
**synchronized(this)**以當前對象爲對象監視器。
**synchronized(非this)**當一個類中有很多synchronized方法時,雖然能實現同步,但會受阻塞而影響運行效率。使用同步代碼塊非this對象不與其他鎖this同步方法爭搶this鎖,可大大提高運行效率。
synchronized(非this)的三個結論:
1).多個線程同時執行synchronized(x){}同步代碼塊呈同步效果。
2)當其他線程執行x對象中的synchronized同步代碼塊呈同步效果。
3)當其他線程執行x對象方法裏面的synchronized(this)代碼塊時也呈同步效果。
多線程死鎖
當線程相互等待對方釋放鎖時就會產生死鎖。
ex7:
class DealThread implements Runnable{
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username){
this.username = username;
}
@Override
public void run() {
if(username.equals("a")){
synchronized (lock1){
try{
System.out.println("username = "+username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println("按lock1->lock2代碼順序執行了");
}
}
}
if(username.equals("b")){
synchronized (lock2){
try {
System.out.println("username = "+username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println("按lock2->lock1代碼順序執行了");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
try {
DealThread t1 = new DealThread();
t1.setFlag("a");
Thread thread1 = new Thread(t1);
thread1.start();
Thread.sleep(100);
t1.setFlag("b");
Thread thread2 = new Thread(t1);
thread2.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
output:
username = a
username = b
可以看到此時程序進入了死鎖狀態。
4.volatile
volatile關鍵字是爲了強制編譯器從主內存中訪問數據,其與synchronized的區別主要如下:
1)從性能來看volatitle更好。
2)從修飾的對象來看,volatile只能修飾於變量,而synchronized可以修飾方法以及代碼塊。
3)多線程訪問volatile不會發生阻塞,而synchronized會,也即volatile不支持原子性。
5.總結
本文對synchronized和volatile關鍵字都做了講解,volatile演示部分較少,但因爲其功能簡單所以略去。學過操作系統我們都知道,線程之間不僅有競爭,還有同步,那麼下一篇文章講介紹線程間的通信。