java 内置锁

1.java内置锁是一个互斥锁,也就说明最多只有一个线程能够获得该锁,当线程A获得锁时,线程B想要尝试获得锁的时候,必须等线程A释放锁,若线程A一直不释放锁,则线程B一直等待处于阻塞状态中。获取锁的唯一途径就是进入这个锁保护的同步代码块或方法,否则没法获取当前锁,在java中每个对象都可以用作锁,这些锁都称为内置锁。

2.线程安全

当多个线程并发访问某个对象时,无论系统如何调度这些线程,无论线程如果交替操作这个对象,其结果都能表现出一致性的、正确的行为,那么对这个对象的操作就是安全,如果这个对象所表现出来不一致的、错误的行为,那么对这个对象的操作就不是安全的,就会发生线程安全的问题。

3. 自增运算不是线程安全的,

其原因为:自增运算符是一个复合操作,至少包括三个JVM内部的指令,内存取值、寄存器增加1、存值到内存,其中这三个指令在JVM内部是独立运行的,中间完全可能会出现多个线程并发的进行。三个指令本身是具有原子性是线程安全的,但是多个原子性操作一起进行就不具有备其原子性。

4.临界区资源

表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能一个线程使用它,一旦临界区资源被占用,想使用改资源的其他线程则必须等待。同时是受保护的对象。

5.临界区代码段

是每个线程访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问,线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后执行临界区代码段,执行完成后释放资源。如果多个线程在临界区代码段的并发执行结果可能会由于执行代码的顺序不同而不同,这个时候就会出现在临界区的竞态条件,这时我们为了执行结果的一致性和正确性,就在使临界代码段具有排他性,只能让一个线程进入执行代码段时,其他线程不能进入,java中可以使用synchronized关键字来进行代码段的排他性处理,或者进行lock显式锁

6.static修饰synchronized关键字,

首先静态方法是属于Class实例而不是当Object实例,在静态方法内部是不可以访问Object实例的this引用的,因此就没有办法获得Object实例的this对象的监视锁,所有被static修饰的synchronize的同步锁是class类对应的监视锁,为了区分Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。当synchronized关键字修饰static方法时,同步锁为类锁;当synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为类锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。所有使用synchronized关键字修饰static方法是非常粗粒度的同步机制。

7.java四种锁

java中存在四种状态的锁,级别等级由低到高依次为:(锁的状态只能由低到高,不能由高到低)

7.1 无锁状态

是java对象刚创建时还没有任何线程来竞争,说明该对象处于无锁状态,没有线程竞争它。

7.2 偏向锁

是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价,如果内置锁处于偏向锁状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要在做任务检查和切换。偏向锁在竞争不激烈的情况下效率非常高。其核心原理为:如果不存在线程竞争的一个线程获得锁,那么锁就进入偏向状态,,此时MarkWord的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。以后改线程获取锁时判断一下线程的Id和标志位,就可以直接进入同步块,连CAS操作都不需要,省去了大量有关锁的申请操作,从而也就提示了程序的性能。偏向锁的主要作用是消除无竞争情况下的同步原语,进一步提升程序性能,所以,在没有锁竞争的场合,偏向锁有很好的优化效果。但是,一旦有第二条线程需要竞争锁,那么偏向模式立即结束,进入轻量级锁的状态。偏向锁的缺点:如果锁对象时常被多个线程竞争,偏向锁就是多余的,并且其撤销的过程会带来一些性能开销,加锁的过程为:新线程只需要判断内置锁的Mark word中的线程ID是不是自己的id,如果是就直接使用这个锁,而不是使用CAS交换,如果不是,比如第一次获得锁时内置锁的线程ID为空。就使用CAS交换,新的线程将自己的线程ID交换到内置锁的mark word重,如果交换成功,就加锁成功。

7.3 轻量级锁,

当两个线程开始竞争这个锁对象时,情况就发生变化,不再是偏向锁了,锁会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁的线程,以便提供性能。目前轻量级锁主要有两种:普通自旋锁和自适应自旋锁,普通自旋锁指的是当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占用的锁线程释放之后,这个抢锁线程就会获得锁,自旋是原地执行一个什么都不干的空循环,默认的情况下是10次。自适应自旋锁就是等待线程空循环的自旋次数是不固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。轻量级锁也被称为非阻塞同步、乐观锁

7.4 重量级锁也称同步锁,

会让其他申请线程之间进入阻塞,性能降低。每个对象都会有一个监视器,监视器和对象一起创建、销毁。监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。本质上,监视器是一种同步工具,也可以说是一种同步机制,主要特点是:(1)同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。(2)协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。

7.5 synchronized执行过程

总结一下synchronized的执行过程,大致如下:
(1)线程抢锁时,JVM首先检测内置锁对象Mark Word中的biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
(2)在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中的线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
(3)如果Mark Word中的线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就将Mark Word中的线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码,此时内置锁对象处于偏向锁状态。
(4)如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
(5)JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
(6)如果JVM的CAS替换锁记录指针自旋失败,轻量级锁就膨胀为重量级锁,后面等待锁的线程也要进入阻塞状态。
总体来说,偏向锁是在没有发生锁争用的情况下使用的;一旦有了第二个线程争用锁,偏向锁就会升级为轻量级锁;如果锁争用很激烈,轻量级锁的CAS自旋到达阈值后,轻量级锁就会升级为重量级锁。

8.线程之间的通讯

8.1线程之间的通讯

是可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,可以避免无效的资源争夺。通讯的方式有很多中:等待-通知、共享内存、管道流。

8.2 wait()、notify()这两种是属于java对象实例的,而不是thread类的。

8.3 wait()方法

主要作用是让当前线程阻塞并等待被唤醒,wait()方法与对象的监视器紧密相关,使用wait()方法时,一定要放在同步块,核心原理为:
(1).当线程调用locko(某个同步锁对象)的wait()方法后,JVM会将当前线程加入locko监视器的waitset,等待被其他线程唤醒。
(2).当前线程会释放locko对象监视器的owner权利,让其线程可以抢夺locko对象的监视器
(3).让当前线程等待,器状态变为waiting

8.4 notify()方法

主要作用是唤醒在等待的线程,notify()方法与对象监视器紧密相关,调用notify()方法是也需要放到同步块中,核心原理为:
(1).当前线程调用locko(某个同步锁对象)的notify()方法后。JVM会唤醒locko监视器waitset中的第一条等待线程。
(2).当前线程调用locko的notifyAll()方法后,JVM会唤醒locko监视器waitset中的所有等待线程。
(3).等待线程被唤醒后,会从监视器的waitset移动到entryList,线程具备了排队抢夺监视器Owner权利的资格,器状态重waiting变成blocked。
(4).entryList中的线程抢夺到监视器的Owner权利之后,线程的状态从blocked变成runnable,具备重新执行的资格。
8.5 java的等待-通知机制是指:一个线程A调用了同步对象的wait()方法进入等待状态,而另一个线程B调用了同步对象的notify()或者notifyAll()方法通知等待线程,当线程A收到通知后,重新进入就行状态,准备开始执行