在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
关键字更灵活的锁定操作。ReentrantLock
是Lock
接口的一个实现,它支持公平锁和非公平锁,并且提供了尝试锁定(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分别持有对方需要的锁资源,导致它们都在等待对方释放锁,从而造成了死锁。
解决死锁的方法:
-
避免锁嵌套:确实,尽量避免在一个锁内部再获取另一个锁,这会增加死锁的风险。如果必须这么做,确保锁的获取顺序在所有线程中都是一致的。
-
设置超时时间:使用
Lock
接口中的tryLock(long time, TimeUnit unit)
方法尝试获取锁,并设置超时时间。如果在指定时间内未能获取锁,则放弃获取锁并可以执行其他操作或抛出异常。这有助于避免永久性的死锁。 -
使用
java.util.concurrent
下的类:Java并发包提供了一系列高级的并发工具,如ReentrantLock
、Semaphore
、CountDownLatch
等,这些工具提供了比synchronized
关键字和wait/notify
机制更灵活的锁和同步控制。 -
检测死锁:虽然Java没有直接提供检测死锁的工具,但可以通过线程转储(thread dump)和JConsole、VisualVM等工具来分析和诊断死锁。
-
避免持有多个锁:尽量减少每个线程持有的锁的数量,这有助于降低死锁的风险。
线程通信
线程通信是指线程之间交换信息,以协调它们的执行。在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
更灵活的等待/通知控制。
通过合理使用锁和同步机制,我们可以有效地管理线程间的并发执行,避免死锁和其他并发问题。
- syn和lock的区别:
- syn 通常指的是
synchronized
关键字,它是Java中的一个内置关键字,用于控制多个线程对共享资源的访问。synchronized
可以用于方法或代码块,当某个线程访问由synchronized
保护的代码块时,它会获得一个锁(监视器锁),其他线程则必须等待直到锁被释放。 - lock 指的是
java.util.concurrent.locks
包下的锁接口及其实现,如ReentrantLock
。与synchronized
相比,Lock
提供了更灵活的锁定机制,如尝试锁定(tryLock)、定时锁定(tryLock(long time, TimeUnit unit))、可中断的锁定(lockInterruptibly)等。此外,Lock
还可以用于实现更复杂的同步控制,如尝试非阻塞地获取锁、可轮询的锁请求等。
- syn 通常指的是
- 什么是死锁和如何避免死锁:
- 死锁 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
- 避免死锁的策略:
- 避免一个线程同时申请多个锁:确保线程按相同的顺序申请锁。
- 使用超时时间尝试锁定:在尝试获取锁时使用超时时间,如果超时则放弃锁的申请。
- 使用可中断的锁:允许在等待锁的过程中被中断。
- 检测死锁并恢复:通过工具或算法检测死锁,并采取相应的恢复措施,如回滚事务等。
- 线程的状态:
- 新建(NEW):线程被创建但尚未启动。
- 可运行(RUNNABLE):线程正在Java虚拟机中运行,但在等待操作系统的资源或调度。
- 阻塞(BLOCKED):线程正在等待监视器锁以便进入一个同步块/方法,或重新进入同步块/方法。
- 等待(WAITING):线程在等待另一个线程执行特定操作,如通过调用
Object
的wait()
方法,或者等待Thread.join()
完成。 - 超时等待(TIMED_WAITING):线程在等待另一个线程执行特定操作,但设置了超时时间,如
Thread.sleep(long millis)
,Object.wait(long timeout)
,Thread.join(long millis)
等。 - 终止(TERMINATED):线程已执行完毕。
- notify和notifyAll的区别:
notify()
方法随机唤醒等待在该对象监视器上的一个线程。notifyAll()
方法唤醒等待在该对象监视器上的所有线程。
- wait和sleep方法:
- wait 和 sleep 的区别已列出:
- 来源:wait 来自 Object 类,sleep 来自 Thread 类。
- 执行位置:wait 必须放入同步代码块或同步方法中,sleep 可以在任何地方执行。
- 锁释放:wait 会释放锁资源,sleep 不会释放锁。
- 唤醒方式:wait 需要其他线程调用该对象的 notify() 或 notifyAll() 方法来唤醒,sleep 时间到了自动唤醒。
- wait 和 sleep 的区别已列出:
- Thread类中常用方法:
- start():启动线程,执行 run() 方法。
- run():线程在被调度时执行的操作。
- sleep(long millis):使当前线程暂停执行一段时间(以毫秒为单位)。
- join():等待该线程终止。
- interrupt():中断线程。中断的线程会抛出 InterruptedException 异常或设置中断状态。
- isAlive():检查线程是否还在运行。
- currentThread():返回对当前正在执行的线程对象的引用。
- setName(String name) 和 getName():设置和获取线程的名称。
- setPriority(int priority) 和 getPriority():设置和获取线程的优先级。
- yield():线程暂停当前正在执行的线程对象,并执行其他线程的线程。
- setDaemon(boolean on):将该线程标记为守护线程或用户线程。守护线程是一种特殊的线程,当程序中不存在非守护线程时,守护线程会自动退出。