本文共 13802 字,大约阅读时间需要 46 分钟。
Java 并发编程一直是 Java 程序员必须懂但又是很难懂的技术内容。
这里不仅仅是指使用简单的多线程编程,或者使用 juc 的某个类。当然这些都是并发编程的基本知识,除了使用这些工具以外,Java 并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做一个简单的整理。
补充
这一组是 Object 类的方法
需要注意的是:这三个方法都必须在同步的范围内调用wait
阻塞当前线程,直到 notify 或者 notifyAll 来唤醒 -
-
wait有三种方式的调用
-
wait()
-
必要要由 notify 或者 notifyAll 来唤醒
-
wait(long timeout)
-
在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
-
wait(long timeout,long nanos)
-
本质上还是调用一个参数的方法
-
public final void wait(long timeout, int nanos) throws InterruptedException {
-
if (timeout < 0) {
-
throw new IllegalArgumentException("timeout value is negative");
-
}
-
if (nanos < 0 || nanos > 999999) {
-
throw new IllegalArgumentException(
-
"nanosecond timeout value out of range");
-
}
-
if (nanos > 0) {
-
timeout++;
-
}
-
wait(timeout);
-
}
-
-
4.2 sleep/yield/join
这一组是 Thread 类的方法
sleep
让当前线程暂停指定时间,只是让出 CPU 的使用权,并不释放锁yield
暂停当前线程的执行,也就是当前 CPU 的使用权,让其他线程有机会执行,不能指定时间。会让当前线程从运行状态转变为就绪状态,此方法在生产环境中很少会使用到,官方在其注释中也有相关的说明 -
-
/**
-
* A hint to the scheduler that the current thread is willing to yield
-
* its current use of a processor. The scheduler is free to ignore this
-
* hint.
-
*
-
* <p> Yield is a heuristic attempt to improve relative progression
-
* between threads that would otherwise over-utilise a CPU. Its use
-
* should be combined with detailed profiling and benchmarking to
-
* ensure that it actually has the desired effect.
-
*
-
* <p> It is rarely appropriate to use this method. It may be useful
-
* for debugging or testing purposes, where it may help to reproduce
-
* bugs due to race conditions. It may also be useful when designing
-
* concurrency control constructs such as the ones in the
-
* {@link java.util.concurrent.locks} package.
-
*/
-
-
join
等待调用 join 方法的线程执行结束,才执行后面的代码其调用一定要在 start 方法之后(看源码可知)使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法5.1 定义
java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁更加方便。如果一个字段被声明成 volatile,java 线程内存模型确保所有线程看到这个变量的值是一致的。
valitate 是轻量级的 synchronized,不会引起线程上下文的切换和调度,执行开销更小。
5.2 原理
1. 使用 volitate 修饰的变量在汇编阶段,会多出一条 lock 前缀指令 2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成 3. 它会强制将对缓存的修改操作立即写入主存 4. 如果是写操作,它会导致其他 CPU 里缓存了该内存地址的数据无效
5.3 作用
内存可见性
多线程操作的时候,一个线程修改了一个变量的值 ,其他线程能立即看到修改后的值防止重排序 即程序的执行顺序按照代码的顺序执行(处理器为了提高代码的执行效率可能会对代码进行重排序)并不能保证操作的原子性(比如下面这段代码的执行结果一定不是 100000)
public class testValitate { public volatile int inc = 0; public void increase() { inc = inc + 1; } public static void main(String[] args) { final testValitate test = new testValitate(); for (int i = 0; i < 100; i++) { new Thread() { public void run() { for (int j = 0; j < 1000; j++) test.increase(); } }.start(); } while (Thread.activeCount() > 2) { //保证前面的线程都执行完 Thread.yield(); } System.out.println(test.inc); } }
确保线程互斥的访问同步代码
6.1 定义
synchronized 是 JVM 实现的一种锁,其中锁的获取和释放分别是
monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中6.2 原理
加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令(利用 javap -verbose 字节码文件可看到关,关于这两条指令的文档如下:
monitorenter
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.monitorexit
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
6.3 关于使用
6.4 缺点
会让没有得到锁的资源进入 Block 状态,争夺到资源之后又转为 Running 状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6 为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
AtomicBoolean,AtomicInteger,AtomicLong 以及 Lock 相关类等底层就是用 CAS 实现的,在一定程度上性能比 synchronized 更高。
7.1 什么是 CAS
CAS 全称是 Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值 (B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
7.2 为什么会有 CAS
如果只是用 synchronized 来保证同步会存在以下问题
synchronized 是一种悲观锁,在使用上会造成一定的性能问题。在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。7.3 实现原理
Java 不能直接的访问操作系统底层,是通过 native 方法(JNI)来访问。CAS 底层通过 Unsafe 类实现原子性操作。
7.4 存在的问题
CAS 只能保证一个共享变量的原子操作
AQS 抽象的队列式同步器,是一种基于状态(state)的链表管理方式。state 是用 CAS 去修改的。它是 java.util.concurrent 包中最重要的基石,要学习想学习 java.util.concurrent 包里的内容这个类是关键。 ReentrantLock、CountDownLatcher、Semaphore 实现的原理就是基于 AQS。
在并发编程我们一般使用 Runable 去执行异步任务,然而这样做我们是不能拿到异步任务的返回值的,但是使用 Future 就可以。使用 Future 很简单,只需把 Runable 换成 FutureTask 即可。使用上比较简单,这里不多做介绍。
如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。
Java 中线程池的实现类 ThreadPoolExecutor,其构造函数的每一个参数的含义在注释上已经写得很清楚了,这里几个关键参数可以再简单说一下
最后,本文主要对 Java 并发编程开发需要的知识点作了简单的讲解,这里每一个知识点都可以用一篇文章去讲解,由于篇幅原因不能对每一个知识点都详细介绍,我相信通过本文你会对 Java 的并发编程会有更近一步的了解。如果您发现还有缺漏或者有错误的地方,可以在评论区补充,谢谢。
转载地址:http://imcib.baihongyu.com/