多线程-- 通过Java中共享变量的内存可见性问题,逐个认识synchronized/volatile/CAS/死锁

多线程-- synchronized同步+死锁问题+volatile关键字

【一】通过Java中共享变量的内存可见性问题,逐个认识synchronized/volatile/CAS

【1】认识多线程下处理共享变量时Java的内存模型:主内存+本地内存

JMM定义了【线程】和【主内存】之间的抽象关系:所有的共享变量都存储在主内存中,当线程使用一个共享变量的时候,就会把主内存里的变量复制到自己私有的本地工作内存里,然后在自己本地工作内存里对变量进行读写操作,在完成所有操作以后再把本地内存里的变量重新更新到主内存里供其他线程获取使用。
请添加图片描述

【2】进一步认识线程私有的本地工作内存

每个cpu核都有自己的控制器和运算器(执行算术逻辑运算),其中控制器又包含一组寄存器和操作控制器。每个核都有自己的一级缓存,在有些架构里还有一个所有CPU都共享的二级缓存。综上所述,本地工作内存就包括【一级缓存+二级缓存+CPU的寄存器】
请添加图片描述

【3】多线程同时处理一个共享变量会出现什么情况:内存不可见问题

因为一级缓存和二级缓存的存在,会导致内存不可见问题,也就是一个线程对共享变量进行的修改结果对其他线程是不可见的。
1-线程A获取变量x=0,存到二级缓存,然后修改x=1,并写入一级缓存和二级缓存,然后把x=2刷新到主内存,此时一级缓存、二级缓存和主内存里都是x=1。
2-线程B获取变量x,先从自己的一级缓存获取,失败,再从共有的二级缓存获取,成功获取到线程A存到二级缓存里的x=1,把x=1复制到自己的本地内存进行修改x=2,然后把x=2存到一级缓存和二级缓存,最后把x=2更新到主内存。
3-(注意)这个时候线程A再去获取变量x,在一级缓存的时候就成功获取到变量x,但是这里的x=1,这就与主内存中的x=2出现了误差

【总结】
内存不可见问题就是,一个线程无法准确的获取其他线程对共享变量操作的结果

【4】解决内存可见性问题方式一:synchronized

(1)synchronized的实现原理

synchronized是Java语言提供一种原子性内置锁,Java内置的使用者看不到的锁被称为内部锁==监视器锁,这种锁是排他锁,只能被一个线程获取。一个线程在进入synchronized代码块之前会自动去获取内置的监视器锁,其他线程再想进入代码块就得等待监视器锁被释放,否则就被阻塞挂起进行等待。当这个线程正常执行完代码块并退出,或者出现错误抛出异常,又或者主动调用了wait系列方法释放了监视器锁,此时调用notify系列方法唤醒那些被阻塞挂起的线程来竞争获取这个监视器锁。

(2)synchronized的弊端:上下文切换问题

Java里的线程和操作系统的原生线程是一一对应的,所以当一个线程来获取Java内置监视器锁并被阻塞挂起的时候,需要从用户态切换到内核态来执行阻塞操作,这个切换是个很耗时的操作。

(3)什么是上下文切换问题

处理器给每个线程分配CPU时间片(Time Slice),一般为几十毫秒,并且有操作系统控制切换,由于时间太短我们根本感知不到,所以看上去像是同时发生的一样。时间片用完或者被迫终止等情况就会发现另一个线程来执行CPU时间片,称为上下文切换(Context Switch)。

一次上下文切换,需要保存和恢复切入切出的线程的进度信息,包括了程序计数器存储内容和指令等。这与并发编程基础 - Thread状态和生命周期中提到的线程状态转换有密切的关系,也与并发编程基础 - synchronized锁优化提到的管程模型中,线程进入不同的条件列表阻塞等有关,当线程阻塞、挂起时就会发生用户态和内核态的切换,就会发生上下文切换。并且代价是比较昂贵的,如果操作系统将单核CPU轮流分配给线程执行任务还好,但是现在的计算机都是多CPU(多核心线程),那么发生跨CPU的山下文切换就更加昂贵了。上下文切换引发的开销包括:
1-操作系统保存和恢复上下文;
2-调度器进行线程调度;
3-处理器高速缓存重新加载;
4-上下文切换也可能导致整个高速缓存区被冲刷,从而带来时间开销

(4)如何解决上下文切换问题

1.无锁并发编程:因为每次锁的竞争都会引起上下文切换
2.CAS算法:CAS算法是一种替代锁的方式,不需要加锁也能达到原子性(我更愿意说数据的一致性)
3.协程:一种比线程还要小的执行单位,能在单线程中进行多任务切换。

(5)synchronized是如何解决内存可见性问题的

共享变量内存可见性的问题主要就是线程的工作内存导致的,而synchronized的本质就是把在synchronized块内使用到的变量从线程的工作内存里清除,这样在synchronized块里要想使用变量,就只能从主内存里获取,在一级缓存和二级缓存里的变量被清除了。退出synchronized块的本质就是把在块内对共享变量的修改刷新到主内存。

所以上述的流程就等于,一个线程进入同步块,拒绝其他线程再进入,先清除同步块里线程本地内存里的变量,然后获取变量都是从主内存里直接获取,在退出的时候把本地内存里修改的变量刷新到主内存。避免了出现可见性问题。

【5】解决内存可见性问题方式二:volatile关键字

(1)volatile与synchronized有什么区别

synchronized太笨重了,会直接阻塞其他线程,带来线程上下文切换和线程调度的开销问题。synchronized是加在代码块/方法/类上面的,直接通过【清除本地内存】和【阻塞其他线程】来保证获取的变量都是从主内存直接获取的,而volatile关键字是加在变量的,让线程在写入变量的时候不会把值放在寄存器或者其他地方,而是直接把值刷新回主内存,这样其他线程再来获取这个共享变量的时候就不会通过缓存来获取了,因为缓存里是没有存入这个值,只能从主内存里获取。

两者的目的都是共享变量只出现在主内存里,不要出现在一级缓存或二级缓存里。synchronized是先把已经放进缓存的变量都清除来保证本地内存为空,然后把修改后的值存入缓存和主内存(存入缓存也没事,反正下一个线程进入同步块的时候会再次清除本地内存)。而volatile是让变量不存缓存直接刷新到主内存,这样下一个线程在缓存里获取不到共享变量,只能从主内存里获取。

(2)什么是有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

(3)volatile的解决有序性问题:禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

(4)volatile的缺点

volatile只能保证可见性,但是不能保证原子性。

(5)什么是原子性

原子性定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

例如a值为1,执行a=a+1操作,会分为三步:获取a的值——a值加1——重新给a赋值,假如线程A在执行a+1且没结束的时候,线程B开始获取a的值为1,线程A在执行完a+1并且把赋值给a时结果为2,接着线程B在执行完a+1后结果也为2,其实经历了两个线程的两次相加,结果应该为3,这里就出现了线程安全的问题。synchronized可以保证只有一个线程进行操作来保证原子性,而volatile允许多线程进行操作,也就无法保证原子性了,拒绝多线程操作共享变量时保证原子性的必要条件。

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

  1. 基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
  2. 所有引用reference的赋值操作
  3. java.concurrent.Atomic.* 包中所有类的一切操作(原子操作)
(6)什么时候才会使用volatile

1-写入变量值不依赖变量的当前值。因为如果依赖当前值,整个步骤就会分为三步:获取值——计算值——写入值,这三步不是原子性的,volatile不能保证原子性。例如a=b+c就是不依赖当前值,a=a+b就是依赖当前值。
2-读写变量值时没有加锁。因为加锁本身已经可以保证内存的可见性了

(7)volatile的使用案例:单例模式的双重锁为什么要加volatile

单例模式的代码如下:

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

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a. memory = allocate() 
//分配内存b. ctorInstanc(memory) 
//初始化对象c. instance = memory 
//设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

为什么又要double-check判断是否为空呢?
因为synchronized加锁是很费时间的,如果对象不是空的,就可以省掉这一步骤

【6】解决内存可见性问题方式三:非阻塞CAS算法实现的原子性操作类

(1)为什么要用到CAS算法的原子性操作类

看下面的代码,我们想保证获取的value值都是从主内存里直接获取,不能用volatile,因为inc方法有++value操作,volatile不能保证原子性会导致inc方法出现线程安全问题。可以用synchronized保证可见性和原子性,但是看方法getCount,我们希望在读取value值的时候,把本地内存清除,直接从主内存读取value值,但是这个方法只是读数据,不会出现线程安全问题,也就不用考虑原子性了,我们为了保证可见性直接用synchronized去阻塞其他线程读数据,这样的代价有点太大了。

private Long value;
public synchronized Long getCount() {
	return value;
}
public synchronized void inc() {
	++value;
}
(2)原子性操作类的原理:自旋锁+CAS算法

synchronized可以保证可见性和原子性,但是开销太大。非阻塞的volatile可以保证可见性,降低开销,但是又不能保证“读改写”的原子性。那么使用CAS(比较并交换)非阻塞算法可以保证更新操作的原子性,操作的含义就是在执行操作之前保存旧值,在执行完操作后判断保存的值与原来的值是否相同,相同则说明原来的值没有被其他线程修改,则可以把执行后的新值赋值给原来的值。如果旧值和原来的值不相等了,就说明这期间有其他线程来修改了原来的值,就取消此次操作,进入do-while循环,重新尝试操作。

如:int i = 10;i++;线程a先从主内存读取i = 10到工作内存;然后在工作内存执行i + 1;然后在写入主内存时,检查主内存中 i 的值(10)是否等于工作内存 i 值(10);如果不一致,则本次操作取消。

(3)CAS导致的ABA问题

加上单向递增的版本号,避免出现环形转换,例如给每个变量的状态值设置一个时间戳。

【二】synchronized的原理分析

【1】synchronized保证线程安全的三大特性

(1)原子性
原子性指的是一个或多个操作执行过程中不被打断的特性。被synchronized修饰的代码是具有原子性的,要么全部都能执行成功,要么都不成功。

synchronized无论是修饰代码块还是修饰方法,本质上都是获取监视器锁monitor。获取了锁的线程就进入了临界区,锁释放之前别的线程都无法获得处理器资源,保证了不会发生时间片轮转,因此也就保证了原子性。

(2)可见性
所谓可见性,就是指一个线程改变了共享变量之后,其他线程能够立即知道这个变量被修改。我们知道在Java内存模型中,不同线程拥有自己的本地内存,而本地内存是主内存的副本。如果线程修改了本地内存而没有去更新主内存,那么就无法保证可见性。

一个线程进入synchronized同步块,拒绝其他线程再进入,先清除同步块里线程本地内存里的变量,然后获取变量都是从主内存里直接获取,在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。

(3)有序性
有序性是指程序按照代码先后顺序执行。

synchronized是能够保证有序性的。根据as-if-serial语义,无论编译器和处理器怎么优化或指令重排,单线程下的运行结果一定是正确的。而synchronized保证了单线程独占CPU,也就保证了有序性。

【2】synchronized锁的使用场景分析

(1)synchronized锁有哪些使用场景?

synchronized锁住的是括号里的Class对象,而不是代码。所以我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。

1-修饰实例方法,对当前实例对象this加锁
2-修饰静态方法,对当前类的Class对象加锁
3-修饰代码块,指定加锁对象,对给定对象加锁

(1)同步代码块:(锁指定对象/类)
对象锁/类锁

synchronized (this) {...} //表示进入同步代码库前要获得当前对象的锁。
synchronized (object) {...} //表示进入同步代码库前要获得 给定对象的锁。
synchronized(.class) //表示进入同步代码前要获得 给定 Class 的锁

(2)同步方法:(锁当前对象实例)
对象锁,指获取当前类创建的实例对象中的内置锁,锁定的是当前方法执行者对象
同一个类的其他对象来获取锁的时候,依然可以获得锁

public synchronized void method(){...}

(3)同步静态方法:(锁当前类)
类锁,锁的是整个类,而不是对象
同一个类的其他对象来获取锁的时候,会被阻塞挂起

public static synchronized void method(){...}

(4)同步代码块:类锁
同一个类的其他对象来获取锁的时候,会被阻塞挂起

synchronized (Demo.class) {...}

总结:
(1)synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
(2)synchronized 关键字加到实例方法上是给对象实例上锁;
(3)尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

(2)synchronized对象锁和类锁的使用案例?

(1)同步代码块(对象锁)
线程1和线程2都是执行的demo对象的代码块,所以使用的是同一把锁,会互斥

public class Demo implements Runnable{
 
    @Override
    public void run() {
        /**
         * 同步代码块形式--->锁为this,两个线程使用的锁是一样的,
         * 线程1必须要等到线程2释放了该锁后,才能执行
         */
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行结束");
        }
    }
 
    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo,"线程1").start();
        new Thread(demo,"线程2").start();
    }
}

(2)同步方法(对象锁)
线程1和线程2都是执行的demo2对象的method方法,所以使用的是同一把锁,会互斥

public class Demo2 implements Runnable{
 
    @Override
    public void run() {
        method();
    }
 
    public synchronized void method(){
        System.out.println(Thread.currentThread().getName() + "开始执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
 
    public static void main(String[] args) {
        Demo2 demo2 = new Demo2();
        new Thread(demo2,"线程1").start();
        new Thread(demo2,"线程2").start();
    }
}

线程1用的是t1对象,线程2用的是t2对象,执行的是不同对象的method方法,所以使用的不是同一把锁,不会互斥

public class Demo2 implements Runnable{
 
    @Override
    public void run() {
        method();
    }
 
    /**
     * synchronized用在普通方法上,默认的所就是this,当前实例
     */
    public synchronized void method(){
        System.out.println(Thread.currentThread().getName() + "开始执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
 
    public static void main(String[] args) {
        Demo2 t1 = new Demo2();
        Demo2 t2 = new Demo2();
        new Thread(t1,"线程1").start();
        new Thread(t2,"线程2").start();
    }
}

(3)静态方法(类锁)
线程1用的是t1对象,线程2用的是t2对象,执行的是不同对象的method方法,但是method是静态方法,获取的是Demo3的类锁,和实例无关。且类锁只有一个,所以实现了同步。

public class Demo3 implements Runnable{
 
    @Override
    public void run() {
        method();
    }
 
    /**
     * synchronized用在静态方法上,默认锁的就是当前所在的Class类,
     * 所以无论是哪个线程访问它,需要的锁都只有一把
     */
    public static synchronized void method(){
        System.out.println(Thread.currentThread().getName() + "执行了");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束了");
    }
 
    public static void main(String[] args) {
        Demo3 t1 = new Demo3();
        Demo3 t2 = new Demo3();
        new Thread(t1,"线程1").start();
        new Thread(t2,"线程2").start();
 
    }
}

(4)同步代码块(类锁)
获取的是类Demo的类锁

public class Demo implements Runnable{
 
    @Override
    public void run() {
        /**
         * 所有线程需要的锁都是同一把
         */
        synchronized (Demo.class) {
            System.out.println(Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行结束");
        }
    }
 
    public static void main(String[] args) {
        Demo t1 = new Demo();
        Demo t2 = new Demo();
        new Thread(t1,"线程1").start();
        new Thread(t2,"线程2").start();
    }
}
(3)对象锁和类锁的区别?

(1)对象锁的特征
1-多个线程去调用同一个对象的synchronized代码块方法,需要获取对象锁,一个对象的对象锁是唯一的,会等待。
2-多个线程去调用同一个对象的synchronized普通房案发,也是需要等待

(2)类锁的特征
1-多个线程,调用一个类的不同对象的synchronized代码块方法,需要获取类锁,一个类的类锁是唯一的,会等待
2-多个线程,调用一个类的不同对象的synchronized方法,需要获取类锁,也会等待

(3)类锁和对象锁的区别?
Java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是两个锁实际是有很大区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用与类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

(4)锁住对象代码块方法 和 普通synchronized方法互斥吗?(对象锁和对象锁互斥)

同个对象在多个线程中去调用不同的synchronized方法时是需要等待的。

第一个方法用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象;第二个方法是修饰方法的方式进行同步。因为第一个同步代码块传入的this,所以两个同步代码所需要的对象锁都是同一个TestSynchronized对象的锁,下面main方法开启两个线程,分别别调用test1()和test2()方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test1执行完毕,释放掉锁,test2线程才开始执行。

public class TestSynchronized {
 
    public void test1(){
        /**
         * synchronized修饰同步代码块
         */
        synchronized (this){
            for (int i = 1;i < 6;i++){
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
    /**
     * synchronized修饰非静态方法
     */
    public synchronized void test2(){
        for (int i = 1;i < 6;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        TestSynchronized demo = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.test1();
            }
        },"test1");
        Thread test2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.test2();
            }
        },"test2");
        test1.start();
        test2.start();
    }
}
(5)静态 synchronized 方法和 类锁互斥吗?(类锁和类锁互斥)

互斥!因为两个synchronized加的都是类锁,而且是同一个类的锁,一个类只有一把锁,所以加的是同一个类的同一把锁,所以要等待

public class TestSynchronized {
 
    public void test1(){
        /**
         * synchronized修饰同步代码块,
         * 传的是一个class对象
         */
        synchronized (TestSynchronized.class){
            for (int i = 1;i < 6;i++){
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
    /**
     * synchronized修饰静态方法
     */
    public static synchronized void test2(){
        for (int i = 1;i < 6;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        TestSynchronized demo = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.test1();
            }
        },"test1");
        Thread test2 = new Thread(new Runnable() {
            @Override
            public void run() {
                TestSynchronized.test2();
            }
        },"test2");
        test1.start();
        test2.start();
    }
}
test1 : 1
test1 : 2
test1 : 3
test1 : 4
test1 : 5
test2 : 1
test2 : 2
test2 : 3
test2 : 4
test2 : 5
(6)静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?(类锁和对象锁不互斥)

不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

public class TestSynchronized {
 
    /**
     * synchronized修饰非静态方法
     */
    public synchronized void test1(){
        for (int i = 1;i < 6;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    /**
     * synchronized修饰静态方法
     */
    public static synchronized void test2(){
        for (int i = 1;i < 6;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        TestSynchronized demo = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.test1();
            }
        },"test1");
        Thread test2 = new Thread(new Runnable() {
            @Override
            public void run() {
                TestSynchronized.test2();
            }
        },"test2");
        test1.start();
        test2.start();
    }
}
test1 : 1
test2 : 1
test2 : 2
test1 : 2
test2 : 3
test1 : 3
test2 : 4
test1 : 4
test1 : 5
test2 : 5

类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

(7)为什么synchronized的对象锁和类锁不互斥?

因为类本身有一个class对象,class对象不是new出来的,而是系统创建的,并且内存中只有一份,因为类只加载一次。类锁是用与类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象。对象锁是竞争一个对象的内置监视器锁,而类锁是竞争这个类唯一的class对象的锁。

所以类锁和对象锁是不同的,下面这个案例,对象锁会判断object这个对象的锁是否被持有,而类锁会判断当前实例对象所属哪个类,然后判断类的class对象是否被持有。那么如果同一个类的两个对象分别访问下面两个代码块,一个是获取object对象的锁,一个是获取类的class对象的锁,所以这两个对象虽然是同一个类的实例对象,但是不会互斥。

synchronized (object) {...} //表示进入同步代码库前要获得 给定对象的锁。
synchronized(.class) //表示进入同步代码前要获得 给定 Class 的锁

例如Student有两个实例化对象student1和student2,有一个class对象,有两个线程A和B。
(1)如果A和B都是竞争student1对象的锁,会互斥,因为是同一个锁
(2)如果A竞争的是student1对象的锁,B竞争的是student2的锁,不会互斥,因为不是同一个锁
(3)如果A和B都是竞争类锁,也就是类的class对象的锁,会互斥,因为是同一个锁
(4)如果A竞争的是student1对象的锁,B竞争的是class对象的锁,不会互斥,因为不是同一个锁

(8)构造方法可以用 synchronized 修饰么?

构造方法不能使用 synchronized 关键字修饰。

构造方法不需要同步化,因为它只可能发生在一个线程里,在构造方法返回值前没有其他线程可以使用该对象。一个线程已经在构造方法里面了,另外一个线程也可以调用构造方法,第一个线程里面生成的对象和第二个线程里面生成的对象是不同的对象。

【3】synchronized锁的底层实现原理?

【4】synchronized的使用案例

(1)synchronized同步对象

为了避免产生同时加减之类的脏数据,要保证当一个线程在执行操作的时候,其他线程必须等待当前线程结束才能开始,实现线程安全。
可以创建任何一种对象,要求线程执行之前必须先占有这个对象,如果当前这个对象已经被其他线程占有了,就必须等待这个对象被释放。这个对象必须是唯一的,使用final修饰。这就实现了在同一时间,数据只能被一个线程修改。

(1)使用Object类对象

public class TestThreadSyn01 {

    public static void main(String[] args) {
        final Object lock = new Object();
        final Hero gareen = new Hero("盖伦",10000,10000);

        //创建数组存放线程对象
        int n=10000;
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];

        for (int i=0;i<n;i++) {
            Thread t = new Thread() {
                public void run() {
                    //lock对象是final修饰的,每次执行方法之前先占有lock对象,如果占有失败就不执行
                    synchronized (lock) {
                        gareen.recover();
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
        }
        for (int i=0;i<n;i++) {
            Thread t = new Thread() {
                public void run() {
                    //lock对象是final修饰的,每次执行方法之前先占有lock对象,如果占有失败就不执行
                    synchronized (lock) {
                        gareen.hurt();
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
        //之所以把线程对象存到数组中,就是为了每个线程都加上join,这样保证主线程在每个线程执行结束后才结束
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
    }
}

(2)使用class类对象

public class TestThreadSyn02 {

    public static void main(String[] args) {
        final Hero gareen = new Hero("盖伦",10000,10000);

        //创建数组存放线程对象
        int n=10000;
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];

        for (int i=0;i<n;i++) {
            Thread t = new Thread() {
                public void run() {
                    //lock对象是final修饰的,每次执行方法之前先占有lock对象,如果占有失败就不执行
                    synchronized (gareen) {
                        gareen.recover();
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
        }
        for (int i=0;i<n;i++) {
            Thread t = new Thread() {
                public void run() {
                    //lock对象是final修饰的,每次执行方法之前先占有lock对象,如果占有失败就不执行
                    synchronized (gareen) {
                        gareen.hurt();
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
        //之所以把线程对象存到数组中,就是为了每个线程都加上join,这样保证主线程在每个线程执行结束后才结束
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
    }
}

更近一步的,可以在具体的方法内部使用synchronized(this)

public void hurt(){
     //使用this作为同步对象
     synchronized (this) {
         hp=hp-1;   
     }
}
(2)synchronized修饰方法

在具体的方法前加上synchronized修饰,当外部线程调用方法的时候,就不用再额外的使用synchronized了

public synchronized void recover(){
    hp=hp+1;
}
(3)synchronized修饰类

给类加上synchronized修饰,同一时间只有一个线程能够进入这种类的一个实例去修改数据,这个类里的所有方法,在同一时间只能被一个线程使用

(4)把非线程安全的集合转换成线程安全

借助工具类Collections

public class TestThread {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        List<Integer> list2 = Collections.synchronizedList(list1);
    }
}
(5)同步补充说明

1-synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
2-无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
3-每个对象只有一个锁(lock)与之相关联。
4-实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

(3)同步的时候出现死锁的情况

public class TestThreadDeadlock {
    public static void main(String[] args) {
        final Hero ahri = new Hero("九尾妖狐");
        final Hero annie = new Hero("安妮");

        Thread thread1 = new Thread() {
            public void run(){
                synchronized (ahri) {
                    System.out.println("thread1 已占有九尾妖狐");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread1 试图占有安妮");
                    System.out.println("thread1 等待中 。。。。");
                    //试图去占有
                    synchronized (annie) {
                        System.out.println("do something");
                    }
                }
            }
        };
        thread1.start();

        Thread thread2 = new Thread() {
            public void run(){
                synchronized (annie) {
                    System.out.println("thread1 已占有安妮");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread1 试图占有九尾妖狐");
                    System.out.println("thread1 等待中 。。。。");
                    //试图去占有
                    synchronized (ahri) {
                        System.out.println("do something");
                    }
                }
            }
        };
        thread2.start();
    }
}

(1)死锁的条件
1-互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
2-请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
3-不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
4-循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

(2)避免死锁
1-破坏请求与保持条件 :⼀次性申请所有的资源。
2-破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
3-破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

(4)伪共享问题

(1)什么是伪共享

为了解决计算机系统中主内存和CPU之间运行速度差的问题,会在CPU和主内存之间添加一级或者多级高速缓冲存储器(Cache)。这个Cache一般被集成到CPU内部的。

一般地,一台计算机都有多个 CPU 核心,叫作多核 CPU,这些 CPU 都要从一块叫作内存的地方读取数据,经过加工处理,再写回到内存中,如果每次读写数据都跟内存进行交互,太慢了,你可以想像成内存跟硬盘的关系,所以,为了加快 CPU 的处理速度,人们就给 CPU 安上了缓存,一般地,现代处理器都具有三级缓存,这三缓存也有个关系,越接近 CPU 的缓存越快越贵容量越小,越远离 CPU 的缓存越慢越便宜容量越大。

比如,对于一台 4 核 CPU 的计算机,它的缓存布局可能是这样的:
在这里插入图片描述
这样,在处理数据的时候,CPU 就加载内存中的一小块数据到 CPU 缓存中,处理完毕并不用立马写回内存,等下次再读取或修改同一片内存区域的数据时,直接走缓存就好了,这样就极大地提高了数据处理的速度。刚才有提到每次加载一小块数据,那么,这个 “一小块” 是多大呢?通常地,现代 CPU 架构为 64 个字节。

在这里插入图片描述
似乎很完美,试想这样一个问题,在内存中有两个相临的变量 x 和 y,一个线程一直在对 x 进行 ++ 操作,一个线程一直在对 y 进行 ++ 操作,会出现怎样地后果呢?
在这里插入图片描述
假设两个变量初始值为 0,各自增 100 次,因为是两个不同的线程处理,所以这两个线程可能处于不同的 CPU 核中,根据上面的理论,CPU 每次加载 64 字节的数据到缓存中,所以,x 和 y 始终一起被加载到不同的缓存中,那么,各自修改完了如何写回主内存呢?发现没法写回了是不是?因为写回也是整个缓存行一起写回的,不管先写回哪个,都会被后写回的覆盖。

(2)解决伪共享思路

为了解决这种问题,有两种策略:
1 给这个缓存行对应的内存块加锁,每次读写数据的时候都从主内存重新读取,写完之后立马写回主内存,多个线程处理同一块内存区域数据的时候排队进行,这样数据肯定就准确了;

2 把 x 和 y 分隔开,不要让它们相临,让它们始终不会同时被加载到同一个缓存行中,只需要在它们之间补足 64 字节,它们自然就被隔开了,永远不会加载到同一个缓存行中;

两种方案都是可行的。

对于第一种方案,相当于缓存行永远失效,形同虚设了,这种锁又有另外一个名字 —— 内存屏障(Memory Barrier)或者内存栅栏 (Memory Fence),在现代 CPU 架构下,内存屏障主要分为读屏障(Load Memory Barrier)和写屏障(Store Memory Barrier):
1 读屏障,每次都从主内存读取最新的数据;
2 写屏障,将缓存写入到主内存;

内存屏障还有个重要的功能,防止重排序,即不会把内存屏障前后的指令进行重排序。

使用内存屏障这种技术,又引来了新的问题,每次对 x 的操作,同时对 y 产生了影响,反之亦然,相当于 x 和 y 变成了一种共生的状态,但是实际上他们却没有任何关系,这种不同线程对同一块内存区域(缓存行)的不同变量的操作产生了互相影响的现象,就叫作伪共享(False Sharing)。为了解决伪共享带来的问题,就引出了第二种方案。

对于第二种方案,这样的玩法叫作加 Padding,在两个变量之间加一系列无用的变量,使得两个变量永远不会被加载到同一个缓存行,但是,它也有个问题,试想如果两个线程同时修改 x,它就无法处理了,此时,就只能使用第一种方案了。

在 Java 中,这种加 Padding 的玩法主要有三种实现方式:

1 变量前后添加 N 个 long 类型,N 的取值有两种说法,一种是 7,一种是 15,因为内存布局是按 8 字节对齐的,所以加上 7 个 long 正好等于 64 字节,也就是一个缓存行的大小,可以保证这个变量与其它变量分隔开,15 的说法是为了避免相邻扇区预取导致的伪共享冲突,在 Disruptor 框架中使用的是 7,在 jctools 中使用的是 15;

2 使用继承且在父子类中加上 padding,这样是为了防止内存布局重排序,比如,下面这个类,会把 byte 类型的 b 存储在 long 类型的前面,因为对象头占用 12 字节(压缩后),byte 类型占用 1 字节,这样只需要被 3 个字节就可以了,如果不做这种重排序,对象头需要补齐 4 个字节,而 byte 类型需要补齐 7 个字节,造成空间浪费;

3 使用 @sun.misc.Contended 注解,不过这是 Java8 新增的注解,所以,无法兼容之前的版本,现在大部分开源框架还没有使用这个注解;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值