线程同步概念
Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不明确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
一、 同步方法
使用synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用关键字修饰此方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则该线程就处于阻塞状态。
代码如:public synchronized void save(){}
注:synchronized关键字也可以静态方法,此时如果调用该静态方法,将会锁住整个类。
二、同步代码块
即有synchronized关键字修饰的语句块。被关键字修饰的的语句块会自动加上内置锁,从而实现同步。
代码如:synchronized(object){
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要去同步整个方法,使用关键字synchronized修饰关键代码即可。
实例:
public class SynchronizedThread {
public static void main(String[] args) {
SynchronizedThread sThread=new SynchronizedThread();
sThread.useThread();
}
public void useThread(){
Bank bank=new Bank();
SaveBankThread savebankThread=new SaveBankThread(bank);
Thread thread1=new Thread(savebankThread);
thread1.start();
RemoveBankThread removeBankThread=new RemoveBankThread(bank);
Thread thread2=new Thread(removeBankThread);
thread2.start();
}
}
class Bank{
private int count=0;
public void getCount(){
System.out.println("账户余额为:"+count);
}
/**
* 同步方法实现存钱
* @param money
*/
public synchronized void save(int money){
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
/**
* 同步代码块实现取钱
* @param money
*/
public synchronized void remove(int money){
if (count-money<0) {
System.err.println("余额不足。");
return;
}
/*synchronized(Bank.class){*/
count-=money;
System.err.println(System.currentTimeMillis()+"取出:"+money);
//}
}
}
class SaveBankThread implements Runnable{
private Bank bank;
public SaveBankThread(Bank bank){
this.bank=bank;
}
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
bank.save(100);
bank.getCount();
System.out.println("\n");
}
}
}
class RemoveBankThread implements Runnable{
private Bank bank;
public RemoveBankThread(Bank bank){
this.bank=bank;
}
public void run() {
while(true){
bank.remove(100);
bank.getCount();
System.out.println("\n");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
三、使用特殊域变量修饰符volatile实现线程同步
(1)、volatile关键字为域变量的访问提供了一种免锁机制
(2)、使用volatile修饰域变量相当于告诉虚拟机可能会被其他线程更新,因此每次使用该域变量都要同步到内存,从内存中读取,而不是直接使用寄存器中的值。
(3)、volatile不会提供原子操作,他不能用来修饰final类型的变量。
(4)、volatile只对域变量起作用,并不能保证线程安全。
上述实例:只需将count设为volatile修饰,就可以实现同步:
class Bank{
/**
* volatile实现
*/
private volatile int count=0;
public void getCount(){
System.out.println("账户余额为:"+count);
}
/**
* 同步方法实现存钱
* @param money
*/
public void save(int money){
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
/**
* 同步代码块实现取钱
* @param money
*/
public void remove(int money){
if (count-money<0) {
System.err.println("余额不足。");
return;
}
/*synchronized(Bank.class){*/
count-=money;
System.err.println(System.currentTimeMillis()+"取出:"+money);
//}
}
}
注:多线程中的非同步问题主要出现在对域的都写上,如果域自身避免这个问题,那么就不需要修改操作该域的方法。用final域,有锁保护的域可以避免非同步的问题。
四、使用重入锁实现线程同步
在jdk1.5以后新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁。它与synchronized修饰的方法具有相同的基本行为与语义,并且扩展了其能力。
ReentrantLock类的常用方法有:
(1)、ReentrantLock():创建一个ReentrantLock实例。
(2)、lock():获得锁
(3)、unlock():释放锁
上述代码可以修改为:
class Bank{
/**
* volatile实现
*/
private int count=0;
/**
* 使用可重入锁
*/
private Lock lock=new ReentrantLock();
public void getCount(){
System.out.println("账户余额为:"+count);
}
/**
* 同步方法实现存钱
* @param money
*/
public void save(int money){
lock.lock();
try {
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
/**
* 同步代码块实现取钱
* @param money
*/
public void remove(int money){
if (count-money<0) {
System.err.println("余额不足。");
return;
}
lock.lock();
try {
count-=money;
System.err.println(System.currentTimeMillis()+"取出:"+money);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
注:
1、ReentrantLock()还可以通过public ReentrantLock(boolean fair)构造方法创建公平锁,即,优先运行等待时间最长的线程,这样大幅度降低程序运行效率。
2、关于Lock对象和synchronized关键字的选择:
(1)、最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
(2)、如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。
(3)、如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。
五、使用ThreadLocal管理局部变量实现线程同步
ThreadLocal管理变量,则每一个使用该变量的线程都获得一个该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的副本,而不会对其他线程产生影响。
ThreadLocal类常用的方法:
1、get():返回该线程局部变量的当前线程副本中的值。
2、initialValue():返回此线程局部变量的当前线程的”初始值“。
3、remove():移除此线程局部变量当前线程的值。
4、set(T value):将此线程局部便利啊ing的当前线程副本中的值设置为指定值value。
上述代码修改为:
class Bank{
private static ThreadLocal count=new ThreadLocal(){
@Override
protected Integer initialValue() {
// TODO Auto-generated method stub
return 0;
}
};
public void getCount(){
System.out.println("账户余额为:"+count.get());
}
/**
* 局部变量实现存钱
* @param money
*/
public void save(int money){
count.set(count.get()+money);
System.out.println(System.currentTimeMillis()+"存进出:"+money);
}
/**
* 局部变量实现取钱
* @param money
*/
public void remove(int money){
if (count.get()-money<0) {
System.err.println("余额不足。");
return;
}
count.set(count.get()-money);
System.err.println(System.currentTimeMillis()+"取出:"+money);
}
}
注:ThreadLocal与同步机制:
(1)、ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
(2)、前者采用以”空间换时间”的方法,后者采用”时间换空间”