原做地址:https://github.com/frank-lam/2019_campus_applyjavascript
在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通讯、并发包部分。主要分红 “并发编程” 和 “面试指南” 两 部分,在面试指南中将讨论并发相关面经。html
参考资料:java
建立后还没有启动。linux
可能正在运行,也可能正在等待 CPU 时间片。git
包含了操做系统线程状态中的 运行(Running ) 和 就绪(Ready)。程序员
这个状态下,是在多个线程有同步操做的场景,好比正在等待另外一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是线程在等待进入临界区。github
阻塞能够分为:等待阻塞,同步阻塞,其余阻塞面试
等待其它线程显式地唤醒,不然不会被分配 CPU 时间片。算法
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | - |
无需等待其它线程显式地唤醒,在必定时间以后会被系统自动唤醒。数据库
调用 Thread.sleep() 方法使线程进入限期等待状态时,经常用 “使一个线程睡眠” 进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,经常用 “挂起一个线程” 进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,经过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | - |
LockSupport.parkUntil() 方法 | - |
有三种使用线程的方法:
实现 Runnable 和 Callable 接口的类只能当作一个能够在线程中运行的任务,不是真正意义上的线程,所以最后还须要经过 Thread 来调用。能够说任务是经过线程驱动从而执行的。
须要实现 run() 方法。
经过 Thread 调用 start() 方法来启动线程。
public class MyRunnable implements Runnable { public void run() { // ... } }
public static void main(String[] args) { MyRunnable instance = new MyRunnable(); Thread thread = new Thread(instance); thread.start(); }
与 Runnable 相比,Callable 能够有返回值,返回值经过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> { public Integer call() { return 123; } }
public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable mc = new MyCallable(); FutureTask<Integer> ft = new FutureTask<>(mc); Thread thread = new Thread(ft); thread.start(); System.out.println(ft.get()); }
一样也是须要实现 run() 方法,由于 Thread 类也实现了 Runable 接口。
public class MyThread extends Thread { public void run() { // ... } }
public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); }
实现接口会更好一些,由于:
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不须要进行同步操做。
主要有三种 Executor:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executorService.execute(new MyRunnable()); } executorService.shutdown(); }
为何引入Executor线程池框架?
new Thread() 的缺点
采用线程池的优势
Java 中有两类线程:User Thread (用户线程)、Daemon Thread (守护线程)
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程做用是为其余前台线程的运行提供便利服务,并且仅在普通、非守护线程仍然运行时才须要,好比垃圾回收线程就是一个守护线程。当 JVM 检测仅剩一个守护线程,而用户线程都已经退出运行时,JVM 就会退出,由于没有若是没有了被守护这,也就没有继续运行程序的必要了。若是有非守护线程仍然存活,JVM 就不会退出。
守护线程并不是只有虚拟机内部提供,用户在编写程序时也能够本身设置守护线程。用户能够用 Thread 的 setDaemon(true) 方法设置当前线程为守护线程。
虽然守护线程可能很是有用,但必须当心确保其余全部非守护线程消亡时,不会因为它的终止而产生任何危害。由于你不可能知道在全部的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦全部的用户线程退出了,虚拟机也就退出运行了。 所以,不要在守护线程中执行业务逻辑操做(好比对数据的读写等)。
另外有几点须要注意:
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当全部非守护线程结束时,程序也就终止,同时会杀死全部守护线程。
main() 属于非守护线程。
使用 setDaemon() 方法将一个线程设置为守护线程。
public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.setDaemon(true); }
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,由于异常不能跨线程传播回 main() 中,所以必须在本地进行处理。线程中抛出的其它异常也一样须要在本地进行处理。
public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,能够切换给其它线程来执行。该方法只是对线程调度器的一个建议,并且也只是建议具备相同优先级的其它线程能够运行。
public void run() { Thread.yield(); }
线程能够阻塞于四种状态:
注意,并不是全部的阻塞状态都是可中断的,以上阻塞状态的前两种能够被中断,后两种不会对中断作出反应
一个线程执行完毕以后会自动结束,若是在运行过程当中发生异常也会提早结束。
经过调用一个线程的 interrupt() 来中断该线程,若是该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提早结束该线程。可是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于如下代码,在 main() 中启动一个线程以后再中断它,因为线程中调用了 Thread.sleep() 方法,所以会抛出一个 InterruptedException,从而提早结束线程,不执行以后的语句。
public class InterruptExample { private static class MyThread1 extends Thread { @Override public void run() { try { Thread.sleep(2000); System.out.println("Thread run"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
public static void main(String[] args) throws InterruptedException { Thread thread1 = new MyThread1(); thread1.start(); thread1.interrupt(); System.out.println("Main run"); }
Main run java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at InterruptExample.lambda$main$0(InterruptExample.java:5) at InterruptExample$$Lambda$1/713338599.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)
若是一个线程的 run() 方法执行一个无限循环,而且没有执行 sleep() 等会抛出 InterruptedException 的操做,那么调用线程的 interrupt() 方法就没法使线程提早结束。
可是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。所以能够在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提早结束线程。
public class InterruptExample { private static class MyThread2 extends Thread { @Override public void run() { while (!interrupted()) { // .. } System.out.println("Thread end"); } } }
public static void main(String[] args) throws InterruptedException { Thread thread2 = new MyThread2(); thread2.start(); thread2.interrupt(); }
Thread end
调用 Executor 的 shutdown() 方法会等待线程都执行完毕以后再关闭,可是若是调用的是 shutdownNow() 方法,则至关于调用每一个线程的 interrupt() 方法。
如下使用 Lambda 建立线程,至关于建立了一个匿名内部线程。
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { try { Thread.sleep(2000); System.out.println("Thread run"); } catch (InterruptedException e) { e.printStackTrace(); } }); executorService.shutdownNow(); System.out.println("Main run"); }
Main run java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9) at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
若是只想中断 Executor 中的一个线程,能够经过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,经过调用该对象的 cancel(true) 方法就能够中断线程。
Future<?> future = executorService.submit(() -> { // .. }); future.cancel(true);
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另外一个是 JDK 实现的 ReentrantLock。
1. 同步一个代码块
public void func() { synchronized (this) { // ... } }
它只做用于同一个对象,若是调用两个对象上的同步代码块,就不会进行同步。
对于如下代码,使用 ExecutorService 执行了两个线程,因为调用的是同一个对象的同步代码块,所以这两个线程会进行同步,当一个线程进入同步语句块时,另外一个线程就必须等待。
public class SynchronizedExample { public void func1() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } }
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func1()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
对于如下代码,两个线程调用了不一样对象的同步代码块,所以这两个线程就不须要同步。从输出结果能够看出,两个线程交叉执行。
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e2.func1()); }
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2. 同步一个方法
public synchronized void func () { // ... }
它和同步代码块同样,做用于同一个对象。
3. 同步一个类
public void func() { synchronized (SynchronizedExample.class) { // ... } }
做用于整个类,也就是说两个线程调用同一个类的不一样对象上的这种同步语句,也会进行同步。
public class SynchronizedExample { public void func2() { synchronized (SynchronizedExample.class) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } }
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func2()); executorService.execute(() -> e2.func2()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4. 同步一个静态方法
public synchronized static void fun() { // ... }
做用于整个类。
重入锁(ReentrantLock)是一种递归无阻塞的同步机制。
public class LockExample { private Lock lock = new ReentrantLock(); public void func() { lock.lock(); try { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } finally { lock.unlock(); // 确保释放锁,从而避免发生死锁。 } } }
public static void main(String[] args) { LockExample lockExample = new LockExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> lockExample.func()); executorService.execute(() -> lockExample.func()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了如下高级功能:
1. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程能够选择放弃等待,改成处理其余事情。
2. 可实现公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁。
synchronized 中的锁是非公平的,ReentrantLock 默认状况下也是非公平的,但能够经过带布尔值的构造函数要求使用公平锁。
3. 锁绑定多个条件
一个 ReentrantLock 对象能够同时绑定多个 Condition 对象。
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了不少优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,所以性能因素再也不是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
3. 功能
ReentrantLock 多了一些高级功能。
4. 使用选择
除非须要使用 ReentrantLock 的高级功能,不然优先使用 synchronized。这是由于 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是全部的 JDK 版本都支持。而且使用 synchronized 不用担忧没有释放锁而致使死锁问题,由于 JVM 会确保锁的释放。
蘑菇街面试,这里简单论述一下
在 Java 并发包中有这样一个包,java.util.concurrent.atomic,该包是对 Java 部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操做方法,保证了线程安全。下面以 AtomicInteger 为例,来看一下是如何实现的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } public final int decrementAndGet() { for (;;) { int current = get(); int next = current - 1; if (compareAndSet(current, next)) return next; } }
以这两个方法为例,incrementAndGet 方法至关于原子性的 ++i,decrementAndGet 方法至关于原子性的 --i,这两个方法中都没有使用阻塞式的方式来保证原子性(如 Synchronized ),那它们是如何保证原子性的呢,下面引出 CAS。
CAS 指的是现代 CPU 普遍支持的一种对内存中的共享数据进行操做的一种特殊指令。这个指令会对内存中的共享数据作原子的读写操做。
简单介绍一下这个指令的操做过程:
这一系列的操做是原子的。它们虽然看似复杂,但倒是 Java 5 并发机制优于原有锁机制的根本。简单来讲,CAS 的含义是:我认为原有的值应该是什么,若是是,则将原有的值更新为新值,不然不作修改,并告诉我原来的值是多少。
简单的来讲,CAS 有 3 个操做数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改成 B,不然返回 V。这是一种乐观锁的思路,它相信在它修改以前,没有其它线程去修改它;而 Synchronized 是一种悲观锁,它认为在它修改以前,必定会有其它线程去修改它,悲观锁效率很低。
参考资料:
一个是实例锁(锁在某一个实例对象上,若是该类是单例,那么该锁也具备全局锁的概念),一个是全局锁(该锁针对的是类,不管实例多少个对象,那么线程都共享该锁)。
实例锁对应的就是 synchronized关 键字,而类锁(全局锁)对应的就是 static synchronized(或者是锁在该类的 class 或者 classloader 对象上)。
/** * static synchronized 和synchronized的区别! * 关键是区别第四种状况! */ public class StaticSynchronized { /** * synchronized方法 */ public synchronized void isSynA(){ System.out.println("isSynA"); } public synchronized void isSynB(){ System.out.println("isSynB"); } /** * static synchronized方法 */ public static synchronized void cSynA(){ System.out.println("cSynA"); } public static synchronized void cSynB(){ System.out.println("cSynB"); } public static void main(String[] args) { StaticSynchronized x = new StaticSynchronized(); StaticSynchronized y = new StaticSynchronized(); /** * x.isSynA()与x.isSynB(); 不能同时访问(同一个对象访问synchronized方法) * x.isSynA()与y.isSynB(); 能同时访问(不一样对象访问synchronized方法) * x.cSynA()与y.cSynB(); 不能同时访问(不一样对象也不能访问static synchronized方法) * x.isSynA()与y.cSynA(); 能同时访问(static synchronized方法占用的是类锁, * 而访问synchronized方法占用的是对象锁,不存在互斥现象) */ } }
当多个线程能够一块儿工做去解决某个问题时,若是某些部分必须在其它部分以前完成,那么就须要对线程进行协调。
在线程中调用另外一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于如下代码,虽然 b 线程先启动,可是由于在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,所以最后可以保证 a 线程的输出先于 b 线程的输出。
public class JoinExample { private class A extends Thread { @Override public void run() { System.out.println("A"); } } private class B extends Thread { private A a; B(A a) { this.a = a; } @Override public void run() { try { a.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B"); } } public void test() { A a = new A(); B b = new B(a); b.start(); a.start(); } }
public static void main(String[] args) { JoinExample example = new JoinExample(); example.test(); }
A B
调用 wait() 使得线程等待某个条件知足,线程在等待时会被挂起,当其余线程的运行使得这个条件知足时,其它线程会调用 notify()(随机叫醒一个) 或者 notifyAll() (叫醒全部 wait 线程,争夺时间片的线程只有一个)来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用!不然会在运行时抛出 IllegalMonitorStateExeception。
使用 wait() 挂起期间,线程会释放锁。这是由于,若是没有释放锁,那么其它线程就没法进入对象的同步方法或者同步控制块中,那么就没法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,形成死锁。
public class WaitNotifyExample { public synchronized void before() { System.out.println("before"); notifyAll(); } public synchronized void after() { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("after"); } }
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); WaitNotifyExample example = new WaitNotifyExample(); executorService.execute(() -> example.after()); executorService.execute(() -> example.before()); }
before after
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,能够在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 能够指定等待的条件,所以更加灵活。
使用 Lock 来获取一个 Condition 对象。
public class AwaitSignalExample { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void before() { lock.lock(); try { System.out.println("before"); condition.signalAll(); } finally { lock.unlock(); } } public void after() { lock.lock(); try { condition.await(); System.out.println("after"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); AwaitSignalExample example = new AwaitSignalExample(); executorService.execute(() -> example.after()); executorService.execute(() -> example.before()); }
before after
AQS 是 AbstractQueuedSynchronizer 的简称,java.util.concurrent(J.U.C)大大提升了并发性能,AQS (AbstractQueuedSynchronizer) 被认为是 J.U.C 的核心。它提供了一个基于 FIFO 队列,这个队列能够用来构建锁或者其余相关的同步装置的基础框架。下图是 AQS 底层的数据结构:
它底层使用的是双向列表,是队列的一种实现 , 所以也能够将它当成一种队列。
简单的来讲:
AQS其实就是一个能够给咱们实现锁的框架
内部实现的关键是:先进先出的队列、state 状态
定义了内部类 ConditionObject
拥有两种线程模式
通常咱们叫 AQS 为同步器。
CountDownLatch 类位于 java.util.concurrent 包下,利用它能够实现相似计数器的功能。好比有一个任务 A,它要等待其余 4 个任务执行完毕以后才能执行,此时就能够利用 CountDownLatch 来实现这种功能了。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些由于调用 await() 方法而在等待的线程就会被唤醒。
CountDownLatch 类只提供了一个构造器:
public CountDownLatch(int count) { }; // 参数count为计数值
而后下面这 3 个方法是 CountDownLatch 类中最重要的方法:
`public` `void` `await() ``throws` `InterruptedException { }; ``//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行``public` `boolean` `await(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { }; ``//和await()相似,只不过等待必定的时间后count值还没变为0的话就会继续执行``public` `void` `countDown() { }; ``//将count值减1`
下面看一个例子你们就清楚 CountDownLatch 的用法了:
`public` `class` `Test {`` ``public` `static` `void` `main(String[] args) { `` ``final` `CountDownLatch latch = ``new` `CountDownLatch(``2``);`` ` ` ``new` `Thread(){`` ``public` `void` `run() {`` ``try` `{`` ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"正在执行"``);`` ``Thread.sleep(``3000``);`` ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"执行完毕"``);`` ``latch.countDown();`` ``} ``catch` `(InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``};`` ``}.start();`` ` ` ``new` `Thread(){`` ``public` `void` `run() {`` ``try` `{`` ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"正在执行"``);`` ``Thread.sleep(``3000``);`` ``System.out.println(``"子线程"``+Thread.currentThread().getName()+``"执行完毕"``);`` ``latch.countDown();`` ``} ``catch` `(InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``};`` ``}.start();`` ` ` ``try` `{`` ``System.out.println(``"等待2个子线程执行完毕..."``);`` ``latch.await();`` ``System.out.println(``"2个子线程已经执行完毕"``);`` ``System.out.println(``"继续执行主线程"``);`` ``} ``catch` `(InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``}``}`
执行结果:
线程Thread-0正在执行 线程Thread-1正在执行 等待2个子线程执行完毕... 线程Thread-0执行完毕 线程Thread-1执行完毕 2个子线程已经执行完毕 继续执行主线程
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 类似,都是经过维护计数器来实现的。可是它的计数器是递增的,每次执行 await() 方法以后,计数器会加 1,直到计数器的值和设置的值相等,等待的全部线程才会继续执行。和 CountdownLatch 的另外一个区别是,CyclicBarrier 的计数器能够循环使用,因此它才叫作循环屏障。
下图应该从下往上看才正确。
public class CyclicBarrierExample { public static void main(String[] args) throws InterruptedException { final int totalThread = 10; CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < totalThread; i++) { executorService.execute(() -> { System.out.print("before.."); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.print("after.."); }); } executorService.shutdown(); } }
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
Semaphore 就是操做系统中的信号量,能够控制对互斥资源的访问线程数。Semaphore 能够控同时访问的线程个数,经过 acquire() 获取一个许可,若是没有就等待,而 release() 释放一个许可。
Semaphore 类位于 java.util.concurrent 包下,它提供了2个构造器:
`public` `Semaphore(``int` `permits) { ``//参数permits表示许可数目,即同时能够容许多少线程进行访问`` ``sync = ``new` `NonfairSync(permits);``}``public` `Semaphore(``int` `permits, ``boolean` `fair) { ``//这个多了一个参数fair表示是不是公平的,即等待时间越久的越先获取许可`` ``sync = (fair)? ``new` `FairSync(permits) : ``new` `NonfairSync(permits);``}`
下面说一下 Semaphore 类中比较重要的几个方法,首先是 acquire()、release() 方法:
`public` `void` `acquire() ``throws` `InterruptedException { } ``//获取一个许可``public` `void` `acquire(``int` `permits) ``throws` `InterruptedException { } ``//获取permits个许可``public` `void` `release() { } ``//释放一个许可``public` `void` `release(``int` `permits) { } ``//释放permits个许可`
acquire() 用来获取一个许可,若无许可可以得到,则会一直等待,直到得到许可。
release() 用来释放许可。注意,在释放许可以前,必须先获得到许可。
这 4 个方法都会被阻塞,若是想当即获得执行结果,能够使用下面几个方法:
`public` `boolean` `tryAcquire() { }; ``//尝试获取一个许可,若获取成功,则当即返回true,若获取失败,则当即返回false``public` `boolean` `tryAcquire(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { }; ``//尝试获取一个许可,若在指定的时间内获取成功,则当即返回true,不然则当即返回false``public` `boolean` `tryAcquire(``int` `permits) { }; ``//尝试获取permits个许可,若获取成功,则当即返回true,若获取失败,则当即返回false``public` `boolean` `tryAcquire(``int` `permits, ``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { }; ``//尝试获取permits个许可,若在指定的时间内获取成功,则当即返回true,不然则当即返回false`
另外还能够经过 availablePermits() 方法获得可用的许可数目。
下面经过一个例子来看一下 Semaphore 的具体使用:
倘若一个工厂有 5 台机器,可是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完了,其余工人才能继续使用。那么咱们就能够经过 Semaphore 来实现:
`public` `class` `Test {`` ``public` `static` `void` `main(String[] args) {`` ``int` `N = ``8``; ``//工人数`` ``Semaphore semaphore = ``new` `Semaphore(``5``); ``//机器数目`` ``for``(``int` `i=``0``;i<N;i++)`` ``new` `Worker(i,semaphore).start();`` ``}`` ` ` ``static` `class` `Worker ``extends` `Thread{`` ``private` `int` `num;`` ``private` `Semaphore semaphore;`` ``public` `Worker(``int` `num,Semaphore semaphore){`` ``this``.num = num;`` ``this``.semaphore = semaphore;`` ``}`` ` ` ``@Override`` ``public` `void` `run() {`` ``try` `{`` ``semaphore.acquire();`` ``System.out.println(``"工人"``+``this``.num+``"占用一个机器在生产..."``);`` ``Thread.sleep(``2000``);`` ``System.out.println(``"工人"``+``this``.num+``"释放出机器"``);`` ``semaphore.release(); `` ``} ``catch` `(InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``}`` ``}``}`
执行结果:
工人0占用一个机器在生产... 工人1占用一个机器在生产... 工人2占用一个机器在生产... 工人4占用一个机器在生产... 工人5占用一个机器在生产... 工人0释放出机器 工人2释放出机器 工人3占用一个机器在生产... 工人7占用一个机器在生产... 工人4释放出机器 工人5释放出机器 工人1释放出机器 工人6占用一个机器在生产... 工人3释放出机器 工人7释放出机器 工人6释放出机器
下面对上面说的三个辅助类进行一个总结:
在介绍 Callable 时咱们知道它能够有返回值,返回值经过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既能够当作一个任务执行,也能够有返回值。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务须要执行很长时间,那么就能够用 FutureTask 来封装这个任务,主线程在完成本身的任务以后再去获取结果。
public class FutureTaskExample { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() { @Override public Integer call() throws Exception { int result = 0; for (int i = 0; i < 100; i++) { Thread.sleep(10); result += i; } return result; } }); Thread computeThread = new Thread(futureTask); computeThread.start(); Thread otherThread = new Thread(() -> { System.out.println("other task is running..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); otherThread.start(); System.out.println(futureTask.get()); } }
other task is running... 4950
java.util.concurrent.BlockingQueue 接口有如下阻塞队列的实现:
提供了阻塞的 take() 和 put() 方法:若是队列为空 take() 将阻塞,直到队列中有内容;若是队列为满 put() 将阻塞,直到队列有空闲位置。
使用 BlockingQueue 实现生产者消费者问题
public class ProducerConsumer { private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5); private static class Producer extends Thread { @Override public void run() { try { queue.put("product"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("produce.."); } } private static class Consumer extends Thread { @Override public void run() { try { String product = queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("consume.."); } } }
public static void main(String[] args) { for (int i = 0; i < 2; i++) { Producer producer = new Producer(); producer.start(); } for (int i = 0; i < 5; i++) { Consumer consumer = new Consumer(); consumer.start(); } for (int i = 0; i < 3; i++) { Producer producer = new Producer(); producer.start(); } }
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
主要用于并行计算中,和 MapReduce 原理相似,都是把大的计算任务拆分红多个小任务并行计算。
public class ForkJoinExample extends RecursiveTask<Integer> { private final int threshold = 5; private int first; private int last; public ForkJoinExample(int first, int last) { this.first = first; this.last = last; } @Override protected Integer compute() { int result = 0; if (last - first <= threshold) { // 任务足够小则直接计算 for (int i = first; i <= last; i++) { result += i; } } else { // 拆分红小任务 int middle = first + (last - first) / 2; ForkJoinExample leftTask = new ForkJoinExample(first, middle); ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last); leftTask.fork(); rightTask.fork(); result = leftTask.join() + rightTask.join(); } return result; } }
public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinExample example = new ForkJoinExample(1, 10000); ForkJoinPool forkJoinPool = new ForkJoinPool(); Future result = forkJoinPool.submit(example); System.out.println(result.get()); }
ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 实现了工做窃取算法来提升 CPU 的利用率。每一个线程都维护了一个双端队列,用来存储须要执行的任务。工做窃取算法容许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例以下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。可是若是队列中只有一个任务时仍是会发生竞争。
若是多个线程对同一个共享数据进行访问而不采起同步操做的话,那么操做的结果是不一致的。
如下代码演示了 1000 个线程同时对 cnt 执行自增操做,操做结束以后它的值为 997 而不是 1000。
public class ThreadUnsafeExample { private int cnt = 0; public void add() { cnt++; } public int get() { return cnt; } } public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; ThreadUnsafeExample example = new ThreadUnsafeExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }
997
Java 内存模型试图屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,须要一些协议来解决这个问题。
全部的变量都存储在主内存中,每一个线程还有本身的工做内存,工做内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操做工做内存中的变量,不一样线程之间的变量值传递须要经过主内存来完成。
Java内存模型和硬件关系图
Java内存模型抽象结构图
Java 内存模型定义了 8 个操做来完成主内存和工做内存的交互操做。
若是要把一个变量从主内存中复制到工做内存,就须要按顺寻地执行 read 和 load 操做,若是把变量从工做内存中同步回主内存中,就要按顺序地执行 store 和 write 操做。Java内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行。也就是 read 和 load 之间,store 和 write 之间是能够插入其余指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述8种基本操做时,必须知足以下规则:
参考资料:
volatile关键字与Java内存模型(JMM) - yzwall - 博客园
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操做具备原子性,例如对一个 int 类型的变量执行 assign 赋值操做,这个操做就是原子性的。可是 Java 内存模型容许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操做划分为两次 32 位的操做来进行,即 load、store、read 和 write 操做能够不具有原子性。
有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操做以后,获得的值为 997 而不是 1000。
为了方便讨论,将内存间的交互操做简化为 3 个:load、assign、store。
下图演示了两个线程同时对 cnt 变量进行操做,load、assign、store 这一系列操做总体上看不具有原子性,那么在 T1 修改 cnt 而且尚未将修改后的值写入主内存,T2 依然能够读入该变量的值。能够看出,这两个线程虽然执行了两次自增运算,可是主内存中 cnt 的值最后为 1 而不是 2。所以对 int 类型读写操做知足原子性只是说明 load、assign、store 这些单个操做具有原子性。
AtomicInteger 能保证多个线程修改的原子性。
使用 AtomicInteger 重写以前线程不安全的代码以后获得如下线程安全实现:
public class AtomicExample { private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); } public int get() { return cnt.get(); } }
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicExample example = new AtomicExample(); // 只修改这条语句 final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }
1000
除了使用原子类以外,也能够使用 synchronized 互斥锁来保证操做的原子性。它对应的内存间交互操做为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample { private int cnt = 0; public synchronized void add() { cnt++; } public synchronized int get() { return cnt; } } public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicSynchronizedExample example = new AtomicSynchronizedExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }
1000
可见性指当一个线程修改了共享变量的值,其它线程可以当即得知这个修改。Java 内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有有三种实现可见性的方式:
对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,由于 volatile 并不能保证操做的原子性。
有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。
在 Java 内存模型中,容许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字经过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障以前。
也能够经过 synchronized 来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关因而让线程顺序执行同步代码。
在执行程序时为了提升性能,编译器和处理器经常会对指令作重排序。
指令重排序包括:编译器重排序和处理器重排序
重排序分三种类型:
从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序均可能会致使多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是全部的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,经过内存屏障指令来禁止特定类型的处理器重排序(不是全部的处理器重排序都要禁止)。
JMM 属于语言级的内存模型,它确保在不一样的编译器和不一样的处理器平台之上,经过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖分下列三种类型:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量以后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量以后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量以后,再写这个变量。 |
上面三种状况,只要重排序两个操做的执行顺序,程序的执行结果将会被改变。
前面提到过,编译器和处理器可能会对操做作重排序。编译器和处理器在重排序时,会遵照数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操做,不一样处理器之间和不一样线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial 语义的意思指:无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和 处理器 都必须遵照 as-if-serial 语义。
为了遵照 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
上面三个操做的数据依赖关系以下图所示:
如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。所以在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器能够重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:
as-if-serial 语义把单线程程序保护了起来,遵照 as-if-serial 语义的编译器,runtime 和处理器共同为编写单线程程序的程序员建立了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial 语义使单线程程序员无需担忧重排序会干扰他们,也无需担忧内存可见性问题。
根据 happens- before 的程序顺序规则,上面计算圆的面积的示例代码存在三个 happens- before 关系:
这里的第 3 个 happens- before 关系,是根据 happens- before 的传递性推导出来的。
这里 A happens- before B,但实际执行时 B 却能够排在 A 以前执行(看上面的重排序后的执行顺序)。若是A happens- before B,JMM 并不要求 A 必定要在 B 以前执行。JMM 仅仅要求前一个操做(执行的结果)对后一个操做可见,且前一个操做按顺序排在第二个操做以前。这里操做 A 的执行结果不须要对操做 B 可见;并且重排序操做 A 和操做 B 后的执行结果,与操做 A 和操做 B 按 happens- before 顺序执行的结果一致。在这种状况下, JMM 会认为这种重排序并不非法(not illegal),JMM 容许这种重排序。
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽量的开发并行度。编译器和处理器听从这一目标,从 happens- before 的定义咱们能够看出,JMM 一样听从这一目标。
如今让咱们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } Public void reader() { if (flag) { // 3 int i = a * a; // 4 …… } } }
flag 变量是个标记,用来标识变量 a 是否已被写入。这里假设有两个线程 A 和 B,A首先执行 writer() 方法,随后 B 线程接着执行 reader() 方法。线程 B 在执行操做 4 时,可否看到线程 A 在操做 1 对共享变量 a 的写入?
答案是:不必定能看到。
因为操做 1 和操做 2 没有数据依赖关系,编译器和处理器能够对这两个操做重排序;一样,操做 3 和操做 4 没有数据依赖关系,编译器和处理器也能够对这两个操做重排序。让咱们先来看看,当操做 1 和操做 2 重排序时,可能会产生什么效果?请看下面的程序执行时序图:
如上图所示,操做 1 和操做 2 作了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。因为条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!
※注:本文统一用红色的虚箭线表示错误的读操做,用绿色的虚箭线表示正确的读操做。
下面再让咱们看看,当操做 3 和操做 4 重排序时会产生什么效果(借助这个重排序,能够顺便说明控制依赖性)。下面是操做 3 和操做 4 重排序后,程序的执行时序图:
在程序中,操做 3 和操做 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜想(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜想执行为例,执行线程 B 的处理器能够提早读取并计算 a*a,而后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操做3的条件判断为真时,就把该计算结果写入变量 i 中。
从图中咱们能够看出,猜想执行实质上对操做 3 和 4 作了重排序。重排序在这里破坏了多线程程序的语义!
在单线程程序中,对存在控制依赖的操做重排序,不会改变执行结果(这也是 as-if-serial 语义容许对存在控制依赖的操做作重排序的缘由);但在多线程程序中,对存在控制依赖的操做重排序,可能会改变程序的执行结果。
参考资料:
Happens-before 是用来指定两个操做之间的执行顺序。提供跨线程的内存可见性。
在 Java 内存模型中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必然存在 happens-before 关系。
上面提到了能够用 volatile 和 synchronized 来保证有序性。除此以外,JVM 还规定了先行发生原则,让一个操做无需控制就能先于另外一个操做完成。
主要有如下这些原则:
Single Thread rule
在一个线程内,在程序前面的操做先行发生于后面的操做。
Monitor Lock Rule
对一个锁的解锁(unlock ),老是 happens-before 于随后对这个锁的加锁(lock)
Volatile Variable Rule
对一个 volatile 变量的写操做先行发生于后面对这个变量的读操做。
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每个动做。
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过 interrupted() 方法检测到是否有中断发生。
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
Transitivity
若是操做 A 先行发生于操做 B,操做 B 先行发生于操做 C,那么操做 A 先行发生于操做 C。
一个类在能够被多个线程安全调用时就是线程安全的。
线程安全不是一个非真即假的命题,能够将共享数据按照安全程度的强弱顺序分红如下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
不可变(Immutable)的对象必定是线程安全的,不管是对象的方法实现仍是方法的调用者,都不须要再采起任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。
不可变的类型:
对于集合类型,能够使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
public class ImmutableExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map); unmodifiableMap.put("a", 1); } }
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) at ImmutableExample.main(ImmutableExample.java:9)
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,须要对集合进行修改的方法都直接抛出异常。
public V put(K key, V value) { throw new UnsupportedOperationException(); }
多线程环境下,应当尽可能使对象成为不可变,来知足线程安全。
无论运行时环境如何,调用者都不须要任何额外的同步措施。
相对的线程安全须要保证对这个对象单独的操做是线程安全的,在调用的时候不须要作额外的保障措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性。
在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
对于下面的代码,若是删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
public class VectorUnsafeExample { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 100; i++) { vector.add(i); } ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } }); executorService.execute(() -> { for (int i = 0; i < vector.size(); i++) { vector.get(i); } }); executorService.shutdown(); } } }
Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3 at java.util.Vector.remove(Vector.java:831) at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14) at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)
若是要保证上面的代码能正确执行下去,就须要对删除元素和获取元素的代码进行同步。
executorService.execute(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }); executorService.execute(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.get(i); } } });
线程兼容是指对象自己并非线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用,咱们日常说一个类不是线程安全的,绝大多数时候指的是这一种状况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码。因为 Java 语言天生就具有多线程特性,线程对立这种排斥多线程的代码是不多出现的,并且一般都是有害的,应当尽可能避免。
synchronized 和 ReentrantLock。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,所以这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,老是认为只要不去作正确的同步措施,那就确定会出现问题。不管共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分没必要要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要唤醒等操做。
随着硬件指令集的发展,咱们能够使用基于冲突检测的乐观并发策略:先进行操做,若是没有其它线程争用共享数据,那操做就成功了,不然采起补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不须要把线程挂起,所以这种同步操做称为非阻塞同步。
乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操做最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令须要有 3 个操做数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操做时,只有当 V 的值等于 A,才将 V 的值更新为 B。
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操做。
如下代码使用了 AtomicInteger 执行了自增的操做。
private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); }
如下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
如下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操做须要加的数值,这里为 1。经过 getIntVolatile(var1, var2) 获得旧的预期值,经过调用 compareAndSwapInt() 来进行 CAS 比较,若是该字段内存地址中的值 ==var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
能够看到 getAndAddInt() 在一个循环中进行,发生冲突的作法是不断的进行重试。
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
ABA :若是一个变量初次读取的时候是 A 值,它的值被改为了 B,后来又被改回为 A,那 CAS 操做就会误认为它历来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它能够经过控制变量值的版原本保证 CAS 的正确性。大部分状况下 ABA 问题不会影响程序并发的正确性,若是须要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
要保证线程安全,并非必定就要进行同步,二者没有因果关系。同步只是保证共享数据争用时的正确性的手段,若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性,所以会有一些代码天生就是线程安全的。
这种代码也叫作纯代码(Pure Code),能够在代码执行的任什么时候刻中断它,转而去执行另一段代码(包括递归调用它自己),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,由于局部变量存储在栈中,属于线程私有的。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class StackClosedExample { public void add100() { int cnt = 0; for (int i = 0; i < 100; i++) { cnt++; } System.out.println(cnt); } }
public static void main(String[] args) { StackClosedExample example = new StackClosedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> example.add100()); executorService.execute(() -> example.add100()); executorService.shutdown(); }
100 100
若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特色的应用并很多见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽可能在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的 “一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的普遍应用使得不少 Web 服务端应用均可以使用线程本地存储来解决线程安全问题。
能够使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
这是一个很是好的例题,请参考整理:
关于ThreadLocal类如下说法正确的是?_迅雷笔试题_牛客网
示例用法
先经过下面这个实例来理解 ThreadLocal 的用法。先声明一个 ThreadLocal 对象,存储布尔类型的数值。而后分别在main线程、Thread一、Thread2中为 ThreadLocal 对象设置不一样的数值:
public class ThreadLocalDemo { public static void main(String[] args) { // 声明 ThreadLocal对象 ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>(); // 在主线程、子线程一、子线程2中去设置访问它的值 mThreadLocal.set(true); System.out.println("Main " + mThreadLocal.get()); new Thread("Thread#1"){ @Override public void run() { mThreadLocal.set(false); System.out.println("Thread#1 " + mThreadLocal.get()); } }.start(); new Thread("Thread#2"){ @Override public void run() { System.out.println("Thread#2 " + mThreadLocal.get()); } }.start(); } }
打印的结果输出以下所示:
MainThread true Thread#1 false Thread#2 null
能够看见,在不一样线程对同一个 ThreadLocal对象设置数值,在不一样的线程中取出来的值不同。接下来就分析一下源码,看看其内部结构。
结构概览
清晰的看到一个线程 Thread 中存在一个 ThreadLocalMap,ThreadLocalMap 中的 key 对应 ThreadLocal,在此处可见 Map 能够存储多个 key 即 (ThreadLocal)。另外 Value 就对应着在 ThreadLocal 中存储的 Value。
所以总结出:每一个 Thread 中都具有一个 ThreadLocalMap,而 ThreadLocalMap 能够存储以 ThreadLocal 为key的键值对。这里解释了为何每一个线程访问同一个 ThreadLocal,获得的确是不一样的数值。若是此处你以为有点突兀,接下来看源码分析!
源码分析
1. ThreadLocal#set
public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 根据当前线程的对象获取其内部Map ThreadLocalMap map = getMap(t); // 注释1 if (map != null) map.set(this, value); else createMap(t, value); }
如上所示,大部分解释已经在代码中作出,注意注释1
处,获得 map 对象以后,用的 this
做为 key,this 在这里表明的是当前线程的 ThreadLocal 对象。 另外就是第二句根据 getMap 获取一个 ThreadLocalMap,其中getMap 中传入了参数 t (当前线程对象),这样就可以获取每一个线程的 ThreadLocal
了。
继续跟进到 ThreadLocalMap 中查看 set 方法:
2. ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的一个内部类,在分析其 set 方法以前,查看一下其类结构和成员变量。
static class ThreadLocalMap { // Entry类继承了WeakReference<ThreadLocal<?>> // 即每一个Entry对象都有一个ThreadLocal的弱引用(做为key),这是为了防止内存泄露。 // 一旦线程结束,key变为一个不可达的对象,这个Entry就能够被GC了。 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // ThreadLocalMap 的初始容量,必须为2的倍数 private static final int INITIAL_CAPACITY = 16; // resized时候须要的table private Entry[] table; // table中的entry个数 private int size = 0; // 扩容数值 private int threshold; // Default to 0 }
一块儿看一下其经常使用的构造函数:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
构造函数的第一个参数就是本 ThreadLocal 实例 (this),第二个参数就是要保存的线程本地变量。构造函数首先建立一个长度为16的 Entry 数组,而后计算出 firstKey 对应的哈希值,而后存储到 table 中,并设置 size 和 threshold。
注意一个细节,计算 hash 的时候里面采用了 hashCode & (size - 1) 的算法,这至关于取模运算 hashCode % size 的一个更高效的实现(和HashMap中的思路相同)。正是由于这种算法,咱们要求 size必须是 2 的指数,由于这能够使得 hash 发生冲突的次数减少。
3. ThreadLocalMap#set
ThreadLocal 中 put 函数最终调用了 ThreadLocalMap 中的 set 函数,跟进去看一看:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; // 冲突了 e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
在上述代码中若是 Entry 在存放过程当中冲突了,调用 nextIndex 来处理,以下所示。是否还记得 hashmap 中对待冲突的处理?这里好像是另外一种套路:只要 i 的数值小于 len,就加1取值,官方术语称为:线性探测法。
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
以上步骤ok了以后,再次关注一下源码中的 cleanSomeSlots,该函数主要的做用就是清理无用的 entry,避免出现内存泄露:
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
4. ThreadLocal#get
看完了 set 函数,确定是要关注 get 的,源码以下所示:
public T get() { // 获取Thread对象t Thread t = Thread.currentThread(); // 获取t中的map ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 若是t中的map为空 return setInitialValue(); }
若是 map 为 null,就返回 setInitialValue() 这个方法,跟进这个方法看一下:
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
最后返回的是 value,而 value 来自 initialValue()
,进入这个源码中查看:
protected T initialValue() { return null; }
原来如此,若是不设置 ThreadLocal 的数值,默认就是 null,来自于此。
ThreadLocal 从理论上讲并非用来解决多线程并发问题的,由于根本不存在多线程竞争。在一些场景 (尤为是使用线程池) 下,因为 ThreadLocal.ThreadLocalMap 的底层数据结构致使 ThreadLocal 有内存泄漏的状况,尽量在每次使用 ThreadLocal 后手动调用 remove(),以免出现 ThreadLocal 经典的内存泄漏甚至是形成自身业务混乱的风险。
参考资料:
这里的锁优化主要是指虚拟机对 synchronized 的优化。
互斥同步的进入阻塞状态的开销都很大,应该尽可能避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,若是在这段时间内能得到锁,就能够避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减小开销,可是它须要进行忙循环操做占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数再也不固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是经过逃逸分析来支持,若是堆上的共享数据不可能逃逸出去被其它线程访问到,那么就能够把它们当成私有数据对待,也就能够将它们的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了不少锁。例以下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 以前,会转化为 StringBuffer 对象的连续 append() 操做:
public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
每一个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态做用域被限制在 concatString() 方法内部。也就是说,sb 的全部引用永远不会“逃逸”到 concatString() 方法以外,其余线程没法访问到它,所以能够进行消除。
若是一系列的连续操做都对同一个对象反复加锁和解锁,频繁的加锁操做就会致使性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类状况。若是虚拟机探测到由这样的一串零碎的操做都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操做序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操做以前直至最后一个 append() 操做以后,这样只须要加锁一次就能够了。
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
如下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程建立的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操做来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,所以也就不须要都使用互斥量进行同步,能够先采用 CAS 操做进行同步,若是 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,若是锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中建立 Lock Record,而后使用 CAS 操做将对象的 Mark Word 更新为 Lock Record 指针。若是 CAS 操做成功了,那么线程就获取了该对象上的锁,而且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
若是 CAS 操做失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,若是是的话说明当前线程已经拥有了这个锁对象,那就能够直接进入同步块继续执行,不然说明这个锁对象已经被其余线程线程抢占了。若是有两条以上的线程争用同一个锁,那轻量级锁就再也不有效,要膨胀为重量级锁。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在以后获取该锁就再也不须要进行同步操做,甚至连 CAS 操做也再也不须要。
当锁对象第一次被线程得到的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操做将线程 ID 记录到 Mark Word 中,若是 CAS 操做成功,这个线程之后每次进入这个锁相关的同步块就不须要再进行任何同步操做。
当有另一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
蘑菇街面试,设计一个线程池
入队
非阻塞队列:当队列中满了时候,放入数据,数据丢失
阻塞队列:当队列满了的时候,进行等待,何时队列中有出队的数据,那么第11个再放进去
出队
非阻塞队列:若是如今队列中没有元素,取元素,获得的是null
阻塞队列:等待,何时放进去,再取出来
线程池使用的是阻塞队列
线程是稀缺资源,若是被无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有如下好处:
Java1.5 中引入的 Executor 框架把任务的提交和执行进行解耦,只须要定义好任务,而后提交给线程池,而不用关心该任务是如何执行、被哪一个线程执行,以及何时执行。
线程池中的核心线程数,当提交一个任务时,线程池建立一个新线程执行任务,直到当前线程数等于corePoolSize;若是当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;若是阻塞队列满了,那就建立新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。
// 使用Executors静态方法进行初始化 ExecutorService service = Executors.newSingleThreadExecutor(); // 经常使用方法 service.execute(new Thread()); service.submit(new Thread()); service.shutDown(); service.shutDownNow();
用到返回值的例子,好比说我有不少个作 validation 的 task,我但愿全部的 task 执行完,而后每一个 task 告诉我它的执行结果,是成功仍是失败,若是是失败,缘由是什么。而后我就能够把全部失败的缘由综合起来发给调用者。
若是你在你的 task 里会抛出 checked 或者 unchecked exception,而你又但愿外面的调用者可以感知这些 exception 并作出及时的处理,那么就须要用到 submit,经过捕获 Future.get 抛出的异常。
当线程池调用该方法时,线程池的状态则马上变成 SHUTDOWN 状态。此时,则不能再往线程池中添加任何任务,不然将会抛出 RejectedExecutionException 异常。可是,此时线程池不会马上退出,直到添加到线程池中的任务都已经处理完成,才会退出。
public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 线程存活时间(在 corePore<*<maxPoolSize 状况下有用) TimeUnit unit, // 存活时间的时间单位 BlockingQueue<Runnable> workQueue // 阻塞队列(用来保存等待被执行的任务) ThreadFactory threadFactory, // 线程工厂,主要用来建立线程; RejectedExecutionHandler handler // 当拒绝处理任务时的策略 ){ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
关于 workQueue 参数,有四种队列可供选择:
关于 handler 参数,线程池的饱和策略,当阻塞队列满了,且没有空闲的工做线程,若是继续提交任务,必须采起一种策略处理该任务,线程池提供了 4 种策略:
固然也能够根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
其中 AtomicInteger 变量 ctl 的功能很是强大:利用低 29 位表示线程池中线程数,经过高 3 位表示线程池的运行状态:
若是执行了线程池的 prestartAllCoreThreads() 方法,线程池会提早建立并启动全部核心线程。
ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize()。
通常须要根据任务的类型来配置线程池大小:
若是是 CPU 密集型任务,就须要尽可能压榨 CPU,参考值能够设为 NCPU+1
若是是 IO 密集型任务,参考值能够设置为 2*NCPU
在这里将总结面试中和并发编程相关的常见知识点,如在第一部分中出现的这里将不进行详细阐述。面试指南中,我将用最简洁的语言描述,更可能是以一种大纲的形式列出问答点,根据本身掌握的状况回答。
参考资料:
(1)仅靠volatile不能保证线程的安全性。(原子性)
synchronized 不只保证可见性,并且还保证原子性,由于,只有得到了锁的线程才能进入临界区,从而保证临界区中的全部语句都所有执行。多个线程争抢 synchronized 锁对象时,会出现阻塞。
(2)线程安全性
线程安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中能够看出:仅仅使用 volatile 并不能保证线程安全性。而 synchronized 则可实现线程的安全性。
什么是线程池
设计一个动态大小的线程池,如何设计,应该有哪些方法
线程池四种建立方式
Java 经过 Executors 提供四种线程池,分别为:
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(须要多核CPU)
好比我跟两个网友聊天,左手操做一个电脑跟甲聊,同时右手用另外一台电脑跟乙聊天,这就叫并行。
服务能同时处理不少请求,提升程序性能
参考资料:
当多个线程访问同一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替运行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以获取正确的结果,那这个对象是线程安全的。——来自《深刻理解Java虚拟机》
定义
如何保证线程安全?(更加详细的请转向第一部分 11. 线程安全
)
注意
有人在使用过程当中有一个不正确的观点:个人程序是多线程的,不能使用 ArrayList 要使用 Vector,这样才安全。
非线程安全并非多线程环境下就不能使用。注意我上面有说到:多线程操做同一个对象。注意是同一个对象。好比最上面那个模拟,就是在主线程中 new 的一个 ArrayList 而后多个线程操做同一个 ArrayList 对象。
若是是每一个线程中 new 一个 ArrayList,而这个 ArrayList 只在这一个线程中使用,那么确定是没问题的。
问:平时项目中使用锁和 synchronized 比较多,而不多使用 volatile,难道就没有保证可见性?
答:锁和 synchronized 便可以保证原子性,也能够保证可见性。都是经过保证同一时间只有一个线程执行目标代码段来实现的。
问:锁和 synchronized 为什么能保证可见性?
答:根据 JDK 7的Java doc 中对 concurrent
包的说明,一个线程的写结果保证对另外线程的读操做可见,只要该写操做能够由 happen-before
原则推断出在读操做以前发生。
The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.
问:既然锁和 synchronized 便可保证原子性也可保证可见性,为什么还须要 volatile?
答:synchronized和锁须要经过操做系统来仲裁谁得到锁,开销比较高,而 volatile 开销小不少。所以在只须要保证可见性的条件下,使用 volatile 的性能要比使用锁和 synchronized 高得多。
问:既然锁和 synchronized 能够保证原子性,为何还须要 AtomicInteger 这种的类来保证原子操做?
答:锁和 synchronized 须要经过操做系统来仲裁谁得到锁,开销比较高,而 AtomicInteger 是经过CPU级的CAS操做来保证原子性,开销比较小。因此使用 AtomicInteger 的目的仍是为了提升性能。
问:还有没有别的办法保证线程安全
答:有。尽量避免引发非线程安全的条件——共享变量。若是能从设计上避免共享变量的使用,便可避免非线程安全的发生,也就无须经过锁或者 synchronized 以及 volatile 解决原子性、可见性和顺序性的问题。
问:synchronized 便可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized 修饰非静态同步方法时,锁住的是当前实例;synchronized 修饰静态同步方法时,锁住的是该类的 Class 对象;synchronized 修饰静态代码块时,锁住的是 synchronized 关键字后面括号内的对象。
参考资料:
volatile 关键字的做用
内存可见性
当且仅当知足如下全部条件时,才应该使用 volatile 变量
volatile 使用建议
volatile 和 synchronized区别
volatile 变量是一种稍弱的同步机制在访问 volatile 变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制。
从内存可见性的角度看,写入 volatile 变量至关于退出同步代码块,而读取 volatile 变量至关于进入同步代码块。
在代码中若是过分依赖 volatile 变量来控制状态的可见性,一般会比使用锁的代码更脆弱,也更难以理解。仅当 volatile 变量能简化代码的实现以及对同步策略的验证时,才应该使用它。通常来讲,用同步机制会更安全些。
加锁机制(即同步机制)既能够确保可见性又能够确保原子性,而 volatile 变量只能确保可见性,缘由是声明为volatile的简单变量若是当前值与该变量之前的值相关,那么 volatile 关键字不起做用,也就是说以下的表达式都不是原子操做:“count++”、“count = count+1”。
(1)线程和进程
(2)使用线程的缘由
不论哪一种语言的内存分配方式,都须要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用 new、反射、clone、反序列化等方法建立的, 这些对象的建立都是在堆(Heap)中分配的,全部对象的回收都是由Java虚拟机经过垃圾回收机制完成的。GC 为了可以正确释放对象,会监控每一个对象的运行情况,对他们的申请、引用、被引用、赋值等情况进行监控,Java 会使用有向图的方法进行管理内存,实时监控对象是否能够达到,若是不可到达,则就将其回收,这样也能够消除引用循环的问题。
在 Java 语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值 null,如下再没有调用过,另外一个是给对象赋予了新值,这样从新分配了内存空间。
首先,什么是内存泄露?常常听人谈起内存泄露,但要问什么是内存泄露,没几个说得清楚。
内存泄露:是指无用对象(再也不使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而形成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示 Out of memory
。
内存溢出:指程序运行过程当中没法申请到足够的内存而致使的一种错误。内存泄露是内存溢出的一种诱因,不是惟一因素
那么,Java 内存泄露根本缘由是什么呢?长生命周期的对象持有短生命周期对象的引用就极可能发生内存泄露,尽管短生命周期对象已经再也不须要,可是由于长生命周期对象持有它的引用而致使不能被回收,这就是 Java 中内存泄露的发生场景。具体主要有以下几大类
静态集合类,使用Set、Vector、HashMap等集合类的时候须要特别注意。当这些类被定义成静态的时候,因为他们的生命周期跟应用程序同样长,这时候就有可能发生内存泄漏。
// 例子 class StaticTest { private static Vector v = new Vector(10); public void init() { for (int i = 1; i < 100; i++) { Object object = new Object(); v.add(object); object = null; } } }
在上面的代码中,循环申请object对象,并添加到Vector中,而后设置object=null(就是清除栈中引用变量object),可是这些对象被vector引用着,必然不能被GC回收,形成内存泄露。所以要释放这些对象,还须要将它们从vector中删除,最简单的方法就是将vector=null,清空集合类中的引用。
在 Java 编程中,咱们都须要和监听器打交道,一般一个应用中会用到不少监听器,咱们会调用一个控件,诸如 addXXXListener()
等方法来增长监听器,但每每在释放的时候却没有去删除这些监听器,从而增长了内存泄漏的机会。
好比数据库链接(dataSourse.getConnection()),网络链接 (socket) 和 IO 链接,除非其显式的调用了其close() 方 法将其链接关闭,不然是不会自动被 GC 回收的。对于 Resultset 和 Statement 对象能够不进行显式回收,但 Connection 必定要显式回收,由于 Connection 在任什么时候候都没法自动回收,而 Connection一旦回收,Resultset 和 Statement 对象就会当即为 NULL。可是若是使用链接池,状况就不同了,除了要显式地关闭链接,还必须显式地关闭 Resultset Statement 对象(关闭其中一个,另一个也会关闭),不然就会形成大量的 Statement 对象没法释放,从而引发内存泄漏。这种状况下通常都会在 try 里面去的链接,在 finally 里面释放链接。
内部类的引用是比较容易遗忘的一种,并且一旦没释放可能致使一系列的后继类对象没有释放。在调用外部模块的时候,也应该注意防止内存泄漏,若是模块A调用了外部模块B的一个方法,如: public void register(Object o)
这个方法有可能就使得A模块持有传入对象的引用,这时候须要查看B模块是否提供了出去引用的方法,这种状况容易忽略,并且发生内存泄漏的话,还比较难察觉。
由于单利对象初始化后将在 JVM 的整个生命周期内存在,若是它持有一个外部对象的(生命周期比较短)引用,那么这个外部对象就不能被回收,从而致使内存泄漏。若是这个外部对象还持有其余对象的引用,那么内存泄漏更严重。
使用多线程时,不是多线程能提高程序的执行速度,使用多线程是为了更好地利用 CPU 资源!
程序在执行时,多线程是 CPU 经过给每一个线程分配 CPU 时间片来实现的,时间片是CPU分配给每一个线程执行的时间,因时间片很是短,因此CPU 经过不停地切换线程执行。
线程不是越多就越好的,由于线程上下文切换是有性能损耗的,在使用多线程的同时须要考虑如何减小上下文切换
通常来讲有如下几条经验
还能够考虑咱们的应用是IO密集型的仍是CPU密集型的。
synchronized 同步
while 轮询的方式
(list.size()==5)
是否成立 ,从而实现了线程间的通讯。可是这种方式会浪费 CPU 资源。之因此说它浪费资源,是由于 JVM 调度器将 CPU 交给线程B执行时,它没作啥“有用”的工做,只是在不断地测试某个条件是否成立。就相似于现实生活中,某我的一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。wait/notify 机制
当条件未知足时,线程A调用 wait() 放弃CPU,并进入阻塞状态。(不像 while 轮询那样占用 CPU)
当条件知足时,线程B调用 notify() 通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
管道通讯
参考资料:
同步和异步关注的是消息通讯机制 (synchronous communication/ asynchronous communication)
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
参考资料:
本小结参考:Java 中的锁 - Java 并发性和多线程 - 极客学院Wiki
锁像 synchronized 同步块同样,是一种线程同步机制,但比 Java 中的 synchronized 同步块更复杂。由于锁(以及其它更高级的线程同步机制)是由 synchronized 同步块的方式实现的,因此咱们还不能彻底摆脱 synchronized 关键字(译者注:这说的是 Java 5 以前的状况)。
自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,所以你不用去实现本身的锁了。可是你仍然须要去了解怎样使用这些锁,且了解这些实现背后的理论也是颇有用处的。能够参考我对 java.util.concurrent.locks.Lock 的介绍,以了解更多关于锁的信息。
让咱们从 java 中的一个同步块开始:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
能够看到在 inc()方法中有一个 synchronized(this)代码块。该代码块能够保证在同一时间只有一个线程能够执行 return ++count。虽然在 synchronized 的同步块中的代码能够更加复杂,可是++count 这种简单的操做已经足以表达出线程同步的意思。
如下的 Counter 类用 Lock 代替 synchronized 达到了一样的目的:
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; } }
lock()方法会对 Lock 实例对象进行加锁,所以全部对该对象调用 lock()方法的线程都会被阻塞,直到该 Lock 对象的 unlock()方法被调用。
这里有一个 Lock 类的简单实现:
public class Counter{ public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); } }
注意其中的 while(isLocked) 循环,它又被叫作 “自旋锁”。自旋锁以及 wait() 和 notify() 方法在线程通讯这篇文章中有更加详细的介绍。当 isLocked 为 true 时,调用 lock() 的线程在 wait() 调用上阻塞等待。为防止该线程没有收到 notify() 调用也从 wait() 中返回(也称做虚假唤醒),这个线程会从新去检查 isLocked 条件以决定当前是否能够安全地继续执行仍是须要从新保持等待,而不是认为线程被唤醒了就能够安全地继续执行了。若是 isLocked 为 false,当前线程会退出 while(isLocked) 循环,并将 isLocked 设回 true,让其它正在调用 lock() 方法的线程可以在 Lock 实例上加锁。
当线程完成了临界区(位于 lock()和 unlock()之间)中的代码,就会调用 unlock()。执行 unlock()会从新将 isLocked 设置为 false,而且通知(唤醒)其中一个(如有的话)在 lock()方法中调用了 wait()函数而处于等待状态的线程。
Java 中的 synchronized 同步块是可重入的。这意味着若是一个 Java 线程进入了代码中的 synchronized 同步块,并所以得到了该同步块使用的同步对象对应的管程上的锁,那么这个线程能够进入由同一个管程对象所同步的另外一个 java 代码块。下面是一个例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意 outer()和 inner()都被声明为 synchronized,这在 Java 中和 synchronized(this) 块等效。若是一个线程调用了 outer(),在 outer()里调用 inner()就没有什么问题,由于这两个方法(代码块)都由同一个管程对象(”this”) 所同步。若是一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的全部代码块。这就是可重入。线程能够进入任何一个它已经拥有的锁所同步着的代码块。
前面给出的锁实现不是可重入的。若是咱们像下面这样重写 Reentrant 类,当线程调用 outer() 时,会在 inner()方法的 lock.lock() 处阻塞住。
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); } }
调用 outer() 的线程首先会锁住 Lock 实例,而后继续调用 inner()。inner()方法中该线程将再一次尝试锁住 Lock 实例,结果该动做会失败(也就是说该线程会被阻塞),由于这个 Lock 实例已经在 outer()方法中被锁住了。
两次 lock()之间没有调用 unlock(),第二次调用 lock 就会阻塞,看过 lock() 实现后,会发现缘由很明显:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
一个线程是否被容许退出 lock()方法是由 while 循环(自旋锁)中的条件决定的。当前的判断条件是只有当 isLocked 为 false 时 lock 操做才被容许,而没有考虑是哪一个线程锁住了它。
为了让这个 Lock 类具备可重入性,咱们须要对它作一点小的改动:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.curentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } ... }
注意到如今的 while 循环(自旋锁)也考虑到了已锁住该 Lock 实例的线程。若是当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该 Lock 实例加了锁,那么 while 循环就不会被执行,调用 lock()的线程就能够退出该方法(译者注:“被容许退出该方法”在当前语义下就是指不会调用 wait()而致使阻塞)。
除此以外,咱们须要记录同一个线程重复对一个锁对象加锁的次数。不然,一次 unblock()调用就会解除整个锁,即便当前锁已经被加锁过屡次。在 unlock()调用没有达到对应 lock()调用的次数以前,咱们不但愿锁被解除。
如今这个 Lock 类就是可重入的了。
Java 的 synchronized 块并不保证尝试进入它们的线程的顺序。所以,若是多个线程不断竞争访问相同的 synchronized 同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权老是分配给了其它线程。这种状况被称做线程饥饿。为了不这种问题,锁须要实现公平性。本文所展示的锁在内部是用 synchronized 同步块实现的,所以它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。
若是用 Lock 来保护临界区,而且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得很是重要了。这样能够保证这个锁对象能够被解锁以便其它线程能继续对其加锁。如下是一个示例:
lock.lock(); try{ //do critical section code, //which may throw exception } finally { lock.unlock(); }
这个简单的结构能够保证当临界区抛出异常时 Lock 对象能够被解锁。若是不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会致使其它全部在该 Lock 对象上调用 lock()的线程一直阻塞。
从 volatile 说到,i++原子操做,线程安全问题 - CSDN博客
https://blog.csdn.net/zbw18297786698/article/details/53420780