一 線程的同步 ***************
同步問題:每一個線程對象輪番搶占共享資源帶來的問題
首先看一段代碼:
class MyThread implements Runnable{
private int ticket=10;
@Override
public void run() {
while (this.ticket>0){
try{
//模拟網絡延遲
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
}
}
}
public class Test2 {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
new Thread(mt,"黃牛A").start();
new Thread(mt,"黃牛B").start();
new Thread(mt,"黃牛C").start();
}
}
這時會發現運作到最後時會出現如下結果,當隻剩下一張票的時候,卻被三個人拿去同時買:
......
黃牛B,還有1張票
黃牛C,還有0張票
黃牛A,還有-1張票
上面出現現象就可以稱之為不同步操作,其唯一好處就是處理速度快(多個線程并發執行)
1 同步處理
1.1 使用synchronize關鍵字(内建鎖,JDK1.0作為關鍵字提供的同步手段)來處理同步問題 對象鎖一定要清楚鎖的對象
synchronize處理同步有兩種模式:同步代碼塊,同步方法
同步代碼塊:(推薦使用,鎖粒度較細)要使用同步代碼塊必須設定一個鎖定的對象,一般可以鎖目前對象this
同一時刻隻能有一個線程進入代碼塊,方法内仍然是多線程
synchronized(this){
//需要同步代碼塊
}
class MyThread implements Runnable{
private int ticket=1000;
@Override
public void run() {
for(int i=0;i<1000;i++){
synchronized (this){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
}
}
}
}
}
public class Test2 {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
new Thread(mt,"黃牛A").start();
new Thread(mt,"黃牛B").start();
new Thread(mt,"黃牛C").start();
}
}
會發現執行結果中沒有負數:
......
黃牛B,還有3張票
黃牛B,還有2張票
黃牛B,還有1張票
同步方法:在方法上添加synchronize關鍵字,表示此方法隻有一個線程能進入。隐式鎖對象,this
同一時刻隻有一個線程能進入此方法
範例:
class MyThread implements Runnable{
private int ticket=1000;
@Override
public void run() {
for(int i=0;i<1000;i++){
if(this.ticket>0){
this.sala();
}
}
}
public synchronized void sala(){
if(this.ticket>0){
try {
//模拟網絡延遲
Thread.sleep(20);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
}
}
}
public class Test2 {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
new Thread(mt,"黃牛A").start();
new Thread(mt,"黃牛B").start();
new Thread(mt,"黃牛C").start();
}
}
會發現執行結果中沒有負數:
......
黃牛C,還有3張票
黃牛A,還有2張票
黃牛A,還有1張票
同步的缺點:雖然可以保證資料的完整性(線程安全操作),但是其執行的速度回很慢 StringBuilder 和 StringBuffer
- StringBuffer JDK1.0 ,采用同步處理,線程安全,效率較低
- StringBuilder JDK1.5,采用異步處理,線程不安全,效率較高
1.2 關于synchronized的額外說明
①實際上synchronized(this)
非static的synchronized方法,隻能防止多個線程同時執行同一個對象的同步代碼塊。
synchronized叫對象鎖(預設隻能鎖一個對象),鎖的是對象本身即this。
範例:觀察synchronized的額外說明
class Sync{
public synchronized void test(){ //預設鎖的是目前對象
System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
//使用static隻執行個體化一個Sync對象,這樣三個線程使用同一塊鎖
//static Sync sync=new Sync();
@Override
public void run() {
Sync sync=new Sync();
sync.test();
}
}
public class Test2 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
Thread thread=new MyThread();
thread.start();
}
}
}
觀察結果,三個線程同時開始,同時結束:
test方法開始,目前線程Thread-0
test方法開始,目前線程Thread-1
test方法開始,目前線程Thread-2
test方法結束,目前線程為Thread-0
test方法結束,目前線程為Thread-1
test方法結束,目前線程為Thread-2
若要鎖住這段代碼還有三中思路:
範例: 鎖同一個對象
class Sync{
public synchronized void test(){
System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
private Sync sync;
public MyThread(Sync sync){
this.sync=sync;
}
@Override
public void run() {
this.sync.test();
}
}
public class Test2 {
public static void main(String[] args) {
//執行個體化一個Sync對象
Sync sync=new Sync();
for(int i=0;i<3;i++){
Thread thread=new MyThread(sync);
thread.start();
}
}
}
對象鎖與全局鎖:
synchronized預設對象鎖,鎖的是目前對象而非代碼塊
全局鎖鎖的是真正代碼段,與對象無關!
實作全局鎖的兩種方式:
1.在同步代碼段鎖Class對象
2.使用static synchronized方法
範例:使用全局鎖,鎖的是類而不是this對象
class Sync{
public void test(){
//使用全局鎖,鎖的是以下代碼段,與對象無關
synchronized (Sync.class){
System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync() ;
sync.test();
}
}
public class Test2 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
Thread thread=new MyThread();
thread.start();
}
}
}
static synchronized方法,static方法可以直接類名加方法名調用,方法中無法使用this,是以它鎖的不是this,而是類 的Class對象,是以,static synchronized方法也相當于全局鎖,相當于鎖住了代碼段。
範例:使用static synchronized方法
class Sync{
//此時鎖的為一下代碼塊,與對象無關
public static synchronized void test(){ //預設鎖的是目前對象
System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync=new Sync();
sync.test();
}
}
public class Test2 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
Thread thread=new MyThread();
thread.start();
}
}
}
範例:看一段代碼( **** 重要 **** )典型面試題
class MyThread implements Runnable{
private ThreadTest test;
public MyThread(ThreadTest test){
super();
this.test=test;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-1")){
this.test.testA();
}else if(Thread.currentThread().getName().equals("Thread-2")){
this.test.testB();
}
}
}
class ThreadTest{
public synchronized void testA(){
System.out.println(Thread.currentThread().getName()+"tsetA方法");
while(true){}
}
public synchronized void testB(){
System.out.println(Thread.currentThread().getName()+"tsetB方法");
}
}
public class Test2 {
public static void main(String[] args) throws InterruptedException {
ThreadTest test=new ThreadTest();
MyThread mythread=new MyThread(test);
Thread thread1=new Thread(mythread);
Thread thread2=new Thread(mythread);
thread1.start();
Thread.sleep(1000);
thread2.start();
}
}
線程2是否能進入到testB中嗎?
鎖的是目前對象,兩個同步方法鎖的是同一個對象
同一個對象,當一個類中有兩個同步方法時,有一個線程已經進了一個同步方法時,另外一個線程絕對不能進入另外的同步方法
如果有一個同步方法一個普通方法,另外一個線程可以進到普通方法
1.3 對象鎖(monitor)機制 --- JDK6之前的synchronized(重量級鎖)
同步代碼塊:執行同步代碼塊後首先要執行monitorenter指令,退出時執行monitorexit指令。
使用内建鎖(synchronized)進行同步,關鍵在于擷取指定鎖對象monitor對象,當線程擷取monitor後才能繼續向下執行,否則就隻能等待。這個擷取過程是互斥的,即同一時刻隻有一個線程能夠擷取到對象monitor。
通常一個monitorenter指令會包含若幹個monitorexit指令。原因在于JVM需要確定在正常執行路徑以及異常執行路徑上都能正确的解鎖,任何情況下都能夠解鎖。
同步方法:當使用所有synchronized标記方法時,編譯後位元組碼中方法的通路标記多了一個 ACC_SYNCHRONIZED。該标記表示,進入該方法時,JVM需要進行monitorenter操作,退出該方法時,無論是否正常傳回,JVM均需要進行monitorexit操作。
當執行monitorenter時,如果目标鎖對象的monitor計數器為0,表示此對象沒有被任何其他對象所持有。此時JVM會将該鎖對象的持有線程設定為目前線程,并且有計數器+1;如果目标鎖對象計數器不為0,判斷鎖對象的持有線程是否是目前線程,如果是再次将計數器+1(鎖的可重入性),如果鎖對象的持有線程不是目前線程,目前線程需要等待,直至持有線程釋放鎖。
當執行monitorexit時,JVM會将鎖對象的計數器-1,當計數器減為0時,代表該鎖已經被釋放。
JDK1.5提供的Lock鎖(了解即可 )(任然是對象鎖)
範例:使用ReentrantLock進行同步處理,在需要上鎖的時候Lock,在finally中unLock
class MyThread implements Runnable{
private int ticket=100;
private Lock ticketLock=new ReentrantLock();
@Override
public void run() {
for(int i=0;i<100;i++){
try{
ticketLock.lock();
if(this.ticket>0) {
System.out.println(Thread.currentThread().getName() +
"還剩下:" + this.ticket-- + "票");
}
}finally{
//手工釋放鎖
ticketLock.unlock();
}
}
}
}
public class Test2 {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
Thread thread1=new Thread(mt,"黃牛1");
Thread thread2=new Thread(mt,"黃牛2");
Thread thread3=new Thread(mt,"黃牛3");
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread3.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
}
}