精华内容
下载资源
问答
  • Java多线程|同步机制
    千次阅读
    2016-06-03 20:06:43

    目录

    1 同步的含义

    2 保证线程安全的本质

    3 死锁发生的四个条件

    4 JVM发生死锁如何恢复

    5 乐观锁与悲观锁

    (1)乐观锁

    (2)悲观锁

    6 活锁与死锁

    (1)活锁

    (2)死锁

    7 锁的形式与种类

    (1)内置锁(独占锁的形式)

    (2)显式锁

    (3)显式锁-ReentrantLock

    (4)读写锁-ReadWriteLock(读写锁接口)

    8 可重入锁机制

    9 锁优化手段

    (1)锁分解与锁分段技术(阿姆达尔定律应用)

    (2)替代独占锁

    (3)非阻塞同步机制

    (4)原子变量

    (5)CAS(Compare And Swap)机制

    (6)CAS之ABA问题

    (7)偏向锁


    ==========================【导读】[开始]========================== 

            工作中实践到了多线程与高并发应用,也踩了一些沉重的坑。
    万丈高楼起于垒土,学习与总结+工作实践不可相离。分三部分总结这块知识。
    知识体系详见第一张思维导图。本篇主题“同步与锁机制”。

    ==========================【导读】[结束]========================== 

    1 同步的含义

    1 原子性
    2 内存可见性(一个线程修改对象的状态后,其他线程可看到状态的改变)

    2 保证线程安全的本质

    保证线程安全的手段
    (1)不在线程之间共享该状态变量。
    (2)将状态变量修改为不可变的变量。
    (3)在访问状态变量时使用同步。

    3 死锁发生的四个条件

    互斥,持有,不可剥夺,环形等待

    4 JVM发生死锁如何恢复

    JVM 在解决死锁,只能重启恢复程序。

    5 乐观锁与悲观锁

    (1)乐观锁

    对于细粒度的操作,有一种更高效的方法,既乐观锁机制。通过这种方法可以在不发生干扰的情况下完成更新操作。
    这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,
    并且可以重试(也可以不重试)。

    (2)悲观锁

    独占锁是一项悲观锁技术一它假设最坏的情况(如果你不锁门,那么搞蛋鬼就会闯人并搞得一团糟),
    并且只有在确保其他线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去。

    6 活锁与死锁

    (1)活锁

        活锁(Livelock)是另一种形式的话跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,
    而且总会失败。活锁通常发生在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并
    将它重新放到队列的开头,线程将不断重复执行相同的操作,而且总会失败。
        当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个
    过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了。因此他们就这样反复地避让下去。
        要解决这种活锁问题,需要在重试机制中引人随机性。

    (2)死锁

    <2.1>致命拥抱死锁

    	在单线程的Executor中,如果一个任务将另一个任务提交到同一个 Excutor,并且等待这个
    被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务完
    成,而第一个任务又无法完成,因为它在等待第二个任务的完成。资源循环等待死锁。

    <2.1>线程饥饿死锁

    在线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件,例如某个
    任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

    7 锁的形式与种类

    (1)内置锁(独占锁的形式)

         任何Java对象都可以用作同步锁,为了便于区分,将其称为内置锁。当然这个“内置锁”是
    要与synchronized 配合使用的。所以内置锁指的是“synchronized”与synchronized配合使用的
    任何Java对象。
        Object中对内置锁进行操作的一些方法。
    Wait系列:使当前已经获得该对象锁的线程进入等待状态,并释放该对象锁。
    Notify系列:唤醒那些正在等待该对象锁的线程,使其继续运行。

    (2)显式锁

        Lock接口中定义了一组抽象的加锁操作。与内置加锁机制不同的是,Lock提供了一种无条件的、
    可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在Lock的实现中
    必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方
    面可以有所不同。
        ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。
    ReentrantLock还提供了可重人的加锁语义。与synchronized相比提供了更高的灵话性。

    (3)显式锁-ReentrantLock

    <3.1>使用形式

    //可指定公平锁,非公平锁(默认)模式
    //ReentrantLock(boolean fair)
    Lock lock = new ReentrantLock();
    lock.lock();
    //定时的与可轮询的锁获取模式tryLock
    //boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    try{
    	//更新对象的操作
    	//捕获异常并处理
    }
    finally {
    	lock.unlock();
    }

    <3.2>为什么要创建一种与内置锁如此相似的新加锁机制?

        内置锁无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去。内置锁必须在获取该锁
    的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。
        ReentrantLock使用确更加灵活。ReentrantLock不能完全替代synchronized的原因:它更加“危险”,当程序的执行控
    制离开被保护的代码块时,不会自动情除锁。需要在finally 块中释放锁。

    <3.3>ReentrantLock公平性

        在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。
    在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”。非公平锁
    的性能要高于公平锁的性能。

    <3.4>ReentrantLock定时锁与轮询机制

    支持定时与轮询锁机制
    可以检测死锁和从死锁中恢复过来,即显式使用Lock类中的定时tryLock 功能来代替内置锁(Synchronized)机制。
    当使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限( Timeout),  在等待超过
    该时间后tryLock会返回一个失败信息。如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外情况后重
    新获得控制权。

    (4)读写锁-ReadWriteLock(读写锁接口)

        ReentrantReadWriteLock实现了一种在多个读取操作以及单个写人操作情况下的加锁规则:
    如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行
    写人操作时必须以独占方式来获取锁。对于读取操作占多数的数据结构,ReadWriteLock能提供比
    独占锁更高的并发性。

    8 可重入锁机制

        当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重人的,
    因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的
    操作的粒度是“线程”,而不是“调用。重人的一种实现方法是,为每个锁关联一个获取计数值和一个所有
    者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。  当线程请求一个未被持有的锁时,JVM
    将记下锁的持有者,并且将获取计数值置为1.  如果同一个线程再次获取这个锁,计数值将递增,而当线程
    退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。重入避免了死锁的发生。
        内置锁,显式锁ReentrantLock都是可重入锁。

    9 锁优化手段

    (1)锁分解与锁分段技术(阿姆达尔定律应用)

    阿姆达尔定律应用(降低锁的粒度)
        大多数井发程序都与农业耕作有着许多相似之处,它们都是由一系列的并行工作和串行工作组成的。
    Amdahl定律描述的是: 在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程
    序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含
    N个处理器的机器中,最高的加速比为:
        S<=1/(F+(1-F)/N)
        注:F为串行执行占比。
    该定律的应用:降低锁的粒度。
        具体包括:锁分解(一个锁分解为两个)
        锁分段(一个锁分解为多个,效率提升更加有效)
        ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的116,其中
    第N个散列桶由第(N mod 16) 个锁来保护。假设散列函数具有合理的分布性,并且关键字能够实现
    均匀分布,那么这大约能把对于锁的请求减少到原来的1/16. 

    (2)替代独占锁

    内置属于独占锁机制。可用显式锁,读写锁机制替代独占锁使用。
    内置锁,在java6中得到了优化,和显式锁性能几乎相当,ReentrantLock,性能略微高于内置锁。

    (3)非阻塞同步机制

        非阻塞算法被广泛地用于在操作系统和JVM中实现线程/进程调度机制、垃圾回收机制以及销和其他并发数据结构。
    非阻塞算法是基于底层原子机器指令(如比较交换指令CAS)代替锁确保数据并发访问的一致性。非阻塞同步机制在多
    个线程在竟争相同的数据时不会发生阻塞;不存在死销和其他活跃性问题,效率更高。

    (4)原子变量

        原子变量类提供了在整数或者对象引用上的细粒度原子操作,并使用了现代处理器中提供的底层原语(如CAS)。
    共有12个原子变量类,可分为4组:标量类(Scalar). 更新器类、数组类以及复合变量类。最常用的原子变量就是标量
    类: AtomicInteger, AtomicLong、AtomicBoolcan以及AtomicReference,所有这些类都支持CAS,此外,AtomicInteger
    和AtomicLong还支持算术运算。
        在原子变量类中同样没有重新定义hashCode或equals方法,每个实例都是不同的。与其他可变对象相同,它们也
    不宜用做基于散列的容器中的键值。

    (5)CAS(Compare And Swap)机制

        CAS(比较并交换)包含了3个操作数一需要读写的内存位置V、进行比较的值A和拟写人的新值B。
    当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论
    位置V的值是否等于A,都将返回V原有的值。这种变化形式被称为比较并设置,无论操作是否成功都
    会返回.CAS的含义是:“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的
    值实际为多少”。CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另一个线程
    在最近一次检查后更新了该变量,那么CAS能检测到这个错误。
        当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线
    程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由
    于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也
    或者不执行任何操作。这种灵话性就大大减少了与锁相关的话跃性风险。

    (6)CAS之ABA问题

        ABA问题是一种异常现象: 如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能
    出现这种问题(主要在没有垃圾回收机制的环境中)。在CAS操作中将判断“V的值是否仍然为A?",并且如果是的
    话就继续执行更新操作。 在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,
    并需要重新执行算法中的某些步骤。
        如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。在这种情况下,有一个相对
    简单的解决方案: 不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,
    然后又变为A,版本号也将是不同的。

    (7)偏向锁

        偏向锁也是JDK 1.6 中引人的一项锁优化,它的目的是消除数据在无竞争情况下
    的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用
    CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消
    除掉,连CAS操作都不做了。

     

    更多相关内容
  • 同步控制是并发程序必不可少的重要手段,本文我们将通过重入、读写、信号量、倒计数器和循环栅栏以及他们的实例来介绍Java并发程序中的同步控制。 目录线程安全 Thread Safety重入 ReentrantLock读写 ...
  • Java多线程-6】synchronized同步锁

    千次阅读 2020-03-31 15:35:49
    前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。 例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。 public ...


    前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。

    例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。

    public class SellTickets {
        public static void main(String[] args) {
            TicketSeller seller = new TicketSeller();
    
            Thread t1 = new Thread(seller, "窗口1");
            Thread t2 = new Thread(seller, "窗口2");
            Thread t3 = new Thread(seller, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    class TicketSeller extends Thread {
        private static int tickets = 20;
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
                }
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
    

    运行后,发现会出现多个售票员出售同一张票的现象:
    在这里插入图片描述
    为了解决线程安全的问题,Java提供了多种同步锁。

    1 synchronized 原理概述

    1.1 操作系统层面

    synchronized的底层是使用操作系统的mutex lock实现的。下面先了解一些相关的概念。

    • 内存可见性:同步块的可见性是由以下两个规则获得的:
      1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
      2. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)。
    • 操作原子性:持有同一个锁的两个同步块只能串行地进入

    锁的内存语义:

    • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
    • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

    锁释放和锁获取的内存语义:

    • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
    • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
    • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息

    在这里插入图片描述

    Mutex Lock

    监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

    互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

    mutex的工作方式:
    在这里插入图片描述

    1. 申请mutex,如果成功,则持有该mutex,如果失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止
    2. 依据工作模式的不同选择yiled还是sleep
    3. 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1-2步,直到获得为止

    由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中。

    synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

    1.2 JVM层面

    synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?

    Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述。

    长度内容说明
    32/64bitMark Word存储对象的hashCode 或锁信息
    32/64bitClass Metadata Address存储对象类型数据的指针
    32/64bitArray length数组的长度(如果当前对象是数组)

    Mark Word

    Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

    对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
    在这里插入图片描述

    Monitor

    什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。

    与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

    Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

    其结构如下:
    在这里插入图片描述

    • Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。
    • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
    • RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
    • Nest:用来实现重入锁的计数。HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
    • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

    2 synchronized 使用

    synchronized是Java中的关键字,是一种同步锁,它修饰的对象有以下几种:

    序号类别作用范围作用对象
    1同步代码块被synchronized修饰的代码块调用这个代码块的单个对象
    2同步方法被synchronized修饰的方法调用该方法的单个对象
    3同步静态方法被synchronized修饰的静态方法静态方法所属类的所有对象
    4同步类被synchronized修饰的代码块该类的所有对象

    2.1 同步代码块

    同步代码块就是将需要的同步的代码使用同步锁包裹起来,这样能减少阻塞,提高程序效率。

    同步代码块格式如下:

        synchronized(对象){
        	同步代码;
        }
    

    同样对于文章开头卖票的例子,进行线程安全改造,代码如下:

    public class SellTickets {
        public static void main(String[] args) {
            TicketSeller seller = new TicketSeller();
    
            Thread t1 = new Thread(seller, "窗口1");
            Thread t2 = new Thread(seller, "窗口2");
            Thread t3 = new Thread(seller, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    class TicketSeller implements Runnable {
        private static int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                synchronized (this) {
                    try {
                        Thread.sleep(10);
                        if (tickets > 0) {
                            System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    同步代码块的关键在于锁对象,多个线程必须持有同一把锁,才会实现互斥性。

    将上面代码中的 synchronized (this) 改为 synchronized (new Objcet()) 的话,线程安全将得不到保证,因为两个线程的持锁对象不再是同一个。

    又比如下面这个例子:

    public class SyncTest implements Runnable {
        // 共享资源变量
        int count = 0;
    
        @Override
        public void run() {
            synchronized (this) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
    //        test1();
            test2();
        }
    
        public static void test1() {
            SyncTest syncTest1 = new SyncTest();
            Thread thread1 = new Thread(syncTest1, "thread-1");
            Thread thread2 = new Thread(syncTest1, "thread-2");
            thread1.start();
            thread2.start();
        }
    
        public static void test2() {
            SyncTest syncTest1 = new SyncTest();
            SyncTest syncTest2 = new SyncTest();
    
            Thread thread1 = new Thread(syncTest1, "thread-1");
            Thread thread2 = new Thread(syncTest2, "thread-2");
            thread1.start();
            thread2.start();
        }
    }
    

    从输出结果可以看出,test2() 方法无法实现线程安全,原因在于我们指定锁为this,指的就是调用这个方法的实例对象,然而 test2() 实例化了两个不同的实例对象 syncTest1,syncTest2,所以会有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行 run() 方法,造成线程不安全。

    如果要使用这个经济实惠的锁并保证线程安全,那就不能创建出多个不同实例对象。如果非要想 new 两个不同对象出来,又想保证线程同步的话,那么 synchronized 后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步了。

    synchronized(xxxx.class){
      //todo
    }
    

    一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

    例如下面的例子:

    public class SyncTest {
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter, "线程-1");
            Thread thread2 = new Thread(counter, "线程-2");
            thread1.start();
            thread2.start();
        }
    }
    
    class Counter implements Runnable {
        private int count = 0;
    
        public void countAdd() {
            synchronized (this) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 同步计数:" + (count++));
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public void printCount() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 非同步输出:" + count);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void run() {
            String threadName = Thread.currentThread().getName();
            if (threadName.equals("线程-1")) {
                countAdd();
            } else if (threadName.equals("线程-2")) {
                printCount();
            }
        }
    }
    

    我们也可以用synchronized 给对象加锁。这时,当一个线程访问该对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码,,例如下例:

    public class SyncTest {
        public static void main(String args[]) {
            Account account = new Account("zhang san", 10000.0f);
            AccountOperator accountOperator = new AccountOperator(account);
    
            final int THREAD_NUM = 5;
            Thread threads[] = new Thread[THREAD_NUM];
            for (int i = 0; i < THREAD_NUM; i++) {
                threads[i] = new Thread(accountOperator, "Thread-" + i);
                threads[i].start();
            }
        }
    }
    
    class Account {
        String name;
        double amount;
    
        public Account(String name, double amount) {
            this.name = name;
            this.amount = amount;
        }
    
        //存钱
        public void deposit(double amt) {
            amount += amt;
            try {
                Thread.sleep(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        //取钱
        public void withdraw(double amt) {
            amount -= amt;
            try {
                Thread.sleep(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public double getBalance() {
            return amount;
        }
    }
    
    class AccountOperator implements Runnable {
        private Account account;
    
        public AccountOperator(Account account) {
            this.account = account;
        }
    
        public void run() {
            synchronized (account) {
                String name = Thread.currentThread().getName();
                account.deposit(500);
                System.out.println(name + "存入500,最新余额:" + account.getBalance());
                account.withdraw(400);
                System.out.println(name + "取出400,最新余额:" + account.getBalance());
                System.out.println(name + "最终余额:" + account.getBalance());
            }
        }
    }
    

    同步锁可以使用任意对象作为锁,当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

    class Test implements Runnable {
       private byte[] lock = new byte[0];  // 特殊的instance变量
       public void method() {
          synchronized(lock) {
             // todo 同步代码块
          }
       }
     
       public void run() {
     
       }
    }
    

    2.2 同步方法

    Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。

    public synchronized void method(){
       // todo
    }
    

    下面用同步函数的方式解决售票场景的线程安全问题,代码如下:

    public class SellTickets {
        public static void main(String[] args) {
            TicketSeller seller = new TicketSeller();
    
            Thread t1 = new Thread(seller, "窗口1");
            Thread t2 = new Thread(seller, "窗口2");
            Thread t3 = new Thread(seller, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    class TicketSeller implements Runnable {
        private static int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                sellTickets();
            }
        }
    
        public synchronized void sellTickets() {
            try {
                Thread.sleep(10);
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    同步方法有以下特征:

    1. synchronized关键字不能继承。 虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
    2. 在定义接口方法时不能使用synchronized关键字。
    3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

    2.3 同步静态方法

    Synchronized也可修饰一个静态方法,静态方法是不属于当前实例的,而是属性类的,那么这个锁就是类的class对象锁。同步静态方法可以解决同步方法和同步代码块中的一个问题:new 两个对象的话,等于有两把锁,无法保证线程安全。

    public class SyncTest {
        public static void main(String args[]) {
            SyncThread syncThread1 = new SyncThread();
            SyncThread syncThread2 = new SyncThread();
            Thread thread1 = new Thread(syncThread1, "Thread-1");
            Thread thread2 = new Thread(syncThread2, "Thread-2");
            thread1.start();
            thread2.start();
        }
    }
    
    class SyncThread implements Runnable {
        private static int count = 0;
    
        public synchronized static void method() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public synchronized void run() {
            method();
        }
    }
    

    syncThread1 和 syncThread2 是 SyncThread 的两个对象,但在 thread1 和 thread2 并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。

    2.4 同步类

    Synchronized还可作用于一个类,用法如下:

    class ClassName {
       public void method() {
          synchronized(ClassName.class) {
             // todo
          }
       }
    }
    

    同步类与同步静态方法有相同的效果,该类的所有对象都是持有同一把锁:

    public class SyncTest {
        public static void main(String args[]) {
            SyncThread syncThread1 = new SyncThread();
            SyncThread syncThread2 = new SyncThread();
            Thread thread1 = new Thread(syncThread1, "Thread-1");
            Thread thread2 = new Thread(syncThread2, "Thread-2");
            thread1.start();
            thread2.start();
        }
    }
    
    class SyncThread implements Runnable {
        private static int count = 0;
    
        public void method() {
            synchronized (SyncThread.class) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public synchronized void run() {
            method();
        }
    }
    
    展开全文
  • Java线程同步和锁定

    千次阅读 2022-03-23 09:08:29
    同步和锁定 Java中每个对象都有一个内置。 当程序运行到synchronized同步方法或代码... 释放是指持锁线程退出了synchronized同步方法或代码块。 关于同步的几个要点 1)只能同步方法,而不能同步变量和类;

    同步和锁定

    Java中每个对象都有一个内置锁。

    1、synchronize

    当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

    一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

    释放锁是指持锁线程退出了synchronized同步方法或代码块。

    关于锁和同步的几个要点

    1. 只能同步方法,而不能同步变量和类;
    2. 每个对象只有一个锁;当提到同步时,应该清楚在哪个对象上同步。
    3. synchronized 锁的是对象,在应用数据共享时,要保证被锁的对象不要发生变化。
    4. 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
    5. 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
    6. 线程睡眠时,它所持的任何锁都不会释放。
    7. 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

    在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。synchronized (this) 和 用synchronized修饰方法 有相同的效果,例如:

    public int add(int m) {  
        synchronized (this) {  
            n = m + n;  
        }
        return n;  
    } 
    

    等同于

    public synchronized int add(int m) {  
        n = m + n;
        return n;  
    } 
    

    静态方法同步

    同步静态方法,需要一个类对象的锁,即Class对象

    public static int add(int m) {  
        synchronized (Test.class) {  
            n = m + n;  
        }
        return n;  
    } 
    

    等同于

    public static synchronized int add(int m) {  
        n = m + n;
        return n;  
    } 
    

    什么情况下,线程无法获得锁?

    当线程A试图调用同步方法或同步代码块,要清楚这个同步是在哪个对象上的锁,如果此时锁被线程B占用,那么线程A在该对象上被阻塞,等待,直到锁被释放,线程A再次变为可运行或运行状态。

    • 对于非静态的同步方法,当然是,调用同一个对象的同步方法会被阻塞,调用不同对象的同步方法不会被阻塞
    • 对于静态的同步方法,都是在Class对象上加锁,调用它的同步方法当然会彼此阻塞

    下面的例子可以帮助理解。两个不同的线程thread1和thread2,想同时操作Worker里面的index

    public static void main(String[] args) throws InterruptedException {
       Worker worker = new Worker(5); 
       // 下面两个线程用的是同一个对象worker
       Thread thread1 = new Thread(worker);
       Thread thread2 = new Thread(worker);
       thread1.start();
       thread2.start();
    }
    
    private static class Worker implements Runnable {
        private Integer index;
        public Worker(Integer index) {
            this.index = index;
        }
        @Override
        public void run() {
       	    // 这里的this,表示当前对象,不同的实例,this指代的对象是不同的
            synchronized (this) {
                index++;
                System.out.println(Thread.currentThread().getName() + "-------" + index);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "-------" + index + ", END");
            }
        }
    }
    

    synchronized (this),即对Worker对象加锁,打印结果如下:

    Thread-0-------6
    Thread-0-------6, END
    Thread-1-------7
    Thread-1-------7, END
    

    上面new Thread用的是同一个worker对象,那么针对两个线程,一个获得对象锁后,另一个必然阻塞。

    2、volatile关键字

    • 保证了不同线程对这个变量进行操作时的可见性。
      即一个线程修改了某个变量的值, 这新值对其他线程来说是立即可见的。
    • 不保证原子性
      即不能保证数据在多个线程下同时写时的线程安全

    volatile最适用的场景: 一个线程写, 多个线程读
    一个线程在修改某个变量的值,其他线程来读取该变量的值都是实时可见的。

    3、ThreadLocal

    1. 介绍

    规避线程不安全的方法,除了加锁之外,还可以使用ThreadLocal 。
    如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。可理解为:线程局部变量。

    2. ThreadLocal使用

    在这里插入图片描述

      public class UseThreadLocal {
    	    // int型ThreadLocal变量
    		private static ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>(){
    	        @Override
    	        protected Integer initialValue() {
    	            return 100;
    	        }
    	    };
    
    	    // String型ThreadLocal变量
    	    private static ThreadLocal<String> stringThreadLocal;
    
    		// 开启线程
    	    public void StartThread(){
    			Thread thread1 = new Thread(new TestRunnable(1));
    			thread1.start();
    	        Thread thread2 = new Thread(new TestRunnable(2));
    			thread2.start();
    	    }
    	    
    	    public static class TestRunnable implements Runnable{
    	        public int id;
    	        public TestRunnable(int id){
    	            this.id = id;
    	        }
    	        public void run() {
    	            System.out.println(Thread.currentThread().getName()+", intLocal.get:" + intLocal.get());
    	            Integer i = intLocal.get();
    	            i = i + id;
    	            intLocal.set(i);
    	            System.out.println(Thread.currentThread().getName() +", after set:"+ intLocal.get());
    	            // ThreadLocal变量不再使用时,须remove
    	            intLocal.remove();
    	        }
    	    }
    
    	    public static void main(String[] args){
    	    	UseThreadLocal test = new UseThreadLocal();
    	        test.StartThread();
    	    }
      }
    

    输出结果:

    Thread-0, intLocal.get:100
    Thread-1, intLocal.get:100
    Thread-0, after set:101
    Thread-1, after set:102
    

    3. ThreadLocal的实现原理

    在这里插入图片描述

    (1) Thread类

    主要看它的成员变量 threadLocals

    • 类型:ThreadLocal.ThreadLocalMap(类似于HashMap),key是ThreadLocal,value是set的值
    • 初始化:此变量初始为null,只有调用ThreadLocal.set或get方法时,才会创建它
    • 赋值:ThreadLocal.set()、get()、remove()
    • 作用:存放该线程的ThreadLocal类型的本地变量
    • 注意:不使用本地变量时,需要调用remove方法将此线程在threadLocals中的本地变量删除

    (2)ThreadLocal类
    这里看下set()、get()、remove()方法源码

    set()

     public void set(T value) {
        // (1) 获取当前线程(调用者线程)
        Thread t = Thread.currentThread();
        // (2) 获取Thread的成员变量threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //  (3) 直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为value
             map.set(this, value);
         } else {
             // (4) 当map为null,说明首次添加,需要首先创建出对应的map
             createMap(t, value);
          }
     }
    
     ThreadLocalMap getMap(Thread t) {
         // 获取Thread的成员变量threadLocals
         return t.threadLocals; 
     }
    
     void createMap(Thread t, T firstValue) {
          // 创建threadLocals
          t.threadLocals = new ThreadLocalMap(this, firstValue);
     }
    

    get()

     public T get() {
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             ThreadLocalMap.Entry e = map.getEntry(this);
             T result = (T)e.value;
             return result;
         }
         return setInitialValue();
     }
     
     private T setInitialValue() {
         T value = initialValue(); // new ThreadLocal时会重写initialValue()进行赋值
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null)
             map.set(this, value);
         else
             createMap(t, value);
         return value;
     }
    

    remove()

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    

    ThreadLocalMap
    ThreadLocalMap有一个内部类Entry,有一个Entry[]类型的变量,这个数组可以保存多个Entry对象。
    值得注意的是,Entry持有ThreadLocal对象的弱引用。

    弱引用:只要GC时,弱引用就会被回收。

    因此,当GC时,Entry指向的ThreadLocal对象会被回收,那么Entry的key不在了,其value永远不会被访问,内存将暴增。因此,对于不再使用的线程本地变量,应及时remove。

    展开全文
  • synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;  2. ...

    synchronised

    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种: 

    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象; 
    2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 
    3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 
    4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

    修饰一个代码块

     1.一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。我们看下面一个例子:

    【Demo1】:synchronized的用法:

    /**
     * 同步线程
     */
    class SyncThread implements Runnable {
        private static int count;
    
        public SyncThread() {
            count = 0;
        }
    
        public void  run() {
            synchronized(this) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public int getCount() {
            return count;
        }
    }

    SyncThread的调用:

    SyncThread syncThread = new SyncThread();
    Thread thread1 = new Thread(syncThread, "SyncThread1");
    Thread thread2 = new Thread(syncThread, "SyncThread2");
    thread1.start();
    thread2.start();

    结果如下:

    SyncThread1:0
    SyncThread1:1
    SyncThread1:2
    SyncThread1:3
    SyncThread1:4
    SyncThread2:5
    SyncThread2:6
    SyncThread2:7
    SyncThread2:8
    SyncThread2:9

    当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

    我们再把SyncThread的调用稍微改一下:

    Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
    Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
    thread1.start();
    thread2.start();

    结果如下:

    SyncThread1:0
    SyncThread2:1
    SyncThread1:2
    SyncThread2:3
    SyncThread1:4
    SyncThread2:5
    SyncThread2:6
    SyncThread1:7
    SyncThread1:8
    SyncThread2:9

    不是说一个线程执行synchronized代码块时其它的线程受阻塞吗?为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联,而上面的代码等同于下面这段代码:

    SyncThread syncThread1 = new SyncThread();
    SyncThread syncThread2 = new SyncThread();
    Thread thread1 = new Thread(syncThread1, "SyncThread1");
    Thread thread2 = new Thread(syncThread2, "SyncThread2");
    thread1.start();
    thread2.start();

    这时创建了两个SyncThread的对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

    2.当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。 

    【Demo2】:多个线程访问synchronized和非synchronized代码块

    class Counter implements Runnable{
        private int count;
    
        public Counter() {
            count = 0;
        }
    
        public void countAdd() {
            synchronized(this) {
                for (int i = 0; i < 5; i ++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
        public void printCount() {
            for (int i = 0; i < 5; i ++) {
                try {
                    System.out.println(Thread.currentThread().getName() + " count:" + count);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void run() {
            String threadName = Thread.currentThread().getName();
            if (threadName.equals("A")) {
                countAdd();
            } else if (threadName.equals("B")) {
                printCount();
            }
        }
    }

    调用代码:

    Counter counter = new Counter();
    Thread thread1 = new Thread(counter, "A");
    Thread thread2 = new Thread(counter, "B");
    thread1.start();
    thread2.start();

    结果如下:

    A:0
    B count:1
    A:1
    B count:2
    A:2
    B count:3
    A:3
    B count:4
    A:4
    B count:5

    上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

    3.指定要给某个对象加锁:

    **
     * 银行账户类
     */
    class Account {
        String name;
        float amount;
    
        public Account(String name, float amount) {
            this.name = name;
            this.amount = amount;
        }
        //存钱
        public  void deposit(float amt) {
            amount += amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取钱
        public  void withdraw(float amt) {
            amount -= amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public float getBalance() {
            return amount;
        }
    }
    
    /**
     * 账户操作类
     */
    class AccountOperator implements Runnable{
        private Account account;
        public AccountOperator(Account account) {
            this.account = account;
        }
    
        public void run() {
            synchronized (account) {
                account.deposit(500);
                account.withdraw(500);
                System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
            }
        }
    }

    调用代码:

    Account account = new Account("zhang san", 10000.0f);
    AccountOperator accountOperator = new AccountOperator(account);
    
    final int THREAD_NUM = 5;
    Thread threads[] = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i ++) {
            threads[i] = new Thread(accountOperator, "Thread" + i);
            threads[i].start();
    }
    运行结果:
    Thread0:10000.0
    Thread1:10000.0
    Thread2:10000.0
    Thread3:10000.0
    Thread4:10000.0
    Thread5:10000.0

    在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。 
    当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。

    public void method3(SomeObject obj)
    {
        //obj 锁定的对象
        synchronized(obj)
        {
            // todo
        }
    }

    当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

    lass Test implements Runnable
    {
        private byte[] lock = new byte[0];  // 特殊的instance变量
        public void method()
        {
            synchronized(lock) {
                // todo 同步代码块
            }
        }
    
        public void run() {
    
        }
    }

    说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

    修饰一个方法

    Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将【Demo1】中的run方法改成如下的方式,实现的效果一样。

    【Demo4】:synchronized修饰一个方法

    public synchronized void run() {
        for (int i = 0; i < 5; i ++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    Synchronized作用于整个方法的写法。 
    写法一:

    public synchronized void method()
    {
        // todo
    }

    写法二:

    public void method()
    {
        synchronized(this) {
            // todo
        }
    }

    写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的,都是锁定了整个方法时的内容。

    在用synchronized修饰方法时要注意以下几点: 


    1.synchronized关键字不能继承。 

    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:

    在子类方法中加上synchronized关键字

    class Parent {
        public synchronized void method() { }
    }
    class Child extends Parent {
        public synchronized void method() { }
    }

    在子类方法中调用父类的同步方法

    class Parent {
        public synchronized void method() {   }
    }
    class Child extends Parent {
        public void method() { super.method();   }
    } 

       2.在定义接口方法时不能使用synchronized关键字。

       3.构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。 

    修饰一个静态的方法

    Synchronized也可修饰一个静态方法,用法如下:

    public synchronized static void method() {
        // todo
    }

    我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。我们对Demo1进行一些修改如下:

    【Demo5】:synchronized修饰静态方法

    **
     * 同步线程
     */
    class SyncThread implements Runnable {
        private static int count;
    
        public SyncThread() {
            count = 0;
        }
    
        public synchronized static void method() {
            for (int i = 0; i < 5; i ++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public synchronized void run() {
            method();
        }
    }

    调用代码:

    SyncThread syncThread1 = new SyncThread();
    SyncThread syncThread2 = new SyncThread();
    Thread thread1 = new Thread(syncThread1, "SyncThread1");
    Thread thread2 = new Thread(syncThread2, "SyncThread2");
    thread1.start();
    thread2.start();

    结果如下:

    SyncThread1:0
    SyncThread1:1
    SyncThread1:2
    SyncThread1:3
    SyncThread1:4
    SyncThread2:5
    SyncThread2:6
    SyncThread2:7
    SyncThread2:8
    SyncThread2:9

    syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。

    修饰一个类

    Synchronized还可作用于一个类,用法如下:

    class ClassName {
        public void method() {
            synchronized(ClassName.class) {
                // todo
            }
        }
    }

    我们把Demo5再作一些修改。 
    【Demo6】:修饰一个类

    /**
     * 同步线程
     */
    class SyncThread implements Runnable {
        private static int count;
    
        public SyncThread() {
            count = 0;
        }
    
        public static void method() {
            synchronized(SyncThread.class) {
                for (int i = 0; i < 5; i ++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ":" + (count++));
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public synchronized void run() {
            method();
        }
    }

    其效果和【Demo4】是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

    总结:

    A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 
    B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。 
    C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    java.util.concurrent.locks.lock

     Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我们拿Java线程(二)中的一个例子简单的实现一下和sychronized一样的效果,代码如下:

    1.public class LockTest {  
    2.    public static void main(String[] args) {  
    3.        final Outputter1 output = new Outputter1();  
    4.        new Thread() {  
    5.            public void run() {  
    6.                output.output("zhangsan");  
    7.            };  
    8.        }.start();        
    9.        new Thread() {  
    10.            public void run() {  
    11.                output.output("lisi");  
    12.            };  
    13.        }.start();  
    14.    }  
    15.}  
    16.class Outputter1 {  
    17.    private Lock lock = new ReentrantLock();// 锁对象  
    18.    public void output(String name) {  
    19.        // TODO 线程输出方法  
    20.        lock.lock();// 得到锁  
    21.        try {  
    22.            for(int i = 0; i < name.length(); i++) {  
    23.                System.out.print(name.charAt(i));  
    24.            }  
    25.        } finally {  
    26.            lock.unlock();// 释放锁  
    27.        }  
    28.    }  
    }

    这样就实现了和sychronized一样的同步效果,需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。

    synchronized与lock的区别

    java.util.concurrent.locks.lock类

    · 临界区边界灵活了

    · 锁释放的顺序由用户决定

    · 可以有多个Condition




    ReentrantLock

       java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

      可重入锁 ReentrantLock 的含义是: 当某个线程获取某个锁后,在未释放锁的情况下,第二次再访问该锁锁定的另一代码块时,可以重新进入该块。 

      reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

      在基本用法上,二者类似。但是ReentrantLock增加了一些高级特性,只要有以下三个:

    第一、等待可中断

    第二、公平锁

    第三、锁绑定多个条件

      等待可中断是指,等待的线程在等待超过一定时间之后,可以选择继续等待,或者也可以不等待直接去做其他事情了。这对于执行时间非常长的同步块来说很有用。

      公平锁是指,线程必须按照排队的顺序来获得锁,而非公平锁则不保证这一点。在非公平锁中,任何一个等待的线程都有可能获得锁。

      锁绑定多个条件是指,一个 ReentrantLock对象可以同时绑定多个Condition对象。而在syncronized中,锁对象的wait和notify或者是 notifyall方法可以实现一个隐含的条件,如果要和多于一个的条件进行关联的时候,则不得不额外的添加一个锁。ReentrantLock不需要额外的添加一个锁,只需要多次调用newCondition()方法即可。

    ReentrantLock Synchronized 区别

    1、ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了锁投票,定时锁,等候和中断锁等候
         线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
         如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
         如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
     
        ReentrantLock获取锁定与三种方式:
        a)  lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
        b)  tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
        c)  tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
        d)  lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
     
    2、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
     
    3、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

    与syncronized相比,ReentrantLock的性能可以保持在一个比较稳定的水平。从java1.6开始,syncronized与ReentrantLock在性能上完全持平了。所以在1.6之后,性能就不是选择ReentrantLock的原因了。

      最后总结一下

      很多人都知道重入锁的性能高于synchronized,其实这个是停留在JDK1.5的阶段,在JDK到1.6的时候引入了许多针对synchronized的优化措施,如自旋锁、轻量级锁、偏向锁等,性能上也基本和重入锁持平了。个人觉得如果用不到重入锁的一些特殊的功能尽量不要用重入锁,原因主要有两个方面:

      1、毕竟大部分开发者对synchronized的熟悉远远超过重入锁且重入锁需要手动释放锁,如果一旦忘记释放就很悲剧了;

      2、synchronized毕竟是JVM语义级别的,JDK1.6性能已经不弱于Lock了,以后的肯恩会更多的针对这部分做性能优化。

    参考:

    并发编程之ThreadLocal、Volatile、synchronized、Atomic关键字扫盲


    展开全文
  • 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。本篇文章主要介绍了Java多线程-线程的同步的问题,有兴趣的可以了解一下。
  • Java多线程 -

    千次阅读 2022-03-28 15:31:49
    Java多线程 - 三性 可见性 指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Java中 volatile、synchronized 和 final 实现可见性。 原子性 如果一个操作是不可分割的,我们则称之为...
  • 讲述了线程同步问题,同步锁,死锁以及加锁的缺点。主要是分享认知上的普及,框架一定要简单,不要炫技,可以出错,但不要让错误向下传递。
  • 并发操作之——java多线程常用的

    千次阅读 2021-09-07 19:29:21
    并发操作之——java多线程常用的并发操作前言一、共享二、互斥三、死锁1、偏向2、轻量3、重量级总结 前言 并发操作之——java多线程常用的。 一、共享 也叫S/读,能查看但无法修改和删除的一...
  • 本文介绍使用javasynchronized同步锁来实现对相同userId进行加锁 众所周知synchronized只能锁对象地址,而对于如下加锁是完全没有用的 public void test(Long userId) { synchronized (userId) {//除了-127-128...
  • Java多线程的理解与使用

    万次阅读 多人点赞 2017-10-14 18:45:51
    作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等 )
  • 一、线程并发同步概念 线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”。...在多线程编程...
  • 线程的安全问题主要体现在,当需要访问公共资源是两个线程可能会出现问题 举个例子 class YdThread implements Runnable { private int num = 10; public void run() { while(num &amp;gt; 0) { ...
  • 一、本例需要分析的地方不,只需要使用一个同步锁+一个计数器就能搞定,直接奉送源码吧: package com.example.liuxiaobing.statemodel.mutil_thread.onebyoneprint; /** * Created by liuxiaobing * ...
  • Java多线程编程总结

    2018-01-14 21:05:07
    Java线程线程同步 Java线程线程的交互 Java线程线程的调度-休眠 Java线程线程的调度-优先级 Java线程线程的调度-让步 Java线程线程的调度-合并 Java线程线程的调度-守护线程 Java线程:...
  • Java实现的几种方式

    千次阅读 2021-03-16 22:41:00
    同步,学习多线程避不开的两个问题,Java提供了synchronized关键字来同步方法和代码块,还提供了很多方便易用的并发工具类,例如:LockSupport、CyclicBarrier、CountDownLatch、Semaphore…有没有想过自己实现...
  • java多线程同步5种方法

    万次阅读 多人点赞 2018-05-28 22:11:07
    二、为什么要线程同步因为当我们有个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作...
  • JAVA多线程——实现同步

    万次阅读 多人点赞 2018-07-26 17:20:33
    java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了...
  • 多线程】线程安全、同步和异步

    千次阅读 多人点赞 2018-01-22 20:13:36
     非线程安全:非线程主要是指线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。 synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为...
  • java多线程的15种

    万次阅读 多人点赞 2019-05-05 19:19:24
    悲观和乐观是一种广义的概念,体现的是看待线程同步的不同的角度 悲观认为自己在使用数据的时候,一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。 实现:关键字...
  • Java 多线程同步和异步详解

    千次阅读 2018-05-31 10:00:32
    转载自 https://www.cnblogs.com/mengyuxin/p/5358364.htmljava线程 同步与异步 线程池1)多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了...
  • Java多线程同步优化的6种方案

    万次阅读 2020-06-24 17:44:34
    Java中可以使用来解决多线程的同步问题,保障了数据的一致性,但也会代理很多问题,本章总结了多线程同步的几种优化方案:包括读写、写时复制机制、细化等方案。
  • 文章目录多线程的升级原理是什么? 多线程的升级原理是什么?
  • Java多线程同步机制

    千次阅读 2018-08-12 14:29:33
    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的(一个对象只有一把); 如果这个时候同步对象的被其他线程拿走了,他(这个线程)就只能等了...
  • 1.在java多线程编程中对象、类同步机制synchronized详解:  对象:在java中每个对象都有一个唯一的,对象用于对象实例方法或者一个对象实例上面的。  类:是用于一个类静态方法或者class对象的,一...
  • Java 多线程全局与对象

    万次阅读 2018-05-12 17:49:04
      我们看一个例子: class Demo { public synchronized void test() { ...test方法开始执行,当前线程为:&quot;+Thread.currentThread().getName()); try { Thread.sleep(1000); ...
  • Java 多线程 —— 同步代码块(解决线程安全问题)

    千次阅读 多人点赞 2021-10-23 19:59:26
    目录火车站抢票问题同步代码块同步方法(this同步方法,在public的后面加上synchronized关键字this静态同步方法 火车站抢票问题 由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠...
  • 一、多线程同步关键字-synchronized1.概念 synchronized保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。当多个并发线程访问同一个对象object中的同步...
  • java线程锁

    2021-03-03 23:47:43
    线程安全问题产生的原因:线程在操作共享的数据操作共享数据的线程代码有条当一个线程在执行操作共享数据的条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。解决思路:就是将条操作共享...
  • java 多线程 面试题整理(更新......)

    千次阅读 2021-11-30 16:23:06
    3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 277,367
精华内容 110,946
关键字:

java多线程同步锁

java 订阅