synchronized&volatile

synchronized(JVM实现的锁)

通过这两个关键字,我们可以很容易的实现同步多个任务的行为,可以实现同一时刻,只能有一条线程去访问共享资源

一: 修饰普通方法

多个线程,共同去竞争访问,方法内部的变量,永远是线程安全的!!!

    public class HasSelfPrivateNum{
      public void  add(String name){
        try{
            int num=0;  //num在方法内部,永远是线程安全的!!!
             if(name.equals("a"){
                num =100;
            }else{
                num =200;
            }
        System.out.println("num=="+num);    
            
        }catch(InterruptedException e){
            e.printStackTrace();
        }
      }
        
    }
    

多个线程,共同去竞争访问对象中的实例变量,可能导致非线程安全

    public class HasSelfPrivateNum{
   private int num=0;  //num是本类的实例变量,可能出现非线程安全问题!!!
      public void  add(String name){
        try{
   
             if(name.equals("a"){
                num =100;
            }else{
                num =200;
            }
        System.out.println("num=="+num);    
            
        }catch(InterruptedException e){
            e.printStackTrace();
        }
      }
        
    }
    

结论:

只有共享的资源才可能出现非线程安全性问题,需要同步化...就像方法内部的变量,根本不可能共享,所以说没必要同步化

解决非线程安全问解决

这一问题引入了Synchronized关键字,让出现非线程安全问题的方法,保持前后的同步,让线程拿到对象锁(this对象锁),当某一个线程进去后,其他线程只能等它释放对象锁之后,获取到对象锁在进入同步方法(Synchronized可以保证它修饰的方法实现原子性的操作)

拓展:A线程持有object对象的锁,B线程可以异步的调用object对象中的非Synchronized类型的方法

    public class HasSelfPrivateNum{
   private int num=0;  //num是本类的实例变量,可能出现非线程安全问题!!!
     Synchronized public void  add(String name){
        try{
   
             if(name.equals("a"){
                num =100;
            }else{
                num =200;
            }
        System.out.println("num=="+num);    
            
        }catch(InterruptedException e){
            e.printStackTrace();
        }
      }
        
    }
    

多个对象多个锁

比如说,分别给两个线程分别去访问同一个类两个不同不对象同名同步方法,结果不是顺序执行,而是异步执行的...

结论:

关键字Synchronized获取到的锁全部是对象锁,而不是把一段代码或者一个方法当作锁!真出现了多个线程访问多个对象,那么JVM就会创建出,多把对象锁...

内置锁

java中每一个对象都可被当作同步的锁,这些锁就叫做内置锁

互斥锁

一个线程进来之后,另一个线程不能进来, Synchronized就是典型的互斥锁

二: 修饰代码块

Synchronized的弊端:

声明方法在某些情况下是有弊端的,比如说A线程抢到了cpu的执行权,在调用同步方法执行某一个比较长的任务的,A还没来得及释放这个对象锁,紧接着B线程抢到了cpu的执行权,他也想去访问A线程访问的方法,然而B线程拿不到这个对象锁,所以被拒绝访问,因此B线程必须等待较长的时间

同步代码块语法:

    synchronized(Object){
        //同步执行的任务
    }

sychronized代码块里面的方法是同步的,另一个线程只有等待当前线程执行完这个代码块之后,才能执行此代码块

使用同步代码块解决同步方法的弊端,提升效率 一半同步一半异步!-->

    public task{
        //类的实例属性  --> 共享资源
        private int a;
        private int b;

        try{
            ....  // 非共享资源
            ....  // 非共享资源
            
            synchronized(this){
                a++;
                b++;
            }
        }catch(InterruptedExceotion e){
            e.printStackTrace();
        }
        
    }

可以看到,我们只是把可能出现非线程的共享资源放在同步代码块中...

当一个线程访问object的一个Synchronized(this){}同步代码块里面的内容时,另一个线程可以访问此object对象中的非synchronized代码块!!!

也就是一半同步,一半异步,不在同步代码块中的代码,就是异步执行,在同步代码块中的代码,就是同步执行!

Synchronized代码块的本身具有同步性,而且,Synchronized代码块之间也具有同步性!

当一个线程访问object对象中的Synchronized代码块的时候,其他的任何线程对object的其他任何Synchronized同步代码块的访问也是阻塞的,这也说明Synchronized同步代码块和Synchronized修饰的方法一样,使用的对象监视器是一个

将任意对象,作为对象监视器:

Synchronized(非this对象){} 同步代码块可以是任意对象,这个非this对象大多数是,方法的参数,类的实例变量

Synchronized(非this对象){}优点

如果一个类中有很多的Synchronized同步方法,虽然可以实现同步,但是,会发生阻塞而降低效率,因为所有的Synchronized同步方法,他们拥有的都是this锁,而Synchronized(非this对象){}同步代码块可以使用的是非this锁,拥有不同的锁,因此他们两者之间是异步执行的,对他们自己来说又是同步执行,提升了效率

注意点:

同步代码块放在非同步Synchronized方法中进行声明,不能保证调用方法的线程执行同步,因为线程调用方法的顺序是无序的,虽然在同步代码块中执行的顺序依然有序,但如果有分支逻辑判断(逻辑判断没有在同步代码块中),就可能会出现脏读

    //创建一个只能存储一个元素的集合...
    List list = new ArrayList();
    public void unsafe(){
    try{
        if(list.size()<0){
             Thread.sleep(2000);
            synchronized(){ 
            list.add("a");
         }
    }catch(InterruptedException e){
        e.printStackTrace();
    }
  }
  //此段代码就会出现脏读...
  
  //解决方法: 同步化
    List list = new ArrayList();
   synchronized public void unsafe(){
    try{
        if(list.size()<0){
             Thread.sleep(2000);
            synchronized(){ 
            list.add("a");
         }
    }catch(InterruptedException e){
        e.printStackTrace();
    }
  }

Synchronized(非this对象){}总结:

  1. 在多个线程持有"对象监视器"为同一个对象的前提下,同一时间,只有一个线程可以执行synchronized(非this对象){}同步代码块中的代码.
  2. 同一时间,线程想去执行synchronized(非this对象){}同步代码块里面的代码,它必须拿到这个非this对象
  3. 其他线程调用Synchronized(非this##### 象X){}中的X的同步代码块,同步方法,依然是线程安全的.
  4. 在多线程访问的情况下,拥有不同对象监视器的Synchronized(){}同步代码块之间是异步执行的

三: 修饰静态XX

Synchronized public static void ...... 静态同步锁

Synchronized方法同样可以修饰静态方法,运行的结果证明它同样可以实现线程安全,顺序执行,但是Synchronized修饰普通方法Synchronized修饰静态方法是有本质上的区别的,Synchronized修饰静态方法实际上是给Class类上锁 而前者是给this对象上锁.

换言之,两个自身顺序执行,同时出现则异步执行,(锁不同)

四 修饰同步代码块

Synchronized(类名.class){...}
作用和其修饰静态方法一样

五 . synchronized锁重入

synchronized关键字具有锁重入的功能,也就是说,当一个线程拿到锁对象之后,当它在本synchronized方法中访问本类对象的其他synchronized方法时,它是可以重复拿到锁的!

六. 出现异常锁自动释放

在synchronized修饰的同步方法中,若在运行时,出现了异常,对象锁会自动释放,意味着其他线程可以直接在此获取到对象锁

七. 数据类型String的常量池特性

  • JVM中具有String常量池缓存的功能,如下一段代码返回true
    public static void mainString[] avgs(){
        String a ="a";
        String b = "a";
        System.out.println(a==b);
    }

这也就是意味着,假如在两个线程给一个Synchronized同步代码块中传递进同样的字符串,也就意味着,他们要去竞争同一把锁,难免会出现阻塞的情况.所以绝大多数情况下,我们是不使用String字符串,来当作锁对象的

八. 同步Synchronized方法的无限循环的等待和解决

同步方法synchronized同步方法,容易造成死循环,如下代码,method永远不可能得到执行

  public class Service{
        synchronized public void methodA(){
            System.out.println("methodA Begin");
            boolean tag = true;
            while(tag){}
            System.out.println("methodA end");
        }

        synchronized public void methodB(){
            System.out.println("methodB Begin");
            System.out.println("methodB end");
        }

    }

使用同步代码块解决无限循环问题

    public class Service{
         public void methodA(){
             Object o1 = new Object();
             synchronized(o1){
                 System.out.println("methodA Begin");
                 boolean tag = true;
                 while(tag){}
                 System.out.println("methodA end");
             }
          
        }

        synchronized public void methodB(){
            Object o2 = new Object();
            synchronized(o2){
            System.out.println("methodB Begin");
            System.out.println("methodB end");
        }
            
        }

    }

九 多线程的死锁

  • 多线程的死锁就是说,已经准备就绪的线程们在等待一个根本不可能被释放的锁,从而导致所有的线程任务都无法继续完成,导致了线程的假死,死锁是必须要避免的问题

如下代码死锁现象

public class demoSiSuo implements Runnable{
public class demoSiSuo implements Runnable{
    private String username;
    private  Object lock1 = new Object();
    private  Object lock2 = new Object();

    public void set(String username) {
        this.username = username;
    }

    @Override
    public void run() {
        if (username.equals("a")){
            synchronized (lock1){

                try {
                    System.out.println("a");
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    //do something
                    System.out.println("接着lock1 lock2的代码执行了");
                }
            }
        }

        if (username.equals("b")){
            synchronized (lock2){
                System.out.println("b");
                //do something
                synchronized (lock1){
                    //do something
                    System.out.println("接着lock2 lock1的代码执行了");
                }
            }
        }
    }


    public static void main(String[] args) {
        demoSiSuo demoSiSuo = new demoSiSuo();

        new Thread(()->{
            demoSiSuo.set("a");
            demoSiSuo.run();
        }).start();


        new Thread(()->{
            demoSiSuo.set("b");
            demoSiSuo.run();
        }).start();
    }
}

使用JDK自带的检查死锁的工具

在cmd窗口切换到bin目录,输入指令jps 找到正在运行的实例id, 输入 jstack -l [id] 可查看到
Found 1 deadlock.

示例如下:


C:\Users\acer>jps
335844 ThreadPool
370676 DeadLock
366976 ThreadPool
163348 Jps
297244
256296 RemoteMavenServer
397720 Launcher

C:\Users\acer>jstack 370676
    ...
    ===================================================
"Thread-1":
        at com.changwu.MultiThread.lock.HoldLock.run(DeadLock.java:22)
        - waiting to lock <0x000000076b6f7f58> (a java.lang.String)
        - locked <0x000000076b6f7f90> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.changwu.MultiThread.lock.HoldLock.run(DeadLock.java:22)
        - waiting to lock <0x000000076b6f7f90> (a java.lang.String)
        - locked <0x000000076b6f7f58> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

十 . 锁对象的改变

    public static class myService{
         private String lock="123";

         public void method() throws InterruptedException {
             synchronized (lock){
                 lock="456";
                 System.out.println(Thread.currentThread().getName());
                 Thread.sleep(1000);
                 System.out.println(Thread.currentThread().getName()+"end");
             }
         }
    }

    public static void main(String[] args) {
        myService  j = new myService();
        new Thread(()->{
            try {
                j.method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
            try {
                j.method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }


}

结果
Thread-0
Thread-1
Thread-0end
Thread-1end

  • Thread-0把当前对象的lock属性给改了,然后自己休眠, Thread-1 拿到的是456,所以异步执行

当注释Thread.sleep(1000);结果如下
Thread-0 同步执行
Thread-0end
Thread-1
Thread-1end

  • 去掉注释,他们同时抢到的锁都是123,

对象锁改变,依然是遵循高并发下 拥有同一把锁的同步方法或代码块会阻塞,拥有不同锁,是不会阻塞的

  • 此外,只要锁对象不变,即使锁对象的属性改变了,运行的结果依然是同步的

volatile

1 volatile概览:

volatile是jvm的提供的一把轻量级的锁, 为什么说它的轻量级呢? 因为jmm规范要求多线程并发变成保证线程安全性需要满足如下三条性质

  1. 可见性
  2. 原子性
  3. 有序性

但是volatile的实现情况如下:

  1. 可见性
  2. 不满足原子性
  3. 有序性

volatile通过加入内存屏障来实现可见性:

  • 对volatile变量进行写操作的时候,会在写操作前加入一条store屏障指令,将本地内存中的共享变量的值,刷新会主内存
  • 对volatile变量进行读操作的时候,会在读操作前加入一条load屏障指令,从主内存中读

volatile通过禁止重排序实现有序性

什么是指令重排序:

如下:

// 当我们new 对象时, 底层会分下面三步执行
1. memory = allcate();
2. instance(memory);
3. instance = memory   

但是这个过程中存在优化的与指令重排序, 如 源码-> 编译器优化重排 -> 处理器优化重排 -> 内存系统优化重排 - > 最终执行的指令

// 所以经过排序后很可能顺序变成这个样子
1. memory = allcate();
3. instance = memory   
2. instance(memory);

于是我们可以看到, 经过重新排序后代码就出问题了, 什么问题呢? instance 引用指向了memory,表示instance不为空, 但是这会内存中并没有真实的对象,一旦使用就会出错, 加上volatile关键字可以实现禁止这个重新排序的过程

2 volatile & synchronized

  • volatile 修饰类的属性,它的作用就是 强制从公共堆栈中获取变量的值,而不是从线程的私有数据栈中获取变量的值
  • volatile 只能修饰属性,-----synchronized修饰方法,代码块
  • 多线程访问volatile,不会发生阻塞 ,------Synchronized会发生堵塞
  • volatile 保证了线程的数据的可见性,也就是说,他可以保证线程始终如一的获取volatile属性的最新值!但是如果在对这个变量进行了其他操作,比如i++,那么volatile就变的没有任何意义,非线程安全.
    • i++是非原子性操作:
      • 从内存中取出i的值
      • 计算i的值
      • 将i的值写回内存
  • synchronized保证了线程的同步性,安全性
  • volatile不具备原子性----Synchronized可以实现原子性
  • synchronized代码块拥有volatile关键字的特性,既能保证数据的可见性,又能保证互斥性

再次重申:volatile解决的是变量在多个线程之间的可见性, synchronized解决的是多个线程之间访问资源的同步性

我们完全可以使用Synchronized替代volatile 但是 后者不一定能替代前者,比如在获取变量的时候进行++操作,非原子性,导致非线程安全!

3 前行知识补充:

把JVM的运行环境该变成 -server, 当JVM运行在Server环境下,为了提高线程的效率,线程获取到的 类的属性值时,始终在自己的私有堆栈中获取,但是当它去更改类的属性值的时候,改变的确是公共堆栈中的属性!,也就是说,它获取到的属性值,就是一开始从公共堆栈中复制过去的值,不曾,也不能修改,

4 那么想同步数据怎么办?

这也是volatile关键字出现的必要,保证了多个线程之间 属性的可见性 --> 强制从公共堆栈中获取变量的值,而不是从线程的私有数据栈中获取变量的值

4.1 volatile解决同步死循环

直接运行下面一段代码:

public class demo01 {
    boolean tag= true;

    public void methodA(){
        while(tag==true){

        }
        System.out.println("methodA  end...");
    }

    public void setTag(){
        this.tag=false;
    }

    public static void main(String[] args) {
        demo01 demo01 = new demo01();

        new Thread(()->{
            demo01.methodA();
        }).start();

        new Thread(()->{
            demo01.setTag();
        }).start();

        System.out.println("main endl...");
    }

}

直接运行结果:
main endl...
methodA end...

更改JVM运行参数 -server再次运行

结果:
main endl;

更改JVM运行参数 -server再次运行,并将tag用volalite修饰

结果:
main endl...
methodA end...
验证了volatile实现了多个线程之间数据的可见性

4.2验证Synchronized代码块实现了数据的可见性

将mathodA()进行如下修改

public void methodA(){
        while(tag==true){
            String string = new String();
            synchronized (string){
            }
        }
        System.out.println("methodA  end...");
    }

4.3 volatile常用的情景

使用volatile做标记变量,如下代码,假设线程2执行前必须要等线程1做好初始化工作

volatile boolean tag = false;

//线程1: 
Context = loadContext();
tag=true;

//线程2:
while(!tag){
    sleep();
}
doSomethingWithContext();

4.4 用于安全发布对象时的双重检测禁止指令的重排序

5 volatile原理

硬盘-->内存-->CPU的缓存

volatile关键字起作用,依赖的是Lock指令

  • 在多处理器的系统上:
    • 将处理器缓存行里面的内容,写回到系统内存
    • 这个操作同时会使其他线程cpu的缓存里面存储的对应数据失效,故,不得不重新去内存中加载最新的数据

大量使用volatile关键字,会降低CPU缓存的使用量,增加内存的储存量,进而导致程序运行效率降低!

对象锁

在java中每一个对象都存在一个monitor对象, 这个对象其实就是java的对象的锁, 大家平时所说的内置锁, 对象锁也是它, 一个类可以new 出多个对象, 因此每一个对象的对象锁也是相互独立的互相不干预

synchronized(this){
    // todo 
}

类锁

每一个类都有一个类锁, 类锁实际上也能理解成对象锁来实现的, 即类的Class的对象锁, 每一个类都有一份唯一的Class描述对象, 因此每一个类中只有一个类锁: 像下面这样这样

synchronized(User.class){
    // todo 
}

当一个线程想使用这个对象时, 会先检查一下这个对象的monitor是否是0, 如果是0,说明没有其他的线程在使用这个对象, 于是这个线程就能使用这个对象, 然后在这个对象的 monitor+1, 如果monitor不为0, 说明对象正在被其他线程使用, 因此等待, 其他线程使用对这个对象的占有权时,使 monitor-1

通过JVM指令分析

命令:

javap -verbose jishuqi.class

对代码块进行加锁

如下图:

对代码块进行加锁

对方法进行加锁

对方法加锁

JVM对Synchronized的优化

实例对象图

实例对象

对象头信息:如下图

对象头

锁状态

偏向锁

什么是偏向锁?

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

偏向锁

轻量级锁

JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。

以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
对象头

下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
虚拟机栈
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。

当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
尝试获取对象锁

如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现 , 当锁被升级成重量级锁时, 所有的线程想获取到锁,不得不同步等待


参考书籍<<java多线程编程核心技术>> 高洪岩著

posted @ 2019-07-08 11:56  赐我白日梦  阅读(496)  评论(0编辑  收藏  举报