Java的锁
线程同步
Java使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行
使用synchronized:
1.找出修改共享变量的线程代码块
2.选择一个共享实例作为锁;
3.使用synchronized(lockObject){}
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值 引用类型赋值,List<String> list = anotherList
1 public class Main{ 2 public static void main(String[] args) throws Exception{ 3 var add = new AddThread(); 4 var dec = new DecThread(); 5 add.start(); 6 dec.start(); 7 add.join(); 8 dec.join(); 9 System.out.println(Counter.count); 10 } 11 } 12 13 class Counter{ 14 public static final Object lock = new Object(); 15 public static int count = 0; 16 } 17 18 class AddThread extends Thread{ 19 public void run(){ 20 for (int i=0; i<10000; i++){ 21 synchronized(Counter.lock){ 22 Counter.count += 1 23 } 24 } 25 } 26 } 27 28 class DecThread extends Thread{ 29 public void run(){ 30 for (int i=0; i<10000; i++){ 31 synchronized(Counter.lock){ 32 Counter.count -= 1; 33 } 34 } 35 } 36 }
同步方法
public void add(int n) {
synchronized(this) {
count += n;
}
}
线程调用add()
、dec()
方法时,它不必关心同步逻辑,因为synchronized
代码块在add()
、dec()
方法内部。并且,我们注意到,synchronized
锁住的对象是this
,即当前实例,这又使得创建多个Counter
实例的时候,它们之间互不影响,可以并发执行
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的
Java标准库的java.lang.StringBuffer
也是线程安全的。
还有一些不变类,例如String
,Integer
,LocalDate
,它们的所有成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList
,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList
是可以安全地在线程间共享的。
public void add(int n) { synchronized(this) { // 锁住this count += n; } // 解锁 } 等价 public synchronized void add(int n) { // 锁住this count += n; } // 解锁
用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
如果对一个静态方法添加synchronized修饰符,它锁住的是该类的class实例
因为对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的class实例。
Java的线程锁是可重入的锁:
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
使用wait和notify
synchronized解决了多线程竞争的问题,但是并没有解决多线程协调的问题
多线程协调运行的原则就是:
当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务
public synchronized String getTask(){ while (queue.isEmpty()){ this.wait(); } return queue.remove(); } #wait方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait() #wait方法的执行机制非常复杂。他是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。 #当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在其他方法获得this锁。 #在相同的锁对象上调用notify方法,就可以让等待的线程被重新唤醒,这个方法会唤醒一个正在this锁等待的线程,从而使得等待线程从this.wait()方法返回。 #notifyAll()更安全,有些时候,如果代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去。 #wait()方法返回时需要重新获得this锁 #要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断
ReentrantLock 可重入锁
高级的处理并发的java.util.concurrent
包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
synchronized缺点:重;获取时必须一直等待,没有额外的尝试机制
synchronized是Java语言层面提供的语法,所以不需要考虑异常,而ReentrantLock是Java代码实现的锁,就必须先获取锁,然后在finally中正确释放锁。
优势:可以尝试获取锁,设置等到时间,超时未获取到锁,则返回False,程序就可以做一些额外处理,而不是无限等待下去。
使用ReentrantLock
比直接使用synchronized
更安全,线程在tryLock()
失败的时候不会导致死锁。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
使用Condition
用可重入锁时候怎么编写wait和notify的功能呢?使用condition对象来实现
Condition:
await()会释放当前锁,进入等待状态
signal()会唤醒某个等待线程
signalAll()会唤醒所有等待线程
唤醒线程从await()返回后需要重新获得锁
此外,和tryLock类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来
ReadWriteLock
只允许一个线程写入(其他线程既不能写入也不能读取)
没有写入时,多个线程允许同时读(提高性能)
StampedLock
ReadWriteLock有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁
新的读写锁StampedLock
StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入。这样一来,读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
StampedLock能进一步提升并发效率
但是是有代价的:一是代码更加复杂,二是它是不可重入锁,不能再一个线程中反复获取同一个锁
使用Concurrent集合
BlockingQueue
java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue
Atomic
线程池:Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
能够接收大量小任务并进行分发处理的就是线程池。简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
ExecutorService接口表示线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
提供的几个常用实现类:
FixedThreadPool:线程数固定的线程池
CachedThreadPool:线程数根据任务动态调整的线程池
SingleThreadExecutor:仅单线程执行的线程池
如何在一个线程内传递状态?ThreadLocal