线程的同步与死锁

同步问题:每一个线程对象轮番“抢占”共享资源带来的问题。(实际上占主导权的还是CPU,这里只是形象的描述)

引例: 卖票小程序

package 同步;

/**
 * @BelongsProject: untitled
 * @BelongsPackage: 同步
 * @Author: mcc
 * @CreateTime: 2020-10-09 08:27
 * @Description:模拟卖票
 */
class Window implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(100);//模拟网络延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window window = new Window();

        Thread thread1 = new Thread(window);
        Thread thread2 = new Thread(window);
        Thread thread3 = new Thread(window);
        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    }

}

在这里插入图片描述

这时我们发现,票数不仅出现重复,而且出现负数,这种问题我们称之为不同步问题。


问:三个线程共享一个资源,为什么会出现负数票?
在这里插入图片描述

答:我们认为,当ticket为1时,只能打印1张票;但是,当一个线程进入if语句还没出来时,其余线程也可通过判断进入if语句,再有线程没有任何限制的去执行后面的代码,而线程获取cpu时间片是随机的,所以哪一个线程先出来是不确定的,所以会出现重复/负数。为了解决这种不同步操作造成的问题,提出了同步处理。

一、同步处理

所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照“顺序”一个一个来。

1.synchronized同步机制

  • 同步代码块
  • 同步方法

1.1同步代码块

synchronized (同步监视器){
	//需要被同步的代码
}

说明:

  • 操作共享数据的代码即为需要被同步的代码
  • 共享数据:多个线程共同操作的变量,例如ticket
  • 同步监视器:俗称:锁,任何一个类的对象都可以充当锁
  • 要求:多个线程必须共用同一把锁
  • 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器
具体分类同步监视器伪代码
实例方法类的实例对象synchronized (this){ }
静态方法类对象public static synchronized (类名称.class){ } 【全局锁】
任意实例对象Object实例对象ObjectString lock = " ";synchronized(lock){ }
class Window implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);//模拟网络延时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
class Window2 extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (Window2.class) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ": 卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

1.2同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的

具体分类同步监视器伪代码
实例方法类的实例对象public synchronized void method(){ }
静态方法类对象public static synchronized void method(){ }【全局锁】
class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);//模拟网络延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
class Window4 extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private static synchronized void show() {//同步监视器:Window4.class
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

关于同步方法总结:

  • 同步方法任然涉及到同步监视器,只是不需要我们显示声明
  • 非静态的同步方法,同步监视器是:this
    静态的同步方法,同步监视器是:当前类本身

同步代码块是在方法里拦截的,也就是说进入方法的线程依然可能会有多个。而同步方法是在方法上拦截的,保证了同一时刻只有一个线程进入该方法

2.synchronized 锁多对象问题

class Sync{
	public synchronized void test() {
		System.out.println("test方法开始,当前线程为:"+Thread.currentThread().getName());
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("test方法结束,当前线程为:"+Thread.currentThread().getName());
	}
}
class MyThread extends Thread{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		Sync sync = new Sync();
		sync.test();
	}
}
public class Test{
	public static void main(String[] args) {
		for(int i = 0;i<3;i++) {
			Thread thread = new MyThread();
			thread.start();
		}
	}
}

在这里插入图片描述
按照之前对synchronized的理解,当一个线程开始执行到结束后,另一个线程才进入该同步方法,而实际情况却是交叉执行与设想不同,这是为什么?
:实际上synchronized(this)与以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。而本代码中是多个线程同时执行多个对象的同步代码段,所以synchronized关键字在此处看似无用。要明确synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身,也就是this


那么怎么锁住这段代码?
Ⅰ.锁同一个对象
class Sync {
    public void test() {
        synchronized (this) {
            System.out.println("test方法开始,当前线程为:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test方法结束,当前线程为:" + Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread {
    private Sync sync;

    public MyThread(Sync sync) {
        this.sync = sync;
    }

    @Override
    public void run() {//在这里,有三个线程,而sync对象依然只有一个
        this.sync.test();
    }
}

public class Test {
    public static void main(String[] args) {
        Sync sync = new Sync();//这里产生了一个对象
        for (int i = 0; i < 3; i++) {
            Thread thread = new MyThread(sync);//构造注入
            thread.start();
        }
    }
}
Ⅱ.synchronized锁这个类对应的Class对象【全局锁】
class Sync{
	public void test() {
		synchronized (Sync.class) {
			System.out.println("test方法开始,当前线程为:"+Thread.currentThread().getName());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("test方法结束,当前线程为:"+Thread.currentThread().getName());
		}
	}
}
class MyThread extends Thread{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		Sync sync = new Sync();
		sync.test();
	}
}
public class Test{
	public static void main(String[] args) {
		for(int i = 0;i<3;i++) {
			Thread thread = new MyThread();
			thread.start();
		}
	}
}

如果想要锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this

3.synchronized实现原理
Ⅰ.同步代码块

执行同步代码块后首先要执行monitorenter指令,退出时执行monitorexit指令。使用内建锁(synchronized)进行同步,关键是必须要对对象监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。通常一个monitorenter指令会搭配若干个monitorexit指令,这是因为Java虚拟机要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁

Ⅱ.同步方法

当用synchronized标记方法时,编译后的字节码中方法的访问标记包括ACC_SYNCHRONIZED。该标记表示在进入该方法时,Java虚拟机要进行monitorenter操作。而在退出方法时,无论是否正常退出,Java虚拟机均需要进行monitorexit操作。


monitorenter和monitorexit的作用:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
在这里插入图片描述
之所以采取这种计数器的方式,是 为了允许同一个线程重复获取同一把锁

4.JDK1.5提供的锁代码块的Lock锁

  • 从JDK1.5开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了共享资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有synchronized相同的并发性和内存语意,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁
package LockTest;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @BelongsProject: untitled
 * @BelongsPackage: LockTest
 * @Author: mcc
 * @CreateTime: 2020-10-16 07:37
 * @Description:
 */
class Window implements Runnable {
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //2.调用锁定方法lock()
                lock.lock();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //3.调用解锁方法unlock()
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized与Lock的异同:

相同:二者都可以解决线程安全问题
不同:

  • synchronized机制在执行完相应的同步代码块以后,自动的释放同步监视器
    Lock需要手动的启动同步(lock()),同时结束同步也需要手动实现(unlock())

优先使用顺序:Lock–>同步代码块(已经进入了方法体,分配了相应资源)–>同步方法(在方法体之外)

5.synchronized的优化

5.1CAS操作:Compare And Swap( )——乐观锁

悲观锁(JDK1.6前的内建锁):假设每一次执行同步代码块均会产生冲突,所以当线程获取锁成功,会阻塞其他尝试获取该锁的线程。
乐观锁:假设所有线程访问共享资源时不会出现冲突,既然不会出现冲突就不会阻塞其他线程。线程不会出现阻塞状态。
什么是CAS操作?
CAS(无锁操作),使用CAS叫做比较交换来判断是否出现冲突,如果出现冲突就重试当前操作,知道不冲突为止。


Ⅰ.CAS操作过程

  • V:内存中地址存放的实际值
  • O:预期值(旧值)
  • N:更新后的值
    在这里插入图片描述

当执行CAS后:
如果V==0,即旧值与内存中实际值相等,表示上次修改该值后没有任何线程再次修改此值,因此可以将N替换到内存中。
如果V!=0,表示该内存的值已经被其他线程做了修改,所以无法将N替换,返回最新的值V。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其余线程均会失败。失败的线程会重新尝试将该线程挂起(阻塞)。

CAS和synchorized(未优化前)的区别:

  • synchorized(未优化前)在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。
  • CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步

Ⅱ.CAS问题
①ABA问题
解决思路:沿用数据库的乐观锁机制,添加版本号
例如:1A–2B–3A,JDK1.5提供atomic包下的StampedReference类来解决CAS的ABA问题。
②自旋会浪费大量的处理器资源
因为此时线程仍处于运行状态,只不过跑的是无用指令,期望在运行无用指令的过程中,锁能被释放出来。
解决思路:自适应自旋–>根据以往自旋等待时能否获取到锁,来动态调整自旋的时间(循环尝试变量),如果在上次一自旋时获取到锁,则此次自旋时间稍微变长一点;如果上一次在自旋结束还没有获取到锁,此次自旋时间稍微短一点。
③公平性
处于阻塞状态的线程无法立刻竞争被释放的锁;而处于自旋状态的线程很有可能立刻获取到锁。内建锁无法实现公平性,lock体系可以实现公平性。

5.2偏向锁、轻量级锁、重量级锁

JDK1.6之后对内建锁做了优化,新增了偏向锁和轻量级锁。
在这里插入图片描述

锁状态是否是偏向锁锁标志位
无锁状态001
偏向锁101
轻量级锁00
重量级锁10

这四种状态随着竞争情况逐渐升级,锁可以升级不可以降级,为了提高获得锁与释放锁的效率。


偏向锁:最乐观的锁,从始至终只有一个线程请求一把锁。
引入偏向锁的目的:由于大多数情况下,锁不仅存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低
偏向锁的获取:当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录中记录存储偏向锁的线程ID。以后该线程再次进入同步块时不再需要CAS来加锁和减锁,只需要简单测试一下对象头markword中偏向锁的线程ID是否是当前线程ID,如果成功,表示线程已经获取到锁直接进入代码块运行。如果测试失败,检查当前偏向锁字段是否位0,如果为0,采用CAS操作将偏向锁字段设置为1,并且更新自己的线程ID到markword字段中。如果为1,表示此时偏向锁已经被别的线程获取,则此线程不断尝试使用CAS获取偏向锁,或者将偏向锁撤销,升级为轻量级锁(升级概率较大)。
偏向锁撤锁:偏向锁使用一种等待竞争出现才释放锁的机制,当有其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁。
注:偏向锁的撤销开销较大,需要等待线程进入全局安全点safepoint(当前线程在CPU上没有执行任何有用的字节码)。偏向锁从JDK1.6后默认开启,但是它在应用程序启动几秒后才激活。

  • —XX:Biased LockingStartupDelay = 0,将延迟关闭,JVM一启动就激活偏向锁。
  • —XX:UseBiased Locking = false,关闭偏向锁,程序里默认进入轻量级锁。
    在这里插入图片描述

轻量级锁:多个线程在不同时间段请求同一把锁,也就是基本不存在锁竞争。针对此种情况,JVM采用轻量级锁来避免线程的阻塞以及唤醒。
加锁:线程在执行同步代码块前,JVM先在当前线程栈帧中创建用于建立存储锁记录的空间,并将对象头的Mark Word字段直接复制到此空间中。然后线程尝试使用CAS将对象头的Mark Word替换为指向锁记录的指针(指向当前线程),如果成功则表示获取到轻量级锁;如果失败,表示其他线程竞争轻量级锁,当前线程便使用自旋来不断尝试。
释放锁:解锁时会使用CAS将复制的Mark Word替换回对象头,如果成功,表示没有竞争发生,正常解释;如果失败,表示当前锁存在竞争,进一步膨胀为重量级锁。重量级锁会阻塞、唤醒请求加锁的线程,针对的是多个线程同一时刻竞争同一把锁的情况,JVM采用自适应自旋来避免线程在面对非常小的同步块时,仍会被阻塞以及唤醒。
在这里插入图片描述
偏向锁和轻量级锁的区别:轻量级锁采用CAS操作将锁对象的标记字段替换为指向线程的指针,存储着所对象原本的标记字段,针对的 是多个线程在不同时间段申请同一把锁的情况。偏向锁只会在第一次请求时采用CAS操作,在锁对象Mark Word字段中记录下当前线程的ID,此后运行中持有偏向锁的线程不再有加锁过程,针对的是锁仅会被同一线程持有。
总结:

Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。

  1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
  2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
    在这里插入图片描述

6.死锁

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
package 死锁;

/**
 * @BelongsProject: untitled
 * @BelongsPackage: 死锁
 * @Author: mcc
 * @CreateTime: 2020-10-14 08:16
 * @Description: 演示线程的死锁问题
 */
public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

解决方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值