【java多线程】线程同步问题:用同步代码块、同步方法和重入锁实现线程同步

本文详细介绍了Java中线程同步的概念,包括同步代码块和同步方法的使用示例,以及ReentrantLock的原理和应用,探讨了线程同步与异步的区别,并通过卖票场景展示了线程同步的必要性。
摘要由CSDN通过智能技术生成

目录

1、线程的同步问题

1.1、线程的同步与异步

 1.2、实现线程同步的几种方式

2、同步代码块用例子实现线程同步

2.1、线程不同步的情况

2.2、线程同步的代码

3、同步方法实现线程同步

4、重入锁(ReentrantLock)实现线程同步

1、线程的同步问题

1.1、线程的同步与异步

        同步是执行或调用一个方法时,每次都需要拿到对应的结果才会继续往后执行;异步与同步相反,它会在执行或调用一个方法后就继续往后执行,不会等待获取执行结果。二者的区别就是处理请求发出后,是否需要等待请求结果,再去继续执行其他操作。

       

         线程安全是同步,线程不安全是异步

        同步:排队执行,效率低但是安全

        异步:同时执行,效率高但是数据不安全

线程同步: 

 1.2、实现线程同步的几种方式

        处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

        实现线程同步的7种方式:

        (1)同步代码块

        (2)同步方法

        (3)使用重入锁实现线程同步(ReentrantLock)

        (4)使用阻塞队列实现线程同步(BlockingQueue (常用)add(),offer(),put()

        (5)使用特殊域变量(volatile)实现同步(每次重新计算,安全但并非一致)

        (6)使用局部变量实现线程同步(ThreadLocal)以空间换时间

        (7)使用原子变量实现线程同步(AtomicInteger(乐观锁))

2、同步代码块用例子实现线程同步

同步代码块就是把操作共享数据的代码锁起来

格式:

synchronized(锁){

        操作共享数据的代码

}

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2:里面的代码全部执行完毕,线程出来,锁自动打开

注意:

(1)synchronized(锁)不要写在循环的外面,因为线程一但进入synchronized(锁)中,就会上锁会在里面的循环执行完全部才会退出synchronized(锁)并释放锁,这就会导致这时候所有任务都被这个线程做完了,其他线程进来也没任务做了

(2)synchronized(锁)中的锁对象必须是唯一的,如果每个线程的锁不是同一个的话,就起不到锁的作用。synchronized(this)这里的this代表的是当前进来的线程,当把锁对象定义为this时,因为每个线程是不同的,所以this代表的锁对象也不是唯一的。所以一般会把当前类的字节码文件,因为类的字节码文件是唯一的,在同一个文件里只能有一个与文件同名的Class文件,即下面需求的代码可以写成synchronized(MyThread.class),括号里面写当前类的字节码文件。

比如我现在有这么一个需求:某电影院目前正在上映新电影,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

2.1、线程不同步的情况

创建线程类,创建多个此线程进行同时出售电影票

public class MyThread extends Thread{
    //表示这个线程类所有的对象,都共享ticket数据
    static int ticket = 0;//0 ~ 99
    @Override
    public void run() {
        while (true) {
            if (ticket < 50) {
                try {
                    //设置线程睡眠
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票");
            } else {
                break;
            }
        }
    }
}

创建线程对象,然后启动线程

public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //给线程命名
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果

从运行的结果可以发现,卖出去的电影票重复了,并且还超出范围了

总结:从运行的结果可以发现,卖出去的电影票重复了,并且还超出范围了,这是因为线程执行时时具有随机性的,比如当线程1运行sleep()方法后,CUP的执行权会落到线程2或线程3的手中,这时候就有可能发生多个线程同时执行到了 tickte++; 这条语句,然后它们在买票时的票号(ticket)会是一样的,即打印出来的 ticket 是一样的,而最后电影票的票号超出范围也是这个原因

2.2、线程同步的代码

只需要同步代码块:把MyThread中操作共享数据的代码用 synchronized 锁起来就可以了

public class MyThread extends Thread{
    //表示这个线程类所有的对象,都共享ticket数据
    static int ticket = 0;//0 ~ 99
    //锁对象,一定要是唯一的
    static Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (ticket < 50) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }
            }
        }
    }
}

3、同步方法实现线程同步

同步方法就是把synchronized关键字加到方法上

格式:

修饰符 synchronized 返回值类型 方法名(方法参数) {

        . . . . . .

}

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己指定,如果方法是非静态的,那么它的锁对象是this,就是当前方法的调用者;如果是静态方法,那么锁对象是当前类的字节码文件对象

现在改上面的需求为:某电影院目前正在上映新电影,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票,利用同步方法完成

实现代码如下:

public class MyRunnable implements Runnable{
    int ticket = 0;
    //现在实现线程的方式是实现Runnable接口的方式,在这种方式中的MyRunnable是作为一个参数让线程去执行的,
    //所以我只会创建一次MyRunnable的对象,既然只创建一次,那么ticket前就不需要加static关键字来让线程共享它,
    //这种方式下的几个线程本就是共享ticket的
    @Override
    public void run() {
        //1.循环
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2.同步代码块(同步方法)
            if (method()) break;
        }
    }

    //这里的method()方法是非静态的,这里的对象锁就是this,
    //这个this就是在测试类中创建的MyRunnable对象,而在测试类中我只会创建一次MyRunnable的对象,
    //所以在测试类中的MyRunnable对象是唯一的,即锁对象就是唯一的
    private synchronized boolean method() {
        //3.判断共享数据是否到了末尾,如果到了末尾
        if (ticket == 50) {
            return true;
        } else {
            //4.判断共享数据是到了末尾,如果没有到末尾
            ticket++;
            System.out.print(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
            if (ticket%2 == 0) {
                System.out.println();
            }
        }
        return false;
    }
}

代码中需要注意的点:

(1)现在这里实现线程的方式是实现Runnable接口的方式,在这种方式中的MyRunnable是作为一个参数让线程去执行的,所以我只会创建一次MyRunnable的对象,既然只创建一次,那么ticket前就不需要加static关键字来让线程共享它,这种方式下的几个线程本就是共享ticket的

(2)这里的method()方法是非静态的,这里的对象锁就是this,这个this就是在测试类中创建的MyRunnable对象,而在测试类中我只会创建一次MyRunnable的对象,所以在测试类中的MyRunnable对象是唯一的,即锁对象就是唯一的

 创建线程对象,然后启动线程

public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr,"窗口1");
        Thread t2 = new Thread(mr,"窗口2");
        Thread t3 = new Thread(mr,"窗口3");

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

 运行结果:

4、重入锁(ReentrantLock)实现线程同步

        Lock锁:synchronized属于隐式锁,即锁的持有与释放都是隐式的,虽然可以理解同步代码块和同步方法的锁对象问题,但是并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock(显式锁)

        Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法:void lock():获得锁,手动上锁;void unlock():释放锁,手动释放锁

        Lock是接口不能直接实例化,可以使用它的实现类ReentrantLock,ReentrantLock的一个重要特点是,它允许一个线程多次获取同一个锁而不会产生死锁。这与synchronized关键字提供的锁定机制非常相似,但ReentrantLock提供了更高的扩展性。

这里用ReentrantLock的构造方法ReentrantLock():创建一个ReentrantLock的实例 

public class MyThread extends Thread{
    //表示这个线程类所有的对象,都共享ticket数据
    static int ticket = 0;//0 ~ 99
    //锁对象,一定要是唯一的
    static Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            //同步代码块
            lock.lock();
            try {
                if (ticket < 50) {
                    Thread.sleep(100);
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

 运行结果

推荐:

【Java多线程】多线程的三种实现方式和多线程常用方法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/136961604?spm=1001.2014.3001.5501

【Java多线程】多线程的三种实现方式和多线程常用方法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/136961604?spm=1001.2014.3001.5501

【操作系统】进程基础知识-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/136845709?spm=1001.2014.3001.5501

  • 32
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值