1、synchronized
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
1.1 原子性
原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。
1.2 可见性
可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。
一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized
块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized
块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile
变量上。
——volatile只保证可见性,不保证原子性!
1.3 何时要同步?
可见性同步的基本规则是在以下情况中必须同步:
- 读取上一次可能是由另一个线程写入的变量
- 写入下一次可能由另一个线程读取的变量
一致性同步:当修改多个相关值时,您想要其它线程原子地看到这组更改—— 要么看到全部更改,要么什么也看不到。
这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。
在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:
- 由静态初始化器(在静态字段上或 static{} 块中的初始化器)
- 初始化数据时
- 访问 final 字段时 ——final对象呢?
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
1.4 synchronize的限制
synchronized是不错,但它并不完美。它有一些功能性的限制:
- 它无法中断一个正在等候获得锁的线程;
- 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
- 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。
2、ReentrantLock
java.util.concurrent.lock
中的Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
- class Outputter1 {
- private Lock lock = new ReentrantLock();// 锁对象
- public void output(String name) {
- lock.lock(); // 得到锁
- try {
- for(int i = 0; i < name.length(); i++) {
- System.out.print(name.charAt(i));
- }
- } finally {
- lock.unlock();// 释放锁
- }
- }
- }
区别:
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!
3、读写锁ReadWriteLock
上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
- class syncData {
- private int data;// 共享数据
- public synchronized void set(int data) {
- System.out.println(Thread.currentThread().getName() + "准备写入数据");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.data = data;
- System.out.println(Thread.currentThread().getName() + "写入" + this.data);
- }
- public synchronized void get() {
- System.out.println(Thread.currentThread().getName() + "准备读取数据");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "读取" + this.data);
- }
- }
然后写个测试类来用多个线程分别读写这个共享数据:
- public static void main(String[] args) {
- // final Data data = new Data();
- final syncData data = new syncData();
- // final RwLockData data = new RwLockData();
- //写入
- for (int i = 0; i < 3; i++) {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int j = 0; j < 5; j++) {
- data.set(new Random().nextInt(30));
- }
- }
- });
- t.setName("Thread-W" + i);
- t.start();
- }
- //读取
- for (int i = 0; i < 3; i++) {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int j = 0; j < 5; j++) {
- data.get();
- }
- }
- });
- t.setName("Thread-R" + i);
- t.start();
- }
- }
运行结果:
- Thread-W0准备写入数据
- Thread-W0写入0
- Thread-W0准备写入数据
- Thread-W0写入1
- Thread-R1准备读取数据
- Thread-R1读取1
- Thread-R1准备读取数据
- Thread-R1读取1
- Thread-R1准备读取数据
- Thread-R1读取1
- Thread-R1准备读取数据
- Thread-R1读取1
- Thread-R1准备读取数据
- Thread-R1读取1
- Thread-R2准备读取数据
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R2读取1
- Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!
- Thread-R0读取1
- Thread-R0准备读取数据
- Thread-R0读取1
- Thread-R0准备读取数据
- Thread-R0读取1
- Thread-R0准备读取数据
- Thread-R0读取1
- Thread-R0准备读取数据
- Thread-R0读取1
- Thread-W1准备写入数据
- Thread-W1写入18
- Thread-W1准备写入数据
- Thread-W1写入16
- Thread-W1准备写入数据
- Thread-W1写入19
- Thread-W1准备写入数据
- Thread-W1写入21
- Thread-W1准备写入数据
- Thread-W1写入4
- Thread-W2准备写入数据
- Thread-W2写入10
- Thread-W2准备写入数据
- Thread-W2写入4
- Thread-W2准备写入数据
- Thread-W2写入1
- Thread-W2准备写入数据
- Thread-W2写入14
- Thread-W2准备写入数据
- Thread-W2写入2
- Thread-W0准备写入数据
- Thread-W0写入4
- Thread-W0准备写入数据
- Thread-W0写入20
- Thread-W0准备写入数据
- Thread-W0写入29
现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!
我们可以用读写锁ReadWriteLock实现:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
- class Data {
- private int data;// 共享数据
- private ReadWriteLock rwl = new ReentrantReadWriteLock();
- public void set(int data) {
- rwl.writeLock().lock();// 取到写锁
- try {
- System.out.println(Thread.currentThread().getName() + "准备写入数据");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.data = data;
- System.out.println(Thread.currentThread().getName() + "写入" + this.data);
- } finally {
- rwl.writeLock().unlock();// 释放写锁
- }
- }
- public void get() {
- rwl.readLock().lock();// 取到读锁
- try {
- System.out.println(Thread.currentThread().getName() + "准备读取数据");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "读取" + this.data);
- } finally {
- rwl.readLock().unlock();// 释放读锁
- }
- }
- }
测试结果:
- Thread-W1准备写入数据
- Thread-W1写入9
- Thread-W1准备写入数据
- Thread-W1写入24
- Thread-W1准备写入数据
- Thread-W1写入12
- Thread-W0准备写入数据
- Thread-W0写入22
- Thread-W0准备写入数据
- Thread-W0写入15
- Thread-W0准备写入数据
- Thread-W0写入6
- Thread-W0准备写入数据
- Thread-W0写入13
- Thread-W0准备写入数据
- Thread-W0写入0
- Thread-W2准备写入数据
- Thread-W2写入23
- Thread-W2准备写入数据
- Thread-W2写入24
- Thread-W2准备写入数据
- Thread-W2写入24
- Thread-W2准备写入数据
- Thread-W2写入17
- Thread-W2准备写入数据
- Thread-W2写入11
- Thread-R2准备读取数据
- Thread-R1准备读取数据
- Thread-R0准备读取数据
- Thread-R0读取11
- Thread-R1读取11
- Thread-R2读取11
- Thread-W1准备写入数据
- Thread-W1写入18
- Thread-W1准备写入数据
- Thread-W1写入1
- Thread-R0准备读取数据
- Thread-R2准备读取数据
- Thread-R1准备读取数据
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R1读取1
- Thread-R0读取1
- Thread-R1准备读取数据
- Thread-R0准备读取数据
- Thread-R0读取1
- Thread-R2读取1
- Thread-R2准备读取数据
- Thread-R1读取1
- Thread-R0准备读取数据
- Thread-R1准备读取数据
- Thread-R0读取1
- Thread-R2读取1
- Thread-R1读取1
- Thread-R0准备读取数据
- Thread-R1准备读取数据
- Thread-R2准备读取数据
- Thread-R1读取1
- Thread-R2读取1
- Thread-R0读取1
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。
4、线程间通信Condition
Condition可以替代传统的线程间通信,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!
传统线程的通信方式,Condition都可以实现。
注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Condition的强大之处在于它可以为多个线程间建立不同的Condition
看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition
实例来做到这一点。
——其实就是java.util.concurrent.ArrayBlockingQueue的功能
- class BoundedBuffer {
- final Lock lock = new ReentrantLock(); //锁对象
- final Condition notFull = lock.newCondition(); //写线程锁
- final Condition notEmpty = lock.newCondition(); //读线程锁
- final Object[] items = new Object[100];//缓存队列
- int putptr; //写索引
- int takeptr; //读索引
- int count; //队列中数据数目
- //写
- public void put(Object x) throws InterruptedException {
- lock.lock(); //锁定
- try {
- // 如果队列满,则阻塞<写线程>
- while (count == items.length) {
- notFull.await();
- }
- // 写入队列,并更新写索引
- items[putptr] = x;
- if (++putptr == items.length) putptr = 0;
- ++count;
- // 唤醒<读线程>
- notEmpty.signal();
- } finally {
- lock.unlock();//解除锁定
- }
- }
- //读
- public Object take() throws InterruptedException {
- lock.lock(); //锁定
- try {
- // 如果队列空,则阻塞<读线程>
- while (count == 0) {
- notEmpty.await();
- }
- //读取队列,并更新读索引
- Object x = items[takeptr];
- if (++takeptr == items.length) takeptr = 0;
- --count;
- // 唤醒<写线程>
- notFull.signal();
- return x;
- } finally {
- lock.unlock();//解除锁定
- }
- }
优点:
假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。
那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。
【Java线程】锁机制:synchronized、Lock、Condition(转)的更多相关文章
-
Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁
(1)synchronized 是互斥锁: (2)ReentrantLock 顾名思义 :可重入锁 (3)ReadWriteLock :读写锁 读写锁特点: a)多个读者可以同时进行读b)写者必须互斥 ...
-
Java线程锁一个简单Lock
/** * @author * * Lock 是java.util.concurrent.locks下提供的java线程锁,作用跟synchronized类似, * 单是比它更加面向对象,两个线程执行 ...
-
[置顶] 深入探析Java线程锁机制
今天在iteye上提了一个关于++操作和线程安全的问题,一位朋友的回答一言点醒梦中人,至此我对Java线程锁有了更加深刻的认识.在这里也做个总结供大家参考. 先看几段代码吧! 代码一: public ...
-
Java线程锁,synchronized、wait、notify详解
(原) JAVA多线程这一块有点绕,特别是对于锁,对锁机制理解不清的话,程序出现了问题也很难找到原因,在此记录一下线程的执行以及各种锁. 1.JAVA中,每个对象有且只有一把锁(lock),也叫监视器 ...
-
java的锁机制——synchronized
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...
-
java线程锁之synchronized
声明:该博客参考https://www.cnblogs.com/kaituorensheng/p/10079916.html,感谢哥们. 1.Sync.java package com.cn.comm ...
-
JAVA线程锁-读写锁
JAVA线程锁,除Lock的传统锁,又有两种特殊锁,叫读写锁ReadWriteLock 其中多个读锁不互斥,读锁和写锁互斥,写锁和写锁互斥 例子: /** * java线程锁分为读写锁 ReadWri ...
-
工作常用4种Java线程锁的特点,性能比较、使用场景
多线程的缘由 在出现了进程之后,操作系统的性能得到了大大的提升.虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对实时性有了要求. 使用多线程的理由之一是和进程相比,它是一种非常花 ...
-
Java的锁机制--synchronsized关键字
引言 高并发环境下,多线程可能需要同时访问一个资源,并交替执行非原子性的操作,很容易出现最终结果与期望值相违背的情况,或者直接引发程序错误. 举个简单示例,存在一个初始静态变量count=0,两个线程 ...
-
Java线程锁&;分布式锁的理解及应用
了解Java线程锁之前,先理解线程和进程的定义.进程是操作系统分配资源(CPU)的基本单位,线程是CPU执行的基本单位,一个进程可拥有多个线程,同进程间的多个线程共享分配给进程的资源.比如启动JVM时 ...
随机推荐
-
HTML5와 CSS3 적용기
HTML5의 DTD 선언 <!DOCTYPE html> HTML5의 인코딩 선언 <meta charset="utf-8"> 그리고나서는 새로 ...
-
网络配置之基本网络配置(cenos6)
目录: 关于IP的管理 Linux网卡的卸载与装载 配置网络接口 网络IP配置文件路由管理 路由管理命令 配置动态路由(简介) route的配置文件netstat命令IP命令 ip link 查看网络 ...
-
BZOJ 1927: [Sdoi2010]星际竞速 [上下界费用流]
1927: [Sdoi2010]星际竞速 题意:一个带权DAG,每个点恰好经过一次,每个点有曲速移动到他的代价,求最小花费 不动脑子直接上上下界费用流过了... s到点连边边权为曲速的代价,一个曲速移 ...
-
回顾4180天在腾讯使用C#的历程,开启新的征途
今天是2018年8月8日,已经和腾讯解除劳动关系,我的公司正式开始运营,虽然还有很多事情需要理清,公司官网也没有做,接下来什么事情都需要自己去完成了,需要一步一个脚印去完善,开启一个新的征途,我将在博 ...
-
MySQL 还原
## sql 还原:mysql -default-character-set=utf8 -h127.0.0.1 -uroot -pxxxxxx test2 < /data/test/db/201 ...
-
Python基础教程2上的一处打印缺陷导致的代码不完整#1
#1对代码的完善的 出现打印代码处缺陷截图: 图片上可以看到,定义的request根本没有定义它就有了.这个是未定义的,会报错的,这本书印刷问题,这个就是个坑,我也是才发现.花了点时间脱坑. 现在发完 ...
-
BZOJ2150 部落战争 【带上下界最小流】
题目链接 BZOJ2150 题解 复习: 带上下界网络流两种写法: 不建\(T->S\)的\(INF\)的边,即不考虑源汇点,先求出此时超级源汇的最大流,即无源汇下最大的自我调整,再加入该边,求 ...
-
NO.1: 视C++为一个语言联邦
C++由4个部分组成: 1.C part of C++; 2.Object-Oriented C++; 3.Template C++; 4.STL 请记住:C++的高效编程视状况而变化,取决你使用C+ ...
-
SpringBoot入门 (三) 日志配置
上一篇博文记录了再springboot项目中读取属性文件中配置的属性,本文学习在springboot项目中记录日志. 日志记录在项目中是很常见的一个功能了,对排查问题有很大帮助,也可以做分类分析及统计 ...
-
Python之美[从菜鸟到高手]--2+2=5
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/yueguanghaidao/article/details/35644165 今天在伯乐在线 ...