JUC编程

目录

一.计算机的组成

 二.缓存行对齐,伪共享​编辑

三.缓存行填充的编成技巧

四.有序性带来的this溢出问题

五.有序性带来的半程对象问题

 六.线程和锁

​1.Synchnoized

1.1 特性

1.2 注意事项

1.3 同步代码块与同步方法的使用注意事项

1.4 synchronized 原理 (重量级锁 JDK1.6以前)

1.4.1 对象头

1.4.2 Mark Word

1.4.3 重量级锁JVM层面的实现原理:monitor

(1)monitor结构

(2)monitor的获取和释放(重量级锁的获取和释放)

(3)同步方法和同步代码块的实现原理

1.4.4 重量级锁OS层面的实现原理:互斥锁

1.5 synchronized 优化(JDK1.6开始)

1.5.1 锁升级

1.5.1.1 偏向锁

1.5.1.2 轻量级锁

1.5.1.3 自旋锁

1.5.1.4 偏向锁、轻量级锁、重量级锁的比较

1.5.1.5  对象信息存在哪

1.5.2 锁消除

1.5.3 锁粗化 

1.6 synconised(重量级锁)内部(锁升级的过程)

 2.CAS

七.对象的内存布局:

八.锁降级的过程

九.Volatile 

十.线程

1.线程安全

2.互斥同步

十一.java中得四种引用类型,强软弱虚

十二.ThreadLocal

十三:云相关提纲

十四. 各种锁

十五.线程池

十六.JUC同步锁



一.计算机的组成

1.CPU:计算

   内存:存储数据和程序

2.缓存行填充的编成技巧

cpu和内存之间,一般分为三级缓存L1、L2、L3,比如一个双核的cpu,那么L1和L2在cpu内核里,L3的数量与cpu数量一样,多个cpu共享内存

 二.缓存行对齐,伪共享

三.缓存行填充的编成技巧

缓存一致性协议(硬件级别协议):当cpu中得一个值改了之后,要通知其他cpu

当创建两个x的时候,这两个x永远不可能与其他有效数据位于同一行,所以当一个cpu的值修改的时候,并不需要通知其他cpu,这时候大大提高cpu 的运行速度。

四.有序性带来的this溢出问题

 3.并发编程的三大特性:可见性(线程内部数据修改完值,如何让其他人可见)、有序性、原子性

有序性:

(1)Object ob=new Object();

问题一:创建对象的步骤:申请内存空间,赋默认值,调用构造方法,建立关联

DCL:double check lock

双重检查:1.如果把第一个if去掉,那么必须先加锁,才知道对象是否创建,如果有1w个线程,那么会非常消耗资源,加上的话,第一个线程为空,然后等暂停,如果这时候第二个对象来了,加锁,然后创建对象,这时候第一个线程的第二个if不通过,不会创建第二个对象。

五.有序性带来的半程对象问题

问题二:DCL 单例模式需不需要volatile?需要,禁止指令重排

volatile只能保证有序性、可见性、不能保证原子性,也就不能保证线程安全。

jvm有四种内存屏障:LoadLoad(读读之间有LoadLoad不可以指令重排),StoreStore,LoadStore,StoreLoad

 下列图中,7和4 乱序执行了,正常应该先4后7,发生了指令重排

在单线程最终结果一致的时候,会发生指令重排

 

第3题:

 第7题:可以答16个字节

第8个问题:虚函数表

class对象既在堆,又在方法区

 六.线程和锁

 任何对象都可以当成锁来用

1.Synchnoized

1.1 特性
  • 原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。

  • 可见性: 保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。

  • 有序性: synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。

  • 悲观锁: synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。

  • 独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。

  • 非公平锁: synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。

  • 可重入锁: synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。

1.2 注意事项
  • 普通同步方法,锁是当前实例对象this:public synchronized void fun(){ }
  • 静态同步方法,锁是当前类的Class对象:public synchronized static void fun(){ }
  • 同步代码块中,锁为syncronized括号中配置的对象:synchronized (SyncTest.class) { }
  • 静态同步方法的锁对象是当前类的Class对象,类被装载后,在内存中有且仅有一个该类的Class对象,因此 可以保证静态同步方法的锁对象是同一个
  • 只有同步代码块与同步方法的锁对象是同一个对象时,才能保证同一时刻,只有一个线程访问共享资源。

Tips:

按锁的性质可以将锁分为以下几种类别:

  1. 悲观锁 or 乐观锁:是否一定要锁

  2. 共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁

  3. 公平锁 or 非公平锁:是否按阻塞顺序拿锁

  4. 可重入锁 or 不可重入锁:拿锁线程是否可以多次拿锁

1.3 同步代码块与同步方法的使用注意事项

(1) 只有同步代码块与同步方法的锁对象是同一个对象时,才能保证同一时刻,只有一个线程访问共享资源。

class SyncWrong implements Runnable {

    private static int i = 0;
  
    private synchronized void add() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(new SyncWrong());
        Thread t2 = new Thread(new SyncWrong());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

可以看到,虽然我们对 add方法加了锁,但依然不能保证输出结果的正确性。

这段代码违反了上述规则,add方法是普通同步方法,因此该方法的锁对象是当前实例对象this,主函数中的 t1、t2 线程传入了不同的SyncWrong 对象,因此,synchronized 锁对象不是同一个,自然就无法达到同步的目的。解决办法就是让 synchronized 锁对象是同一个。

解决方式一:只创建一个SyncWrong对象

//此时,synchronized 锁对象 就是 wrong 这个对象了
public static void main(String[] args) throws Exception {

		SyncWrong wrong = new SyncWrong();
		
        Thread t1 = new Thread(wrong);
        Thread t2 = new Thread(wrong);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
 }

解决方式二:将add方法改为静态方法

//静态同步方法的锁对象是当前类的Class对象,类被装载后,在内存中有且仅有一个该类的Class对象,
//因此 可以保证静态同步方法的锁对象是同一个
private synchronized static void add() {
    	i++;
}

(2)  每个访问共享资源的代码都必须被同步

class Resource{

    private static int i = 0;

    public synchronized void add() {
        i++;
    }
    
    public void reduce() {
        i--;
    }
}

对于上述代码,add 和 reduce 都对共享资源 i 进行了处理,你必须同步这两个方法(将 reduce 改造为同步方法)。如果不这样做,那么reduce()方法就会完全忽略这个对象锁,也就是在当前线程获得到锁,执行add操作时,其他线程可以随意的调用 reduce方法,从而导致并发修改的问题。

(3) 私有化共享资源

共享资源一般以对象形式存在于内存中,因此要控制共享资源的访问

class Resource{

    public static int i = 0;

    public synchronized void add() {
        i++;
    }
    
	public synchronized void get() {
        return i;
    }
}

上述代码的问题在于任何线程都可以通过 Resource.i 来获取共享变量的值,若当前线程获得锁,正在执行add同步方法的内容,其他线程通过 Resource.i 则会忽略锁,可能获取到中间结果。正确的方法是将 共享资源 i 私有化(用 private 修饰), 然后提供一个被同步的获取方式。

1.4 synchronized 原理 (重量级锁 JDK1.6以前)
1.4.1 对象头

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头( Header )、实例数据( Instance Data ) 和对齐填充( Padding )。

HotSpot虚拟机的一般对象头(Header)包括两部分信息:“Mark Word”、“Class Pointer”,数组类对象还包括“Array Length”。

  • Mark Word:存储对象自身的运行时数据
  • Class Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • Array Length:如果是数组对象,则 Array Length 存储数组的长度。

Tip:

  • 对齐填充( Padding ):Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的整数倍。若Java对象的bytes数不是8的整数倍,则会填充一些字节,让Java对象占用的空间是8的整数倍。
1.4.2 Mark Word

synchronized底层实现与Java对象头中的Mark Word密不可分的。Java对象头里的Mark Word存储对象自身的运行时数据,如 HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

32位JVM在不同状态下Mark Word的组成如下表:

64位JVM在不同状态下Mark Word的组成如下表:

  • unused:就是表示没有使用的区域,在64位虚拟机的对象头中会出现。

  • hash:对象的hashcode,如果对象没有重写hashcode()方法,那么通过System.identityHashCode()方法获取。采用延迟加载技术,不会主动计算,但是一旦生成了hashcode,JVM会将其记录在 Mark Word 中。

  • age:4位的Java对象GC年龄。对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15。最大值也是15。

  • biased_lock:1位的偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

  • lock:2位的锁状态标记位,其中无锁和偏向锁的锁标志位都是01,只是在前面的1 bit的biased_lock区分了这是无锁状态还是偏向锁状态。lock和biased_lock共同表示对象处于什么状态:

  • thread Id:持有偏向锁的线程ID。

  • epoch:偏向锁的时间戳。

  • Lock record address:轻量级锁状态下,指向栈中锁记录的指针。

  • Monitor address:重量级锁状态下,指向对象监视器Monitor的指针。

1.4.3 重量级锁JVM层面的实现原理:monitor

Monitor是一种用来实现同步的工具,又称为对象监视器/管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

注意:JDK1.6之后对synchronized 进行了优化,在重量级锁的基础上,又引入了偏向锁和轻量级锁,但只有重量级锁的实现是和monitor相关的。

(1)monitor结构

在HotSpot虚拟机中,最终采用C++的ObjectMonitor类实现monitor。

ObjectMonitor C++ 结构:

ObjectMonitor() {
   //成员变量简单的初始化
    _header       = NULL;  //markOop对象头,重量级锁储存锁对象头信息的地方
    _count        = 0;
    _waiters      = 0,  //等待线程数
    _recursions   = 0;   //锁的重入次数,作用于可重入锁
    _object       = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _owner        = NULL; //指向持有ObjectMonitor对象的线程地址
    _WaitSet      = NULL; //处于wait状态的线程,会被包装成ObjectWaiter,加入到_WaitSet集合(调用wait方法)
    _WaitSetLock  = 0 ;  // 保护等待队列,作用于自旋锁。
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  //阻塞在EntryList上的最近可达的的线程列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block阻塞状态的线程,会被包装成ObjectWaiter,加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
OwnerIsThread = 0 ;
    _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  }

ObjectMonitor 中 几个比较重要的字段:

  • _WaitSet :处于wait状态的线程,会被加入到_WaitSet。
  • _EntryList:处于等待锁block状态的线程,会被加入到_EntryList。
  • _owner:指向持有ObjectMonitor对象的线程。
  • _count:锁计数器,获取锁则 count+1,释放锁 - - count-1,count直到为0,才可以被其他线程持有。
  • _recursions:记录锁的重入次数。
(2)monitor的获取和释放(重量级锁的获取和释放)

对于重量级锁,获取锁的过程实际上就是获取monitor的过程,释放锁的过程实际上就是释放monitor的过程。 monitor的竞争获取是在ObjectMonitor的enter方法中,而释放则是在exit方法中。

  1. 通过CAS尝试把monitor的_owner字段设置为当前线程,然后对_count++,若在设置_owner为当前线程时,发现原来的_owner指向当前线程,则说明当前线程再次进入monitor,还应让_recursions++;

  2. 如果获取锁失败,则当前线程加入_EntryList,等待锁的释放;

已获取锁的线程执行wait操作,则 _count–,_recursions–,设置_owner 为 null,然后当前线程加入 _WaitSet中,等待被唤醒。

已获取锁的线程通过 notify 唤醒 _WaitSet中的线程,被唤醒的线程继续竞争锁。

已获取锁的线程执行完同步代码块时,释放锁(ObjectMonitor.exit),_count–,_recursions–,若 _count = 0 ,则设置 _owner 为 null,然后唤醒 _EntryList竞争锁(竞争是非公平的)。

Tip:

  • Java中的wait、notify、notifyAll等方法依赖于ObjectMonitor对象内部方法来完成,这也就是为什么要在同步方法或同步代码块中调用他们的原因(需要先获取对象的锁),否则就会抛出 IllegalMonitorException 。

  • 由于每个Java对象都可以关联一个Monitor对象,所以任何对象都是可以是锁对象,而基本数据类型不是对象,所以不能作为锁。

(3)同步方法和同步代码块的实现原理

同步代码块原理

public class SyncTest {

    public static void main(String[] args) {

        synchronized (SyncTest.class) {
        
        }
    }
}

对上面同步代码块编译:javac -encoding UTF-8 SyncTest.java

然后反汇编:javap -v SyncTest.class

得到字节码文件如下:

同步语句块的实现使用的是monitorenter 和 monitorexit指令:

monitorenter指令指向同步代码块的开始位置
monitorexit指令则指明同步代码块的结束位置。
为了保证方法异常结束时正确释放锁,编译器会自动产生一个异常处理器表,当发生异常时进行处理,确保该线程能正确的执行monitorexit指令。

同步方法原理

public class SyncTest {

    public synchronized void fun(){
    
    }
}

 反汇编后得到的字节码文件内容:

同步方法通过ACC_SYNCHRONIZED来标志一个方法是同步方法。

当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后在无论方法是否正常完成,都会释放monitor。

1.4.4 重量级锁OS层面的实现原理:互斥锁

重量级锁是依赖对象内部的monitor锁来实现的,而monitor又是依赖操作系统的MutexLock(互斥锁、互斥量)来实现的。

(1)互斥锁

互斥锁(互斥量)是一个二元变量,其状态为0/1,0表示未加锁,1表示已加锁。

  • 访问公共资源前,必须申请该互斥锁,若处于未加锁状态,则申请到锁对象,并立即占有该锁。

  • 如果该互斥锁处于锁定状态,则阻塞当前线程。

  • 持锁线程使用完共享资源,释放共享资源,将互斥锁置0,并唤醒被阻塞的线程。

(2)重量级锁开销大的原因

在JDK1.6之前,synchronized属于重量级锁,效率低下,因为Monitor是依赖于底层的操作系统的互斥原语mutex来实现,JDK1.6之前实际上加锁时会调用Monitor的enter方法,解锁时会调用Monitor的exit方法。

多个线程竞争monitor时,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙完成,这会导致线程在 “用户态 和 内核态” 之间来回切换,这个状态之间的转换需要相对比较长的时间,对性能有较大影响。

1.5 synchronized 优化(JDK1.6开始)

JDK1.6开始,对synchronized 进行了很多方面的优化,如:适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。

1.5.1 锁升级

JDK1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在JDK 1.6中,锁对象一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。

**锁可以升级但不能降级,**这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

Tip:实际上锁也是可以发生降级的,只不过锁降级的条件比较苛刻。而且锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。

1.5.1.1 偏向锁

偏向锁指,锁偏向于第一个获取他的线程,若接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。

目的:消除数据在无竞争情况下的同步,也就是说优化了只有一个线程执行 同步代码块 时的同步操作。
原理:CAS

(1)偏向锁的获取

  1. 检测锁对象的MarkWord是否为可偏向状态,即是否为偏向锁标识1,锁标识位为01;
  2. 如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。若为不可偏状态,直接升级为轻量级锁,进入轻量级锁逻辑。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

(2)偏向锁的释放 

偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

(3)偏向锁的撤销(升级)

偏向锁释放失败(其他线程通过CAS替换原来线程的ThreadID失败),说明当前存在多线程竞争锁的情况,则开始进行偏向锁的撤销(升级)。

  1. 对持有偏向锁的线程进行撤销时,需要等待到达全局安全点。当到达全局安全点(safepoint,代表了一个状态,在该状态下所有线程都是暂停的)时,获得偏向锁的线程被挂起。

  2. 如果获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,不存在锁竞争,那么这个时候争抢锁的线程可以基于 CAS 重新偏向当前线程,此过程称为重偏向。

  3. 如果获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,存在锁竞争,这个时候会把原获得偏向锁的线程升级为轻量级锁(标志位为“00”),并将指向当前线程的锁记录地址的指针放入对象头Mark Word,后唤醒持有锁的当前线程,进入轻量级锁的竞争模式。

注意:其他情况下的偏向锁撤销

  • 当对象计算过一致性哈希吗后(Object::hashCode() 或 System::identityHashCode(Object)方法的调用,重写的hashCode方法则不会),立即撤销偏向锁。
  • 调用 wait/notify 方法

(4)批量重偏向、批量撤销

  • 批量重偏向:当一个锁对象类的撤销次数达到20次时,虚拟机会认为这个锁不适合再偏向于原线程,于是会在偏向锁撤销达到20次时让这一类锁尝试偏向于其他线程。

  • 批量撤销:当一个锁对象类的撤销次数达到40次时,虚拟机会认为这个锁根本就不适合作为偏向锁使用,因此会将类的偏向标记关闭(0),之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。

(5)偏向锁的关闭

偏向锁在JDK1.6、JDK1.7中是默认启用的,并且它在应用程序启动后几秒钟才被激活。

  • JVM参数关闭延迟:-XX:BiasedLockingStartupDelay=0
    如果你确定应用程序里所有的锁通常情况下处于竞争状态,则可以用以下参数关闭偏向锁,那么程序默认会进入轻量级锁状态:

  • JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false

1.5.1.2 轻量级锁

轻量级锁是JDK1.6时加入的新型锁机制,“轻量级” 是相对于使用操作系统的 互斥量 实现的传统锁而言的。

  • 目的:同步周期内,没有多线程竞争的前提下(多线程之间交替执行同步代码块,没有发生竞争),避免使用互斥量带来的性能损耗。
  • 原理:CAS + 自旋
  • 特点:非阻塞同步、乐观锁

(1)轻量级锁的获取

1.在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的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是否指向当前线程的栈帧。如果是就说明当前线程已经拥有了这个对象的锁,说明是一次重入,创建锁记录并设置Displaced Mark Word为null,起到了一个重入计数器的作用,然后进入同步块继续执行。否则执行步骤(6)。

6.若Mark Word不指向当前线程的栈帧,则说明多个线程竞争锁,此时强锁失败的线程通过自旋,不断重试步骤(3),自旋次数超过10次,则进行锁膨胀,即轻量级锁膨胀为重量级锁。

执行完同步代码块代码,退出同步代码块,使用CAS开始轻量级锁解锁,线程会使用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。

  • CAS 替换成功,成功释放锁,恢复到无锁的状态(01)。
  • CAS 替换失败,则释放锁,唤醒被挂起阻塞的线程。

当超过自旋阈值,竞争的线程就会把锁升级为重量级锁,因此CAS 替换可能会失败,由于此时已经是重量级锁,则唤醒被挂起阻塞的线程,之后开始重量级锁的竞争逻辑了。

锁升级的整个流程:

1.5.1.3 自旋锁

自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待(让该线程执行一段无意义的忙循环,CPU忙等待),而不是把该线程给阻塞,直到获得锁的线程释放锁之后,这个线程就可以马上获得到锁。

轻量级锁的竞争就是采用的自旋锁机制。

  • 引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为阻塞和唤醒线程需要用户态和内核态的相互转换,这些操作给系统的并发性能带来很大的影响。

  • 自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启;在JDK1.6中默认开启。

  • 自旋锁的缺点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。

  • 自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该进入阻塞状态。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。

  • 自适应自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定: 在同一锁对象上,若线程刚刚的一次自旋操作成功过,那么JVM会认为这次自旋成功的可能性会很高,就多自旋几次;反之,就少自旋甚至不自旋。

  • 适用范围:适用于临界区代码少,执行快、锁竞争频率较低的情况。

1.5.1.4 偏向锁、轻量级锁、重量级锁的比较
优点缺点适用场景
偏 向 锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。
1.5.1.5  对象信息存在哪
  • 重量级锁:对象信息存储在ObjectMonitor的_header成员变量中;

  • 轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;

  • 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;

  • 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;

1.5.2 锁消除

**锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。**锁消除可以节省毫无意义的请求锁的时间。

JVM参数禁止锁消除优化: -XX:-EliminateLocks

StringBuffer类的append()方法:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
package cu.edu.xupt.acat.sync;

public class EliminateLocksTest {

    public static void fun() {

        StringBuffer buffer = new StringBuffer();
        buffer.append("hello");
        buffer.append("world");
    }

    public static void main(String[] args) {


        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            fun();
        }

        long end = System.currentTimeMillis();

        System.out.println(end - start);
    }
}

StringBuffer作为局部变量,作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作,因此Java即时编译器会进行锁消除的优化。

 

1.5.3 锁粗化 

通常情况下,为了保证多线程间的有效并发,应该将临界区范围限制的尽量小,但是某些情况下,如果一系列的连续操作都对 同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

  • 如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
    频繁加锁、解锁:
    for(int i=0;i<size;i++){
        synchronized(lock){
        }
    }
    
    

    锁粗化后

    synchronized(lock){
        for(int i=0;i<size;i++){
        }
    }
    
    
1.6 synconised(重量级锁)内部(锁升级的过程)

锁升级的过程:new-偏向锁-轻量级锁(无锁,自旋锁,自适应自旋)-重量级锁

synchronized锁升级常见问题:

 (1)偏向锁和轻量级锁是用户空间完成,重量级锁需要像内核申请(这时候markword记录的是ObjectMonitor)

 (2)锁升级的过程:当第一个线程来的时候,把线程id写到markword里(偏向锁),当第二个线程来的时候,先把偏向锁撤销,线程1和线程2自旋竞争,这两个线程在自己的线程栈内部生成LR(Lock Record),用自旋的方式,当竞争的锁的指针指向哪个线程的LR,那么哪个线程就持有了这把锁,另一个线程继续CAS等待。

(3)自旋锁什么时候升级为重量级锁

   答:竞争加剧或自旋线程数超过CPU核数的一半

(4)synchronized优化的过程和markword息息相关

(5)什么叫偏向锁启动和偏向锁未启动

      答:偏向锁未启动,new出来的对象markword是001,偏向锁启动new出来的对象是101

(6)为什么有自旋锁还需要重量级锁?

      答:自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗,重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。

(7)偏向锁是否一定比自旋锁效率高?

     答:不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。JVM启动过程,会有很对线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段时间再打开。

 (8)匿名偏向锁加锁,一定是偏向锁 

(10)

 (11)

 

(12)synchroize(this)与synchroise锁这个方法(非静态)是等价的

            synchroize(T.Class)与synchroise锁这个方法(静态)是等价的

            synchroize保证原子性和可见性

            synchronize锁的是对象,不是代码

            锁定方法和非锁定方法,同时执行

synchronized(object)->不能用String常量,Integer,Long等基础数据类型

(13)synchronize可重入锁

 (14)异常跟锁

  (15)

(15)sync(object)
        markword记录这个线程的id(偏向锁)

        如果线程争用,升级为自旋锁->10次以后->升级为重量级锁-os

        执行时间短(加锁代码),线程数少,用自旋锁

        执行时间长,线程数多,用系统锁

(16)锁优化

锁细化:

 下面的问题需要给Object 加final,就可以保证这个对象不变

eg:

 

 2.CAS

CAS(自旋锁是一个乐观锁、轻量级锁):读取一个值,修改它,再往回写的时候,判断这个值是不是当初读的那个值,如果不是拿到这个值,修改它,再往回写的时候,再次判断,直到新值与原值相等

 ABA问题:读取一个值,修改它,再往回写的时候,这个值是当初读的那个值,但是只是值相等,中间这个值可能被改过,然后又改回来了。如果是基础类型,那么无所谓,如果是引用类型,就有所谓。

解决ABA问题:利用版本号解决,如果有修改,版本号就加一,如果用布尔类型,如果有修改,就变为false

如果在判断完原值和新值一致的时候,被其他线程打断了,那么最终的值就会有问题,这时候就要保证原子性

 原子性:执行一段代码,不可以被其他线程打断

最终实现:

 JVM能处理的锁叫轻量级锁,JVM不能处理,交给操作系统处理的锁叫重量级锁。

轻量级锁:自旋锁(也叫无锁)

轻量级锁自旋等

重量级锁在等待队列等

调用wait()方法,轻量级锁转化成重量级锁

偏向锁:不是一把锁,只是简单标记

 

3.锁重入

(1)sychronized:可重入锁(可重入锁必须记录,因为要解锁几次必须得对应)

         偏向锁、自旋锁->线程锁->LR+1

         重量级锁->?Object Monitor字段上

4.下面这个类等同于c->malloc free c++->new delete,直接操作内存

七.对象的内存布局

上锁在hotspot的实现就是修改markword,所以锁信息被记录在markword里面

markword里面还记录了JC和hashcode的信息

注:如果偏向锁打开,默认是匿名偏向状态

markword占8个字节,所以前8位是markword,接下来四位是类型指针class pointer ,接下来是成员变量instance status (不是所有变量都有,object就没有)具体这个变量是多少字节就是多少字节,如果是long就是8位,如果加一起不够被8整除,那么需要添加字节

八.锁降级的过程

这把锁不被其他任何线程锁定,也就是GC的时候才会降级,所以可以简单认为锁降级不存在。

https://www.zhihu.com/question/63859501

九.Volatile 

1.Volatile保证线程的可见性

(1)MESI-缓存一致性协议

2.禁止指令重排序(有序性)

(1)DCL单例

下列线程不安全

 加锁-可以解决线程不安全问题

 

一上来就加锁不太好,先检查,后加锁(锁细化),但是这种写法不对,第一个请求来的时候通过了第一层检查,还没有执行下面代码的时候,这时候第二个请求也来了,也通过了第一层检查

 

 

双重检查:先不加volatile,双重检查基本上可以保证线程安全,但是会有小概率出现指令重排序

 

 加上volatile,防止指令重排序

创建对象有3步:1.申请内存,变量初始化(赋初值) 2.把真正的值赋值给赋初值的变量 3.把创建变量的地址指向这个变量,这三步可能会出现指令重排,加了volatile,就不能指令重排了

 

3.volatile不能保证原子性,正常执行加一,第一个线程编成2,第二个线程和第三个线程虽然对这个变量可见,但是他们各自加一,就会导致这个变量编成3,并没有执行加一再加一。

 

十.线程

1. 进程:程序,任务或者进程,进程是静态概念,分配资源,进程是系统资源分配的基本单元

2. 线程:动态概念,共享资源,线程是系统执行任务的基本单元

3. 协程:轻量级线程

1.线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

Java语言中得各种操作共享数据可以分成五类,按安全程度由强到弱,来排序:

1. 不可变: 不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用者都不需要在采取安全措施。

如果共享数据是一个基本数据类型,那么只要在定义的时候使用final关键字修饰就可以保证它不可变;如果是共享数据是一个对象,需要对象自行保证其行为不会对其状态产生任何影响。

2. 不可变: 绝对线程安全是不管运行是环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。

3. 相对线程安全: 就是通常意义的线程安全,确保这个对象单独的操作是操作安全的。Java语言中,大部分声称线程安全的类都属于这种类型,如:Vector、Hashtable等

3. 线程对立: 无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。

2.互斥同步

互斥和同步是一种最常见、最主要的实现并发正确性(相对线程安全)的保障手段。

  • 同步:指的是在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用
  • 互斥指的是实现同步的一种手段,临界区互斥量和信号量都是主要的互斥实现方式

在 Java 中,最基本的互斥同步手段就是synchronized关键字。synchronized可以保证线程竞争共享资源的正确性(多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用)

十一.java中得四种引用类型,强软弱虚

1.强:当有任何一个引用指向一个对象的时候,这个对象叫有用的对象,引用没有了,对象就变成了没用的对象,就被回收了

2.弱引用

(1)只要遇到垃圾回收,就当弱引用不存在(只有弱引用指向的对象直接被回收),强引用的对象继续存在

 (2)应用常景:ThreadLocal,weakHashmap,这两个都是为了防止内存泄漏

3.软引用

(1)m是强引用,SoftReference指向new byte是软引用

(2)如果一个内存空间只能放两个对象,其实一个对象是强引用指向,另一个是软引用指向,这时候来了一个新对象,发现空间不够了,把软引用干掉,如果空间够,就不干掉软引用指向的对象。

图解:

 (3)垃圾回收器遇到软引用时,不会回收

 (4)做备用,缓存

4.虚引用        

    (1) 通知垃圾回收器进行特殊回收

                                                                         

十二.ThreadLocal

1.相当于一个容器,在这里放的东西,只有本线程可以读出来,其他线程读不出来

应用:@transaction(Spring声明式事务)

图解:从数据库连接拿多个连接出来,注入bean,connect连接是一个,才能保证事物

        原理:拿第一个connection连接的时候,注入到ThreadLocal里,当第二次拿连接的时候,从TheadLocal里拿,就可以保证是同一个连接。

ThreadLocal原理:弱引用

十三:云相关提纲

十四. 各种锁

1.乐观锁:坏事未必会发生,所以事后补偿

-----自旋锁:一种常见的乐观锁实现

(1)ABA问题(加版本号或者布尔类型)

(2)保障CAS操作的原子性问题(lock指令)

  2.悲观锁:坏事一定会发生,所以先做预防(上锁)

3.读写锁:读锁和写锁

(1)读锁:读的时候,不允许写,但是允许同时读

(2)写锁:写的时候,不允许写,也不允许读

4.排他锁:只有一个线程能访问代码(sychronized)

5.共享锁:可以允许有多个线程访问代码

6.统一锁:大粒度的锁

(1)锁定A等待B,锁定B等待A 这种可能会产生死锁,这时候A+B统一成为大锁

7.分段锁:分成一段一段的小粒度锁

(1)JDK1.7 CHM ConcurrentHashMap

8.间隙锁

十五.线程池

1.线程池:

2.线程池7个参数:

(1)核心线程数

(2)最大线程数

(3)生存空间

(4)时间单位

(5)任务队列

(6)线程工厂

(7)拒绝策略
 

 3.什么是纤程或者协程?和普通的的java纤程有啥不同?

为什么它能提高效率?是不是M:N永远优于1:1?

 

4.线程:

启动线程的三种方式:(1)Thread (2)Runnable (3)Excutors.newCachedThread

线程的3个方法:sleep,yeild,join

线程的状态:

十六.JUC同步锁

1.ReentranLock 可重入锁,可以替代synchronized

(1)trylock

(2)底层是CAS

(3)lockinterupptibly和公平及非公平

 

 2.countDownLatch 门栓

 也可以用join方法:

 3.CycleBarrier

 JUC 包中提供类似的工具,可以设置一个简单的集合点,等所有成员到齐了之后,再执行下一步操作。CycleBarrier 它就相当于是一个栅栏,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后,再一起通过。(线程到了parties数量的时候,继续执行)

CyclicBarrier是回环屏障的意思,可以让一组线程全部达到一个状态后再全部同时执行。
这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程就会冲破屏障,继续向下运行
————————————————

:(1)Semaphore信号量机制,CountDownLatch、循环栅栏Cycle barrier并称为三大队列式同步器

      (2) Sychronised和ReentrantLock并称为两大重量级锁,ReentrantReadWriteLock是           ReentrantLock的子类。

     (3)CyclicBarrier阻塞一组线程,直至某个状态之后再全部同时执行,并且所有线程都被释放。和之前说的CountdownLatch功能相反,CountdownLatch阻塞主线程,等所有子线程完结了再继续下去。
上节介绍的CountDownLatch在解决多个线程同步方面相对于调用线程的join方法已经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownIatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,可以让一组线程全部达到一个状态后再全部同时执行。
这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程就会冲破屏障,继续向下运行

 (4)从javadoc的描述可以得出:

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行
  • 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。

    CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行

4.Phaser

先指定N个线程等N个线程到齐了开始干第一阶段的活,等第一阶段所有线程干完活了,接着这N个线程继续开始干下一阶段的活,以此类推直至干完业务逻辑了所有阶段的活(当然每个阶段可以干完进入下一个阶段可以踢掉一下不需要的线程,当然也可以全部留下来,不剔除)

5.ReadWriteLock

共享锁(读)和排他锁(写)

ReadWriteLock的升级态:StampedLock

6.Semaphore信号量

应用:限流,车道和收费站

7.Exchanger(交换器)

8.LockSupport

可以实现当前线程阻塞,但是不需要加锁,unpark可以在park之前使用

 以下解答问题的程序是否正确?

 

 结果:

 答:程序有问题,看结果可知,并没有在5停住,第一个原因:在集合里加元素的时候,sise还没有加一,第二个线程就读了,可能会有错误输出,需要加同步synchnised(给add方法和size方法加)。第二个原因:两个线程之间对变量不是实时可见的,所以停止不了,需要加valatile,改进:在lists之前加volatile,容器变成同步容器

 结果是对的,但是把睡眠1s去掉之后就不好用了。原因:volatile修饰的是lists,lists是一个引用,但是实际上引用并没有变化,变化的是引用中得值,volatile检测不到引用中得值得变化,所以在实际使用中,轻易不要用volatile,并且volatile修饰的变量越简单越好。

 另一种实现:

 

 

 

 还是不行,wait释放锁,notify不释放锁,所以必须t1线程执行完了,t2才可以继续执行,

解决方法1:notify之后,执行wait方法,让线程t1阻塞,这时候线程t2就可以获得锁,继续执行了,不用notifyAll的原因是因为只有两个线程,没必要

 

解决方法2:

 

 

 

 如果注释掉t1线程里的睡眠1s,就会发现t2结束在add6之后,所以上述程序也有问题,解决方法,用两个countDown

解决方法3:

 

 

 

 去掉t1里面的睡眠1s之后,也会

 也是不对的,与countDown问题是一样的,需要两个LockSupport.park才能解决

解决方法3的改进:

 

 

结果没问题。t2线程的if(c.size!=5)这句话可以去掉,直接执行t2线程的LockSupport.park(),这样也是没问题的

 解决方法4:

 

 

 t1到5之后,release掉资源,这时候不能保证是t2 acquire到,还是t1再次acquire到,所以程序还是有点问题,改进:

在t1 release之后,修改为如下代码:

 

 这时候由于t2先启动,所以可能会出现t2 acquire到资源,然后直接release掉了

 修改方法:在t2.join()之前,加上t2.start(),t2线程在这里启动,结果就对了

 题干:要求A线程打印A~Z,B线程打印1~26,出现A1B2C3这样的结果

如果用if,唤醒之后就往下执行了,而用while会在判断一遍,看看是不是还是等于max,所以必须用Max

 

 

 上述写法有个小瑕疵,如果是生产者被阻塞,notifyAll会叫醒所有线程,但是生产者阻塞的话,是没有必要叫醒生产者,只要叫醒消费者就可以  

 

 

eg:

 

 

 

 

 

4.高并发三大解决策略:缓存,限流,降级

如有侵权,请联系我删除

参考博客:

Java并发:synchronized关键字如何保证线程安全_synchronized是如何保证线程安全-CSDN博客

参考笔记:B站马士兵视频及笔记(建议大家去看视频)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值