并发编程
1、进程与线程
-
进程:资源分配的基本单位。
-
线程:调度的基本单位。
在Java中一个Java程序对应一个进程,一个进程可以拥有多个线程。线程共享进程的内存。
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
- 并行(parallel)是同一时间动手做(doing)多件事情的能力。
引于Rob Pike 的一段描述。
2、Java线程
2.1 线程的创建
// 实现Runnable接口
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":create by implements Runnable with lambda");
}, "Thread A").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":create by implements Runnable");
}
}, "Thread B").start();
// 继承Thread
Thread thread = new Thread("Thread C") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":create by extends Thread");
}
};
thread.start();
// 实现callable接口
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + ":create by implements callable");
return true;
}
};
FutureTask<Boolean> futureTask = new FutureTask<>(callable);
new Thread(futureTask, "Thread D").start();
System.out.println("re1:" + futureTask.get());// get()获取返回值
FutureTask<Boolean> futureTask1 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + ":create by implements callable with lambda");
return false;
});
new Thread(futureTask1, "Thread E").start();
System.out.println("re2:" + futureTask1.get());
2.2 常用方法
- start 与 run
直接调用 run 是在主线程中执行了 run,没有启动新的线程。
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。
- sleep、wait 与 yield
调用sleep
会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)。
调用wait
会使当前线程进入睡眠,可被notify
、notifyAll
、Thread.interrupt()
或在时间片用完是主动唤醒。
调用 yield
会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程。
- join 与 State
可以理解为插队,阻塞当前线程,等插队线程执行完再继续执行。
thread.getState();
获取线程当前状态(线程状态后面讲解)。
- 线程优先级(Priority)
设置线程优先级,优先级大小只是给CPU选择调度的参考,具体调度由CPU决定。
- interrupt
设置中断标志为 true,并立即返回。设置标志仅仅是设置标志,线程 并没有实际被中断,会继续往下执行的,然后线程 可以调用 isInterrupted()
方法来看自己是不是被中断了,返回 true 说明自己被别的线程中断了,然后根据状态来决定是否终止自己活或 者干些其他事情。
2.3 线程状态
3、数据共享
3.1 数据共享带来的问题
简单的计数器示例:
public class UnSafeCounter {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
运行后发现,结果基本不可能为0。
原因分析:Java方法执行时会先把需要的数据获取保存在方法对应的栈帧的局部变量表里面,带运算完成后再把结果写回主存。这个时候就回出现一个问题,在A线程出去count值后B线程对其进行了更新,但A线程不知道count的值发生了更新,会直接把直接运算的结果回写进主存,这样就会带来数据错误。
3.2 临界区与竞态条件
- 临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
- 竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
3.3 synchronized使用
synchronized是一种阻塞式解决方案,俗称【对象锁】。采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码。
代码示例:
public class SafeCounter {
static int count = 0;
static final Object room = new Object();
// synchronized method
private synchronized static int countInc(Integer n){
return count+n;
}
private static void method1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// synchronized obj
synchronized (room) {
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
private static void method2() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
countInc(1);
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
countInc(-1);
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
public static void main(String[] args) throws InterruptedException {
// method1();
method2();
}
}
synchronized关键字无论是加载方法还是对象上面,实际都是添加了一个同步监视器,推荐使用共享十元作为同步监视器,synchronized method 自动把this
作为同步监视器。
3.4 Lock使用
public class SafeByTicket {
public static void main(String[] args) {
ByTicket byTicket = new ByTicket();
new Thread(byTicket, "a").start();
new Thread(byTicket, "b").start();
new Thread(byTicket, "c").start();
}
}
class ByTicket implements Runnable {
private int ticket = 20;
Boolean flag = true;
/**
* 添加私有ReentrantLock对象
*/
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (flag) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
private void buy() {
//加锁
lock.lock();
try {
if (ticket <= 0) {
System.out.println(Thread.currentThread().getName() + "没票了");
stop();
} else {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "张票");
}
} finally {
//解锁
lock.unlock();
}
}
public void stop() {
this.flag = false;
}
}
当读操作远远大于写操作的时候可以使用ReentrantReadWriteLock
允许读的并发,来提升系统效率。
public class Ticket {
private int ticket = 20;
private Boolean flag = true;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public void buy() {
while (flag) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
doBuy();
}
}
public void get() {
while (flag) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
doGet();
}
}
private void doGet(){
// 加读锁
readLock.lock();
try {
if (ticket <= 0) {
System.out.println(Thread.currentThread().getName() + "发现没票了");
stop();
} else {
System.out.println(Thread.currentThread().getName() + "查看得知还有:" + ticket + "张票");
}
} finally {
// 释放读锁
readLock.unlock();
}
}
private void doBuy() {
// 加写锁
writeLock.lock();
try {
if (ticket <= 0) {
System.out.println(Thread.currentThread().getName() + "没买到票");
stop();
} else {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "张票");
}
} finally {
// 释放写锁
writeLock.unlock();
}
}
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
for (int i = 0; i < 10; i++) {
new Thread(()->{
ticket.buy();
},"Thread buy:"+i).start();
}
for (int i = 0; i < 10; i++) {
new Thread(()->{
ticket.get();
},"Thread get:"+i).start();
}
}
}
3.5 变量的线程安全分析
- 成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全。
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
如果只有读操作,则线程安全。
如果有读写操作,则这段代码是临界区,需要考虑线程安全。
- 局部变量是否线程安全?
局部变量是线程安全的。
但局部变量引用的对象则未必。
如果该对象没有逃离方法的作用访问,它是线程安全的。
如果该对象逃离方法的作用范围,需要考虑线程安全。
3.6 常见的线程安全的类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
注意:这里的线程安全指的是他们的方法的原子性。
伪代码示例:
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
3.7 wait¬ify使用
熟悉操作使用的都知道,当一个线程请求一个资源时,便会有运行状态变为阻塞状态,等资源准备完毕,系统便唤醒线程继续执行下去。JAVA并发编程亦是如此。
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
示例代码:
public class Demo {
final static Object obj = new Object();
static boolean hasA = false;
static boolean hasB = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始执行");
synchronized (obj) {
while (!hasA) {
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "执行完了");
}, "T1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始执行");
synchronized (obj) {
while (!hasB) {
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "执行完了");
}, "T2").start();
// 主线程两秒后执行
Thread.sleep(2);
synchronized (obj) {
System.out.println("A补货了!");
hasA = true;
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
sleep(long n) 和 wait(long n) 的区别
-
sleep 是 Thread 方法,而 wait 是 Object 的方法 。
-
sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用 。
-
sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4) 它们状态 TIMED_WAITING。
3.8 park&unpark使用
- 暂停当前线程:
LockSupport.park()
。 - 恢复某个线程的运行
LockSupport.unpark
。
示例代码:
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":start...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":park...");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+":resume...");
},"t1");
t1.start();
Thread.sleep(2);
System.out.println(Thread.currentThread().getName()+":unpark-->t1");
LockSupport.unpark(t1);
}
}
wait & notify
使用有强制的先后顺序,而park&unpark
不必。
示例代码:
public class ChangeOrder {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":start...");
try {
Thread.sleep(2);// 确保先unpark
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":park...");
LockSupport.park();
System.out.println(Thread.currentThread().getName()+":resume...");
},"t1");
t1.start();
System.out.println(Thread.currentThread().getName()+":unpark-->t1");
LockSupport.unpark(t1);
}
}
3.9 总结和补充
-
使用
synchronized
和lock
保证共享资源的安全性。 -
使用
wait & notify
和park&unpark
进行线程中的通信。 -
产生死锁的四个必要条件:通过破话任一条件来避免死锁。
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺,只能在进程使用完时由自己释放。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
4、无锁
还记得前面计数器的例子吗我们为了保证共享资源count
的安全而使用了锁,但锁的使用还是比较麻烦,那有没有一种无锁的解决方案呢?当然有:
public class NoLockSafeCounter {
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndAdd(1);
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndAdd(-1);
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
4.1 CAS 与 volatile
细心的朋友可能已经发现了,我们使用AtomicInteger
替换了int
、count.getAndAdd(1)
替换了count++
,这样简单的操作便保证了共享资源的安全。那么这是怎么做到的呢。查看getAndAdd
方法的源码如下:
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;
}
中间还用到了Unsafe 对象,其提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。
再深入发现其是一个用native
标识的本地方法,并不由Java语言实现:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
方法的作用是,读取传入对象var1
在内存中偏移量为var2
位置的值与期望值var4
作比较。相等就把var5
值赋值给var2
位置的值。方法返回true。不相等,就取消赋值,方法返回false。
这就是是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。
实现CAS的时候存在一个关键问题:一个线程对共享资源的写操作必须对其他线程具有可见性。什么意思呢?简单来说一旦发生写,其他线程局部变量表里的共享资源拷贝立即失效,使用时必须从主存中获取最新的值。JAVA中通过使用volatile
关键字来保证共享资源的可见性。
- 可用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)。
有锁无锁对比
CAS
是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。synchronized
是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。CAS
体现的是无锁并发、无阻塞并发。- 因为没有使用
synchronized
,所以线程不会陷入阻塞,这是效率提升的因素之一。 - 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
4.3 J.U.C 的atomic下的类
5、线程池
5.1 ThreadPoolExecutor
- 线程池状态
ThreadPoolExecutor
使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作
进行赋值。
-
构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize
核心线程数目 (最多保留的线程数)maximumPoolSize
最大线程数目keepAliveTime
生存时间 - 针对救急线程unit
时间单位 - 针对救急线程workQueue
阻塞队列threadFactory
线程工厂 - 可以为线程创建时起个好名字handler
拒绝策略 -
工作方式
刚开始线程池为空,当任务提交线程池就会创建一个线程来执行任务;当线程数达到核心线程数并且没有空闲线程,任务就会进入阻塞队列;当阻塞队列满时(有限队列时),就会创建最大线程数减核心线程数的急救线程来执行任务;如果线程数达到了最大线程数时,仍有新任务提交到线程池就会执行拒绝策略(4种);当一段时间过后,急救线程空闲达到释放调教,就要结束其来节省资源。
5.2 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
阻塞队列是无界的,可以放任意数量的任务
评价 适用于任务量已知,相对耗时的任务
5.3 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程数是 0, 最大线程数是 Integer.MAX_VALUE
,救急线程的空闲生存时间是 60s,意味着
-
全部都是救急线程(60s 后可以回收)
-
救急线程可以无限创建
队列采用了 SynchronousQueue
实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交
货)
评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况
5.4 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景:
- 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
Executors.newSingleThreadExecutor()
线程个数始终为1,不能修改。FinalizableDelegatedExecutorService
应用的是装饰器模式,只对外暴露了ExecutorService
接口,因此不能调用ThreadPoolExecutor
中特有的方法。Executors.newFixedThreadPool(1)
初始时为1,以后还可以修改。- 对外暴露的是
ThreadPoolExecutor
对象,可以强转后调用setCorePoolSize
等方法进行修改
6、结束语
JAVA并发编程基础部分就到这里了,内容还是比较多,更多原理和源码分析请期待并发编程原理分析篇。