java的同步方式
在java主要有以下几种方式
1.给变量/代码块/方法/类添加悲观锁避免一个变量更改值的时候对这个变量进行更改。
用 sychronized 来进行同步
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
优缺点:
优点:加上synchronized锁是为了保证服务器选择的顺序性,使得并发操作的同一时刻有且仅有一个线程能够读取/写入数据表记录,防止多线程并发造成的脏数据。
缺点:悲观锁的引入,将会导致并发吞吐量发生明显下降,付出相当大的性能代价。且不能获得锁是否是已经加上去了。
2.使用特殊域变量(volatile)实现线程同步
它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
因为volatile使用了复合操作的原子性,所以下面的代码结果一定<=正确值
/**
* 使用特殊域变量(volatile)实现线程同步
* a.volatile关键字为域变量的访问提供了一种免锁机制,
* b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
* c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
* d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
*/
public class VolatileKeywordSynchronization {
class Bank {
private volatile int accout = 10;
public int getAccout() {
return accout;
}
public void saveAccount(int money) {
accout += money;
System.out.println("account:" + accout);
}
}
class VolatileThread implements Runnable {
private Bank bank;
public VolatileThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.saveAccount(10);
System.out.println(Thread.currentThread().getName() + "-->第" + i + "次当前账户余额:" + bank.getAccout() + "元。");
}
}
}
public void userVolatileThread() {
Bank bank = new Bank();
VolatileThread volatileThread = new VolatileThread(bank);
Thread thread1 = new Thread(volatileThread);
Thread thread2 = new Thread(volatileThread);
System.out.println("线程1:");
thread1.start();
System.out.println("线程2:");
thread2.start();
}
public static void main(String[] args) {
VolatileKeywordSynchronization volatileKeywordSynchronization = new VolatileKeywordSynchronization();
volatileKeywordSynchronization.userVolatileThread();
}
}
volatile的优缺点
优点:Volatile修饰的成员变量在每次被线程访问是,都强迫从共享内存中重读成员变量的值;当成员变量发生变化是,强迫线程将变化值回写到共享内存;这样可以让多个线程总是看到某个成员变量的同一个值,Volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应该直接语共享成员变量交互;
缺点: 关键字能保证数据的可见性,但不能保证数据的原子性,且volatile只适用变量。
3.AtomicInteger(类)
public class VolatileKeywordSynchronization {
class Bank {
private AtomicInteger accout = new AtomicInteger(10);
public int getAccout() {
return accout.get();
}
public void saveAccount(int money) {
accout.getAndAdd(money);
System.out.println("account:" + accout);
}
}
class VolatileThread implements Runnable {
private Bank bank;
public VolatileThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.saveAccount(10);
System.out.println(Thread.currentThread().getName() + "-->第" + i + "次当前账户余额:" + bank.getAccout() + "元。");
}
}
}
public void userVolatileThread() {
Bank bank = new Bank();
VolatileThread volatileThread = new VolatileThread(bank);
Thread thread1 = new Thread(volatileThread);
Thread thread2 = new Thread(volatileThread);
System.out.println("线程1:");
thread1.start();
System.out.println("线程2:");
thread2.start();
}
public static void main(String[] args) {
VolatileKeywordSynchronization volatileKeywordSynchronization = new VolatileKeywordSynchronization();
volatileKeywordSynchronization.userVolatileThread();
}
}
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
优缺点:
优点:不需要加锁,就可以保证原子性,减少大量的开销。
缺点:不能保证代码块的原子性
ReentrantLock
ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。
其原理是使用AQS(AbstractQueuedSynchornized)机制,是采用双向队列来进行加锁。
AQS机制
AQS 是一个设计模板,具体的实现由子类完成,AQS中维护了一个state(共享资源,代表是否可以被加锁)和一个CLH(先入先出)双向队列。把需要等待的线程放进CLH队列。
ReentrantLock 实现原理
ReentrantLock支持公平锁和非公平锁,其中的一些原理是稍微由不同的。
公平锁
ReentrantLock(true)
reentrantLock.lock(); reentrantLock.unlock();
线程加锁,先把线程加入队列中,如果该线程是头节点,就加锁执行,如果不是调用本地方法park等待。线程解锁,更新状态,并且从尾节点向上追溯到head的下一个节点,并且更新head节点。
非公平锁
ReentrantLock() ReentrantLock(false)
reentrantLock.lock(); reentrantLock.unlock();
AQS中维护了一个volatile int state(共享资源)和一个CLH队列。当state=1时代表当前对象锁已经被占用,其他线程来加锁时则会失败,失败的线程被放入一个FIFO的等待队列中,然后会被UNSAFE.park()操作挂起,等待已经获得锁的线程释放锁才能被唤醒。
public class VolatileKeywordSynchronization {
ReentrantLock reentrantLock = new ReentrantLock();
class Bank {
private int accout = 10;
public int getAccout() {
return accout;
}
public void saveAccount(int money) {
reentrantLock.lock();
try {
accout+=money;
}finally {
reentrantLock.unlock();
}
System.out.println("account:" + accout);
}
}
class VolatileThread implements Runnable {
private Bank bank;
public VolatileThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
bank.saveAccount(10);
System.out.println(Thread.currentThread().getName() + "-->第" + i + "次当前账户余额:" + bank.getAccout() + "元。");
}
}
}
public void userVolatileThread() {
Bank bank = new Bank();
VolatileThread volatileThread = new VolatileThread(bank);
Thread thread1 = new Thread(volatileThread);
Thread thread2 = new Thread(volatileThread);
System.out.println("线程1:");
thread1.start();
System.out.println("线程2:");
thread2.start();
}
public static void main(String[] args) {
VolatileKeywordSynchronization volatileKeywordSynchronization = new VolatileKeywordSynchronization();
volatileKeywordSynchronization.userVolatileThread();
}
}