為何要使用同步?
java允許多執行緒並發控制,當多個執行緒同時操作一個可共享的資源變數(如資料的增刪改查),
將會導致資料不準確,相互之間產生衝突,因此加入同步鎖定以避免在該線程沒有完成操作之前,被其他線程的調用,
從而保證了該變數的唯一性和準確性。
1.同步方法
即有synchronized關鍵字修飾的方法。
由於java的每個物件都有內建鎖定,當使用此關鍵字修飾方法時,
內建鎖定會保護整個方法。在呼叫方法前,需要取得內建鎖,否則就處於阻塞狀態。
程式碼如:
public synchronized void save(){}
## public synchronized void save(){}
## 註: synchronized關鍵字也可以修飾靜態方法,此時若呼叫此靜態方法,將會鎖定整個靜態方法類別
2.同步程式碼區塊
被該關鍵字修飾的語句區塊會自動被加上內建鎖定,從而實現同步
synchronized(object){ }
package com.xhj.thread;
/**
* 线程同步的运用
*
* @author XIEHEJUN
*
*/
public class SynchronizedThread {
class Bank {
private int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
}
3.使用特殊領域變數(volatile)實作執行緒同步
## a.volatile關鍵字為網域變數的存取提供了一種免鎖機制,
b.使用volatile修飾域相當於告訴虛擬機器該網域可能會被其他執行緒更新,
c.因此每次使用該網域就要重新計算,而不是使用暫存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變數
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變數
例如:
在上面的例子前面加上上volatile修飾,即可實現執行緒同步。
//只给出要修改的代码,其余代码与上同 class Bank { //需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
附註:多執行緒中的非同步問題主要出現在對域的讀寫上,如果讓域本身避免這個問題,則不需要修改操作該域的方法。
用final域,有鎖定保護的域和volatile域可以避免非同步的問題。
在JavaSE5.0中新增了一個java.util.concurrent套件來支援同步。
它與使用synchronized方法和快具有相同的基本行為和語義,並且擴展了其能力
Lock類別的常用方法有:
ReentrantLock() : 建立一個ReentrantLock實例
lock() 21一個可以創造公平鎖定的建構方法,但由於能大幅降低程式運作效率,不建議使用
例如:
在範例的基礎上,而改寫後的程式碼為:
//只给出要修改的代码,其余代码与上同 class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } }
能夠協助使用者處理與鎖定相關的所有程式碼。
b.如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化程式碼
c.如果需要更高階的功能,就用ReentrantLock類,此時要注意及時釋放鎖定會出現死鎖,通常在finally程式碼釋放鎖定
5.使用局部變數實現執行緒同步 如果使用ThreadLocal管理變數,則每個使用該變數的執行緒都獲得此變數的副本,
副本之間相互獨立,以便每個執行緒都可以隨意修改自己的變數副本,而不會對其他執行緒產生影響。
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
例如:
在上面例子基础上,修改后的代码为:
代码实例:
//只改Bank类,其余代码与上同 public class Bank{ //使用ThreadLocal类管理共享变量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public void save(int money){ account.set(account.get()+money); } public int getAccount(){ return account.get(); } }
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方法
6.使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
本小节主要是使用LinkedBlockingQueue来实现线程的同步
LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~
LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞
代码实例:
实现商家生产商品和买卖商品的同步
1 package com.xhj.thread; 2 3 import java.util.Random; 4 import java.util.concurrent.LinkedBlockingQueue; 5 6 /** 7 * 用阻塞队列实现线程同步 LinkedBlockingQueue的使用 8 * 9 * @author XIEHEJUN 10 * 11 */ 12 public class BlockingSynchronizedThread { 13 /** 14 * 定义一个阻塞队列用来存储生产出来的商品 15 */ 16 private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); 17 /** 18 * 定义生产商品个数 19 */ 20 private static final int size = 10; 21 /** 22 * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程 23 */ 24 private int flag = 0; 25 26 private class LinkBlockThread implements Runnable { 27 @Override 28 public void run() { 29 int new_flag = flag++; 30 System.out.println("启动线程 " + new_flag); 31 if (new_flag == 0) { 32 for (int i = 0; i < size; i++) { 33 int b = new Random().nextInt(255); 34 System.out.println("生产商品:" + b + "号"); 35 try { 36 queue.put(b); 37 } catch (InterruptedException e) { 38 // TODO Auto-generated catch block 39 e.printStackTrace(); 40 } 41 System.out.println("仓库中还有商品:" + queue.size() + "个"); 42 try { 43 Thread.sleep(100); 44 } catch (InterruptedException e) { 45 // TODO Auto-generated catch block 46 e.printStackTrace(); 47 } 48 } 49 } else { 50 for (int i = 0; i < size / 2; i++) { 51 try { 52 int n = queue.take(); 53 System.out.println("消费者买去了" + n + "号商品"); 54 } catch (InterruptedException e) { 55 // TODO Auto-generated catch block 56 e.printStackTrace(); 57 } 58 System.out.println("仓库中还有商品:" + queue.size() + "个"); 59 try { 60 Thread.sleep(100); 61 } catch (Exception e) { 62 // TODO: handle exception 63 } 64 } 65 } 66 } 67 } 68 69 public static void main(String[] args) { 70 BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); 71 LinkBlockThread lbt = bst.new LinkBlockThread(); 72 Thread thread1 = new Thread(lbt); 73 Thread thread2 = new Thread(lbt); 74 thread1.start(); 75 thread2.start(); 76 77 } 78 79 }
注:BlockingQueue
add()方法会抛出异常
offer()方法返回false
put()方法会阻塞
7.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
代码实例:
只改Bank类,其余代码与上面第一个例子同
1 class Bank { 2 private AtomicInteger account = new AtomicInteger(100); 3 4 public AtomicInteger getAccount() { 5 return account; 6 } 7 8 public void save(int money) { 9 account.addAndGet(money); 10 } 11 }
补充--原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。
相关文章:
以上是【java】為何要使用同步?關於線程同步(7種方式)的詳細內容。更多資訊請關注PHP中文網其他相關文章!