写点什么

Java 多线程 —— 同步代码块,联通 java 开发面试

用户头像
极客good
关注
发布于: 刚刚

}


}


package test.MyThread.ticketDemo;


public class ticketDemo1 {


public static void main(String[] args) {


RunnableThread r1 = new RunnableThread();


Thread t1 = new Thread(r1,"窗口一");


Thread t2 = new Thread(r1,"窗口二");


Thread t3 = new Thread(r1,"窗口三");


t1.start();


t2.start();


t3.start();


}


}



但是结果和我们想象中的不一样,三个窗口卖出了同样的票


这是因为,CPU 的操作具有原子性,单独执行一条指令或者说语句,在执行完毕前不会被中断。


三个线程被启动后,都会处于就绪状态,然后开始抢夺 CPU 执行语句。


  1. 语句一:Thread.sleep(100);

  2. 语句二: System.out.println(Thread.currentThread().getName()+“正在出售第 “+ticket+” 张票”);

  3. 语句三: ticket–;


我将程序中需要执行的三条主要语句列了出来


三条线程中,加入线程一先抢到了 CPU,这时就会开始执行语句,也就是至少会完成一条语句一,然后进入休眠。


注:如果语句一不是休眠语句,而是别的语句,那么线程一就可以继续往下执行,因为原子性,正在执行的语句不会被打断,所以只会在一条语句结束,下一条语句未开始时,被抢走 CPU 或者中断,导致线程退出运行状态,转为就绪或者阻塞状态。所以线程一可以一次性完成多条语句,也有可能刚完成一条语句就被抢走了 CPU。


接着,线程二,线程三也抢到了 CPU,也开始执行语句一,然后也进入休眠状态。之后线程一二三从休眠中醒来,开始争抢 CPU 完成语句二,但是三者都在完成语句三之前被抢走了 CPU,导致一直没有执行 ticket–语句,ticket 也就没有减少,因此三条线程一共打印三条输出语句,里面的 ticket 都是相同。


然后三条线程又开始争抢 CPU 来完成语句三,一个线程让 ticket 减一,三个线程减少三张票。完成语句三后,又开始新的循环,三个线程开始争抢 CPU 完成语句一。


因此,看到的结果会是,三条语句的 ticket 都相同,然后 ticket 突然减三,接着又输出三条 ticket 相同的输出语句。


那么,该如何解决这种情况呢?


这种延迟卖票的问题被称为线程安全问题,要发生线程安全问题需要满足三个条件(任何一共条件不满足都不会造成线程安全问题):


  1. 是否存在多线程环境

  2. 是否存在共享数据/共享变量

  3. 是否有多条语句操作着共享数据/共享变量


火车站延迟卖票问题满足这三个条件,因此造成了线程安全问题,而前两条都不可避免,那么就可以着手于破坏掉第三个条件,让线程安全问题不成立。


思路是将多条语句包装成一个同步代码块,当某个线程执行这个同步代码块的时候,就跟原子性一样,其他的线程不能抢占 CPU,只能等这个同步代码块执行完毕。


解决办法:


  1. synchronized —— 自动锁

  2. lock —— 手动锁

synchronized

synchronized(对象){


//可能会发生线程安全问题的代码


}


//这里的对象可以是任意对象,我们可以用 Object obj = new Object()里面的 obj 放入括号中


使用 synchronized 的条件:


  1. 必须有两个或两个以上的线程

  2. 同一时间只有一个线程能够执行同步代码块

  3. 多个线程想要同步时,必须共用同一把锁


synchronized(对象)括号里面的对象就是一把锁


使用 synchronized 的过程:


  1. 只有抢到锁的线程才可以执行同步代码块,其余的线程即使抢到了


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


CPU 执行权,也只能等待,等待锁的释放。


  1. 代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争抢锁,谁抢到谁就能运行同步代码块。

同步代码块

因此,修改后的代码为:


package test.MyThread.ticketDemo;


public class RunnableThread implements Runnable{


private int ticket = 100;


Object obj = new Object();


@Override


public void run(){


while(true){


synchronized (obj) {


if (ticket > 0) {


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + " 张票");


ticket--;


}


}


}


}


}


package test.MyThread.ticketDemo;


public class ticketDemo1 {


public static void main(String[] args) {


//这里没有改动,只是在上一个代码中加了一把锁


RunnableThread r1 = new RunnableThread();


Thread t1 = new Thread(r1,"窗口一");


Thread t2 = new Thread(r1,"窗口二");


Thread t3 = new Thread(r1,"窗口三");


t1.start();


t2.start();


t3.start();


}


}



可以看出来结果符合我们的预期,是正确的


现在又有了新的问题,那就是如果我在构造线程的 RunnableThread 类里面加入方法呢?同步代码块里面出现方法时,我们应该怎么“上锁”呢?

同步方法(this 锁)

同步方法,在 public 的后面加上 synchronized 关键字

package test.MyThread.ticketDemo;


public class RunnableThread1 implements Runnable{


private int ticket = 100;


Object obj = new Object();


public boolean flag = true;


@Override


public void run(){


if(flag==true){


while(ticket>0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


SellTicket1();


}


}


}


//同步方法,在 public 的后面加上 synchronized 关键字


public synchronized void SellTicket1(){


if(ticket>0){


System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 张票");


ticket--;


}


}


}


package test.MyThread.ticketDemo;


public class ticketDemo2 {


public static void main(String[] args) throws InterruptedException {


RunnableThread1 r = new RunnableThread1();


Thread t1 = new Thread(r,"窗口一");


Thread t2 = new Thread(r,"窗口二");


t1.start();


t2.start();


}


}

this 锁

先来看看,如果有两条路径,一条路径是使用同步代码块,但是对象是 obj,另一条路径是使用同步方法


package test.MyThread.ticketDemo;


public class TicketWindow2 implements Runnable{


//定义 100 张票


private static int tickets = 100;


Object obj = new Object();


int i =0;


@Override


public void run() {


while (true){


if(i%2==0){


synchronized (obj){


if(tickets>0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");


}


}


}else {


sellTicket();


}


i++;


}


}


public synchronized void sellTicket(){


if(tickets>0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");


}


}


}



结果出错,说明同步方法的用的对象锁不能是任意的对象,不同的线程应该用相同的锁。同步方法是属于对象,而在这个类里面调用方法的是 this 对象,也就是 this.sellTicket(),因此把 this 提取出来作为对象锁中的对象。这样多个线程都用的是 this 锁


package test.MyThread.ticketDemo;


public class TicketWindow2 implements Runnable{


//定义 100 张票


private static int tickets = 100;


Object obj = new Object();


int i =0;


@Override


public void run() {


while (true){


if(i%2==0){


synchronized (this){


if(tickets>0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");


}


}


}else {


sellTicket();


}


i++;


}


}


public synchronized void sellTicket(){


if(tickets>0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");


}


}


}


修改完成后再运行代码,发现没有错误


注:


  1. 一个线程使用同步方法,另一个线程使用同步代码块 this 锁,可以实现同步

  2. 一个线程使用同步方法,另一个线程使用同步代码块,但是不是 this 锁。这种情况不能实现同步。

静态同步方法

同步方法的锁对象是 this,


静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件


下面代码挺长的,但其实就修改了上面同步方法的代码的两处地方

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java 多线程 —— 同步代码块,联通java开发面试