java并发知识点总结(一)

87 阅读10分钟

1. volatile、synchronized、lock

1. volatile、synchronized区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定被同步的代码块或方法中的共享变量,只有当前线程可以访问该变量,其他线程被阻塞住
  • volatile仅能使用在变量级别;synchronized则可以使用在方法、代码块
  • volatile仅能实现变量的修改可见性,不能保证原子性(例如i++);而synchronized则可以确保被同步的代码块或方法对共享变量的修改具有可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化(指令重排);synchronized同步的代码块或方法中的共享变量可以被编译器优化

2. 使用场景

  1. 当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量
  2. 当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。
  3. ReentranLock 可以通过代码释放锁,可以设置锁超时。
  4. 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

3. synchronized和Lock比较

  • synchronized是关键字,内置语言实现,Lock是接口。
  • synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
  • Lock是可以中断锁,synchronized是非中断锁,必须等待线程执行完成释放锁。
  • Lock可以使用读锁提高多线程读效率。

4. synchronized 什么时候释放锁

1、当前线程的同步方法、代码块执行结束的时候释放

2、当前线程在同步方法、同步代码块中遇到break 、 return 终止该代码块或者方法的时候释放。

3、出现未处理的error或者exception导致异常结束的时候释放

4、程序执行了同步对象wait方法,当前线程暂停,释放锁

2. wait( )和notify( )

Java多线程wait()和notify()系列方法使用教程

notify,notifyAll区别(生产者消费者案例)

1. 用法说明

  • 线程调用了wait()方法,便会释放锁,并进入等待池中,不会参与锁的竞争
  • wait(long timeout)方法可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,则该线程会进入该对象的锁池中参与锁的竞争。如果传递一个负数timeout会抛出IllegalArgumentException异常
  • 调用notify()后,等待池中的某个线程(只会有一个)会进入该对象的锁池中参与锁的竞争,若竞争成功,获得锁,竞争失败,继续留在锁池中等待下一次锁的竞争
  • 调用notifyAll()后,等待池中的所有线程都会进入该对象的锁池中参与锁的竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁

2. 注意事项

  • 调用wait()、notify()方法时,当前线程必须要成功获得锁(必须写在同步代码块锁中),否则将抛出异常,是为了防止死锁和永久等待,使线程更安全
  • 只对当前单个共享变量生效,多个共享变量需要多次调用wait()方法
  • 如果线程A调用wait()方法后处于堵塞状态时,其他线程中断(在其他线程调用A.interrupt()方法)A线程,则会抛出InterruptExcption异常而返回并终止

3. Condition与Object中的wati,notify,notifyAll区别

Java并发控制:ReentrantLock Condition使用详解

  1. Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。 不同的是,Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。

  2. Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition,而Object只支持一个等待队列。 例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。 如果采用Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。

  3. Condition能够支持不响应中断(awaitUninterruptibly()方法),而Object的方法不支持

  4. 使用场景:ArrayBlockingQueue

4. LockSupport

1. 概述

LockSupport来自于JDK1.5,位于JUC包的locks子包,是一个非常方便实用的线程阻塞工具类,它定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,可以在线程内任意位置让线程阻塞、唤醒;具体实现依赖Unsafe类

2. 基本使用

  • LockSupport.park() 阻塞当前线程
  • LockSupport.unpark(Thread thread) 使指定的线程停止阻塞

3. 与wait、notify的区别

  • park和unpark不需要在同步代码块中,不需要先获取锁,wait和notify是需要的
  • 被park的线程不会释放锁,wait会释放锁
  • pork和unpark是针对线程的,而wait和notify可以是任意对象
  • unpark可以让指定线程被唤醒,但是notify是随机唤醒一个,notifyAll是全部唤醒,不够灵活
  • 线程中断情况
    • 如果线程阻塞在 LockSupport.park(Object obj) 方法,这个时候的中断会导致线程唤醒,唤醒后不会抛出InterruptedException异常,也不会重置中断状态;唤醒后再次调用park阻塞线程,这时候线程已经是中断状态,所以再调用线程的interrup()就不会唤醒了
    • 当一个线程处于sleep、wait、join这三种状态之一的时候,如果此时他的中断状态为true(不管是调用这三个方法之前被中断还是调用之后被中断),那么它就会抛出一个InterruptedException的异常,并将中断状态重新设置为false

啃透Java并发-LockSupport源码详解

Java LockSupport以及park、unpark方法源码深度解析

5. synchronized 用法

1.不同用法

  • synchronized(this) 锁对象是类的实例
  • synchronized(object) 锁对象是object对象
  • synchronized(xx.class) 锁对象是xx的类所
  • synchronized 修饰非静态方法,锁对象是类的实例;修饰静态方法,锁对象是该类的类锁

2.锁分类

  • 对象锁:在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰
  • 类锁:在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁

Java 之 synchronized 详解

Java synchronized(this)与synchronized(object)的区别

6. 线程终止

1. interrupt()

  • 该方法不会真正地终止线程,只是在当前线程中打了一个中断的标记,线程中的代码还会继续执行

        public void interrupt() {
            if (this != Thread.currentThread())
                checkAccess();
    
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupt0();           // Just to set the interrupt flag
                    b.interrupt(this);
                    return;
                }
            }
            interrupt0();
        }
    
  • 调用该方法后再调用isInterrupted()会返回true,此时可以根据实际情况来决定是否终止线程中未执行的代码

2. 判断线程是否终止

  • isInterrupted() 不会清除interrupt flag,每次调用都返回true
  • interrupted() 第一次调用返回true并清除中断标记,再次调用会返回false

==注意==:这两个方法都是在线程调用interrupt()后才会返回true,否则一直返回false

参考Java停止(终止)线程详解版

7. 真正理解Thread类

Thread类的对象其实也是一个java对象,只不过每一个Thread类的对象对应着一个线程。Thread类的对象就是提供给用户用于操作线程、获取线程的信息。真正的底层线程用户是看不到的了。因此,当一个线程结束了,死掉了,对应的Thread的对象仍能调用,除了start()方法外的所有方法(死亡的线程不能再次启动),如run( )、getName( )、getPriority()等等

深入线程Thread类的start()方法和run()方法

8. 线程的生命周期

  • NEW(新建):使用new关键字创建了一个线程
  • RUNNABLE(就绪):当线程对象调用了start()方法之后
  • BLOCKED(阻塞):阻塞等待监视器锁的线程的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调用Object.wait后重新进入同步块/方法
  • WAITING(等待):处于等待状态的线程正在等待另一个线程执行特定操作 调用以下方法之一,线程处于等待状态:
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
  • TIMED_WAITING:具有指定等待时间的等待线程的线程状态 调用以下方法之一,线程处于定时等待状态:
    • Thread.sleep
    • Object.wait(long timeout)
    • Thread.join(long timeout)
    • LockSupport.parkNanos(Object blocker, long nanos)
    • LockSupport.parkUntil(Object blocker, long deadline)
  • TERMINATED(终止):
    • run()或call()方法执行完成,线程正常结束
    • 线程抛出一个未捕获的Exception或Error

参考资料:

Java中sleep()方法和wait()方法的异同点

Java中yield()方法作用讲解,与sleep()的区别

啃碎并发(二):Java线程的生命周期

volatile的理解