1.学习目标
什么是Object锁?Object锁的几种方式 优缺点是什么?
什么是Class锁? Class锁的几种方式
可重入锁是什么意思? 锁支不支持继承?
多线程下同步和异步的核心是看什么?
数据类型为String的常量池的特性
写一个死锁的程序出来,不要用死循环
关键字volatile的主要作用
关键字Synchronized和volatile的主要区别以及使用情况
先来理解理解什么是对象的发布与逸出?
发布(publish) 使对象能够在当前作用域之外的代码中使用
逸出(escape) 当某个不应该发布的对象被发布了
常见逸出的有下面几种方式:
-
静态域逸出
-
public修饰的get方法
-
方法参数传递
-
隐式的this
静态域逸出:
public修饰get方法:
逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!理解起来是不是简单了一丢丢?
安全发布对象有几种常见的方式:
-
在静态域中直接初始化 :
public static Person = new Person()
;-
静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
-
-
对应的引用保存到volatile或者AtomicReferance引用中
-
保证了该对象的引用的可见性和原子性
-
-
由final修饰
-
该对象是不可变的,那么线程就一定是安全的,所以是安全发布~
-
-
由锁来保护
-
发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出
-
Synchronized是什么?有什么作用?
synchronized是一种互斥锁
-
一次只能允许一个线程进入被锁住的代码块
synchronized是一种内置锁/监视器锁
-
Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!
有什么作用?
-
synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
-
synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
synchronized的原理是什么?
2. Synchronized对象监视器为Object时的使用
1.synchronized可以修饰方法,两个线程访问同一个对象(这个对象只new了一次)中的同步方法的时候一定是线程安全的,即按照顺序执行的,也就是说A线程执行完了再执行B线程。如果两个线程访问同一个类的不同的两个实例(即便他的方法是同步的,这会创建两个锁),这其实数据不共享了,不会出现线程安全的问题,两个线程是异步执行的。为什么他们是异步执行?这就说明synchronized取得的锁是对象锁,而不是把一段代码或者方法当作锁。需要注意的是:方法中的变量不存在线程安全的问题,永远都是线程安全的,这是方法内部的私有变量的特性造成的。
总结:
A线程先持有object对象的Lock锁(就是A线程调用object的同步方法),这时候B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
如果A线程先持有object对象的Lock锁(就是A线程调用object的同步方法),B线程这时候再调用object对象中的synchronized类型的方法时则需等待。
脏读:发生脏读的情况是在读取实力变量时,此值已经被其他线程修改过了。即便赋值的方法是同步的,取值的方法不同步也会出现脏读的情况,解决办法当然是同步。
Synchronized锁重入:自己可以再次获取自己的内部锁,比如有一条线程获得了某个对象锁,此时这个对象锁还没有被释放,当其再次想要获取这个对象的锁的时候还是可以获取的。比如在一个类中,一个线程同步的方法可以调用另一个线程同步的方法。如果不可重入锁的话就会造成死锁。需要注意的是:可重入锁也支持在父子类继承的环境中,当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。需要注意的是:这里是可重入锁支持继承,而不是同步支持继承,锁具有继承性是指子类调用父类的同步方法,而同步不具有继承性是子类重写父类的同步方法不具有同步的效果。
2.synchronized同步代码块的使用,Synchronized方法存在的弊端,如果A线程需要执行一个长时间的任务,那么B线程必须等待很长的时间,用同步代码块可以解决这个问题。因为synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象(某一个对象可以是this,非this等等)进行加锁。
使用同步代码块的好处当然是可以弥补同步方法的缺陷,即:使用同步代码块来加快同步方法的执行时间,就是把可能出现线程不安全的赋值操作等等用synchronized(this)包裹起来。因为当一个线程访问object的一个synchronized同步代码块时。另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。如果同步方法出现无限等待的问题,就是如果一个线程调用的同步方法陷入一个死循环,其他线程永远没有机会执行该对象的其他同步方法。有没有解决的办法呢?当然有,就是把同步方法换成同步代码块代替,这样进入方法的的时候就不会被阻塞。
总结:
同步代码块和同步方法的作用:对其他synchronized方法或synchronized(this)同步代码块调用呈阻塞状态。同一时间只有一个线程可执行同步代码块或同步方法。
synchronized不仅可以锁this对象,可以将任意对象作为对象监视器,这个任意对象大多数是实例变量及方法的参数,使用的格式为synchronized(非this对象),当然多个线程持有对象监视器持为同一个对象的前提下(必须是同一个锁),同一时间只有一个线程可执行synchronized(非this对象)同步代码块中的代码。
锁非this对象相比锁this对象和同步方法的好处,如果在一个类中有很多个synchronized方法,这时虽然能够实现同步,但是会受到阻塞,所以会影响运行效率,但是使用同步代码块锁非this对象。则synchronized(非this)代码块中的程序与同步方法是异步的,不会与其他锁this同步方法争抢this锁,则可大大提高运行效率。再次强调:对象监视器必须监视的是同一个对象(synchronized锁的是同一个对象)如果监视的不是同一个对象,运行的结果就是异步了。
同步代码块放在非同步的方法中进行声明,并不能保证调用方法的线程的执行同步,也就是说线程调用方法的顺序是无序的。虽然在同步块中执行的顺序是同步的,这样极易出现脏读的问题。
3.Synchronized对象监视器为Class时的使用
synchronized作用在静态方法上相当于对当前.java文件所对应的Class类进行持锁。而synchronized关键字加到非静态方法是给对象上锁。他们有什么区别呢?由于类是由Class文件生成,如果对Class文件上锁,那么对类的所有实例起作用。
4.数据类型String的常量池特性
因为JVM中具有String常量池的缓冲功能,所以String a=”a” String b=”a”这样的话a和b是完全相等的。如果此时用synchronized锁住这两个相同的字符串,当多个线程传入相同的字符串进去的时候,只会有一个线程被执行,因为锁是一样的,最好的是锁一个new Object的对象,这样拿到的是不同锁,线程异步执行。
最终总结:在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象。如果同时持有相同的锁对象,则这些线程之间就是同步的。如果分别获得锁对象,则这些线程之间就是异步的。
写一个死锁的程序 不使用死循环
public class DeadThead extends Thread {
private String param;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void setParam(String param) {
this.param = param;
}
@Override
public void run() {
super.run();
if (param.equals("a")) {
synchronized (lock1) {
System.out.println("param is a");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("a end");
}
}
}
if (param.equals("b")) {
synchronized (lock2) {
System.out.println("param is b");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("b end");
}
}
}
}
}
public static void main(String[] args) throws Exception {
DeadThead deadThead = new DeadThead();
deadThead.setParam("a");
Thread thread1 = new Thread(deadThead);
thread1.start();
Thread.sleep(100);
deadThead.setParam("b");
Thread thread2 = new Thread(deadThead);
thread2.start();
}
5.关键字volatile的主要作用
关键字volatile主要作用是使变量在多个线程之间可见,关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
线程启动的时候线程的变量会存在于公共堆栈及线程的私有堆栈中。在JVM被设置为-server模式时为了线程的效率,线程一直在私有堆栈中拿值,普通情况下线程修改变量的值只是修改公共堆栈的值,而私有堆栈的值仍未改变,所以解决这个问题的办法就是用volatile修饰变量的值。迫使线程去从公共堆栈取值,达到变量在多个线程之间可见的一个效果。但是volatile最致命的缺点是不支持原子性。
如何理解volatile不支持原子性,比如volatile修饰的变量i去执行++i,--i等运算的时候,当多个线程来执行这个操作的时候还是会出现不同步的情况,解决的办法当然是使用synchronized来关键字。
Synchronized和volatile 的区别?
- 首先要明白的一点是volatile解决变量在多个线程之间的可见性。而synchronized是解决多个线程之间访问资源的同步性。
- Volatile是线程同步的轻量级实现,效率要比synchronied要好,但是volatile只能修饰变量,而synchronized可以修饰方法和代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- Volatile能保证数据的可见性,但是不能保证数据的原子性。Sychronized既可以保证原子性也可以保证可见性,因为他会将私有内存和公共内存的数据做同步。
Volatile的本质是JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
使用了volatile修饰的变量保证了三点:
-
一旦你完成写入,任何访问这个字段的线程将会得到最新的值
-
在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
-
volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:
-
修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
-
该变量不会纳入到不变性条件中(该变量是可变的)
-
在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)