Shiliew 植树人

一点关于Java锁和同步的记录

2017-05-05

  最近想研究一下公司的缓存,然后看到一点关于Java锁的东西,然后查阅了一些资料,学一点,记一点。

乐观锁和悲观锁

  悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

  上面引用的两段关于悲观锁和乐观锁的解释,非常通俗易懂。自己理解就是,悲观锁在对数据做任何操作之前都会先上锁,操作完了就解锁,这期间不允许有任何其它事务去操作该数据;乐观锁就是在更新的时候会检查数据是否被更新过,并在提交的时候上锁,其他时候都不会对该数据上锁。悲观锁适用于写操作较多的并发环境,乐观锁适用于读操作较多,写操作较少的并发环境。

  上面关于乐观锁和悲观锁只是之前听到的,然后查询之后做的笔记,下面的记录则为主要的学习内容。

Java中锁的概念

  可以理解,在Java中需要被上锁或者说同步的数据肯定是线程所共享的数据,那这样的数据分为两类:

  1. 保存在堆中的实例变量(定义在类中,方法之外的变量)

  2. 保存在方法区中的类变量(静态变量)

  保存在Java栈中的数据为拥有该栈的线程私有,所以不会上锁。

  在Java程序中,synchronized关键字表示同步,标志出一个监视区域,每当一个线程进入该区域时,Java虚拟机都会自动锁上对象或者类。(在这里有一个监视器的概念,暂时没理解)

  根据网上的教程,有三个示例,我也照样实现了这三个示例,并有所理解。

示例一 synchronized成员方法

public class ThreadTest extends Thread{

    private int threadNo;

    public ThreadTest(int threadNo){
        this.threadNo = threadNo;
    }

    public static void main(String [] args) throws InterruptedException {
        for (int i = 0; i < 10; i++){
            new ThreadTest(i).start();
            Thread.sleep(1);
        }
    }

    @Override
    public synchronized void run(){
        for (int i = 0; i<1000; i++){
            System.out.println("No."+threadNo+":"+i);
        }
    }
}

  程序很简单,开10个线程分别从0数到999。加上synchronized关键字是希望一个线程执行完之后,再执行另一个线程。但是结果是10个线程都是抢着报数,没有任何秩序。可见synchronized并没有使run方法在这10个线程中同步,这是因为给类中的一个成员方法加上synchronized关键字,实际上是给该方法所在的对象加上对象锁。上面的main方法中new了10个对象,就有10把锁,每个线程分别拥有一把锁,这自然不能达到同步的效果。因此,如果要这10个线程同步,那么这些线程所持有的对象锁应该是共享且唯一的。

示例二 synchronized块

public class ThreadTest2 extends Thread {

    private int threadNo;

    private String lock;

    public ThreadTest2(int threadNo, String lock){
        this.threadNo = threadNo;
        this.lock = lock;
    }

    public static void main(String [] args) throws InterruptedException {
        String lock = new String("lock");
        for (int i = 0; i < 10; i++){
            new ThreadTest2(i, lock).start();
            Thread.sleep(1);
        }
    }

    @Override
    public void run(){
        synchronized (lock){
            for (int i = 0; i < 1000; i++){
                System.out.println("No."+threadNo+":"+i);
            }
        }
    }
}

  示例二的程序加了一个String类型的变量lock,在启动10个线程之前,先创建了一个String类型的对象,并把该对象传入了ThreadTest2的构造函数中。根据Java的传值特点,我们知道这是引用传递,即lock变量实际上指向的是堆内存中的存放该String对象的区域。这样做使得这10个线程会共享lock变量,而拿去了修饰run方法的synchronized关键字后,用其修饰lock变量,这时是给lock这个共享的对象加上了对象锁,于是我们能看到10个线程能在一个执行完后再执行另外一个。(并不能保证10个线程按照顺序依次执行,哪个线程抢到了当前资源,就会一直执行完成,然后再执行下一个线程)

示例三 synchronized类方法

public class ThreadTest3 extends Thread {

    private int threadNo;

    public ThreadTest3(int threadNo){
        this.threadNo = threadNo;
    }

    public static void main(String [] args) throws InterruptedException {
        for (int i = 0; i < 10 ; i++){
            new ThreadTest3(i).start();
            Thread.sleep(1);
        }
    }

    public static synchronized void abc(int threadNo){
        for (int i = 0; i < 1000; i++){
            System.out.println("No." + threadNo + ":" + i);
        }
    }

    @Override
    public void run(){
        abc(threadNo);
    }
}

  在示例三中,大部分代码和示例一相同,最大的区别就在于拿去了示例一中修饰run()方法的synchronized关键字,并将run()方法中的代码封装在一个由synchronized修饰的类方法中。这样做同样能达到同步的效果,因为所有的类在加载时都会生成唯一的一个类实例对象,而类变量和类方法就存在于其中,都是唯一的。给类方法加上synchronized,则会给该方法所在的类加上对象锁,于是该锁加载了唯一的类实例对象ThreadTest3.class对象上,于是达到了同步的效果。无论我们创建多少该类的实例,但该类的类实例对象只有一个。

总结

  通过以上三个示例,我们能总结出如下几点:

  1. synchronized关键字只是给对象加上对象锁,修饰方法则会加在该方法所在对象上。

  2. 成员函数所在的对象为类的实例,类函数所在的对象为该类的类实例。

  3. 只有获得了对象锁,才能进入同步方法或者同步块中进行操作。

  4. 若同步方法,类方法一定会同步,而成员方法只有在单例模式中才能同步。

参考

Java的锁机制

一分钟教你知道乐观锁和悲观锁的区别

深入理解乐观锁与悲观锁


Similar Posts

Comments

comments powered by HyperComments