synchronize与lock
1. synchronize的作用
synchronize是java最原始的同步关键字,通过对方法或者代码块进行加锁实现对临界区域的保护.线程每次进去同步方法或者代码块都需要申请锁,如果锁被占用则会等待锁的释放,值得注意的是,等待锁的线程不会响应中断.synchronize的锁分为对象所和类锁,当synchronize修饰静态方法或者synchronize(Object.class)这样写时是类锁,当synchronize修饰普通方法或者synchronize(this)这样写时是对象锁(this可以替换成其他对象的引用).synchronize是官方推荐使用的同步工具,synchronize主要是在JVM层面实现的同步,官方已经对synchronize的性能进行了多次优化,有兴趣可以自行百度.
2. synchronize的使用
package main; public class Service implements Runnable { @Override
public void run() { synchronized (this) {
System.out.println(Thread.currentThread().getName() + "进入了代码块");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "准备退出代码块");
}
} public static void main(String[] args) {
Service service=new Service();
new Thread(service).start();
new Thread(service).start();
}
}
输出结果:
Thread-0进入了代码块
Thread-0准备退出代码块
Thread-1进入了代码块
Thread-1准备退出代码块
synchronize的基本使用如上,其在使用和理解上非常容易理解这里不做多余解释.我们在使用synchronize要注意同步代码范围不能太大,并且耗时操作最好不要在同步中进行,这样会极大程度的影响程序效率.
3. synchronize锁机制
https://blog.csdn.net/chenssy/article/details/54883355
https://tech.meituan.com/2018/11/15/java-lock.html
4. Lock的作用
Lock同样是实现同步的工具,但是他的实现与synchronize有本质上的差别,synchronize基于JVM的同步,Lock是一个接口,他是基于AQS的实现,底层是使用CAS和volatile变量结合实现.同样在进入临界区之前需要申请锁,退出临界区域需要手动释放锁,Lock主要实现类是ReetrantLock.
5. Lock的使用
public class Service implements Runnable { private Lock lock=new ReentrantLock(); @Override
public void run() {
lock.lock();
System.out.println("进入临界资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("准备提出临界资源");
lock.unlock();
} public static void main(String[] args) {
Service service=new Service();
new Thread(service).start();
new Thread(service).start();
}
}
输出结果
进入临界资源
准备提出临界资源
进入临界资源
准备提出临界资源
以上代码使用的是ReetrantLock,他默认是使用非公平锁,要使用公平锁就给构造函数传一个true.上面的代码先调用lock方法获取锁,如果获取到锁则进入到临界资源区没有获取到则阻塞,操作完后调用unlock方法释放锁并唤醒在锁上等待的线程.
Lock中获取锁的主要方法有lock(),lockInterruptibly(),tryLock().
lock()方法不会响应中断,lockInterruptibly()会响应中断,tryLock()方法是尝试获取锁,如果没获取到则返回false,否则true.
6. 公平锁与非公平锁在ReetrantLock的实现
在ReentrantLock类中有一个Sync内部类,他继承自AbstractQueuedSynchronizer(即AQS,介绍看这里).Sync子类就是公平锁(FairSync)和非公平锁(NonfairSync),这两个类也是ReentrantLock的内部类.
首先来看非公平锁,非公平锁是指后来线程具有极大的概率获得锁,来看看他的代码实现.
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L; final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());//将当前线程设置为锁的拥有者
else
acquire(1);
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
lock()就是ReentrantLock类中的lock()方法具体调用的方法,compareAndSetState方法是一个CAS操作,它是AQS中的方法,它的功能是判断锁的状态是否为0,如果为0则为1返回true,否则返回false.
acquire()方法是AQS的方法,他的功能是尝试获取锁,下面是该方法的代码
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //将当前线程放入等待队列
selfInterrupt(); //中断自己
}
该方法首先会调用tryAcquire()方法,这个方法就是NonfairSync类中的tryAcquire().下面是nonfairTryAcquire方法代码
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //如果锁未被占用
if (compareAndSetState(0, acquires)) { //CAS操作获取锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果锁被占用且申请锁的是锁的拥有线程
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//改变锁状态值
return true;
}
return false;
}
从上面代码逻辑来看,非公平锁是具有可重入性,如果获取锁失败就返回false,否则返回true.我们在返回来看acquire()方法中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这一句,addWaiter(Node.EXCLUSIVE)方法是将当前线程放入等待队列中,acquireQueued()方法再次尝试获取锁,如果再次获取失败,则将当前线程阻塞,代码如下
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取等待队列前一个节点
if (p == head && tryAcquire(arg)) { //如果前一个节点是头结点则再次获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;//返回中断标记
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);//取消当前线程
}
}
其中shouldParkAfterFailedAcquire(p, node)这个方法,是在再次获取锁失败之后的处理,由于等待队列中可能会有线程被取消,所以当前线程要去寻找自己前面的节点,直到找到一个没有被取消的线程为止,这样能够保证自己能够被唤醒.
parkAndCheckInterrupt()是将当前线程阻塞的方法,他调用了Unsafe类的本地方法.
以上就是非公平锁获取锁的过程,值得注意的是,在线程阻塞阶段,是不会响应中断的,代码中的响应中断是线程被唤醒之后才响应,响应手段是通过执行selfInterrupt()方法,该方法就是调用了Thread类的interrupt()方法.也就是说,只有会响应中断的方法才会被中断,以第4节的代码为例,线程被中断的话,依然会输出两句话,只是线程不会睡眠而已.
接下来是锁资源的释放,AQS中已经实现好了锁资源释放方法release(),但是tryRelease()方法没有实现,一下是ReetrantLock类的实现
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //是否为当前线程
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //如果状态变为0即锁变为未被拥有
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
接下来是release()方法
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放资源
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒线程
return true;
}
return false;
}
unparkSuccessor()代码如下
private void unparkSuccessor(Node node) { int ws = node.waitStatus;
if (ws < 0) //如果线程状态为小于0,表示有效状态,大于0表示取消状态
compareAndSetWaitStatus(node, ws, 0); Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒线程
}
这里会唤醒后继节点,如果后继节点为null或者被取消,则从队尾开始向前回溯,为什么从队尾开始?博主也无法理解.
以上便是非公平锁的实现原理,非公平锁在获取锁时如果有新线程进来,那么新线程有很大可能性会获取到锁资源,因为等待队列中的线程被唤醒到重新请求锁会消耗相当大的时间,公平锁就能够解决这个问题.
公平锁与非公平锁的唯一区别在于,公平锁的tryAcquire()方法与非公平锁不同,看代码
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //唯一不同点
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
hasQueuedPredecessors()方法是用于判断等待队列是否存在,如果等待队列中有节点,那么等待队列肯定存在,那么线程就不能直接获取锁资源,必须去排队,以下是源码
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
以上是公平锁与非公平锁的实现.
7. condition
我们现在有个问题是:一个线程要进行下去,就必须一个条件满足.我们这里有两种有两种实现.
1:无限循环,一直循环访问条件,这样显然是极大程度浪费CPU资源
2:使用wait()方法,当条件满足时被其他线程唤醒,这个方法是常用方法,但是这种方法依赖于synchronize
condition就是用来解决上面这个问题的.看代码
public class ServiceCondition implements Runnable { private static Lock lock = new ReentrantLock();
private static boolean flag = false;
private static Condition condition = lock.newCondition(); @Override
public void run() {
try {
lock.lock();
while (!flag) {
System.out.println("条件为假,等待");
condition.await();
}
System.out.println("条件为真,执行");
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
ServiceCondition serviceCondition=new ServiceCondition();
new Thread(serviceCondition).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
flag=true;
condition.signal();
lock.unlock();
}
}
condition.await()相当于wait()方法,singal()相当于notify()方法.必须获取到锁才能调用这两个方法,原因是调用await()方法时,会释放锁资源,要释放必须先要获得;调用signal()方法时会判断锁的拥有者是否是当前线程,如果是才会允许调用,这两个方法在未获取到锁时调用会抛出IllegalMonitorStateException异常.
8. 可重入性
synchronize具有可重入性,当一个线程获取到锁时,锁会将当前线程设置为拥有线程,并且状态值加1表示该锁被获取了1次,当该线程再次获取同一个锁对象时,锁会判断线程是否为拥有线程,如果是则允许获取,并且状态加1,否则拒绝获取,释放时必须一层一层释放资源,直到状态值为0,表示该锁被完全释放.
Lock与synchronize同理,我们从上面的代码就可以看出来,Lock在获取锁资源时都会判断是否为锁拥有线程.
9. 内存可见性
内存可见性涉及到java内存模型,建议不了解的朋友看一下我的另一篇博文:理解JVM之java内存模型。
当线程进入synchronize代码块时,会将共享变量全部置为失效,后续在操作共享变量时会从主存中获取最新的数据;当退出代码块时,线程会把共享变量的最新值写回主存,保证了内存可见性。synchronize的内存可见性直接由JVM提供支持,但是Lock只是一个实现了同步锁的工具类,他是如何实现内存可见性的呢?
我们回过头看一下上面的tryAcquire()方法,其中在第二行 int c = getState(); 获取state变量的值,这个值是AQS中表示线程当前同步状态的变量,该值是一个volatile变量。在后续的获取锁、重入锁时都是在操作这个变量,而在java内存模型中的规则对于变量读/写有这样的规定:
1.读普通变量时,如果跟随在读volatile变量之后,将会从主存刷新到工作区,读到最新值
2.普通变量随同volatile变量,在同一个线程中的赋值,将跟随volatile变量被刷新到主存
我们在进入调用lock(),unlock()的时候其实对volatile变量进行了操作,所以锁住的代码块同样具有线程可见性。
10. 是否响应中断
当使用synchronize进行同步时,如果线程未获取到锁将会挂起自己等待唤醒,线程再被唤醒之前不会响应中断,也就是说即使调用Thread.Interrupt()方法也不会使挂起的线程中断。但是Lock就可以响应中断,我们可以回过头看一下 public final void acquire(int arg); 方法,在 acquireQueued() 方法中会进行中断标志位判断,如果中断则会中断自己。
可以看出来,synchronize使用比起Lock更加方便,但是不够灵活,一旦未获取到锁就只能够进入挂起等待唤醒;而Lock灵活,但是不够方便,需要手动进行加锁、解锁等操作。
synchronize与lock的更多相关文章
- 多线程---再次认识volatile,Synchronize,lock
在多线程中我们常用的保证共享变量的方法有很多,现在我们介绍其中的一种,volatile,也是效率最高的一种. 一 .volatile的意义: 为了确保共享变量能被正确和一 ...
- synchronize、Lock、ReenTrantLock 的区别
synchronize 和Lock: 1.synchronize 系java 内置关键字:而Lock 是一个类 2.synchronize 可以作用于变量.方法.代码块:而Lock 是显式地指定开始和 ...
- Synchronize 和 Lock 的区别与用法
一.synchronized和lock的用法区别 (1)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要 ...
- 深入研究 Java Synchronize 和 Lock 的区别与用法
在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方. ...
- 关于synchronize与lock的区别
参考文献:https://www.cnblogs.com/cloudblogs/p/6440160.html 一.synchronize修饰不同代码都是锁住了什么? 大家都知道synchronize可 ...
- Synchronize,Lock, ReentrantLock,ReentrantReadWriteLock
下面ReentrantLock是Lock接口的具体实现 如果想在线程等待时(即等待获得锁时)可以被中断,用lockInterruptibly:注意:在线程已经获得锁正在执行时,用Synchronize ...
- synchronize和lock的区别 & synchionzie与volatile的区别
synchronized与Lock的区别 https://www.cnblogs.com/iyyy/p/7993788.html Lock和synchronized和volatile的区别和使用 ht ...
- java的锁机制,synchronize与Lock比较
参考:https://blog.csdn.net/dahongwudi/article/details/78201082
- 解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度-美团)
还有其他的锁,如果想要了解,参考:JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁, 用synchronized实现ReentrantLock 美团面试题参考:使用synchronized ...
随机推荐
- Spring cloud微服务安全实战-4-3常见的微服务安全整体架构
整体架构 这个图适合中小公司.麻雀虽小 五脏俱全.微服务架构所需要做的事在这个图里基本都有了. 绿色的不讲,主要讲的是这三块(橘黄色的).后面的和运维相关,会讲,不会讲的太深 订单服务 首先来写一个订 ...
- 【428】Dijkstra 算法
算法思想:(单源最短路径) 1个点到所有其他点的最短路径 查找顶点到其他顶点的最短路径,无法到达的记为+∞,找到最小的,就找到了最短路径的顶点 查看上一轮找到的最小点到达其他点的最小值,找到最短路径的 ...
- DB2中ALTER TABLE的使用
今天在看DB2存储过程的时候发现了如下语句能够清空表: ... SET EX_SQL='ALTER TABLE TEST_TABLE ACTIVATE NOT LOGGED INITIALLY WIT ...
- docker中的fastdfs
准备环节)(本文遗漏当初出现的一个问题由于是docker装的fastdfs所以tracker storage client,nginx,nginx module都在同一个容器中只需要修改配置 特别注意 ...
- 解决javascript - node and Error: EMFILE, too many open files
For some days I have searched for a working solution to an error Error: EMFILE, too many open files ...
- AtCoder Beginner Contest 147 E. Balanced Path
思路: dp,使用了bitset优化. 实现: #include <bits/stdc++.h> using namespace std; ; const int INF = 0x3f3f ...
- C++标准模板库STL算法与自适应容器(栈和队列)
参考<21天学通C++>第23与第24章节,对STL算法与自适应容器进行介绍. 实际上在前面的STL顺序容器.关联容器进行介绍时或多或少引用到了一些STL算法中的模板函数.而自适应容器是在 ...
- VC.DNS解析(winsock)
1.尝试了 gethostbyname(...) 和 Qt598中的qhostinfo::fromname(...),都可以解析出来IP,但是有一个问题:域名指向的IP改变了,这两种方式需要等较长时间 ...
- 页面数据加载完成时,显示loading页面.数据加载完,loading隐藏.
一,引入三个文件 jQuery版本使用 jQuery v1.7.1 jquery-easyui文件中,引入easyui-lang-zh_CN.js的js 做数据加载时使用jquery.blockui. ...
- python爬虫-豆瓣电影的尝试
一.背景介绍 1. 使用工具 Pycharm 2. 安装的第三方库 requests.BeautifulSoup 2.1 如何安装第三方库 File => Settings => Proj ...