前言:
多线程开发中往往需要同步处理了,这是因为一个进程中的线程是共享JVM中的方法区和堆区,同时操作临界区资源的时候会破坏了原子性,导致数据出现错误。就需要同步操作,也就有了锁。
先从一个简单的银行转账例子开始:
public class Bank{
List accounts = new ArrayList<>();
// 虚拟创建10个账号
public Bank(){
for(int i=0;i<10;i++){
accounts.add(new Account());
}
}
// 获取总资金
public int getTotalMoney(){
int total = 0;
for(int i = 0;i
原因:转账的时候,转出和转入是个原子的操作,两个线程同时操作同一个账户的时候就很容易出错。线程的执行是没有顺序可言的,一行代码的指令会有多行,没执行完就被剥夺了运行权,另一个Thread再次处理就会导致数据不一致。
一、ReentrantLock锁对象
java5.0版本引入了ReentrantLock类,它位于java.util.concurrent包下面。它是一个可以被用来保护临界区的可重入锁,只能有一个线程获得锁对象,其它线程执行lock()方法时,会阻塞在这里,直到当前获得锁对象的线程释放了锁即unlock(),其它线程才可以竞争。
// ReentrantLock使用步骤
myLock.lock();
try {
同步代码
} finally {
myLock.unlock();
}
在上面的例子中,只要改变给临界区加上ReentrantLock就可以了。但是同一个线程可以多次获得锁对象(即lock.lock()操作),该ReentrantLock会有一个计数加锁几次,必须全部释放锁的时候才是线程真正的释放当前锁对象,这时锁计数为0。
Lock lock = new ReentrantLock();
// 转账操作
public void transfers(int from,int to,int money){
lock.lock(); // 加锁
if(accounts.get(from).money
二、条件对象Condition
条件对象,是配合ReentrantLock对象使用的,他也是在java.util.concurrent包下面的。应用场景:刚获得锁的线程,并不满足一些必备的条件,如账号金额不足。这个时候就必须阻塞当前线程,释放当前锁对象。其它线程获得锁对象,执行成功后再通知解除等待线程的阻塞,但不是立即的就能获得锁对象,想要获得锁对象,还是要重新的竞争。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); // 增加一个条件对象,用ReentrantLock创建条件对象
// 转账操作
public void transfers(int from, int to, int money) {
lock.lock();
try {
while (accounts.get(from).money < money) { // 通常都是用循环,防止重新获得锁的时候,条件依旧不能保证是否能满足条件
condition.await(); // 将线程加入等待集,阻塞当前线程
}
accounts.get(from).money -= money;
accounts.get(to).money += money;
System.out.printf("Bank总共money = %d \n", getTotalMoney());
condition.signalAll(); //必须要通知解除阻塞
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
三、synchronized关键字
有了对象锁和条件对象Condition后,为什么会有synchronized了。synchronized更加的简洁减少出错的概率,锁的开启和释放均有JVM来操作,ReentrantLock则需要手动的调动加锁和释放锁。ReentrantLock是可重入锁,synchronized锁仅有单一的条件。synchronized只能是非公平锁,而ReentrantLock可以自己设置公平和非公平。总的来说java希望两者最好都不使用,而是用阻塞队列等来实现。
java中存在类锁和对象锁,作用如字面所描述。猜测java类锁应该作用于方法区当中,对象锁则是作用在堆区中。因为类信息加载在方法区,对象则分配中堆中。
synchronized代码块是由一对monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。
3.1、synchronized作用在方法中
// 这个就是对象锁
public synchronized void method(){
//同步代码块
}
// 这个就是类锁
public static synchronized void method(){
//同步代码块
}
对象锁和类锁的区别,简单来说就是,类锁方法怎么调用都是排斥的,而不同的对象调用同一个对象锁方法是不互斥的,不同对象间没有任何关系。如果不同线程,调用一个对象的对象锁方法,那么就会互斥。具体的可以看透彻理解 Java synchronized 对象锁和类锁的区别,使用了synchronized非常简单。
在synchronized 对象锁同步代码块中,就意味着已经获得了该对象锁了,这对下面的wait()和notifyAll()方法也有用。wait()和notifyAll()方法是Object类的,属于final不能被修改。需要和synchronized配合使用。
将代码改成如下就可以了,如果没有加入synchronized就调用wait()是会抛异常的
// 转账操作
public synchronized void transfers(int from, int to, int money) {
try {
while (accounts.get(from).money < money) {
wait();
}
accounts.get(from).money -= money;
accounts.get(to).money += money;
System.out.printf(Thread.currentThread().getName() + "Bank总共money = %d \n", getTotalMoney());
notifyAll();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
3.2、同步阻塞
格式如下:是对该obj对象加入对象锁
synchronized (obj){
... 同步代码块
}
四、volatile域用法(可见性无原子性)
有了锁机制,为什么又有了volatile了,难道volatile有什么更优的地方。无论是synchronized 还是 ReentrantLock都是比较重量级的,有时只是一个变量的同步问题,所有java引入了更为精简的volatile修饰。
volatile是修饰变量,当一个变量被volatile修饰后会有以下功能:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,对其他线程来说是立即可见的。造成不一致的原因在于,电脑是有高速缓存和内存的。如果这两个内存中的数据不一致,就会造成错误。如果加入volatile后,就会强制将修改的值立即写入到内存中。
- 禁止进行指令重排序。CPU会优化指令,以此增加速度。加入volatile之后的变量,不会采用优化策略。volatile前面的指令全部执行完才能执行volatile的代码,同样volatile代码没执行完成,不能开始后面的指令执行。
五、AtomicInteger
先看下下面这个例子:
public class Test {
public int num = 0;
public void increase() {
num++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<100;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.num);
}
}
结果不意外的是小于1000,我这个运行结果是9191。这是因为num++这个操作不是原子性的,所以这会导致操作是小于1000,若加入volatile修饰结果也是一样,volatile不能保证操作的原子性,只能让多线程的正确结果可见。
AtomicInteger就是这个int原子性操作问题的。得到的结果才是期望的1000,简单用法如下:
public class Test {
public AtomicInteger num = new AtomicInteger(0);
public void increase() {
num.getAndIncrement();
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.num);
}
}
六、读写锁
摘自《java核心技术卷一》第663页ReentrantReadWriteLock读写锁描述。
- 1、首先构造一个读写锁
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock();
Lock writeLock = rwl.writeLock();
- 2、读数据加锁操作
public double getTotalBalance() {
readLock.lock();
try {
} finally {
readLock.unlock();
}
}
- 3、写数据加锁操作
public void transfer() {
writeLock.lock();
try {
} finally {
writeLock.unlock();
}
}
小结:如果多线程中,大量的会用到数据的读取工作,只有少量的写数据操作,这个时候可以考虑采用读写锁分离控制。
七、同步器
- 1、CountDownLatch(倒计时门栓)
让一个线程集等待,直到计数变成0。await()之后的线程才停止阻塞。一但计数变成0之后,就不能再次利用了。
public static void main(String[] args) {
final int count = 10; // 计数次数
final CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < count; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// do anything
System.out.println("线程"
+ Thread.currentThread().getName());
} catch (Throwable e) {
// whatever
} finally {
// 很关键, 无论上面程序是否异常必须执行countDown,否则await无法释放
latch.countDown();
}
}
}).start();
}
try {
// 10个线程countDown()都执行之后才会释放当前线程,程序才能继续往后执行
latch.await();
} catch (InterruptedException e) {
}
System.out.println("main thread Finish");
}
结果:
线程Thread-2
线程Thread-1
线程Thread-0
线程Thread-3
线程Thread-4
线程Thread-5
线程Thread-6
线程Thread-7
线程Thread-8
线程Thread-9
main thread Finish
等到前面的全部执行完才会放行。
- 2、CyclicBarrier (障栅)
大量线程运行在一次计算的不同部分的情形,当所有的部分都准备好了,需要把结果组合在一起。当一个线程完成他的那部分任务后,就让他运行到障栅处。
CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景。
public static void main(String[] args) {
final CyclicBarrier c = new CyclicBarrier(2);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" start");
Thread.sleep(1000);
c.await();
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()+" Finish");
}
}).start();
try {
System.out.println(Thread.currentThread().getName()+" start");
Thread.sleep(1000);
c.await();
System.out.println(Thread.currentThread().getName()+" Finish");
} catch (Exception e) {
}
}
一种结果为:
Thread-0 start
main start
Thread-0 Finish
main Finish
设置拦截两个数量的障栅,等到两个线程都执行到await()之前,才允许后续执行。
- 3、semaphore (信号量)
通常是用来限制访问资源的总数
public class SemaphoreTest {
final Semaphore semaphore = new Semaphore(1);
public void start() {
try {
semaphore.acquire(1);
System.out.println(Thread.currentThread().getName() + " start ");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " finash ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreTest test = new SemaphoreTest();
for(int i=0;i<5;i++) {
new Thread(new Runnable() {
@Override
public void run() {
test.start();
}
}).start();
}
}
}
因为是每次只能允许一个线程访问临界资源,所以结果也是线性执行的:
Thread-0 start
Thread-0 finash
Thread-2 start
Thread-2 finash
Thread-1 start
Thread-1 finash
Thread-3 start
Thread-3 finash
Thread-4 start
Thread-4 finash