volatile关键字、synchronized关键字

1、类锁

    一个类的所有对象共享一个class对象,共享一组静态方法,类锁的作用就是使持有者可以同步地调用静态方法

     当synchronized指定修饰静态方法或者class对象的时候,拿到的就是类锁,下面例子拿到的是类锁
class B {
    synchronized public static void mB(String value) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            System.out.print(value);
        }
    }

    public static void mC(String value) {
        synchronized (B.class) {
            for (int i = 0; i < 1000; i++) {
                System.out.print(value);
            }
        }
    }
}
mB()方法是静态的,synchronized 修饰的方法获取的类锁,

mC()方法中synchronized (B.class),获取的也是类锁,,类锁共享的是静态方法和属性

一个线程  B.mB("1");

一个线程  B.mC("2");,先输出1,后输出2

2、对象锁

synchronized修饰非静态方法或者this的时候拿到的就是对象锁,对象锁是每个对象各有一把的

class C {
    synchronized publi void mB(String value) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            System.out.print(value);
        }
    }

    public void mC(String value) {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                System.out.print(value);
            }
        }
    }
}
mB非静态方法,是对象锁,

mC() 中 synchronized (this) {  持有的对象锁

//线程1的调用处 b.mB("1");

//线程2的调用处 b.mB2("2");

两个线程调用同一个对象b的mB方法。最终结果是输出了1000次“1”之后输出了1000次“2”。可见两个线程对此方法的调用实现了同步。

3、类锁和对象锁同时存在

类锁和对象锁是两种锁,二者做不到同步,

class B {
    //静态方法,上类锁,函数功能为连续打印1000个value值,调用时会传1,所以会打印1000个1
    synchronized public static void mB(String value) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            System.out.print(value);
        }
    }
    
    public void mC(String value) {
        //修饰this上对象锁,函数功能也是连续打印1000个value值,调用时会传2,所以会打印1000个2
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                System.out.print(value);
            }
        }
    }

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    B.mB("1");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                new B().mC("2");
            }
        });
        thread.start();
        thread2.start();
        
    }
}
静态方法是类锁,而代码块锁this,是对象锁。所以代码块和静态方法交替执行、交替打印,做不到同步

4、volatile关键字

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。 
  volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。 

原来的例子:

public class Singleton3 {
    private static Singleton3 instance = null;

    private Singleton3() {}

    public static Singleton3 getInstance() {
        if (instance == null) {
            synchronized(Singleton3.class) {
                if (instance == null)
                    instance = new Singleton3();// 非原子操作
            }
        }

        return instance;
    }
}

可以使用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序

public class Singleton3 {
    private static volatile Singleton3 instance = null;

    private Singleton3() {}

    public static Singleton3 getInstance() {
        if (instance == null) {
            synchronized(Singleton3.class) {
                if (instance == null)
                    instance = new Singleton3();
            }
        }
        return instance;
    }
}

(4)当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile; 

5、volatile使用场景

1、状态标志

volatile boolean shutdownRequested;  

public void shutdown() {   

    shutdownRequested = true;   

}  

public void doWork() {   

    while (!shutdownRequested) {   

        // do stuff  

    }  

}  

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。

这种类型的状态标记的一个公共特性是:通常只有一种状态转换shutdownRequested 标志从false 转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从false 到true,再转换到false)。此外,还需要某些原子状态转换机制,例如原子变量。

2、一次性安全发布

 

//注意volatile!!!!!!!!!!!!!!!!!    

private volatile static Singleton instace;     

    

public static Singleton getInstance(){     

    //第一次null检查       

    if(instance == null){              

        synchronized(Singleton.class) {    //1       

            //第二次null检查         

            if(instance == null){          //2    

                instance = new Singleton();//3    

            }    

        }             

    }    

    return instance;   

如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。

考察上述代码中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在Singleton 构造函数体执行之前,变量instance 可能成为非 null 的!
什么?这一说法可能让您始料未及,但事实确实如此。

在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设上述代码执行以下事件序列:

  1.     线程 1 进入 getInstance() 方法。
  2.     由于 instance 为 null,线程 1 在 //1 处进入synchronized 块。
  3.     线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非null
  4.     线程 1 被线程 2 预占。
  5.     线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将instance 引用返回,返回一个构造完整但部分初始化了的Singleton 对象。
  6.     线程 2 被线程 1 预占。
  7.     线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

你可能感兴趣的:(java)