Java二阶-38-线程安全-锁

在Java中,解决线程安全问题确实主要依赖于同步机制,包括使用synchronized关键字和java.util.concurrent.locks.Lock接口的实现类(如ReentrantLock)。下面将详细解释这两种方式。

使用synchronized关键字

synchronized关键字可以用于方法或代码块上,以确保同一时刻只有一个线程可以执行该段代码。

  • 同步方法:在方法声明时使用synchronized关键字,它会锁定调用该方法的对象(对于非静态同步方法)或该类的Class对象(对于静态同步方法)。

    public synchronized void sell() {  
        // 同步代码  
    }
  • 同步代码块:通过指定一个锁对象,可以更加灵活地控制同步范围。

    public void sell() {  
        synchronized (lockObject) {  
            // 同步代码  
        }  
    }

    在例子中,使用了this作为锁对象,这意味着每个SellTicket实例的线程都会独立地锁定自己的实例。如果所有线程共享同一个SellTicket实例,这将有效防止超卖。但如果每个线程都创建了SellTicket的不同实例,则这些实例之间不会有同步效果。

使用Lock接口

java.util.concurrent.locks.Lock接口提供了比synchronized关键字更灵活的锁定操作。ReentrantLockLock接口的一个实现,它支持公平锁和非公平锁,并且提供了尝试锁定(tryLock)、定时锁定(tryLock(long time, TimeUnit unit))以及可中断的锁定(lockInterruptibly)等高级功能。

使用ReentrantLock的示例:

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class SellTicket implements Runnable {  
    private int tick = 100;  
    private final Lock lock = new ReentrantLock();  
  
    @Override  
    public void run() {  
        while (true) {  
            lock.lock(); // 获取锁  
            try {  
                if (tick > 0) {  
                    try {  
                        Thread.sleep(20);  
                    } catch (InterruptedException e) {  
                        Thread.currentThread().interrupt(); // 恢复中断状态  
                        break;  
                    }  
                    tick--;  
                    System.out.println(Thread.currentThread().getName() + "卖了一张票;剩余:" + tick + "张");  
                } else {  
                    break;  
                }  
            } finally {  
                lock.unlock(); // 释放锁  
            }  
        }  
    }  
}


在这个例子中,我们使用了ReentrantLock来确保在减少票数和打印信息时,不会有其他线程同时执行这些操作。注意,无论操作是否成功完成,我们都应该在finally块中释放锁,以避免死锁。

总结

  • 使用synchronized关键字时,它简化了同步代码的编写,但可能不够灵活。
  • 使用Lock接口(如ReentrantLock)时,提供了更高的灵活性和控制能力,但代码可能更复杂一些。

选择哪种方式取决于具体的应用场景和性能要求。

死锁

死锁是多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,这些线程都将无法向前推进。在您给出的例子中,线程A和线程B分别持有对方需要的锁资源,导致它们都在等待对方释放锁,从而造成了死锁。

解决死锁的方法

  1. 避免锁嵌套:确实,尽量避免在一个锁内部再获取另一个锁,这会增加死锁的风险。如果必须这么做,确保锁的获取顺序在所有线程中都是一致的。

  2. 设置超时时间:使用Lock接口中的tryLock(long time, TimeUnit unit)方法尝试获取锁,并设置超时时间。如果在指定时间内未能获取锁,则放弃获取锁并可以执行其他操作或抛出异常。这有助于避免永久性的死锁。

  3. 使用java.util.concurrent下的类:Java并发包提供了一系列高级的并发工具,如ReentrantLockSemaphoreCountDownLatch等,这些工具提供了比synchronized关键字和wait/notify机制更灵活的锁和同步控制。

  4. 检测死锁:虽然Java没有直接提供检测死锁的工具,但可以通过线程转储(thread dump)和JConsole、VisualVM等工具来分析和诊断死锁。

  5. 避免持有多个锁:尽量减少每个线程持有的锁的数量,这有助于降低死锁的风险。

线程通信

线程通信是指线程之间交换信息,以协调它们的执行。在Java中,wait()notify()/notifyAll()方法是实现线程通信的两种基本方式,它们都是Object类的方法。

wait()方法

  • 调用当前线程的wait()方法会使其进入等待(阻塞)状态,并释放当前持有的锁。
  • 调用wait()方法之前,线程必须持有某个对象的锁。
  • 线程被唤醒(通过notify()notifyAll()方法)后,会重新尝试获取锁,然后才能继续执行。

notify()notifyAll()方法

  • 这两个方法用于唤醒在等待该对象锁的线程。
  • notify()方法唤醒等待该对象锁的线程中的一个(具体唤醒哪个线程取决于JVM的实现)。
  • notifyAll()方法唤醒等待该对象锁的所有线程。
    public class BankCard  {
        private double balance=0;
    
        private boolean flag=false;//true:有钱  false:没钱
    
        public synchronized  void cun(double money){
            if(flag==true){
                try {
                    wait();//属于Object类中。 进入等待队列 并且释放拥有的锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //+余额
            balance+=money;
            //设置有钱标志
            flag=true;
            //唤醒--唤醒等待队列中的某个线程
            notify();
            System.out.println(Thread.currentThread().getName()+"往卡中存入了:"+money+";卡中余额:"+balance);
        }
        public synchronized void qu(double money){
            if(flag==false){
                try {
                    wait(); //放入等待队列中。
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            balance-=money;
            flag=false;
            notify();
            System.out.println(Thread.currentThread().getName()+"从卡中取出了:"+money+";卡中余额:"+balance);
        }
    }
    
    
    public class CunThread extends Thread{
        private BankCard bankCard;
    
        public CunThread(BankCard bankCard){
            this.bankCard=bankCard;
        }
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                bankCard.cun(1000);
            }
        }
    }
    
    
    
    public class QuThread extends Thread{
    
    
        private BankCard bankCard;
    
        public QuThread(BankCard bankCard){
            this.bankCard=bankCard;
        }
    
        @Override
        public void run() {
            for (int i = 0; i <10 ; i++) {
                bankCard.qu(1000);
            }
        }
    }
    
    
    
      public static void main(String[] args) {
            BankCard bankCard=new BankCard();
            CunThread c=new CunThread(bankCard);
            c.setName("李晨");
            QuThread q=new QuThread(bankCard);
            q.setName("范冰冰");
            c.start();
            q.start();
        }

注意

  • 调用wait()notify()notifyAll()方法时,必须同步在某个对象上,即这些方法必须在synchronized方法或synchronized块内部被调用。
  • 等待/通知机制是Java实现线程间通信的一种底层方式,它直接依赖于对象锁。在实际开发中,也可以考虑使用更高级的并发工具,如Condition接口,它提供了比wait/notify更灵活的等待/通知控制。

通过合理使用锁和同步机制,我们可以有效地管理线程间的并发执行,避免死锁和其他并发问题。

  1. syn和lock的区别
    • syn 通常指的是 synchronized 关键字,它是Java中的一个内置关键字,用于控制多个线程对共享资源的访问。synchronized 可以用于方法或代码块,当某个线程访问由 synchronized 保护的代码块时,它会获得一个锁(监视器锁),其他线程则必须等待直到锁被释放。
    • lock 指的是 java.util.concurrent.locks 包下的锁接口及其实现,如 ReentrantLock。与 synchronized 相比,Lock 提供了更灵活的锁定机制,如尝试锁定(tryLock)、定时锁定(tryLock(long time, TimeUnit unit))、可中断的锁定(lockInterruptibly)等。此外,Lock 还可以用于实现更复杂的同步控制,如尝试非阻塞地获取锁、可轮询的锁请求等。
  2. 什么是死锁和如何避免死锁
    • 死锁 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
    • 避免死锁的策略
      1. 避免一个线程同时申请多个锁:确保线程按相同的顺序申请锁。
      2. 使用超时时间尝试锁定:在尝试获取锁时使用超时时间,如果超时则放弃锁的申请。
      3. 使用可中断的锁:允许在等待锁的过程中被中断。
      4. 检测死锁并恢复:通过工具或算法检测死锁,并采取相应的恢复措施,如回滚事务等。
  3. 线程的状态
    • 新建(NEW):线程被创建但尚未启动。
    • 可运行(RUNNABLE):线程正在Java虚拟机中运行,但在等待操作系统的资源或调度。
    • 阻塞(BLOCKED):线程正在等待监视器锁以便进入一个同步块/方法,或重新进入同步块/方法。
    • 等待(WAITING):线程在等待另一个线程执行特定操作,如通过调用 Object 的 wait() 方法,或者等待 Thread.join() 完成。
    • 超时等待(TIMED_WAITING):线程在等待另一个线程执行特定操作,但设置了超时时间,如 Thread.sleep(long millis)Object.wait(long timeout)Thread.join(long millis) 等。
    • 终止(TERMINATED):线程已执行完毕。
  4. notify和notifyAll的区别
    • notify() 方法随机唤醒等待在该对象监视器上的一个线程。
    • notifyAll() 方法唤醒等待在该对象监视器上的所有线程。
  5. wait和sleep方法
    • wait 和 sleep 的区别已列出:
      1. 来源:wait 来自 Object 类,sleep 来自 Thread 类。
      2. 执行位置:wait 必须放入同步代码块或同步方法中,sleep 可以在任何地方执行。
      3. 锁释放:wait 会释放锁资源,sleep 不会释放锁。
      4. 唤醒方式:wait 需要其他线程调用该对象的 notify() 或 notifyAll() 方法来唤醒,sleep 时间到了自动唤醒。
  6. Thread类中常用方法
    • start():启动线程,执行 run() 方法。
    • run():线程在被调度时执行的操作。
    • sleep(long millis):使当前线程暂停执行一段时间(以毫秒为单位)。
    • join():等待该线程终止。
    • interrupt():中断线程。中断的线程会抛出 InterruptedException 异常或设置中断状态。
    • isAlive():检查线程是否还在运行。
    • currentThread():返回对当前正在执行的线程对象的引用。
    • setName(String name) 和 getName():设置和获取线程的名称。
    • setPriority(int priority) 和 getPriority():设置和获取线程的优先级。
    • yield():线程暂停当前正在执行的线程对象,并执行其他线程的线程。
    • setDaemon(boolean on):将该线程标记为守护线程或用户线程。守护线程是一种特殊的线程,当程序中不存在非守护线程时,守护线程会自动退出。
  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值