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. 树链剖分——NOI2015

    8说了上代码 给定一棵树,两种操作 a x:x->root路径上的点权值置1 b x: 把x的子树所有结点权值置0 树上的区间更新操作,显然是要维护dfs 第一个操作暴力显然是t,用树剖把复杂度 ...

  2. 网络流24题——数字梯形问题 luogu 4013

    题目描述:这里 极其裸的一道费用流问题 首先分析第一问,由于要求一个点只能经过一次,所以我们将梯形中的每一个点拆成两个点(记为入点和出点,顾名思义,入点用来承接上一行向这一行的边,出点用来向下一行连边 ...

  3. 探索Java9 模块系统和反应流

    Java9 新特性 ,Java 模块化,Java 反应流 Reactive,Jigsaw 模块系统 Java平台模块系统(JPMS)是Java9中的特性,它是Jigsaw项目的产物.简而言之,它以更简 ...

  4. youtube去广告

    https://www.digitbin.com/youtube-ads-block/ 1. OGYouTube | Mod AdBlocker YouTube OGYouTube App is a ...

  5. 本地电脑通过Navicat连接阿里云的Mysql数据库

    第一步:需要设置mysql的监听地址 查看mysql的监听地址: netstat -nao 如果3306(mysql默认端口)前面是0.0.0.0,则表示端口监听没有问题,如果是127.0.0.1,则 ...

  6. tensorflow优化器-【老鱼学tensorflow】

    tensorflow中的优化器主要是各种求解方程的方法,我们知道求解非线性方程有各种方法,比如二分法.牛顿法.割线法等,类似的,tensorflow中的优化器也只是在求解方程时的各种方法. 比较常用的 ...

  7. nand flash和nor flash的区别

    NOR和NAND是现在市场上两种主要的非易失闪存技术. Intel于1988年首先开发出NOR flash技术,彻底改变了原先由EPROM和EEPROM一统天下的局面. 东芝于1989年开发出NAND ...

  8. HTML学习笔记【思维导图版】

  9. MongoDB安装之window版本的安装

    Windows 平台安装 MongoDB MongoDB 下载 MongoDB 提供了可用于 32 位和 64 位系统的预编译二进制包,你可以从MongoDB官网下载安装,MongoDB 预编译二进制 ...

  10. 10分钟了解JSON Web令牌(JWT)

    JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案.虫虫今天给大家介绍JWT的原理和用法. 1.跨域身份验证 Internet服务无法与用户身份验证分开.一般过程如下. 1.用户 ...