首页 > 其他分享 >线程同步和线程通信

线程同步和线程通信

时间:2023-02-15 13:33:05浏览次数:38  
标签:同步 Thread void 通信 线程 new public

Synchronized锁和Lock锁机制解决线程同步问题,及wait和notify实现线程间通信的简要介绍

Author: Msuenb

Date: 2023-02-15


线程同步

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

通过一个经典案例,演示线程的安全问题: 模拟火车站的卖票过程。假设本趟列车有100张票,开启三个窗口售票。

同一资源和线程同步问题

现在需要三个窗口卖的是全部的100张票,而不是每个窗口各自卖一百张票。所以要实现资源的同一。

在Java中局部变量和实例变量都不是共享变量,静态变量对于不同的实例对象是共享的,因此可以用静态变量存储剩余票数。

示例代码:

public class ThreadSync01 {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();

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

class TicketSaleThread extends Thread{
    private static int total = 100;
    public void run(){
        while(total>0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "卖出一张票,剩余:" + --total);
        }
    }
}

运行结果:

Thread-0卖出一张票,剩余:99
Thread-2卖出一张票,剩余:98
Thread-1卖出一张票,剩余:97
Thread-2卖出一张票,剩余:96
Thread-1卖出一张票,剩余:94
Thread-0卖出一张票,剩余:95
...
Thread-0卖出一张票,剩余:4
Thread-0卖出一张票,剩余:2
Thread-1卖出一张票,剩余:2
Thread-2卖出一张票,剩余:1
Thread-2卖出一张票,剩余:0
Thread-1卖出一张票,剩余:-1
售票结束...
Thread-0卖出一张票,剩余:-2
售票结束...
售票结束...

结果:有重复票和负数票问题。导致这样情况的原因如下

Synchronized同步机制

要解决上述多线程并发访问一个资源的安全性问题,也就是解决重复售票与超额售票问题,Java中提供了同步机制(synchronized)来解决。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

同步解决线程安全的原理:

  • 同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,这个锁称为同步锁。

  • 当一个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。

同步代码块和同步方法:

  • 同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

    public synchronized void method(){
        可能会产生线程安全问题的代码
    }
    
  • 同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

    synchronized(同步锁对象){
         需要同步操作的代码
    }
    

同步锁对象的选择:

​ 同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。

​ 对于同步代码块来说,同步锁对象是由程序员手动指定的,但是对于同步方法来说,同步锁对象只能是默认的,

  • 静态方法:当前类的Class对象
  • 非静态方法:this

同步代码的范围选择:

  • 锁的范围太小:不能解决安全问题

  • 锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。

释放同步锁:

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。

注意:以下操作不会释放锁

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()(避免使用)方法将该线程 挂起,该线程不会释放锁

Synchronized使用演示

  • 静态方法加锁

    public class ThreadSync02 {
        public static void main(String[] args) {
            TicketSaleThread t1 = new TicketSaleThread();
            TicketSaleThread t2 = new TicketSaleThread();
            TicketSaleThread t3 = new TicketSaleThread();
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    class TicketSaleThread extends Thread {
        private static int tickets = 100;
    
        @Override
        public void run() { // 直接锁这里,肯定不行,会导致,只有一个窗口卖票
            while (tickets > 0) {
                saleTicket();
            }
        }
    
        // 锁对象是TicketSaleThread类的Class对象 一个类的Class对象只有一个
        private synchronized static void saleTicket() {
            if (tickets > 0) {   // 不加条件 相当于条件判断没有进入锁管控  线程安全问题就没有解决
                System.out.println(Thread.currentThread().getName() +
                        "卖出一张票,剩余:" + --tickets);
            }
        }
    }
    
    
  • 非静态方法加锁

    public class ThreadSync03 {
        public static void main(String[] args) {
            TicketSaleRunnable t = new TicketSaleRunnable();
    
            new Thread(t, "窗口1").start();
            new Thread(t, "窗口2").start();
            new Thread(t, "窗口3").start();
        }
    }
    
    class TicketSaleRunnable implements Runnable {
        private static int tickets = 100;
    
        @Override
        public void run() { // 直接锁这里,肯定不行,会导致,只有一个窗口卖票
            while (tickets > 0) {
                saleTicket();
            }
        }
    
        // 锁对象是 this  这里就是TicketSaleRunnable的对象
        private synchronized void saleTicket() {
            if (tickets > 0) {   // 不加条件 相当于条件判断没有进入锁管控  线程安全问题就没有解决
                System.out.println(Thread.currentThread().getName() +
                        "卖出一张票,剩余:" + --tickets);
            }
        }
    }
    
    
  • 同步代码块

    public class ThreadSync04 {
        public static void main(String[] args) {
            // 2、创建资源对象
            Ticket ticket = new Ticket();
    
            // 3、启动多个线程操作资源类的对象
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    //不能给run()直接加锁,因为t1,t2,t3的三个run方法分别属于三个Thread类对象,
                    // run方法是非静态方法,那么锁对象默认选this,那么锁对象根本不是同一个
                    while (true) {	// 通过异常停止
                        synchronized (ticket) {
                            ticket.sale();
                        }
                    }
                }
            };
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        synchronized (ticket) {
                            ticket.sale();
                        }
                    }
                }
            };
            Thread t3 = new Thread() {
                @Override
                public void run() {
                    while (true) {		
                        synchronized (ticket) {
                            ticket.sale();
                        }
                    }
                }
            };
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    // 1、编写资源类
    class Ticket {
        private static int tickets = 100;
    
        public void sale() {
            if (tickets > 0)
                System.out.println(Thread.currentThread().getName() +
                        "卖出了一张票,还剩:" + --tickets);
            else
                throw new RuntimeException("票已卖完...");
        }
    
        public static int getTickets() {
            return tickets;
        }
    }
    

Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制,通过显式定义Lock锁对象来实现同步。

  • Lock是java.util.concurrent.locks包的接口,它提供了比 synchronized 更加广泛的锁定操作,Lock接口有三个实现类:ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock,即重入锁、读锁和写锁,一般用ReentrantLock为其实例化。
  • Lock锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。Lock需要显式地创建、加锁和释放。

Lock一般结构:

class A{
	private final ReentrantLock lock = new 	ReenTrantLock();
	public void m(){
		lock.lock();
		try{
			//保证线程安全的代码;
		} finally {
			lock.unlock(); 
		}
	}
}

Lock使用演示:

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static void main(String[] args) {
        TicketSaleLock ticketSaleLock = new TicketSaleLock();

        new Thread(ticketSaleLock).start();
        new Thread(ticketSaleLock).start();
        new Thread(ticketSaleLock).start();
    }
}

class TicketSaleLock implements Runnable {
    private static int tickets = 100;
    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            reentrantLock.lock();   // 加锁
            try {
               if (tickets > 0) // 不加条件 相当于条件判断没有进入锁管控  线程安全问题就没有解决
                   System.out.println(Thread.currentThread() +
                           "卖出了一张票,还剩:" + --tickets);
               else break;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock(); // 解锁
            }
        }
    }
}

synchronized 与 Lock 的对比:

  1. Lock是显式锁(手动创建、开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)

线程通信

多个线程处理同一个资源,但线程的任务却不同。而多个线程并发执行时, 默认情况下CPU是随机切换线程的,当需要多个线程来共同完成一件任务,并且希望它们有规律的执行, 则多线程之间需要一些通信机制协调它们的工作。

等待唤醒机制

这是多个线程间的一种协作机制。就是在一个线程满足某个条件时,就进入等待状态wait()/wait(time), 等待其他线程执行完指定代码过后再将其唤醒notify();或可以指定wait的时间,等时间到了自动唤醒;有多个线程进行等待时,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

  1. wait:线程不再活动和参与调度,进入 wait set 中。它要等别的线程notify或者等待时间到,才能从wait set 中释放出来,重新进入到就绪队列中
  2. notify:选取所通知对象的 wait set 中的一个线程释放;
  3. notifyAll:释放所通知对象的 wait set 上的全部线程。

注意: 被通知线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁。

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
  • 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态

wait和notify使用细节:

  1. wait方法与notify方法必须要由同一个锁对象调用。
  2. wait方法与notify方法是属于Object类的方法的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。

生产者与消费者问题

等待唤醒机制可以解决经典的“生产者与消费者”的问题。

生产者与消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

生产者与消费者问题中其实隐含了两个问题:

  • 线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。

  • 线程的协调工作问题:

    可以通过wait/notify机制解决。

    • 让生产者线程在缓冲区满时 wait,等待消费者线程消耗了缓冲区数据之后 notify;
    • 让消费者线程在缓冲区空时 wait,等待生产者进程往缓冲区添加数据之后 notify

单生产者与单消费者

案例:有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有1个厨师和1个服务员。

public class CommunicateTest {
    public static void main(String[] args) {
        // 创建资源对象
        Workbench wb = new Workbench();

        // 启动厨师线程
        new Thread("厨师") {
            @Override
            public void run() {
                while (true) {
                    try {
                        wb.put();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();

        // 启动服务员线程
        new Thread("服务员") {
            @Override
            public void run() {
                while (true) {
                    try {
                        wb.take();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();
    }
}

// 定义资源类
class Workbench {
    private static final int MAX_VALUE = 10;
    private int num;

    public synchronized void put() throws InterruptedException {
        if (num >= MAX_VALUE) {
            this.wait();
        }

        Thread.sleep(500);
        num++;
        System.out.println(Thread.currentThread().getName() +
                "制作了一份快餐,现在工作台上有:" + num + " 份快餐");
        this.notify();
    }

    public synchronized void take() throws InterruptedException {
        if (num <= 0) {
            this.wait();
        }
        Thread.sleep(250);
        num--;
        System.out.println(Thread.currentThread().getName() +
                "取走了一份快餐,现在工作台上有:" + num + " 份快餐");
        this.notify();
    }
}

多生产者与多消费者

案例:有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有多个厨师和多个服务员。

public class CommunicateTest {
    public static void main(String[] args) {
        WindowBoard wb = new WindowBoard();

        Cook c1 = new Cook("厨师1", wb);
        Cook c2 = new Cook("厨师2", wb);
        Waiter w1 = new Waiter("服务员1", wb);
        Waiter w2 = new Waiter("服务员2", wb);

        c1.start();
        c2.start();
        w1.start();
        w2.start();
    }
}

class WindowBoard {
    private static final int MAX_VALUE = 10;
    private int num;

    public synchronized void put() throws InterruptedException {
        while (num >= MAX_VALUE) {  // 不能是if
            this.wait();
        }
        Thread.sleep(500);
        num++;
        System.out.println(Thread.currentThread().getName() +
                "制作了一份快餐,现在工作台上有:" + num + " 份快餐");
        this.notifyAll();
    }

    public synchronized void take() throws InterruptedException {
        while (num <= 0) {  // 不能是if
            this.wait();
        }
        Thread.sleep(250);
        num--;
        System.out.println(Thread.currentThread().getName() +
                "取走了一份快餐,现在工作台上有:" + num + " 份快餐");
        this.notifyAll();
    }
}

class Cook extends Thread {
    private String name;
    private WindowBoard wb;

    public Cook(String name, WindowBoard wb) {
        super(name);
        this.wb = wb;
    }

    @Override
    public void run() {
        while (true) {
            try {
                wb.put();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Waiter extends Thread {
    private String name;
    private WindowBoard wb;

    public Waiter(String name, WindowBoard wb) {
        super(name);
        this.wb = wb;
    }

    @Override
    public void run() {
        while (true) {
            try {
                wb.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

标签:同步,Thread,void,通信,线程,new,public
From: https://www.cnblogs.com/msuenb/p/17122492.html

相关文章

  • 进程间通信
    6种方式:管道、消息队列、共享内存、信号量、信号、socket前提知识:每个进程都有自己的用户空间,而内核空间是每个进程共享的。因此进程之间想要进行通信,就需要通过内核来实......
  • 2-STM32+Air724UG基本控制篇(自建物联网平台)-整体运行测试-Android扫码绑定Air724,并
    <p><iframename="ifd"src="https://mnifdv.cn/resource/cnblogs/ZLAir724UGA/my.html"frameborder="0"scrolling="auto"width="100%"height="1500"></iframe></p>......
  • vue 组件通信方式 ,父子、隔代、兄弟 三类通信,六种方法
    (1)props/$emit 适用父子组件通信(2) ref 与 $parent/$children 适用父子组件通信(3)$attrs/$listeners 适用于隔代组件通信(4)provide/inject 适用于隔代组......
  • 彻底搞懂同步异步与阻塞非阻塞
    上两篇文章讲过了BIO与非阻塞IO以及IO多路复用,洋洋洒洒近3万字。这篇文章我们来聊一个很简单,但是很多人往往分不清的一个问题,同步异步、阻塞非阻塞到底怎么区分?开篇先问......
  • 期末复习——进程与线程
    MEMOPCB块:进程存在唯一唯一唯一!标志程序静态,进程动态每个进程有UID:用户ID,进程创建者的ID;通常大于500EUID:有效用户ID,表示进程对文件资源的访问权限;setuid:对二进制文......
  • 线程私有变量ThreadLocal详解
    本文已收录至Github,推荐阅读......
  • 线程私有变量ThreadLocal详解
    本文已收录至Github,推荐阅读......
  • 【RocketMQ 系列】 RocketMQ 双主双从(同步双写) 集群搭建
    1.各角色介绍Producer:消息的发送者;举例:发信者Consumer:消息接收者;举例:收信者Broker:暂存和传输信息;举例:邮局NameServer:管理Broker;举例:各个邮局的管理机构Topic:区分消息的种......
  • 创建线程的三种基本方式
    多线程的基本概念,线程创建的三种基本方式及线程的生命周期等其他线程相关的简要介绍Author:MsuenbDate:2023-02-14多线程基本概念程序(program):为完成特定任务......
  • 63、时间同步服务
    多主机协作工作时,各个主机的时间同步很重要,时间不一致会造成很多重要应用的故障,如:加密协议,日志,集群等,利用NTP(NetworkTimeProtocol)协议使网络中的各个计算机时间达到同步......