Java 多线程 —— 同步代码块(解决线程安全问题)

火车站抢票问题

由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠语句

package test.MyThread.ticketDemo;

public class RunnableThread 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--;  //语句三
            }
        }
    }
}

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. 只有抢到锁的线程才可以执行同步代码块,其余的线程即使抢到了CPU执行权,也只能等待,等待锁的释放。
  2. 代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争抢锁,谁抢到谁就能运行同步代码块。
同步代码块

因此,修改后的代码为:

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,
静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件

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

  1. public synchronized void sellTicket(){}改为
    public synchronized static void sellTicket(){}
  2. synchronized (this){}改为synchronized (TicketWindow2.class){}
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 (TicketWindow2.class){
                    if(tickets>0){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");

                    }
                }
            }else {
                sellTicket();
            }
            i++;
        }

    }

    public synchronized static void sellTicket(){
        if(tickets>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");
        }
    }
}

main()方法里面创建进程和启动进程的代码,和上面同步方法里面的代码相同
结果也和上面的一样,都不再列出来了

死锁问题
package test.MyThread.ticketDemo;
//两个不同的锁对象
public class LockObject {
    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
}

package test.MyThread.ticketDemo;

public class DieLockThread extends Thread{
    public boolean flag;

    public DieLockThread(boolean flag){
        this.flag = flag;
    }
    @Override
    public void run() {
        if(flag){
            synchronized(LockObject.lock1){
                System.out.println("lock1");
                synchronized(LockObject.lock2){
                    System.out.println("lock2");
                }
            }
        }else{
            synchronized(LockObject.lock2){
                System.out.println("lock2");
                synchronized(LockObject.lock1){
                    System.out.println("lock1");
                }
            }
        }

    }
}

package test.MyThread.ticketDemo;

public class DieLockDemo {
    public static void main(String[] args) {
        DieLockThread d1 = new DieLockThread(true);
        DieLockThread d2 = new DieLockThread(false);

        d1.start();
        d2.start();
    }
}

在这里插入图片描述
程序会卡在这一步,不能进行下一步也不能停止
利用有参构造,构造出来的线程d1应该是先获得锁对象LockObject.lock1然后执行打印语句。接着获取锁对象LockObject.lock2,然后打印lock2。
但是这里因为线程d2是先获取的锁对象LockObject.lock2,并占据这个锁对象,然后想获得锁对象LockObject.lock1,但LockObject.lock1此时被线程d1占据着

两个线程都在等待对方释放锁对象,然后进行下一步,但是两者都不释放,导致程序卡死在这里。这就造成了死锁。

lock

package test.MyThread.ticketDemo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockThread implements Runnable{
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    @Override
    public void run(){
        while(ticket>0){
            try{
                lock.lock();
                if(ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (ticket--) + " 张票");
                }
            }finally {
                lock.unlock();
            }
        }

    }
}

package test.MyThread.ticketDemo;

public class LockDemo {
    public static void main(String[] args) {
        LockThread lt = new LockThread();

        Thread t1 = new Thread(lt,"窗口一");
        Thread t2 = new Thread(lt,"窗口二");
        Thread t3 = new Thread(lt,"窗口三");

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

在这里插入图片描述
结果正确

  • 42
    点赞
  • 231
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一纸春秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值