我们通常说的保持同步,其实就是对共享资源的保护。在单线程模型中, 我们永远不用担心“多个线程试图同时使用同一个资源的问题”, 但是有了并发, 就有可能发生多个线程竞争同一个共享资源的问题。

就好比你正在餐厅里吃饭,当你拿起筷子正要夹盘子里的最后一块肉时, 这片肉突然消失了。因为你的线程被挂起了, 另一个人进入餐厅并吃掉了它。

这就是我们在多线程下需要处理的问题----我们需要某种方式来防止两个任务同时访问相同的资源

那么我们很容易想到第一种方法: 加锁, 好比我们进入卫生间之后要把门关上, 下一个人来到卫生间门口要先敲门,没人的话他就可以直接使用, 否则就要等到里面的人出来。不管你多么着急,不管外面排了多少人,没办法,只要他还在里面,那你就只能等,哪怕他在里面睡觉玩手机。。。 当他终于打开门出来的时候, 离门最近的那么人很有可能会成功进入, 但这一点并不能保证。

同理, 我们给共享资源加一把锁,任意时刻都只允许一个线程操作共享资源,当某个线程试图访问该资源时,需要先检查锁的状态,如果当前没有其他线程在使用, 则获取锁,开始操作资源,操作完成后再释放资源;否则就要排队等待。

常见的加锁方法大致有以下几种:

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种方法的更多相关文章

  1. java中线程同步的几种方法

    1.使用synchronized关键字 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法.在调用该方法前,需要获得内置锁,否则就处于阻塞状态. 注: synchro ...

  2. Java修炼——线程同步的俩种方法

    当多线程去同时抢占CPU资源时,有多线程的安全问题.这时候就需要将线程同步.线程同步有俩个方法. 1.同步代码块(synchronize),同步代码块需要同步监视器,同步监视器是针对对象进行操作.什么 ...

  3. JAVA之线程同步的三种方法

    最近接触到一个图片加载的项目,其中有声明到的线程池等资源需要在系统中线程共享,所以就去研究了一下线程同步的知识,总结了三种常用的线程同步的方法,特来与大家分享一下.这三种方法分别是:synchroni ...

  4. java多线程的实现的两种方法

    通过继承Thread类实现 多线程- public class Hello{ public static void main(String args[]){ MyThread tr1 = new My ...

  5. Java中实现线程同步的三种方法

    实现同步的三种方法 多线程共享数据时,会发生线程不安全的情况,多线程共享数据必须同步. 实现同步的三种方法: 使用同步代码块 使用同步方法 使用互斥锁ReetrantLock(更灵活的代码控制) 代码 ...

  6. Java多线程之同步集合和并发集合

    Java多线程之同步集合和并发集合 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全. 同步集合类 Hashtable Vector 同 ...

  7. Java多线程的同步控制记录

    Java多线程的同步控制记录 一.重入锁 重入锁完全可以代替 synchronized 关键字.在JDK 1.5 早期版本,重入锁的性能优于 synchronized.JDK 1.6 开始,对于 sy ...

  8. 归纳一下:C#线程同步的几种方法

    转自原文 归纳一下:C#线程同步的几种方法 我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库 ...

  9. Java多线程编程(同步、死锁、生产消费者问题)

    Java多线程编程(同步.死锁.生产消费): 关于线程同步以及死锁问题: 线程同步概念:是指若干个线程对象并行进行资源的访问时实现的资源处理保护操作: 线程死锁概念:是指两个线程都在等待对方先完成,造 ...

随机推荐

  1. Spark入门:Spark运行架构(Python版)

    此文为个人学习笔记如需系统学习请访问http://dblab.xmu.edu.cn/blog/1709-2/ 基本概念 *  RDD:是弹性分布式数据集(Resilient Distributed ...

  2. 在多数据源中对部分数据表使用shardingsphere进行分库分表

    背景 近期在项目中需要使用多数据源,其中有一些表的数据量比较大,需要对其进行分库分表:而其他数据表数据量比较正常,单表就可以. 项目中可能使用其他组的数据源数据,因此需要多数据源支持. 经过调研多数据 ...

  3. 刷题-力扣-518. 零钱兑换 II

    518. 零钱兑换 II 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/coin-change-2/ 著作权归领扣网络所有.商业转载 ...

  4. 关于ubuntu使用的那些事儿

    时间:2019-04-09 整理:PangYuaner 标题:Ubuntu18.04安装微信(Linux通用) 地址:https://www.cnblogs.com/dotnetcrazy/p/912 ...

  5. kratos

    技术文章 日志库的使用姿势 通过 layout 探索 kratos 运行原理 发版日志 发布日志 - kratos v2.0.5 版本发布 发布日志 - kratos v2.0.4 版本发布

  6. dotnet C# 给结构体字段赋值非线程安全

    在 dotnet 运行时中,给引用对象进行赋值替换的时候,是线程安全的.给结构体对象赋值,如果此结构体是某个类的成员字段,那么此赋值不一定是线程安全的.是否线程安全,取决于结构体的大小,取决于此结构体 ...

  7. ThreadLocal原理简单刨析

    ThreadLocal原理简单刨析 ThreadLocal实现了各个线程的数据隔离,要知道数据是如何隔离的,就要从源代码分析. ThreadLocal原理 需要提前说明的是:ThreadLocal只是 ...

  8. windows/linux 页面编码区别导致 python 乱码

    http://blog.csdn.net/haiross/article/details/36189103 可以先看下这篇文章..写的比较用心和详细并且高深..我只是记流水账的. 直到今天我才注意到 ...

  9. HZ游记

    HZ 游记 Day -1 收拾东西,准备出发. 话说这几天一直比较懒,也没什么心情和效率学习,颓废好几天了,希望到衡水以后能感觉好点. 不知道衡水有没有妹子 非常想看看衡水的样子,但是又害怕封闭式教学 ...

  10. Pytest系列(19)- 我们需要掌握的allure特性

    如果你还想从头学起Pytest,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1690628.html 前言 前面我们介绍了allure的 ...