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 的!
什么?这一说法可能让您始料未及,但事实确实如此。
在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设上述代码执行以下事件序列: