写在前面:
视频是什么东西,有看文档精彩吗?
视频是什么东西,有看文档速度快吗?
视频是什么东西,有看文档效率高吗?
1. 数据安全问题
诸小亮:多线程的使用虽然可以帮助我们提高效率,但是也可能造成问题
张小飞:是吗?什么样的问题?
诸小亮:数据安全问题
1. 前提
诸小亮:你还记得之前我们说了CPU切换线程的情况吗?
张小飞:当然了,CPU是不断的切换线程执行的,当CPU从 A 线程切换到 B 线程时,A 线程就会暂停执行
诸小亮:没错,那你看看下面这段代码,CPU可能让线程暂停到哪一行?
public void run() {
System.out.println(name + "。。。。。");
System.out.println(name + "。。。。。");
System.out.println(name + "。。。。。");
}
张小飞:上面 3 句代码,这里的每一句代码都有可能被CPU的调度打断,导致当前线程暂停执行
诸小亮:你说的不错,都有可能,具体看 CPU 心情
2. 多线程操作数据
诸小亮:接下来,我们就来看一下——多线程中可能出现的数据安全问题
张小飞:到底什么时候会产生问题呢?
诸小亮:当多个线程同时操作同一个数据时候,可能产生数据安全问题
class Hero implements Runnable{
//1. 定义一个静态变量,多个线程同时操作它
public static int num = 10;
@Override
public void run() {
while (true){// while中用true,这是死循环,谨慎使用,这里是为了演示效果
//2. run方法中,对num--,当num<=0时,跳出循环
if(num > 0){
//sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程
try { Thread.sleep(5); } catch (InterruptedException e) {}
num--;
System.out.println(Thread.currentThread().getName() + "***********" + num);
}else{
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Hero hero = new Hero();
Thread yase = new Thread(hero, "yase");
Thread daji = new Thread(hero, "daji");
//3. 开启两个线程操作num
yase.start();
daji.start();
}
}
结果:
诸小亮:仔细看上面的代码,当 num>0 时,才会执行输出语句,但是却输出了负数
张小飞:是啊,为什么会出现负数?
诸小亮:我们来看一下代码的执行过程:
2个线程刚开始正常执行,都会执行num--。。。。。
当 num=1 时,假如 'yase'先进入 先进入 if ,休眠5毫秒,这时 CPU 切换到线程‘daji’进入 if 语句
‘yase’休眠结束,执行num--,接着‘daji’睡醒后也执行num--,最终num=-1
张小飞:原来是这样,如果不让这两个线程休眠,是不是可以避免这种情况?
诸小亮:即使没有 sleep 语句,也可能输出负数,只不过概率太低而已
张小飞:额。。。,我明白了,线程可能暂停到任意一句代码
诸小亮:是的,所以记住,一个线程在操作数据时,其他线程也参与运算,最终可能造成数据错误
张小飞:那么如何解决这种问题呢?
诸小亮:保证操作数据的代码在某一时间段内,只被一个线程执行,执行期间,其他线程不能参与
2. 线程同步
张小飞:怎么才能保证‘在某一时间段内,只被一个线程执行’呢?
诸小亮:那就要用到——线程同步
- 线程同步:就是让线程一个接一个的排队执行
- 同步:是一个接一个的意思
张小飞:排队执行,有意思,怎么才能让线程排队?
诸小亮:Java中提供 synchronized 关键字用来实现同步,可以解决多线程时的数据安全问题
张小飞:原来这样的关键字,赶快告诉我怎么用啊
1. synchronized 代码块
诸小亮:synchronized 的使用非常简单,其格式:
synchronized(锁对象){
需要同步代码块
}
张小飞:‘锁对象’是什么东西?
诸小亮:可以理解为钥匙、通行证,只有线程拿到通行证后,才能执行 { } 中的代码
1. 演示
诸小亮:我们使用 synchronized 修改一下 run 方法:
public static Object obj = new Object();
@Override
public void run() {
while (true){
//使用同步代码块,把 obj 作为锁对象
synchronized (obj){
if(num > 0){
//sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程
try { Thread.sleep(5); } catch (InterruptedException e) {}
num--;
System.out.println(Thread.currentThread().getName() + "***********" + num);
}else{
break;
}
}
}
}
结果:
张小飞:可以具体解释一下吗?
诸小亮:当然可以
- 当线程执行到 synchronized (obj) 时,会获取尝试 obj 对象
- synchronized 保证多线程下,只有一个线程能获取到obj对象,而其他线程会阻塞(暂停)
- 当多个线程同时执行到 synchronized (obj) 这句代码时,只有一个线程能够拿到obj,其他线程暂停
- 当持有 obj 的线程从 synchronized 退出后,会释放 obj 对象,然后其他线程再次争夺 obj
诸小亮:这样就保证了 synchronized 中的代码在某一时刻只能被一个线程执行
张小飞:原来如此
2. 锁对象
张小飞:不过,synchronized 的锁对象都可以是什么?
诸小亮:好问题!在 synchronized 中,锁对象可以是任意对象,只要是引用类型的对象都可以作为锁对象
张小飞:那么如何选择合适的锁对象呢?
诸小亮:这就要根据实际情况去选择了,在之后我们会介绍两种常用的锁对象
张小飞:好的
3. 好处和弊端
诸小亮:刚才演示了 synchronized 的最用,你能总结一下它的好处和弊端吗?
张小飞:当然可以了
好处:解决多线程数据安全问题
弊端:同步代码块同时只能被一个线程执行,降低效率
诸小亮:嗯嗯,还不错
4. synchronized的注意事项
诸小亮:使用 synchronized 需要注意,必须在多线程场景下使用
- 线程数 > 1,就是多线程
张小飞:明白
诸小亮:另外,多线程争抢的锁对象只能有一个,下图中的做法要坚决抵制
诸小亮:上图中的做法是会被开除的
张小飞:这是自然,确保锁对象的唯一性是确保 synchronized 正确性的前提
诸小亮:哟,没想到你总结的还挺到位
张小飞:那是自然,也不看是跟谁学习的
2. 同步方法
诸小亮:下面,我们来说说——同步方法
张小飞:是把 **synchronized **放到方法上吗?
诸小亮:是的,把 synchronized 放到方法上,那么这个方法就是同步方法
1. 演示
诸小亮:同步方法,限制同一时刻只有一个线程可以执行这个方法,比如:
class Hero implements Runnable{
public static int num = 10;
public static Object obj = new Object();
@Override
public void run() {
//run方法中调用同步方法
this.show();
}
//同步方法
public synchronized void show(){
while (true){
if(num > 0){
try { Thread.sleep(5); } catch (InterruptedException e) {}
num--;
System.out.println(Thread.currentThread().getName() + "***********" + num);
}else{
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Hero hero = new Hero();
Thread yase = new Thread(hero, "yase");
Thread daji = new Thread(hero, "daji");
yase.start();
daji.start();
}
}
结果:
张小飞:上面的输出表示,只有 yase 线程进入了 show 方法吧
诸小亮:不错,yase线程进入show方法后,操作 num,然后退出方法,接着 daji 线程进入,发现num已经为,0所以直接退出
张小飞:不是说使用 synchronized,都需要锁对象吗,同步方法的锁对象是???
2. 同步方法的锁对象
诸小亮:同步代码块需要一个锁对象,但是同步方法并不需要,因为:同步方法的锁是this(当前对象)
张小飞:当前对象?
诸小亮:是的,下面的代码可以证明
class Hero implements Runnable{
public static Object obj = new Object();
@Override
public void run(){
//1. 使用 this 作为锁对象
synchronized (this){
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);
}
}
//2. 同步方法的锁是 this
public synchronized void show(){
System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Hero hero = new Hero();
//3. 启动 ‘亚瑟’ 线程,自动执行run方法
new Thread(hero,"亚瑟").start();
//4. 主线程休眠100毫秒后,执行show这个同步方法
Thread.sleep(100);
hero.show();
}
}
诸小亮:上面代码中,run方法中的 this 就是 hero 对象,这个知道吧
张小飞:这时自然
诸小亮:好的,那我们运行一下,它的结果是
张小飞:看结果,应该是 ‘亚瑟’线程执行结束后,在执行的 main 线程中的 show 方法
诸小亮:不错,其具体过程是
- 亚瑟线程先执行,走到 Thread.sleep(2000) 时,休眠2秒,但这时它已经持有 this (也就是hero对象)
- 然后main线程休眠100毫秒后,执行show方法尝试获取this,无法获取到,于是等待1.9秒
- 所以最终结果。。。。。
张小飞:明白了
3. 静态同步方法
张小飞:synchronized 可以放到静态方法上吗
诸小亮:可以的,这时就是静态同步方法
张小飞:那么,静态同步方法的锁对象是???
诸小亮:锁对象是:Hero.class,也就是方法所在类的 class 对象
张小飞:什么是 class 对象?
诸小亮:Hero.class 也是对象,称为 Class 或字节码对象,是 JVM 把 class 文件加载到内存后创建一个对象
张小飞:这么说,每一个类都有自己的 class 对象了?
诸小亮:是的,我们修改上面的代码
class Hero implements Runnable{
public static Object obj = new Object();
@Override
public void run(){
//1. 锁对象换成了Hero.class,也就是Hero字节码对象
synchronized (Hero.class){
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);
}
}
//2. show方法改成static
public static synchronized void show(){
System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Hero hero = new Hero();
new Thread(hero,"亚瑟").start();
Thread.sleep(100);
Hero.show();
}
}
结果:
4. 死锁
诸小亮:在使用 synchronized 时,要特别注意一种情况——死锁
张小飞:死锁是什么意思?
诸小亮:其实就是两个线程互相影响,A 线程等 B 线程释放锁,同时 B 线程也在等 A 线程释放锁,比如:
张小飞:能写个代码具体看看吗?
诸小亮:代码我都准备好了,死锁——通常发生在同步嵌套
- 同步嵌套:synchronized 中 还有 synchronized
示例:
class Hero implements Runnable{
//1. 搞两个锁对象 A、B
public static Object A = new Object();
public static Object B = new Object();
public boolean bool;
Hero(boolean bool){
this.bool = bool;
}
@Override
public void run(){
//2. 当 bool 是 true
if(bool){
synchronized (A){//先获取 A 锁,然后睡一会儿,再获取 B 锁
System.out.println(Thread.currentThread().getName() + "------拿到A锁,准备获取B锁。。。。。。。");
try { Thread.sleep(200); } catch (InterruptedException e) {}
synchronized (B){
System.out.println(Thread.currentThread().getName() + "。。。。。。。");
}
}
}else{//3. 当 bool 是 false
synchronized (B){ //先获取B锁,睡一会儿,再获取A锁
System.out.println(Thread.currentThread().getName() + "------拿到B锁,准备获取A锁。。。。。。。");
try { Thread.sleep(200); } catch (InterruptedException e) {}
synchronized (A){
System.out.println(Thread.currentThread().getName() + "。。。。。。。");
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
new Thread(new Hero(true), "yase").start();
new Thread(new Hero(false), "daji").start();
}
}
诸小亮:你自己研究一下这个代码吧
张小飞:好的
五分钟后。
张小飞:结果是这样子的
诸小亮:那你能详细解释一下原因吗?
张小飞:我试试吧,其本质是——死锁导致程序无法继续运行,但同时也一直不结束,其过程:
- 假设 yase 线程先执行,在获取的 A 锁后,睡觉
- daji 线程接着执行,在获取 B 锁后,睡觉
- yase 线程睡醒后,尝试获取 B 锁,但是已经被 daji 线程拿到了,于是阻塞
- daji 线程睡醒后,尝试获取 A 锁,但是已经被 yase 线程拿到了,也阻塞
- yase 线程需要获取 B 锁后,执行完代码,再能释放 A 锁
- daji 线程需要获取 A 锁后,执行完代码,再能释放 B 锁
- 最终两个线程都阻塞,程序卡死
诸小亮:嗯嗯,分析的还不错,一定要记住:
死锁是开发中的禁忌,绝对禁止出现,所以开发中尽量避免同步代码块嵌套使用