线程同步和线程通信

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);
            }
        }
    }
}
posted @ 2023-02-15 13:24  msuenb  阅读(16)  评论(0编辑  收藏  举报