一、JMM的三个特性
可见性:
在Java中,不同线程拥有各自的私有工作内存,当线程需要修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到该线程的工作内存中(变量副本),当该线程修改其变量副本后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。原因: 工作内存和主内存存在同步延迟
解决方案:
1. volatile: 可以保证内存可见性
① 规定线程每次修改变量副本后立刻同步到主内存中
② 规定线程每次使用变量前,先从主内存中拷贝最新的值到工作内存中,用于保证能看见其它线程对变量修改的最新值
2. synchronize: 同步锁
注意:
volilate可以保证可见性,但是假设多线程同时在做a++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2
有序性:
在单线程程序中代码逐行执行,但是在多线程并发时,程序的执行就有可能出现乱序。
多线程执行程序时, 为了提高性能,编译器和处理器常常会自动对指令做重排序,目的是进行相关的优化, 指令重排序使得代码在多线程执行时可能会出现一些问题。可以一个值在写的时候是先赋值,经过重排序之后可能是后赋值
解决方案:
1. volatile: 可以保证有序性
在指令序列中插入内存屏障(一组处理器指令), 防止指令重排序
2. synchronize: 同步锁
原子性:
原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行
解决方案:
1. synchronize: 同步锁
总结:
1. volatile可以解决可见性, 有序性, 但是不能解决原子性
2. synchronize可以解决可见性, 有序性, 原子性
二、线程同步
线程同步:多线程在操作同一个资源时,同一时刻只能有一个线程操作,其他线程等待这个线程操作结束后抢占操作这个资源。
优点:线程同步可以保证多线程在操作同一个资源时,结果的正确性, 能够保证可见性, 有序性, 以及原子性
缺点:多线程操作时, 由于只能有一个线程操作, 导致性能的下降
三、synchronized同步锁
锁介绍:在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。 Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
锁分类:如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
synchronized介绍:
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区。
-
synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性, 一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
-
修饰实例方法, 非静态方法(对象锁) 需要在类实例化后, 再进行调用
-
修饰静态方法(类锁)静态方法属于类级别的方法, 静态方法可以类不实例化就使用
-
修饰代码块(对象锁、类锁)
-
synchronized可修饰:
1.修饰实例方法:
锁类型: 使用synchronized修饰实例方法时为对象锁
锁范围: 锁的范围是加锁的方法
锁生效: 必须为同一个对象调用该方法该锁才有作用
2. 修饰静态方法
锁类型: 使用synchronized修饰静态方法时为类锁
锁范围: 锁的范围是加锁的方法
锁生效: 该类所有的对象调用加锁方法, 锁都生效
3. 修饰代码块
语法: synchronized(锁){ // 内容 }
锁代码块是非常重要的地方。添加锁的类型是Object类型。
运行过程:多线程执行时,每个线程执行到这个代码块时首先会判断是否有其他线程持有这个锁,如果没有,执行synchronized代码块。如果已经有其他线程持有锁,必须等待线程释放锁。当一个线程执行完成synchronized代码块时会自动释放所持有的锁。
(1)当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待。
(2)当锁为this时,需要看线程中是否为同一个对象调用的包含synchronized所在的方法。这种写法也是比较常见的写法。
(3)锁为Class时,是一个标准的类锁, 所有的对象调用加锁的代码块都生效。
问题:
1.对象锁和类锁
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用, 锁生效。当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。
结论:多个对象使用时, 锁生效, 使用类锁;同一对象使用时, 所生效, 使用对象锁。
2.什么是可重入锁
某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。 synchronized为可重入锁。例如:在同一个线程中调用方法1已经获得了锁this,在方法1中调用了方法2,方法2也是锁this,允许再次获得锁,那就称为可重入锁。