Java 同步机制

前言:

多线程开发中往往需要同步处理了,这是因为一个进程中的线程是共享JVM中的方法区和堆区,同时操作临界区资源的时候会破坏了原子性,导致数据出现错误。就需要同步操作,也就有了锁。

先从一个简单的银行转账例子开始:

 public class Bank{
    List<Account> 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<accounts.size();i++){
            total+=accounts.get(i).money;
        }
        return total;
    }
    
    // 转账操作
    public void transfers(int from,int to,int money){
        if(accounts.get(from).money<money)
            return;
        accounts.get(from).money -=money;
        accounts.get(to).money +=money;
        System.out.printf("Bank总共money = %d  \n",getTotalMoney());
    }
    

    // 测试两个Thread转账
    public void start(){

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    int money = (int) ((double)50*Math.random());
                    int from =  (int) ((double)9*Math.random());
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int to =  (int) ((double)9*Math.random());
                    transfers(from, to, money);
                }
            
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                  ......省略 与t1一致
            }
        });
        t1.start();
        t2.start();
    }
    // 程序入口
    public static void main(String[] args) {
        Bank b = new Bank();
        b.start();
    }
}

 // 用户账号
class Account{
    int money = 1000; // 原始资金
}
结果

原因:转账的时候,转出和转入是个原子的操作,两个线程同时操作同一个账户的时候就很容易出错。线程的执行是没有顺序可言的,一行代码的指令会有多行,没执行完就被剥夺了运行权,另一个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<money)
            return;
        accounts.get(from).money -=money;
        accounts.get(to).money +=money;
        System.out.printf("Bank总共money = %d  \n",getTotalMoney());
        lock.unlock(); // 转账完成后释放锁
    }

二、条件对象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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,224评论 4 56
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,681评论 0 11
  • 2016年一去不复返,很多人都沉浸在17年的喜悦气氛中,各种总结和计划不断出炉,小糊涂虫一路看下来,发现很少有对自...
    e8d83e9aa398阅读 691评论 21 6
  • 第一次尝试,一个下午的成果! 一位老师说还没有完全达到效果,可是我已经很满意了!嗯! 就是这么容易满足!
    鹿精灵阅读 302评论 3 4
  • 今天是5.12护士节,祝姐妹们节日快乐!明天是母亲节,祝愿所有的母亲节日快乐! 可能是周末的关系,...
    刘学颖妈妈阅读 103评论 0 0