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

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

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

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

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

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

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. 最全华为鸿蒙 HarmonyOS 开发资料汇总

    开发 本示例基于 OpenHarmony 下的 JavaScript UI 框架,进行项目目录解读,JS FA.常用和自定义组件.用户交互.JS 动画的实现,通过本示例可以基本了解和学习到 JavaS ...

  2. Java基础和常用框架的面试题

    前言 最近学校也催着找工作了,于是刷了一些面试题,学习了几篇大佬优秀的博客,总结了一些自认为重要的知识点:听不少职场前辈说,对于应届毕业生,面试时只要能说到核心重要的点,围绕这个点说一些自己的看法,面 ...

  3. 剖析虚幻渲染体系(11)- RDG

    目录 11.1 本篇概述 11.2 RDG基础 11.2.1 RDG基础类型 11.2.2 RDG资源 11.2.3 RDG Pass 11.2.4 FRDGBuilder 11.3 RDG机制 11 ...

  4. java设计模式—单例模式(包含单例的破坏)

    什么是单例模式? 保证一个了类仅有一个实例,并提供一个访问它的全局访问点. 单例模式的应用场景? 网站的计数器,一般也是采用单例模式实现,否则难以同步: Web应用的配置对象的读取,一般也应用单例模式 ...

  5. 20210823 数数,数树,鼠树,ckw的树

    考场 乍一看都不好做 仔细想想发现 T1 的绝对值特别好,轮流选剩余的最大/最小值就行了 T2 又要计数,直接想部分分,发现一个 sb 容斥就有 35ps(但数据锅了,只有 25pts) T3 什么玩 ...

  6. springcloud3(五) spring cloud gateway动态路由的四类实现方式

    写这篇博客主要是为了汇总下动态路由的多种实现方式,没有好坏之分,任何的方案都是依赖业务场景需求的,现在网上实现方式主要有: 基于Nacos, 基于数据库(PosgreSQL/Redis), 基于Mem ...

  7. uniapp获取用户OpenId及用户详情

    页面增加一个按钮 <button type="default" open-type="getUserInfo" @click="getUserI ...

  8. Linux学习笔记--快捷键

    桌面 ALT+空格 打开窗口菜单 ALT+F1 聚焦到桌面左侧任务导航栏,可按上下键导航 ALT+F2     运行命令 ALT+F4 关闭窗口 ALT+TAB 切换程序窗口 PRINT 桌面截图 S ...

  9. 【第四篇】- Maven 构建生命周期之Spring Cloud直播商城 b2b2c电子商务技术总结

    ​ ​ Maven 构建生命周期 Maven 构建生命周期定义了一个项目构建跟发布的过程. 一个典型的 Maven 构建(build)生命周期是由以下几个阶段的序列组成的: ​ 阶段 处理 描述 验证 ...

  10. 5-7接口测试工具之jmeter的使用

    1.安装 免费的,安装jdk配好系统环境变量就能用了 2.jmeter测接口 获取用户信息,接口文档定义有2种请求方式 添加线程组-->添加http请求-->输入接口文档中说明的服务器名称 ...