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


Synchronized

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的三种使用方式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
}
}

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

1
2
3
public static synchronized void add(){
i++;
}


重入锁

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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的功能。并且呢,我们可以看到使用冲入锁是显示的指定什么时候加锁什么时候释放的,这样对于一些流程控制就会更加的有优势。

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

1
2
3
4
5
6
7
8
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了。
来看代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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。

请看代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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提供了读写分离锁来实现读写分离的功能。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

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

  1. 浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

    公平锁和非公平锁 这里主要体现在ReentrantLock这个类里面了 公平锁.非公平锁的创建方式: //创建一个非公平锁,默认是非公平锁 Lock lock = new ReentrantLock( ...

  2. 浅谈Java中的equals和==(转)

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...

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

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  4. 浅谈Java中的equals和==

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...

  5. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

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

    转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...

  7. 【转】浅谈Java中的hashcode方法(这个demo可以多看看)

    浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native i ...

  8. 浅谈Java中的final关键字

    浅谈Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...

  9. 【转】浅谈Java中的hashcode方法

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个 ...

随机推荐

  1. 关联管理器(RelatedManager)

    一.class RelatedManager "关联管理器"是在一对多或者多对多的关联上下文中使用的管理器.它存在于下面两种情况: 1.一对多 ForeignKey关系的“另一边” ...

  2. 马拉车算法——求回文子串个数zoj4110

    zoj的测评姬好能卡时间.. 求回文子串的个数:只要把p[i]/2就行了: 如果s_new[i]是‘#’,算的是没有中心的偶回文串 反之是奇回文串 /* 给定两个字符串s,t 结论:s,t不相同的第一 ...

  3. 马拉车算法——边界拓展时加限制hdu4513

    #include<bits/stdc++.h> using namespace std; #define maxn 500005 int n,p[maxn],s[maxn],s_new[m ...

  4. NLP自然语言处理原理及名词介绍

    1. 自然语言概念 自然语言,即我们人类日常所使用的语言,是人类交际的重要方式,也是人类区别其他动物的本质特征. 但是我们只能通过自然语言与人交流,无法与计算机进行交流. 2. 自然语言处理 自然语言 ...

  5. postgre 常用语法,如 group_concat用法

    1.查询postgre的表所有字段列 select table_name, column_name from information_schema.columns where table_schema ...

  6. C# Common Log function

    public int Log(string info) { info = "-----------------------------" + DateTime.Now.ToStri ...

  7. Docker 学习4 Docker容器虚拟化网络概述

    一.docker 虚拟化网络概述 1.OVS: OpenVSwitch,不仅能模拟二层网络,还能模拟三层网络,或者VLAN,VXLAN,流控 SDN软件定义网络技术等. 2.overlay netwo ...

  8. python打包工具 cx_Freeze介绍

    原理 Python 脚本在装有 Python 的系统中可以直接双击运行,但绝大多数普通用户并没有配置此类环境,而编译为可执行二进制文件后,用户无需预先安装 Python 及依赖库即可像运行普通程序一样 ...

  9. 计蒜客 买书 dfs

    题目: https://www.jisuanke.com/course/2291/182236 思路: 递归解决,从第一本书开始,每本书都有两种选择: //index是book里面每本书价格的下标, ...

  10. Git ignore文件的用法

    这周为了往自己个人代码仓库里囤货,把在公司写的一些东西上传到了自己的GitHub代码仓库,手抖把测试用的日志也一并上传了.上传没多长时间就被运维找上门了,说commit里包含内网相关信息,要求删除.当 ...