06. 多线程锁原理

一.锁的分类

1.什么是锁

锁 大门 很多陌生人(线程)就进不去,很安全;

如果没有锁,无法锁住这个大门,很多陌生人都可以进去,不安全;

人进去后,再锁住(线程进去后,锁定) 那么在外面的很多人就进不去了;

2.Java主流锁的分类

 

1>自旋锁

原理

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XXXX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。

自旋锁时间阈值

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要

JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

JDK1.6-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;

2>锁的状态

一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

偏向锁

引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁获取过程:

步骤1、 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。

步骤2、 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3

步骤3、 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4

步骤4、 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word

步骤5、 执行同步代码。

偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作; 

在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。

jvm开启/关闭偏向锁

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 

轻量级锁的加锁过程:

1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word

2、拷贝对象头中的Mark Word复制到锁记录中。

3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5

4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态

5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象。重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

3>不同锁的比较

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法相比,仅存在纳秒级的差距。

若线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景

轻量级锁

竞争线程不会阻塞,提高了程序响应速度

始终得不到锁竞争的线程,使用自旋会消耗CPU。

追求响应时间,同步块执行速度非常快

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量,同步块执行时间较长

4.synchronized 内置锁

线程开始运行,拥有自己的栈空间,如脚本一样按既定代码一步一步地执行,直至执行完毕,Java支持多线程同时访问一个对象或对象的成员变量,关键字synchronized /'sɪŋ krə naɪ zd/ 可以修饰方法或同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或同步块中,它保证了线程对变量访问的可见性和排他性,它为隐式锁(锁定,解锁)看不到,也改变不了,JDK 内置锁

1> synchronized 的实现原理

SynchronizedJVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnterMonitorExit指令来实现。

对同步块,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorentermonitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

synchronized使用的锁是存放在Java对象头里面,Java对象的对象头由 mark word klass pointer 两部分组成:

1mark word存储了同步状态、标识、hashcodeGC状态等等。

2klass pointer存储对象的类型指针,该指针指向它的类元数据

另外对于数组而言还会有一份记录数组长度的数据。

synchronized使用的锁是存放在Java对象头里面

具体位置是对象头里面的MarkWord,MarkWord里默认数据是存储对象的HashCode等信息,

但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式

synchronized和对象密切密切相关,在对象头中存放锁,如果添加到静态方法中,锁的是类的class对象,一个类只有一个class对象;

代码块:虚拟机指令 monitorenter {同步操作} monitorexit 排他性

同步方法: ACC_SYNCHRONIZED 将synchronized放到方法上

普通是对象锁是对象, 静态方法是锁的class对象

2> synchronized优化

引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析等技术来减少锁操作的开销。

采用轻量级锁(自旋), 和 偏向锁(无竞争时 判断线程ID); 根据竞争激烈程度会 偏向锁->轻量级锁->重量级锁

锁粗化(减少上下文切换(将相邻的同步代码块合并)), 锁消除(发现不会有资源竞争),逃逸分析;

monitor 锁的存放位置

对象头 类 类型指针 ClazzPoint 锁状态 对象的hashCode 对象分代年龄 偏向锁(线程ID) 锁标志位 数组(长度)

轻量级锁: 通过CAS操作来加锁和解锁 自旋锁(适应性自旋锁 由虚拟机动态调整) 指向栈中记录的指针,追求响应时间

重量级锁: 拿不到锁,将线程挂起,上下文切换,追求吞吐量

GC标记

偏向锁 : 1个锁,同一个线程, CAS不想做, 测试当前拥有这把锁的是不是自己,偏向与第一个访问锁的线程

在大多数情况下,锁被同一个线程多次获得,这就引入了偏向锁,即无竞争时不需要进行CAS操作来加锁与解锁;

当发生竞争时,要将偏向锁撤销,然后将锁提升到轻量级锁,启动stop world (stw)将全部线程停止,开始GC

逃逸分析

如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:

同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

锁消除和粗化

锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。

锁粗化:将临近的代码块用同一个锁合并起来。

消除无意义的锁获取和释放,可以提高程序运行性能。

3> synchronized的使用

synchronized 有以下三种用法

1.修饰代码块

2.修饰成员方法 this

3.修饰静态方法 Class

/**
 * synchronized 关键字的三种用法
 * 1.修饰代码块
 * 2.修饰成员方法 this
 * 3.修饰静态方法 Class
 * @author NorthStar
 * @date 2022/1/12 19:08
 */
class Count(var simpleOp: SynTest) : Thread() {
    override fun run() {
        for (i in 0..999) {
            simpleOp.incCount1() //count =count+1000
//            simpleOp.incCount2() //count =count+1000  
//            simpleOp.incCount3() //count =count+1000
        }
    }
}


class SynTest {
    var count: Long = 0
    var obj = Object() //作为一个锁


    /*用在同步块上*/
    fun incCount1() {
        synchronized(obj) {
            count++
        }
    }


    /*用在方法上*/
    @Synchronized
    fun incCount2() {
        count++
    }


    /*用在同步块上,但锁的是当前类的对象实例*/
    fun incCount3() {
        synchronized(this) {
            count++
        }
    }


    //重复进入锁, 可重入锁 在递归时可以反复来拿
    //synchronize 天生也是可重复锁
    private val lock: Lock = ReentrantLock()


    //如果多线程执行,如果不加锁,一定存在安全问题
    fun inc() {
        lock.lock() //显示锁
        try {
            count++
        } catch (e: Exception) { //一定会解锁
            lock.unlock() //开发者自己解锁
        }
    }
}
object SynClient {
    @JvmStatic
    fun main(args: Array<String>) {
        val simpleOp = SynTest()
        //启动两个线程
        val count1 = Count(simpleOp)
        val count2 = Count(simpleOp)
        count1.start()
        count2.start()
        Thread.sleep(50)
        println(simpleOp.count) //2000
    }
}


//修饰静态方法 , 或用类锁
public class GpsEngine {
    private volatile static GpsEngine gpsEngine;


    //持有一把锁,GpsEngine.class对象锁 ==类锁
    public synchronized static GpsEngine getInstance() {
        if (gpsEngine == null) {
            if (gpsEngine == null) {
                //Thread-0 CPU 执行器被操作系统调度 [暂停] [获得CPU执行权 恢复] 继续往下走 new GpsEngine
                gpsEngine = new GpsEngine();//Thread-1 执行一次 new GpsEngine
            }
        }
        return gpsEngine;
    }


    public static GpsEngine getInstance2() {
        if (gpsEngine == null) {
            //Thread-1 Thread-2 Thread-3 ...都可以进来
            //GpsEngine.class == 类锁
            synchronized (GpsEngine.class) {
                //Thread-1 进来也要判断一下对象是否已经初始化
                if (gpsEngine == null) {
                    //Thread-0 CPU 执行器被操作系统调度 [暂停] [获得CPU执行权 恢复] 继续往下走 new GpsEngine
                    gpsEngine = new GpsEngine();//Thread-1 执行一次 new GpsEngine
                }
            }
        }
        return gpsEngine;
    }
}

4>锁的实例不同,多线程可并行

/**
 * 锁的实例不一样,也是可以并行的
 * @author NorthStar
 * @date 2022/1/12 19:23
 */
class InstanceSyn(var diffInstance: DiffInstance) : Runnable{
    override fun run() {
        println("TestInstance is running... $diffInstance")
        diffInstance.instance()
    }
}


class Instance2Syn(var diffInstance: DiffInstance) : Runnable{
    override fun run() {
        println("TestInstance2 is running... $diffInstance")
        diffInstance.instance2()
    }
}
class DiffInstance {
    @Synchronized
    fun instance() {
        SleepTools.second(3)
        println("synInstance is going...$this")
        SleepTools.second(3)
        println("synInstance ended $this")
    }


    @Synchronized
    fun instance2() {
        SleepTools.second(3)
        println("synInstance2 is going...$this")
        SleepTools.second(3)
        println("synInstance2 ended $this")
    }
}


object DiffInstanceClient{
    @JvmStatic
    fun main(args: Array<String>) {
        val instance1 = DiffInstance()
        val t3 = Thread(InstanceSyn(instance1))
        val instance2 = DiffInstance()
        val t4 = Thread(Instance2Syn(instance2))
        t3.start()
        t4.start()
        SleepTools.second(1)
    }
}

5> 错误的加锁和原因分析

/**
 * 错误的加锁和原因分析
 * @author NorthStar
 * @date 2022/1/12 21:06
 */
class Worker(var i: Int) : Runnable{
    override fun run() {
//        synchronized(i){ 不能用i 锁 因为i 如果变化会重新new对象, 这是对象地址发生变化. 锁就会失效
        synchronized(this){
            val thread = Thread.currentThread()
            //identityHashCode 近似为内存地址
//            println("${thread.name} --@ ${System.identityHashCode(i)}")
            i++ // Integer.valueOf(i) return new Integer(i);
            println("${thread.name} ----$i-@${System.identityHashCode(i)}")
            Thread.sleep(1000)
//            println("${thread.name} ----$i-@${System.identityHashCode(i)}")
        }
    }
}
object TestIntegerSyn {
    @JvmStatic
    fun main(args: Array<String>) {
        val worker = Worker(1)
        for (i in 0..4) {
            Thread(worker).start()
        }
    }
}

6>修饰普通与静态方法区别

对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

7>与ReentrantLock的区别

5.ThreadLocal

ThreadLocal多线程数据安全原理:每个线程都有变量的副本,实现线程的隔离:

1> 与Synchonized的比较

ThreadLocal和Synchonized都用于解决多线程并发訪问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

2> ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

• void set(Object value) //设置当前线程的线程局部变量的值。

 public Object get() //该方法返回当前线程所对应的线程局部变量。

 public void remove() //将当前线程局部变量的值删除,目的是减少内存的占用,该方法是JDK5.0新增的方法。需指出的是当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

 protected Object initialValue() //返回该线程局部变量的初始值,该方法是一个protected的方法,是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

RESOURCE代表一个能够存放String类型的ThreadLocal对象。不论哪个线程能够并发访问这个变量,对它进行写入读取操作,都是线程安全的。

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMapThreadLocal的静态内部类,

然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。

看下ThreadLocal的内部类ThreadLocalMap源码:

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。

回顾我们的get方法,其实就是拿到每个线程独有的ThreadLocalMap

然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作。

代码示例

package com.xzh.study.thread.ch1.threadlocal


import com.xzh.study.thread.ch1.threadlocal.UseThreadLocal
import kotlin.jvm.JvmStatic


/**
 * 类说明:演示ThreadLocal的使用, 使用变量的副本
 */
class UseThreadLocal {
    //运行3个线程
    fun startThreadArray() {
        val runs = arrayOfNulls<Thread>(3)
        for (i in runs.indices) {
            runs[i] = Thread(TestThread(i))
        }
        for (i in runs.indices) {
            runs[i]!!.start()
        }
    }


    /**
     * 类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
     */
    class TestThread(var id: Int) : Runnable {
        override fun run() {
            val name = Thread.currentThread().name
            println("$name:start")
            var s = intLocal.get()
            s += id
            intLocal.set(s)
            println("$name : ${intLocal.get()}")
            //intLocal.remove();
        }
    }


    companion object {
        private val intLocal: ThreadLocal<Int> = object : ThreadLocal<Int>() {
            override fun initialValue(): Int {
                return 1 //初始化 intLocal.get()的值
            }
        }
        
        @JvmStatic
        fun main(args: Array<String>) {
            val test = UseThreadLocal()
            test.startThreadArray()
        }
    }
}


//3.ThreadLocal(线程本地变量) 线程隔离
  public class TestThreadLocal {
      static ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
          @Override
          protected String initialValue() {
              return "雄霸";
          }
      };


      //TODO 线程一
      private static class StudentThread extends Thread {
          @Override
          public void run() {
              super.run();
              //copy副本 set 步惊云  都是我的副本  副本==StudentThread
              String threadName = currentThread().getName();
              threadLocal.set("步惊云");//私有的
              //线程一 setA 我在线程二种所获取的所有内容都是 A
              System.out.println("threadName" + threadName + "get" + threadLocal.get());
              //get() 先拿到 当前线程 StudentThread Map===threadLocalMap.get(k=当前线程)=值
              //set(value) 拿到到 当前线程 StudentThread Map===threadLocalMap.set(k=当前线程, value)
          }
      }


      //TODO 线程二
      private static class PersonThread extends Thread {
          @Override
          public void run() {
              super.run();
              String threadName = currentThread().getName();
              System.out.println("threadName" + threadName + "get" + threadLocal.get());
          }
      }
      public static void main(String[] args) {
          new StudentThread().start();
          new PersonThread().start();
      }
  }

3> ThreadLocal的内存泄漏

key: 为 currentThread 虚引用, key

二.CAS(Compare And Swap)基本原理

1.什么是原子操作

假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,

要么完全不执行B,那么A和B对彼此来说是原子的;Java内存模型(JMM)

2.实现原子操作可使用锁机制

满足基本的需求是没有问题的,但是有的时候我们的需求并非那么简单,我们需要更有效,更灵活的机制,

synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一个资源的其他线程需要等待,

直到该线程释放锁; synchronized就是原子操作, 但synchronize 加锁机制会造成CPU的上下文切换,资源消耗大;

上下文切换说的是CPU在执行不同任务之间的切换过程叫做上下文切换(线程状态变更就是一次上下文切换)。

上下文切换是需要耗费时间的,这就是我们在并发编程中要考虑的情况若是上下文切换时间太长那么多线程反而变慢了。

3.CAS()指令

Compare And Swap 比较且交换 无锁化

每个CAS操作过程都包含三个运算符: 一个内存地址V,一个期望的值A一个新值B,

操作时如果地址v上存放的值等于这个期望的值A,则将地址V上的值赋值为B,否则不做任何操作;

CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿

如果内存中的变量变了,需要重新获取循环这个指令,成功为止 自旋(死循环)

4.线程同步,并发操作怎么控制

Java中可在方法名前加关键字synchronized来处理当有多个线程同时访问共享资源时候的问题。synchronized相当于一把锁,

当有申请者申请该资源时,如果该资源没有被占用,那么将资源交付给这个申请者使用,在此期间.其他申请者只能申请而不能

使用该资源,当该资源被使用完成后将释放该资源上的锁,其他申请者可申请使用。

5.并发控制的目的

主要是为了多线程操作时带来的资源读写问题。如果不加以空间可能会出现死锁,读脏数据、不可重复读、丢失更新等异常,

并发操作可以通过加锁的方式进行控制,锁又可分为乐观锁和悲观锁。

悲观锁(synchronized):

悲观锁并发模式假定系统中存在足够多的数据修改操作,以致于任何确定的读操作都可能会受到由个别的用户所制造的数据

修改的影响。也就是说悲观锁假定冲突总会发生,通过独占正在被读取的数据来避免冲突。但是独占数据会导致其他进程

无法修改该数据,进而产生阻塞,读数据和写数据会相互阻塞。就会发生上下文切换,(5000~20000) 3~5ms 效率低

乐观锁(CAS):

乐观锁假定系统的数据修改只会产生非常少的冲突,也就是说任何进程都不大可能修改别的进程正在访问的数据。

乐观并发模式下,读数据和写数据之间不会发生冲突,只有写数据与写数据之间会发生冲突。即读数据不会产生阻塞,

只有写数据才会产生阻塞。0.6ns 效率高 AtomicBoolean AtomicInteger

CAS的问题:

1>ABA问题

A---B---A 要求每个线程操作该值时要带上版本戳, AtomicStampedReference AtomicMarkReference

动过几次 是否被动过

2>开销问题CPU要不停重试

3> Jdk中原子操作类的使用

更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong

更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference 版本戳问题

4>只保证一个共享变量的原子操作,比较内存中某个变量的值 一个地址修改一个变量;

AtomicInteger ai = new AtomicInteger();
ai.addAndGet(24);//累加后的值
ai.getAndAdd(24);//累加前的值

三.AQS(队列同步器)

1.学习AQS的必要性

队列同步器 AbstractQueuedSynchronizer(以下简称同步器 或AQS),使用了构建锁或其他同步组件的基础框架,

它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,它是实现大部分同步需求的基础.

2.AQS的使用方式

AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里由一个int型的state来代表这个状态,

在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:

锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;

同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

3.AQS中的方法

实现自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法同步器提供的模板方法基本上分为3类:

独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

1> 模板方法

public class ReenterSelfLock implements Lock {

private static class Sync extends AbstractQueuedSynchronizer {//静态静态内部类,自定义同步器 AQS

protected boolean isHeldExclusively() {}//是否处于占用状态 private volatile int state;

public boolean tryAcquire(int acquires) {}// 当状态为0时获取锁

protected boolean tryRelease(int releases) {}//释放锁,将状态设置为0

Condition newCondition() {}//返回一个Condition 每个condition都包含一个condition队列

}

2> 可重写的方法

3> 访问或修改同步状态的方法

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

getState():获取当前同步状态。

setState(int newState):设置当前同步状态。

compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。 

4.CLH队列锁

AQS的基本思想CLH队列锁Craig, Landin, and Hagersten (CLH) locks。也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

QNode

myPred locked 排队链表队列 TA <---CAS--线程A

前驱结点 排队列表队列 TA<--TB <---CAS--线程B 自旋检测myPred(前驱结点) 是否释放锁 自旋2次

当一个线程需要获取锁时

1. 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用

2. 线程Atail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred

线程B需要获得锁,同样的流程再来一遍

3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)

4.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点

如上图所示,前驱结点释放锁,线程AmyPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是OL+n),n个线程有nmyNodeL个锁有Ltail)。CLH队列锁常用在SMP体系结构下。Java中的AQSCLH队列锁的一种变体实现。

5.ReentrantLock原理

线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronizedReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。底层则是利用了JUC中的AQS来实现的.

//显示锁 ReentrantLock 程序员可以控制
  private int count = 0;
//重复进入锁, 可重入锁 在递归时可以反复来拿
//synchronize 天生也是可重复锁
  private Lock lock = new ReentrantLock();
//如果多线程执行,如果不加锁,一定存在安全问题
private void inc(){
   lock.lock();
   try {
       count++;
   } catch (Exception e) {//一定会解锁
       lock.unlock();//开发者自己解锁
   }
}


1> 锁的可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。

2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,若是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。

如果该锁被获取了n次,那么前(n-1)tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

int state;



/*当状态为0时获取锁*/

@Override

public boolean tryAcquire(int acquires) {

    //没有占用compareAndSetState会返回true

    if (compareAndSetState(0, 1)) {//CAS 原子操作 比较替换

        setExclusiveOwnerThread(Thread.currentThread());//排他线程

        return true;//拿到锁

    } else if (getExclusiveOwnerThread() == Thread.currentThread()) { //判断是否是重入锁

        setState(getState() + 1); //是重入锁, 持锁状态 + 1

        return true;

    }

    return false;

}



/*释放锁,将状态设置为0*/

@Override

protected boolean tryRelease(int releases) {

    if (getExclusiveOwnerThread() != Thread.currentThread())

    throw new IllegalMonitorStateException();

    if (getState() == 0) throw new IllegalMonitorStateException();

    setState(getState() - 1); //每释放一次锁将当前持锁状态 -1;

    if (getState() == 0) { //当减到0时, 彻底释放锁,可让其他线程获取锁

        setExclusiveOwnerThread(null);

    }

    return true;

}

2> 公平和非公平锁

ReentrantLock的构造函数中,默认的无参构造函数将会把Sync对象创建为NonfairSync对象,这是一个“非公平锁”;而另一个构造函数ReentrantLock(boolean fair)传入参数为true时将会把Sync对象创建为“公平锁”FairSync

nonfairTryAcquire(int acquires)方法,对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire方法,该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

/**
 * 实现我们自己的独占锁,不可重入
 */
public class ReenterSelfLock implements Lock {
    //静态静态内部类,自定义同步器 AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        //是否处于占用状态 private volatile int state;
        @Override
        protected boolean isHeldExclusively() {
            return getState() > 0;
        }


        //当状态为0时获取锁
        @Override
        public boolean tryAcquire(int acquires) {
            //没有占用compareAndSetState会返回true
            if (compareAndSetState(0, 1)) {//CAS 原子操作 比较替换
                setExclusiveOwnerThread(Thread.currentThread());//排他线程
                return true;//拿到锁
            } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
                setState(getState() + 1);
                return true;
            }
            return false;
        }


        //释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int releases) {
            if (getExclusiveOwnerThread() != Thread.currentThread())
                throw new IllegalMonitorStateException();
            if (getState() == 0) throw new IllegalMonitorStateException();
            setState(getState() - 1);
            if (getState() == 0) {
                setExclusiveOwnerThread(null);
            }
            return true;
        }


        //返回一个Condition 每个condition都包含一个condition队列
        Condition newCondition() {
            return new ConditionObject();
        }
    }


    //仅需要将操作代理到sync上即可
    private final Sync sync = new Sync();


    @Override//获取锁
    public void lock() {
        System.out.println(Thread.currentThread().getName() + " ready get lock");
        sync.acquire(1);//AQS内部调用了tryAcquire()
        System.out.println(Thread.currentThread().getName() + " ready got lock");
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }


    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }


    @Override
    public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }


    @Override//释放锁
    public void unlock() {
        System.out.println(Thread.currentThread().getName() + " ready release lock");
        sync.release(1);
        System.out.println(Thread.currentThread().getName() + " ready release lock");
    }


四.死锁

1. 死锁定义

死锁 是指两个或两个以上的进程在执行过程中,由于竞争资源或彼此通信i造成的进程不能向前推进 的 阻塞僵死状态;

产生原因:竞争共享资源且分配资源的顺序不当;T1占R1,T2占R2,T1要R2,T2要R1死锁状态的线程不会理会中断'

2. 死锁产生的(原因和必要条件)

(只有四个条件 同时满足 时才会产生死锁)

*1.互斥条件 : 资源只能供一个进程用 (多个操作者竞争一个资源) 资源数<操作者数

*2.请求和保持条件: 每个进程不放弃请求互斥资源,且不放弃已有的资源

*3.不剥夺条件: 不能剥夺其他进程的互斥资源

*4.环路等待条件:在发生死锁时,必然存在一个进程申请资源的环形链;

只要打破四个必要条件之一就能有效预防死锁的发生

*1.打破互斥条件: 改造独占性资源为虚拟资源,大部分资源已无法改造;

*2.打破不可抢占条件: 当一进程占有一独占资源后又申请一独占性资源而无法满足,则退出占有的资源;

*3.打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请

*4.打破循环等待条件: 实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源;

3. 处理死锁方法

预防,避免,检验,解除,忽略

4. 银行家算法 (Dijkstra 迪杰斯特拉)

是一种能够避免死锁的资源分配算法,其基本思想是一个进程提出资源请求后,

系统先进行 资源试分配 ,然后 检测本次的试分配,是否使系统处于安全状态,

若安全则按试分配方案分配资源,否则不分配资源;该算法需要 数据结构 的支持;

need[] 还需要的数量

max[] 最大需求

allocation[] 已分配的资源数量

need=max-allocation;

needi<=work;

进程名称 allocation已分配 Max最大需求 Need还需要 Available可用

(A B C) 已知 (A B C)已知 (A B C) (A B C)已知

p0 2 0 0 3 2 2 1 2 2 3 3 2

对work和finish初始化->false为没执行 true为已执行, work=available=(3,2,2);//work -> A,B,C各可用资源数

因为 finish[0]=false 且 need0<work->finish1=true, 可释放; 还需要的和可用的做比较, need<=work 就不死锁;

5. 死锁检验

S为死锁状态的充分条件:是当且仅当S状态的 资源分配图 是不可完全简化的;

6. 死锁的危害

1) 线程不工作了,但整个程序还活的,没有任何的异常信息可以供我们检查;

2) 一旦程序发生了死锁,是没有任何办法恢复的,只能重启程序,对正式已发布程序来说,是严重问题

7. 死锁的解决关键

关键是保证拿锁的顺序一致

1)内部通过顺序比较,确定拿锁的顺序;

2)采用尝试拿锁的机制;

//尝试拿锁解决死锁问题
public class TryLock {
    private static Lock No1 = new ReentrantLock();//第一个锁
    private static Lock No2 = new ReentrantLock();//第二个锁


    //先尝试拿No1锁,在尝试拿No2锁,No1没拿到,连同No2锁一起释放掉
    private static void firstToSecond() {
        Thread thread = Thread.currentThread();
        String threadName = thread.getName();
        Random r = new Random();
        while (thread.isInterrupted()) {
            if (No1.tryLock()) {
                System.out.println(threadName + "get No1");
                try {
                    if (No2.tryLock()) {
                        try {
                            System.out.println(threadName + "get No2");
                            System.out.println("firstToSecond do work");
                            break;
                        } finally {
                            No2.unlock();
                        }
                    }
                } finally {
                    No1.unlock();
                }
            }
            sleep(thread, r);
        }
    }


    //先尝试拿No2锁,在尝试拿No1锁,No1没拿到,连同No2锁一起释放掉
    private static void secondToFirst() {
        Thread thread = Thread.currentThread();
        String threadName = thread.getName();
        Random r = new Random();
        while (thread.isInterrupted()) {
            if (No2.tryLock()) {
                System.out.println(threadName + "  get No2");
                try {
                    if (No1.tryLock()) {
                        try {
                            System.out.println(threadName + "  get No1");
                            System.out.println("secondToFirst do work");
                            break;
                        } finally {
                            No1.unlock();
                        }
                    }
                } finally {
                    No2.unlock();
                }
            }
            sleep(thread, r);
        }
    }


    private static void sleep(Thread thread, Random r) {
        try {
            Thread.sleep(r.nextInt(3));
        } catch (InterruptedException e) {
            thread.interrupt();
        }
    }


    //子线程
    private static class TestThread extends Thread {
        private String name;


        public TestThread(String name) {
            this.name = name;
        }


        @Override
        public void run() {
            Thread.currentThread().setName(name);
            secondToFirst();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        //主线程
        Thread.currentThread().setName("mainThread");
        TestThread subThread = new TestThread("subThread");
        subThread.start();
        firstToSecond();


    }
}


public class NormalDeadLock {
    private static Object No1=new Object();//第一个锁
    private static Object No2=new Object();//第二个锁
    //拿第一个锁的方法
    private static void jamesDo() throws InterruptedException{
        String threadName = Thread.currentThread().getName();
        synchronized (No1) {
            System.out.println(threadName + "  get No1");
            Thread.sleep(100);
            synchronized (No2) {
                System.out.println(threadName+"  get No2");
            }
        }
    }


    //拿第二个锁的方法
    private static void lisoDo() throws InterruptedException{
        String threadName = Thread.currentThread().getName();
        synchronized (No2) {
            System.out.println(threadName + "  get No2");
            Thread.sleep(100);
            synchronized (No1) {
                System.out.println(threadName+"  get No1");
            }
        }
    }


    //子线程
    private static class TestThread extends Thread{
        private String name;
        public TestThread(String name) {
            this.name = name;
        }


        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                jamesDo();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        //主线程
        Thread.currentThread().setName("mainThread");
        TestThread subThread = new Lance("subThread");
        subThread.start();
        lisoDo();
    }
}

8. 活锁

在尝试拿锁的机制中,发生多线程间相互谦让,致同一线程总拿到同一把锁,在尝试拿另一把锁时因拿不到而将本已持有锁释放的过程;

9. 线程饥饿

低优先级的线程,总是拿不到执行时间;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值