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也是线程安全的。

还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是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

 

posted @ 2020-04-04 21:05  LinBupt  阅读(153)  评论(0编辑  收藏  举报