什么是多线程同步(管程)?
所谓的同步指的是所有线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
多线程同步
1. synchronized对象锁处理同步问题
锁的对象是什么?
我们来看一个例子:取钱和查看密码。很明显这两个是操作不同的属性,所以是异步的。而取钱和存钱是同步的,查看密码和修改密码也是同步的。如果要用代码实现这个操作,怎么办?
class Account{
double sal;
String pass;
synchronized quSal(){}
synchronized cunSal(){}
synchronized checkPass(){}
synchronized modifyPass(){}
}
如果像上面这样采用同步方法加锁,取钱和存钱同步了,查看密码和修改密码也同步了,但是使用synchronized锁的是当前账户,那么当前账户所有的操作都被锁住了,存钱和查看密码就不再是异步而是同步,性能变低,锁的粒度太粗。
如果要把它变成异步,就要使用不同的锁,锁不同的对象。上面只有一把锁,锁了两个对象,现在要拆成两把锁,注意,任意对象都可以作为锁。
class Account{
double sal;
String pass;
//锁sal属性
private Object salLock = new Object();
//锁pass属性
private Object passLock = new Object();
quSal(){
synchronized(salLock){}
}
cunSal(){
synchronized(salLock){}
}
checkPass(){
synchronized(passLock){}
}
modifyPass(){
synchronized(passLock){}
}
}
1.1 synchronized的使用
使用synchronized关键字处理线程同步有两种模式:同步代码块和同步方法
- 使用同步代码块
- 锁的是任意对象,锁的是类的反射对象
class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for (int i=0;i<1000;i++){
synchronized (this){
if (this.ticket>0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",还有"+this.ticket--+"张票");
}
}
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"黄牛A");
Thread t2 = new Thread(mt,"黄牛B");
Thread t3 = new Thread(mt,"黄牛C");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
- 使用同步方法
- 成员方法:锁的是当前对象
- 静态方法:锁的是当前类的反射对象
class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for (int i=0;i<1000;i++){
this.sale();
}
}
public synchronized void sale(){
if (this.ticket>0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",还有"+this.ticket--+"张票");
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"黄牛A");
Thread t2 = new Thread(mt,"黄牛B");
Thread t3 = new Thread(mt,"黄牛C");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
1.2 synchronized的实现原理
对象锁Monitor机制
Java中所有类的对象都有对象监视器(Monitor),获取一个对象的锁实际就是获取该对象的Monitor。(获取的过程是互斥的,即同一时刻只有一个线程能获取到Monitor,从而进入同步代码块或同步方法中)
当一个线程尝试获取对象的Monitor时:
1)若此时Monitor值为0,表示此对象Monitor未被任何线程持有,当前线程进入同步块,并且将Monitor持有线程置为当前线程,Monitor值加1.
2)若此时Monitor值不为0,并且持有线程不是当前线程,当前线程等待Monitor值减为0.
3)若此时Monitor值不为0,但是持有线程为当前线程,当前线程再次进入同步块,Monitor值再次加1(可重入锁)。
之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。
1.3 JDK1.6对synchronized进行优化
JDK1.5中,synchronized的性能比较低,因为这是一个重量级操作,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力,相比使用Java提供的Lock对象,性能更高一些。
JDK1.6之后,对synchronized进行了优化,导致synchronized的性能并不比Lock差。
对synchronized锁进行优化,也就是对获取锁的时间进行优化。(锁的优化也就是锁的几种状态)
1.3.1 CAS操作
使用锁时,线程获取锁是一种悲观锁的策略,即假设每一次执行临界区的代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS,又称无锁操作,是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突也就不会阻塞其它线程的操作。
无锁操作是使用CAS(Compare and Swap)又叫比较交换来鉴别线程是否发生冲突,出现冲突就重试当前操作知道没有冲突为止。
CAS(V,O,N) 中的三个值分别为:
- V:主内存中存放的实际值
- O:当前线程认为主内存的值
- N:希望更新的值
当V和O相同时,表明该值没有被其他线程更改过,可以将N赋值给V,反之V和O不相同,表明该值已经被其他线程改过了,所以不能将N赋值给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并且成功更新,其余将会失败。失败的线程会重新尝试,也可以选择挂起线程。
synchronized和CAS的主要区别:
synchronized未优化前,在存在线程竞争的情况下,会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步),而CAS并不是直接把线程挂起,当CAS操作失败后会进行一定的尝试,因此也叫非阻塞同步。
CAS问题:
1)ABA问题:因为CAS会检查O有没有变化,如果O由A变成了B,再变成了A,而CAS在检查的时候发现O并没有变化依然为A,但实际上却发生了变化。JDK1.5后的atomic包提供了AtomicStampedReference来解决ABA问题。(添加版本号)
2)自旋
浪费大量的处理器资源
阻塞是线程停止运行,而自旋是线程仍处于运行状态,只不过跑的都是无用指令。它期望在运行无用指令的过程中,锁能被释放出来。(阻塞相当于熄火停车,自旋相当于怠速停车)但是JVM无法根据等待时间的长短来选择是自旋还是阻塞,JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间。
3)自旋还有一个副作用,不公平锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁,然而处于阻塞状态的线程,很有可能优先获得这把锁。
锁的四种状态:无锁、偏向锁、轻量级锁、重量级锁
这几个状态会随着竞争情况逐渐升级,但不能降级,目的是为了提高获得锁和释放锁的效率。
- 偏向锁
偏向锁(乐观锁):JDK1.6之后synchronized默认的锁——任意时刻只有一个线程请求某一把锁。此时只有一个线程在来回尝试获取锁,直接将加锁和解锁的过程都免了,只是简单判断下是否是同一个线程在获取锁,若是直接进入同步块(临界区)。就相当于你在你的私人庄园里装了一个红绿灯,并且庄园里只有你在开车,偏向锁就是在红绿灯处识别车牌号,如果识别到你的车牌号,直接亮绿灯。
当不同时刻有不同的线程尝试获取锁时,偏向锁会膨胀为轻量级锁。
偏向锁等到竞争才释放锁,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
-
轻量级锁
轻量级锁:在不同时刻有不同线程尝试获取锁。每次获取锁均需要加锁与解锁。就相当于深夜的十字路口,四个方向都闪黄灯。由于来往车辆少,如果红绿灯交替,可能出现四个方向只有一辆车在等红灯。闪黄灯代表车辆可以自由通过,但司机需要观察。
当同一时刻有多个线程尝试获取锁时,轻量级锁会膨胀为重量级锁。 -
重量级锁
重量级锁(悲观锁):JDK 1.6之前synchronized的锁,在这种状态下,JVM会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
关于重量级锁的自适应自旋:
获取重量级锁失败的线程不是立即进入阻塞态,而是自旋一段时间,若在此时间内成功获取到锁,则在下次等待时适当延长自旋时间,否则适当缩短自旋时间。
1.3.2 其他优化
- 锁粗化
将多个连续的加锁与解锁过程粗化为一次范围大的加锁与解锁,减少因为加减锁带来的CPU开销。
static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("hello");
sb.append("bit");
sb.append("hello");
}
上面这段代码中每调用一次append()方法,都伴随着一次加锁和解锁,锁粗化就是在第一次调用append()方法的时候进行加锁,而在最后一个append()方法执行完才解锁。
- 锁消除
在不会出现锁竞争的场景,会将线程安全的集合或类中的锁取消。
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("bit");
sb.append("hello");
}
这段代码将StringBuffer的创建放到了方法里边,这时多线程并不会访问到sb,只有主线程可以,因为它在自己的工作内存中,隔离了,不会出现多线程竞争问题,如果再进行加减锁就没有必要,JVM就会将锁取消。
2、死锁
死锁:程序“卡死”,以下四个条件同时满足,会造成程序死锁。
- 互斥:共享资源只能被一个线程占用。
- 占有且等待:线程A已经取得共享资源X,在等待获取资源Y时,不释放X。
- 不可抢占:线程A已经获取X之后,其他线程不能强行抢占X。
- 循环等待:线程A占用X,线程B占用Y,A等待Y,B等待X。
jps命令—查看jvm正在跑的线程
如何解决死锁问题: 只要破坏任何一个条件即可解决。
class Pen {
}
class Book {
}
public class DeadLockTest {
public static void main(String[] args) {
Pen pen = new Pen();
Book book = new Book();
Thread penThread = new Thread(() -> {
synchronized (pen) {
System.out.println("我有笔,需要本子");
synchronized (book) {
System.out.println("笔线程同时获取到笔与本子");
}
}
});
Thread bookThread = new Thread(() -> {
synchronized (book) {
System.out.println("我有本子,需要笔");
synchronized (pen) {
System.out.println("本子线程同时获取本子与笔");
}
}
});
penThread.start();
bookThread.start();
}
}
由于synchronized这种内建锁在解决死锁问题时比较麻烦,所以就产生了Lock体系来解决死锁问题。(性能不是Lock体系产生的原因)