zoukankan      html  css  js  c++  java
  • Java线程知识:二、锁的简单使用

    锁的初步认识


    说到锁,相信大家都不陌生,这是我们生活中非常常见的一种东西,它的形状也各式各样。在生活中,我们通常用锁来锁住房子的大门、装宠物的笼子、装衣服的衣柜、以及装着我们一些小秘密的小抽屉......

    那么相同的,Java中的锁也各式各样,我们往往按照是否含有某一特性来定义锁,并将锁进行归、分组,具体可分为以下几种:

    java_thread_syn

    而这些锁在Java中的具体实现都离不开synchronized 关键字和java.util.concurrent.locks.Lock接口类,本篇随笔就以synchronized关键字和Lock接口的实现类ReentrantLock来展示对锁的简单使用。

    synchronized 内置锁(隐式锁)


    作为Java中53个关键字的其中之一,synchronized占有举重若轻的地位,它是Java语言本身为我们提供的一种同步锁,所以又被称为内置锁隐式锁

    1、从语法维度上来讲,synchronized一共有三种用法:

    • 静态方法上加关键字
     public static synchronized void add(){}
    
    • 实例方法上加关键字
    public synchronized void add(){}
    
    • 方法中使用同步代码块
    public void add(){
    	synchronized(this){}
    }
    

    在讲解这些用法之前,我们先来看一段代码:

    /**
     * @author cai
     */
    public class SynDemo {
        private static int num = 0;
    
        private static final int ADD_NUM = 2000;
        private static final int THREAD_NUM = 5;
    
        private static class UserThread extends Thread {
    
            private SynDemo synDemo;
    
            public UserThread(String threadName, SynDemo synDemo) {
                super(threadName);
                this.synDemo = synDemo;
            }
    
            @Override
            public void run() {
                synDemo.add();
            }
        }
    
        
        public void add() {
            for (int i = 0; i < ADD_NUM; i++) {
                num++;
            }
            System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
        }
    
        public static void main(String[] args) {
            // 开启5个线程,使num累计计数到10000
            SynDemo synDemo = new SynDemo();
            for (int i = 0; i < THREAD_NUM; i++) {
                UserThread userThread = new UserThread("thread_" + i, synDemo);
                userThread.start();
            }
        }
    }
    

    如代码中一般,我们开启5个线程,并循环使num变量累计计数,同时打印每个线程运行完之后,num变量的数值,那么我们所期待的结果一定是这样的:

    然而这是在没有考虑并发的情况下的理想结果,但现实却是:在线程thread_0还没从循环中脱离时,线程thread_1已经进入了循环,从而导致了num变量的多次计数,所以就变成了以下结果(运行结果不止这一种,我只是选取了随机的一种,以下代码的运行结果都是这样。):

    那么我们用上synchronized关键字,再来看看运行结果:

    1.1 实例方法上加关键字

    /**
     * 在实例方法 (普通方法) 上加关键字
     */
    public synchronized void add() {
        for (int i = 0; i < ADD_NUM; i++) {
            num++;
        }
        System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
    }
    

    这时的运行结果就变成了这样:

    这里的线程顺序问题不用纠结,因为synchronized是一种非公平锁,线程不会按顺序去排队,而是争先恐后的去抢这唯一的一把锁,所以每次的运行结果中的线程顺序大多不相同,但num变量的计数结果确实与我们所期望的结果相符合的。

    1.2 静态方法上加关键字

    /**
     * 在静态方法上加关键字
     */
    public static synchronized void add() {
        for (int i = 0; i < ADD_NUM; i++) {
            num++;
        }
        System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
    }
    

    结果:

    1.3 方法中使用同步代码块

    public void add() {
        synchronized (this){
            for (int i = 0; i < ADD_NUM; i++) {
                num++;
            }
            System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
        }
    }
    

    结果:

    从结果上来看,以上三种的加锁方式都能满足我们的需求,使num变量计数到10000,但不论在我们日常使用上,还是从Java语言本身的建议上讲,更推荐使用第三种用法,即在方法中使用同步代码块的用法,这种方法的性能要比前面两种更好一些,至于为什么,就是属于JVM层次的研究了,这里不多赘述。

    再回到我们的第三种用法,其实它不止这一种写法,我们可以按上述的代码样式书写:synchronized(this){},也可以这样写:synchronized(SynDemo.class){},还有private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}这样的写法。看到这里,肯定有很多人的心里不禁的浮现出三个大字:WTF ? ? ?,这都是些什么玩意!!!!synchronized到底锁住的是谁!???

    那么我们就来从另一个维度来揭露一下。

    2、从synchronized锁的是谁的维度来讲,一共有两种情况:

    2.1 对象锁

    我们这里先保留上面 1.3 中的代码不变,稍稍变动一下main方法中的代码:

    如上图所示,将创建synDemo对象的代码从for循环外移入for循环内,这样的话,我们每次新建线程时所传入的synDemo对象是不同的,这时候再来看看运行的结果:

    这样的结果又和我们的期望大相径庭,那么我们是不是可以认定synchronized(this){}锁住的就是对象呢?让我们再来看一个实例:

    /**
     * @author cai
     */
    public class SynDemo {
        private static int num = 0;
    
        private static final int ADD_NUM = 2000;
        private static final int THREAD_NUM = 5;
    
        // 共享的对象
        private static SynDemo synDemo = new SynDemo();
    
        private static class UserThread extends Thread {
    
            /*public UserThread(String threadName, SynDemo synDemo) {
                super(threadName);
                this.synDemo = synDemo;
            }*/
    
            public UserThread(String threadName){
                super(threadName);
            }
            @Override
            public void run() {
                synDemo.add();
            }
        }
    
        public void add() {
            synchronized (synDemo){
                for (int i = 0; i < ADD_NUM; i++) {
                    num++;
                }
                System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
            }
    
        }
    
        public static void main(String[] args) {
            // 开启5个线程,使num累计计数到10000
            // SynDemo synDemo = new SynDemo();
            for (int i = 0; i < THREAD_NUM; i++) {
    //            SynDemo synDemo = new SynDemo();
                UserThread userThread = new UserThread("thread_" + i);
                userThread.start();
            }
        }
    }
    

    如图,我们将UserThread类的构造器做一下改变,并将SynDemo对象共享出来,同时换上第三种写法:private SynDemo synDemo = new SynDemo(); synchronized(synDemo){},这时的结果为:

    从结果我们可以推断出synchronized(this){}private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}这两种写法中synchronized锁住的是类的对象:在类的对象相同的情况下,多个线程访问一段加锁( 对象锁 )的代码时,只有一个线程能拿到锁

    2.2 类锁

    我们来回到2.1中的最初代码,将synchronized (this) {}改为synchronized (SynDemo.class){}

    public void add() {
    //        synchronized (this) {
            synchronized (SynDemo.class){
                for (int i = 0; i < ADD_NUM; i++) {
                    num++;
                }
                System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
            }
    
        }
    
        public static void main(String[] args) {
            // 开启5个线程,使num累计计数到10000
            // SynDemo synDemo = new SynDemo();
            for (int i = 0; i < THREAD_NUM; i++) {
                SynDemo synDemo = new SynDemo();
                UserThread userThread = new UserThread("thread_" + i, synDemo);
                userThread.start();
            }
        }
    

    结果:

    由此可见,synchronized (SynDemo.class){}是对SynDemo整个类进行加锁,所以即便每个线程传入的synDemo对象不同,但在运行加锁代码块时,都要去抢夺锁,所以num变量每次打印的计数值都是符合我们心里的预期的。

    讲到这里,肯定会有人好奇:synchronized另外两种用法锁住的是对象还是呢?让我们修改一下代码看看:

     public synchronized void add() {
    //        synchronized (this) {
    //        synchronized (SynDemo.class){
                for (int i = 0; i < ADD_NUM; i++) {
                    num++;
                }
                System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
    //        }
    
        }
    
        public static void main(String[] args) {
            // 开启5个线程,使num累计计数到10000
            // SynDemo synDemo = new SynDemo();
            for (int i = 0; i < THREAD_NUM; i++) {
                SynDemo synDemo = new SynDemo();
                UserThread userThread = new UserThread("thread_" + i, synDemo);
                userThread.start();
            }
        }
    

    结果:

    public static synchronized void add() {
    //        synchronized (this) {
    //        synchronized (SynDemo.class){
                for (int i = 0; i < ADD_NUM; i++) {
                    num++;
                }
                System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
    //        }
    
        }
    

    结果:

    结论

    由上面的各种代码的运行结果,我们可以得出以下结论

    • public synchronized void add(){}synchronized(this){}private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}这三种写法中的synchronized锁住的都是对象,即对象锁
    • public static synchronized void add(){}synchronized(SynDemo.class){}这两种写法中的synchonized锁住的都是类,即类锁
    • 建议: 在日常工作或学习中,使用代码块加锁的方式。

    Lock 显示锁


    synchronized不同,LockJDK1.5为我们提供的一个api,所以它与synchronized一明一暗,被称为显示锁

    Lock作为一个接口,有着多个实现类:ReadLockReentrantLockWriteLock ......

    而我们今天的主角便是:ReentrantLock,先来看看如何使用:

    private static Lock lock = new ReentrantLock();
    
    public void add() {
        // 拿到锁
        lock.lock();
        try {
            for (int i = 0; i < ADD_NUM; i++) {
                num++;
            }
            System.out.println(Thread.currentThread().getName() 
                               + " 运行完之后的结果为:" + num);
        } finally {
            // 释放锁
            lock.unlock();
    
        }
    }
    

    synchronized不同,lock并没有类锁和对象锁的分类,它的用法也是非常的简单,lock()方法是当前线程拿到锁,unlock()方法是当前线程释放锁。是的,locksynchronized最大的不同就是lock需要线程自己去释放锁,而synchronizedJVM帮我们释放锁。如果当前拿到锁的线程不及时的调用unlock()方法时,程序将不会终止,所有的线程都会卡在方法外。

    我们先来看看上面代码的运行结果:

    我们再将unlock()方法注释掉看看:

    private static Lock lock = new ReentrantLock();
    
        public void add() {
            // 拿到锁
            lock.lock();
            try {
                for (int i = 0; i < ADD_NUM; i++) {
                    num++;
                }
                System.out.println(Thread.currentThread().getName() 
                                   + " 运行完之后的结果为:" + num);
            } finally {
                // 释放锁
    //            lock.unlock();
    
            }
        }
    

    结果:

    正如上面所说的那般,程序不会终止,仅有一个线程打印了结果。

    结论

    • 使用lock锁时,必须在try{}代码块之前调用lock()方法,并在finally{}代码块中调用unlock()方法及时的释放锁。

    最后


    synchronizedLock不论在本质还是用法上面都有很多的不同,不是一两句就能讲清楚的,在以后的随笔中,我会逐步的去分享Lock中的各种方法,会将sychronizedLock的不同做个最后的总结,这也是非常重要的一个知识点。

  • 相关阅读:
    算法导论 第一章
    20155312 2016-2017-2 《Java程序设计》第七周学习总结
    Visual Studio 2005 搭建Windows CE 6.0环境之准备
    C#在winform中调用系统控制台输出
    C# 目录(文件夹)复制实现
    关于加强数据库安全的一些实践
    运维小白部署网站踩坑全过程
    jQuery学习之二 jQuery选择器
    运维系列之二 Linux文件种类和扩展名
    运维系列之一 Linux的文件与目录权限解析
  • 原文地址:https://www.cnblogs.com/caimm/p/13410266.html
Copyright © 2011-2022 走看看