浅谈Java中的锁:Synchronized、重入锁、读写锁

Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制


Synchronized

首先我们来看一段简单的代码:

12345678910111213141516171819
public class NotSyncDemo {    public static int i=0;    static class ThreadDemo extends Thread {        @Override        public void run() {           for (int j=0;j<10000;j++){               i++;           }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1=new ThreadDemo();        ThreadDemo t2=new ThreadDemo();        t1.start();t2.start();        t1.join();        t2.join();        System.out.println(i);    }}

上方的代码使用了2个线程同时对静态变量i进行++操作,理想中的结果最后输出的i的值应该是20000才对,但是如果你执行这段代码的时候你会发现最后的结果始终是一个比20000小的数。这个就是由于JMM规定线程操作变量的时候只能先从主内存读取到工作内存,操作完毕后在写到主内存。而当多个线程并发操作一个变量时很可能就会有一个线程读取到另外一个线程还没有写到主内存的值从而引起上方的现象。更多关于JMM的知识请参考此文章:Java多线程内存模型

想要避免这种多线程并发操作引起的数据异常问题一个简单的解决方案就是加锁。JDK提供的synchronize就是一个很好的选择。
synchronize的作用就是实现线程间的同步,使用它加锁的代码同一时刻只能有一个线程访问,既然是单线程访问那么就肯定不存在并发操作了。
synchronize可以有多种用法,下面给出各个用法的示例代码。


Synchronized的三种使用方式

给指定对象加锁,进入代码前需要获得对象的锁

1234567891011121314151617181920212223
public class SyncObjDemo {    public static Object obj = new Object();    public static int i = 0;    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                synchronized (obj) {                    i++;                }            }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1 = new ThreadDemo();        ThreadDemo t2 = new ThreadDemo();        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}

给方法加锁,相当于给当前实例加锁,进入代码前需要获得当前实例的锁

123456789101112131415161718192021222324
public class SyncMethodDemo {    public static int i = 0;    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                 add();            }        }        public synchronized void add(){            i++;        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo threadDemo=new ThreadDemo();        Thread t1 = new Thread(threadDemo);        Thread t2 = new Thread(threadDemo);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}

给静态方法加锁,相当于给当前类加锁,进入代码前需要获得当前类的锁。这种方式请慎用,都锁住整个类了,那效率能高哪去

123
public static synchronized void add(){            i++;        }


重入锁

在JDK6还没有优化synchronize之前还有一个锁比它表现的更为亮眼,这个锁就是重入锁。
我们来看一下一个简单的使用重入锁的案例:

12345678910111213141516171819202122232425262728
public class ReentrantLockDemo {    public static ReentrantLock lock = new ReentrantLock();    public static int i = 0;

    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                lock.lock();                 try {                     i++;                 }finally {                     lock.unlock();                 }            }        }    }

    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1 = new ThreadDemo();        ThreadDemo t2 = new ThreadDemo();        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}

上方代码使用重入锁同样实现了synchronize的功能。并且呢,我们可以看到使用冲入锁是显示的指定什么时候加锁什么时候释放的,这样对于一些流程控制就会更加的有优势。

再来看这个锁为什么叫做重入锁呢,这是因为这种锁是可以反复进入的,比如说如下操作是允许的。

12345678
lock.lock();lock.lock();try {  i++;}finally {    lock.unlock();    lock.unlock();}

不过需要注意的是如果多次加锁的话同样也要记得多次释放,否则资源是不能被其他线程使用的。

在之前的文章:多线程基本概念 中有提到过因为线程优先级而导致的饥饿问题,重入锁提供了一种公平锁的功能,可以忽略线程的优先级,让所有线程公平竞争。使用公平锁的方式只需要在重入锁的构造方法传入一个true就可以了。

1
public static ReentrantLock lock = new ReentrantLock(true);

重入锁还提供了一些高级功能,例如中断。
对于synchronize来说,如果一个线程获取资源的时候要么阻塞要么就是获取到资源,这样的情况是无法解决死锁问题的。而重入锁则可以响应中断,通过放弃资源而解决死锁问题。
使用中断的时候只需要把原先的lock.lock()改成lock.lockInterruptibly()就OK了。
来看代码示例:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
public class ReentrantLockInterruptDemo {    public static ReentrantLock lock1 = new ReentrantLock();    public static ReentrantLock lock2 = new ReentrantLock();    static class ThreadDemo extends Thread {        int i = 0;        public ThreadDemo(int i) {            this.i = i;        }

        @Override        public void run() {            try {                if (i == 1) {                    lock1.lockInterruptibly();                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    lock2.lockInterruptibly();                } else {                    lock2.lockInterruptibly();                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    lock1.lockInterruptibly();                }                System.out.println(Thread.currentThread().getName() + "完成任务");            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                if (lock1.isHeldByCurrentThread()) {                    lock1.unlock();                }                if (lock2.isHeldByCurrentThread()) {                    lock2.unlock();                }                System.out.println(Thread.currentThread().getName() + "退出");            }        }    }

    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new ThreadDemo(1),"t1");        Thread t2 = new Thread(new ThreadDemo(2),"t2");        t1.start();        t2.start();        Thread.sleep(1500);        t1.interrupt();    }}

查看上方代码我们可以看到,线程t1启动后先占有lock1,然后会在睡眠1秒之后试图占有lock2,而t2则先占有lock2,然后试图占有lock1。这个过程则势必会发生死锁。而如果再这个时候我们给t1一个中断的信号t1就会响应中断从而放弃资源,继而解决死锁问题。

除了提供中断解决死锁以外,重入锁还提供了限时等待功能来解决这个问题。
限时等待的使用方式是使用lock.tryLock(2,TimeUnit.SECONDS)
这个方法有两个参数,前面是等待时长,后面是等待时长的计时单位,如果在等待时长范围内获取到了锁就会返回true。

请看代码示例:

1234567891011121314151617181920212223242526272829303132
public class ReentrantLockTimeDemo {    public static ReentrantLock lock = new ReentrantLock();    static class ThreadDemo extends Thread {        @Override        public void run() {            try {                if (lock.tryLock(2, TimeUnit.SECONDS)) {                    try {                        System.out.println(Thread.currentThread().getName() + "获取锁成功");                        Thread.sleep(3000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                } else {                    System.out.println(Thread.currentThread().getName() + "获取锁失败");                }            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                if (lock.isHeldByCurrentThread()) {                    lock.unlock();                }            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new ThreadDemo(), "t1");        Thread t2 = new Thread(new ThreadDemo(), "t2");        t1.start();        t2.start();    }}

同样的tryLock也可以不带参数,不带参数的时候就是表示立即获取,获取不成功就直接返回false

我们知道synchronize配合wait和notify可以实现等待通知的功能,重入锁同样也提供了这种功能的实现。那就是condition。使用lock.newCondition()就可以获得一个Condition对象。

下面请看使用Condition的代码示例:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
public class ReentrantLockWaitNotifyThread {    public static ReentrantLock lock = new ReentrantLock();    public static Condition condition = lock.newCondition();    static class WaitThreadDemo extends Thread {        @Override        public void run() {            try {                System.out.println("WaitThread wait,time=" + System.currentTimeMillis());                lock.lock();                condition.await();            } catch (InterruptedException e) {                e.printStackTrace();            }finally {                lock.unlock();                System.out.println("WaitThread end,time=" + System.currentTimeMillis());            }        }    }    static class NotifyThreadDemo extends Thread {        @Override        public void run() {                lock.lock();                System.out.println("NotifyThread notify,time=" + System.currentTimeMillis());                condition.signal();                try {                    Thread.sleep(2000);                } catch (InterruptedException e) {                    e.printStackTrace();                }finally {                    lock.unlock();                    System.out.println("NotifyThread end,time=" + System.currentTimeMillis());                }            }    }

    public static void main(String[] args) {        WaitThreadDemo waitThreadDemo = new WaitThreadDemo();        NotifyThreadDemo notifyThreadDemo = new NotifyThreadDemo();        waitThreadDemo.start();        try {            Thread.sleep(100);        } catch (InterruptedException e) {            e.printStackTrace();        }        notifyThreadDemo.start();    }}


读写锁

通过上方的内容我们知道了为了解决线程安全问题,JDK提供了相当多的锁来帮助我们。但是如果多线程并发读的情况下是不会出现线程安全问题的,那么有没有一种锁可以在读的时候不控制,读写冲突的时候才会控制呢。答案是有的,JDK提供了读写分离锁来实现读写分离的功能。

这里给出使用读写锁的一个代码示例

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
public class ReadWriteLockDemo {    public static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();    public static Lock readLock = readWriteLock.readLock();    public static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {        lock.lock();        try {            System.out.println("readTime:" + System.currentTimeMillis());            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }

    public static void write(Lock lock) {        lock.lock();        try {            System.err.println("writeTime:" + System.currentTimeMillis());            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }

    static class ReadThread extends Thread {        @Override        public void run() {            read(readLock);        }    }

    static class WriteThread extends Thread {        @Override        public void run() {            write(writeLock);        }    }

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

上方代码模拟了10个线程并发读,3个线程并发写的状况,如果我们使用synchronize或者重入锁的时候我想上方最后的耗时应该是26秒多。但是如果你执行 一下上方的代码你就会发现仅仅只花费了6秒多。这就是读写锁的魅力。

本文所有源码https://github.com/shiyujun/syj-study-demo

原文地址:https://www.cnblogs.com/zhixiang-org-cn/p/10598501.html

时间: 2024-10-13 06:19:59

浅谈Java中的锁:Synchronized、重入锁、读写锁的相关文章

【转】浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初

浅谈Java中的对象和引用

浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是"对象和对象引用",很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起了解一下对象和对象引用之间的区别和联系. 1.何谓对象? 在Java中有一句比较流行的话,叫做"万物皆对象",这是Java语言设计之初的理念之一.要理解什么是对象,需要跟类一起结合起来理解.下面这段话引自<Java编程思想>中的一段原话: "按照通

浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初

浅谈Java中的hashcode方法 - 海 子

浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 一.hashCode方法的作用 对于包含容器类型的程

浅谈Java中的对象和对象引用

浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起了解一下对象和对象引用之间的区别和联系. 1.何谓对象? 在Java中有一句比较流行的话,叫做“万物皆对象”,这是Java语言设计之初的理念之一.要理解什么是对象,需要跟类一起结合起来理解.下面这段话引自<Java编程思想>中的一段原话: “按照通俗的说法,每个对象都是某个类(class)的一个实

浅谈Java中的深拷贝和浅拷贝

浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: [java] view plaincopyprint? int apples = 5; int pears = apples; int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float

浅谈Java中set.map.List的区别

就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操作数目不固定的一组数据. 所有的JAVA集合都位于 java.util包中! JAVA集合只能存放引用类型的的数据,不能存放基本数据类型. JAVA集合主要分为三种类型: Set(集) List(列表) Map(映射) Collection 接口 :Collection是最基本的集合接口,声明了适用

浅谈JAVA中的“hashcode()”方法

浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 一.hashCode方法的作用 对于包含容器类型的程

浅谈Java中的Set、List、Map的区别(1)

就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操作数目不固定的一组数据. 所有的JAVA集合都位于 java.util包中! JAVA集合只能存放引用类型的的数据,不能存放基本数据类型. JAVA集合主要分为三种类型: Set(集) List(列表) Map(映射) Collection 接口 :Collection是最基本的集合接口,声明了适用