Java多线程--实现同步的9种方法
我们通常说的保持同步,其实就是对共享资源的保护。在单线程模型中, 我们永远不用担心“多个线程试图同时使用同一个资源的问题”, 但是有了并发, 就有可能发生多个线程竞争同一个共享资源的问题。
就好比你正在餐厅里吃饭,当你拿起筷子正要夹盘子里的最后一块肉时, 这片肉突然消失了。因为你的线程被挂起了, 另一个人进入餐厅并吃掉了它。
这就是我们在多线程下需要处理的问题----我们需要某种方式来防止两个任务同时访问相同的资源
那么我们很容易想到第一种方法: 加锁, 好比我们进入卫生间之后要把门关上, 下一个人来到卫生间门口要先敲门,没人的话他就可以直接使用, 否则就要等到里面的人出来。不管你多么着急,不管外面排了多少人,没办法,只要他还在里面,那你就只能等,哪怕他在里面睡觉玩手机。。。 当他终于打开门出来的时候, 离门最近的那么人很有可能会成功进入, 但这一点并不能保证。
同理, 我们给共享资源加一把锁,任意时刻都只允许一个线程操作共享资源,当某个线程试图访问该资源时,需要先检查锁的状态,如果当前没有其他线程在使用, 则获取锁,开始操作资源,操作完成后再释放资源;否则就要排队等待。
常见的加锁方法大致有以下几种:
1, synchronized关键字修饰方法
之前在对HashMap的描述中, 我们说hsahMap是线程不安全的, 但是古老的Hashtable是线程安全的, 就是因为HashTable中对所有操作共享资源的方法都使用了synchronized关键字进行了修饰, 如下:
共享资源一般是以对象形式存在的内存片段,也有可能是文件,输入/输出端口,打印机等。但是要控制对共享资源的访问, 得先把它包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized.
注意, 这里的描述是“所有”。 为什么呢?
因为java中的对象都自动含有单一的锁(也叫监视器或者对象锁), 当某个线程在对象上调用其任意synchronized方法时,此对象会被加锁(锁住的是整个对象),所以在该线程释放锁之前, 其他线程无法访问该对象内任一修饰为synchronized的方法。(其他未被synchronized修饰的方法可以被随意访问)
一个线程可以多次获得对象的锁,JVM负责跟踪对象被加锁的次数,如果锁被释放,则重置为0, 在线程第一次给对象加锁的时候,计数为1,每当这个相同的线程在对象上获得锁时(从一个synchronized方法到另一个synchronized方法),计数+1,显然只有首先获得锁的线程才可以继续获得多个锁。每离开一个synchronized方法, 计数-1。当计数为0时,锁完全释放
注意: 当我们使用synchronized保护共享资源时, 记得声明该资源为private, 防止其他线程直接访问域
java中的类也有一个锁,作为类的class对象的一部分。所以当我们用synchronized关键字修饰一个静态方法时,获取该方法的锁也意味着整个类中的static方法都会被加锁。
2, synchronized关键字锁定代码块
上文中我们说到,古老的hashtable是线程安全的,因为它在源码中对所有操作共享资源的方法都加了锁。
但是我们在日常开发中,很少会用到它。因为在某些情况下,我们其实只需要保护方法里的核心代码,为整个方法加锁会增加多线程访问下的时间成本
大家可以回顾一下单例模式中的双重锁模式
为什么要判空两次?然后在第二次判空之后才加锁呢?
如果我们直接给getSingleTon方法加锁,当然也能实现同步。但带来的问题是, 如果singleTon已经被创建,应该直接返回就好了,但事实上每次线程执行到这里都要试图获取锁,这是不必要的开销。
所以第一层判断如果singleTon已经被创建,则无需获取锁直接返回。
第二层判断的意义是,想象一下,第一个线程来到这里,检查发现singleTon为空,那么它就会获得锁, 并创建了一个singleTon的实例。在它创建singleTon实例的过程中,另一个线程也来到了这里执行了第一层判断,发现singleTon为null(因为此时第一个线程还没有完成创建), 于是它排队等待,然后第一个线程创建完成之后释放锁,第二个线程进入同步块,此时如果没有第二层判空,那么它就会直接创建一个singleTon实例, 这样就有了两个实例
值得一提的是, 当我们使用synchronized同步代码块时, 需要传入一个类或者说对象,如上文中我们传入了SingleTon.class
因为synchronized快必须指定一个在其上进行同步的对象,通常最合理的方式是使用使用其方法正在被调用的当前对象,如
synchronized(this)
当我们传入this时, 如果某个线程获得了同步块中的锁, 那么当前对象中其他的synchronized方法和synchronized块都不能被其他线程调用(其实跟上文说到对象锁的一样)
当然, 如果需要的话,我们也可以在在一个对象的同步块中去同步另一个对象, 比如我们在A对象的某个方法中 synchronized(B), 那么当某个线程获得锁时, 它获得的是B的对象锁, 这也就意味着与此同时B中的synchronized方法/块不能被其他线程执行, 而A中的则不受影响。
3, ReentrantLock显式加锁
java.util.concurrent类库中还包含了定义在java.util.concurrent.locks中的显式互斥机制,如ReentrantLock, 它的简单用法如下:
可以看到, 我们使用lock时,必须显式地创建,锁定和释放,所以与synchronized相比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活。
注意, 我们应当使用 try-finally语句, 确保在finally中unlock。 如果该方法有返回值, return语句应放在try中, 以确保unlock不会过早发生
当我们使用synchronized关键字时, 某些事物失败了就会抛出异常, 但我们没有机会去做一些清理的工作。 显式lock的优点就体现在这里, 我们可以在finally子句中,将系统维护在正确的状态。
4,ReentrantReadWriteLock显式加锁
ReentrantReadWriteLock实现了ReadWriteLock接口,这种锁的特点是,允许同时有多个读取者,只要它们都不试图写入就行。如果写锁已经被其他线程持有, 那么任何读取者都不能访问,直到写锁被释放。
所以,针对那些频繁读取,极少写入的情况, 使用ReentrantReadWriteLock可以提高性能。
ReentrantReadWriteLock的用法大致如下:没有仔细研究过,不保证正确
5, 使用ThreadLocal进行线程本地存储
上文说到的4种方式从本质上来说其实都是一样的, 都是通过对共享资源加锁的方式来实现同步
接下来我们保护共享资源的的另一个解决思路------根除对变量的共享
线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程创建不同的存储, 通常写法如下:
注意: ThreadLocal对象通常当作静态域存储,在创建ThreadLocal时,只能通过get和set方法来访问该对象的内容。
其中value.get()方法将返回与当前线程相关联的对象的副本, 而set会将参数插入到为其线程存储的对象中, 并返回存储中原有的对象
6, 利用可见性实现同步-- volatile
volatile关键字确保了应用中的可视性,如果你将一个域声明为volatile, 那么只要对这个域产生了写操作,那么所有的读操作都可以看到修改,即使用了本地缓存,情况也是如此,因为volatile域会立即被写入到主存中,而读取操作就发生在主存中。
volatile是轻量级的synchronized, 它比 synchronized执行成本低因为它不需要切换上下文以及调度线程。
但是, volatile只适用于静态域,就是说只有一个线程对共享资源进行写操作,可以有多个线程执行读操作。当一个域的值依赖于它之前的值(如递增一个计数器)或者这个域的值受其他域的值限制时,volatile将无法工作。
同时volatile关键字并不保证原子性
7, 使用原子性操作(原子类)来保证同步
在java中,原子的意思就是不可再分,比如 return i 我们可以认为它是原子性的, 但 i++并不是原子性的
原子操作可有线程机制来保证其不可中断。一旦操作开始, 那么它一定可以在可能发生的线程切换之前被执行完毕。
因此,如果我们确定某个操作时原子性的, 那它就是线程同步的。
所以,在某些情况下,我们可以使用原子类来保证同步。如AtomicInteger, AtomicLong, AtomicReference等
这些类是在机器级别上的原子性,因此使用他们的时候,通常不用担心同步问题。
8, 使用SingleThreadExecutor
在上一篇文章--启动线程 中我们提到过,SingleThreadExecutor的调用会产生单线程执行器, 当我们set多个线程时, 她们将按照被提交的顺序依次执行
9, 免锁容器。 如Vector, Hashtable这些早期容易,使用了大量的synchronized方法来保证同步
Java SE5特别添加了一些新的容器,如CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentHashMap
这类免锁容器背后的通用策略是: 对容器的修改可以与读取操作同时发生, 只要读取者能看到修改后的内容就行。
修改是在容器数据结构的某个副本中执行的, 并且这个副本在修改过程中不可视,修改完成后,会立即将修改后的数据与主数据结构进行交换。
值得特别说明的是, ConcurrentHashMap还引入了分段锁,将数据分成多个数据段分别加锁,从而提高并发性能。
小结:
其实从根本上来说,上面的1,2,3,4都可归纳为加锁的方式,所以
一, 加锁 (上面的1,2, 3,4)
二,线程封闭,消除对资源的共享(5)
三, 利用可见性(6)
四,原子类(7)
五,executor框架(8)
六, 使用同步类/免锁容器(9)
Java多线程--实现同步的9种方法的更多相关文章
- java中线程同步的几种方法
1.使用synchronized关键字 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法.在调用该方法前,需要获得内置锁,否则就处于阻塞状态. 注: synchro ...
- Java修炼——线程同步的俩种方法
当多线程去同时抢占CPU资源时,有多线程的安全问题.这时候就需要将线程同步.线程同步有俩个方法. 1.同步代码块(synchronize),同步代码块需要同步监视器,同步监视器是针对对象进行操作.什么 ...
- JAVA之线程同步的三种方法
最近接触到一个图片加载的项目,其中有声明到的线程池等资源需要在系统中线程共享,所以就去研究了一下线程同步的知识,总结了三种常用的线程同步的方法,特来与大家分享一下.这三种方法分别是:synchroni ...
- java多线程的实现的两种方法
通过继承Thread类实现 多线程- public class Hello{ public static void main(String args[]){ MyThread tr1 = new My ...
- Java中实现线程同步的三种方法
实现同步的三种方法 多线程共享数据时,会发生线程不安全的情况,多线程共享数据必须同步. 实现同步的三种方法: 使用同步代码块 使用同步方法 使用互斥锁ReetrantLock(更灵活的代码控制) 代码 ...
- Java多线程之同步集合和并发集合
Java多线程之同步集合和并发集合 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全. 同步集合类 Hashtable Vector 同 ...
- Java多线程的同步控制记录
Java多线程的同步控制记录 一.重入锁 重入锁完全可以代替 synchronized 关键字.在JDK 1.5 早期版本,重入锁的性能优于 synchronized.JDK 1.6 开始,对于 sy ...
- 归纳一下:C#线程同步的几种方法
转自原文 归纳一下:C#线程同步的几种方法 我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库 ...
- Java多线程编程(同步、死锁、生产消费者问题)
Java多线程编程(同步.死锁.生产消费): 关于线程同步以及死锁问题: 线程同步概念:是指若干个线程对象并行进行资源的访问时实现的资源处理保护操作: 线程死锁概念:是指两个线程都在等待对方先完成,造 ...
随机推荐
- kafka零拷贝机制
kafka之所以那么快,其中一个很大的原因就是零拷贝(Zero-copy)技术,零拷贝不会kafka的专利,而是操作系统的升级,又比如Netty,也用到了零拷贝. 传统IO kafka的数据是要落入磁 ...
- 2020最精细的Java学习路线图
在吾爱破解发布的Java学习路线图自我感觉良好,之后看到动力节点Java学院的这份学习路线图感觉专业的东西还得专业的人来做,这份专业的学路线图把我上次的Java学习路线图秒成渣,虽然内容差不多,上份是 ...
- MySQL-存储引擎-MERGE
MERGE存储引擎是一组Myisam表的组合,这些Myisam表必须结构完全相同,MERGE表本身并没有数据,对MERGE类型的表可以进行查询.更新.删除操作,这些操作实际上是对内部的Myisam表进 ...
- QT学习日记篇-03-仿写一个智能家居界面
课程大纲: <1>让界面漂亮起来,仿写一个智能家居界面 ->第一:给QT工程添加图片 进入下一步: <注意路径和名称一定不能有中文> ...
- HTB Hack The Box -- Oopsiec
信息收集 开放了22ssh,80端口,其中ssh有弱口令爆破端口 先打开网页,然后进行目录爆破,在这期间先看一下网页的大概信息 没爆到什么有用的东西,但是有uploads文件夹说明是不是说明有文件上传 ...
- Selenium系列(十八) - Web UI 自动化基础实战(5)
如果你还想从头学起Selenium,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1680176.html 其次,如果你不懂前端基础知识, ...
- C# List集合类常用操作:三、查找
List集合查询数据 List<Employees> employees = new List<Employees>(); employees.Add(new Employee ...
- JS006. 详解自执行函数原理与数据类型的快速转换 (声明语句、表达式、运算符剖析)
今天的主角: Operator Description 一元正值符 " + "(MDN) 一元运算符, 如果操作数在之前不是number,试图将其转换为number. 圆括号运算符 ...
- python库--tensorflow--可视化
方法 返回值类型 参数 说明 tf.summary .FileWrite() 创建事件文件 logdir 文件保存路径(C盘), 通过tensorboard --logdir=文件路径(l ...
- Linux RHCE7.0 笔记(常见符号)
1.无交互式创建用户 echo "password" | passwd --stdin username 2.Linux重定向符号 > :表示将符号左侧的内容,以覆盖的方式输 ...