Java 同步机制

前言:

多线程开发中往往需要同步处理了,这是因为一个进程中的线程是共享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

你可能感兴趣的:(Java 同步机制)