Java 并发编程
一、 并发编程三要素
-
原子性:即一个不可被分割的操作。
Java 中的原子性指:一个或多个操作要么全部执行成功,要么全部执行失败
-
有序性:程序执行的顺序是按照代码的先后顺序执行的。
cpu 有可能会对指令进行重排序
-
可见性:当多个线程访问同一个共享变量时,如果其中一个线程对其进行了修改操作,其它线程能立即获取到最新修改的值。
二、线程的五大状态
对应cpu 的线程状态
- 新建状态:通过
new
创建一个线程 - 就绪状态:调用start 方法,
处于就绪状态的线程不一定立马就会执行run 方法,还需要等CPU的调度。
- 运行状态:cpu开始调度线程,开始执行run方法
- 阻塞状态:线程在执行过程中由于某些原因进入阻塞状态。如:
sleep()、获取锁失败、时间片用完等
- 死亡状态:run方法执行完,或执行过程中遇到异常。
三、悲观锁与乐观锁
悲观锁:每次操作都会加锁,造成线程阻塞。如:synchronized、reentrantlock
乐观锁:每次操作都不会加锁,而是假定没有冲突而去完成某种操作。如果有冲突就重试,直到成功为止,不会造成线程的阻塞。虽然不会造成线程阻塞,但是频繁的重试会导致cpu的长时间被占用。
四、线程之间的协作
wait、notify、notifyAll等
五、synchronized 关键字
synchronized 是Java中的关键字
,是一种同步锁,底层使用的是Monitor监视器。
它修饰的对象有一下几种:
修饰一个代码块
被修饰的代码块称为同步代码块,其作用范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。修饰一个方法
被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。修饰一个静态的方法
其作用范围是整个静态方法,作用的对象是这个类对象修饰一个类
其作用的范围是synchronized后面括起来的部分,作用的对象是这个类对象。
对第4点解释:
public void test(){
// 当前synchronized 作用的范围是 {} 的部分
// 加锁的对象是 A.class 对象
synchronized(A.class){
}
}
六、CAS
CAS:Compare And Swap 比较并交换。
原理:
使用到的是cpu的一个原子操作,操作包含三个操作数,内存值、预期值、更新的值。如果内存值与预期值相同,那么就能将该值修改为更新的值。否则修改失败。
缺点:
- ABA 问题
可通过版本号或者时间戳解决
- 循环时间长对CPU的开销大
可以增加重试次数,超过重试次数则直接放弃
- 只能保证一个共享变量的原子操作。
后续可以使用原子引用对象来解决该问题
七、Thread.State 类中定义的六种线程状态
- new:Thread对象已经创建,但是还没有执行。
- Runnable:Thread对象在java 虚拟机中运行。
- Block:Thread对象被阻塞。
- Waitting:Thread对象在等待另外一个线程的动作。
- Time_Waiting:Thread对象在等待另外一个线程的动作,但是有时间限制。
- Terminated:Thread已经完成了执行
八、Thread类的常用方法:
- 获取和设置Thread对象信息的方法
- getId(): 返回Thread 对象的标识符。该标识符是在线程创建时分配的一个
正整数
。在线程的整个生命周期中是唯一且无法改变的。 - getName()/setName():获取、设置Thead 对象的名称。这个名称是一个String 类型。也可以在Thread的构造方法中指定。
- getPriority()/setPriority():获取或者设置Thread对象的优先级。
- isDaemon()/setDaemon():获取或者设置守护线程。
- getState():返回Thread对象的状态。
- getId(): 返回Thread 对象的标识符。该标识符是在线程创建时分配的一个
- interrupt():中断目标线程。给目标线程打上一个中断标记。
- interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
- isInterrupted():判断目标线程是否被中断,不会清除中断标记。
- sleep(long ms):暂停当前线程ms时间。
- join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另外一个线程结束。
- currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。
九、Callable
- 接口。有简单类型参数,与call() 方法的返回类型相对应
- 声明了call()。执行器运行任务时,该方法会被执行器执行。必须返回声明中指定类型的对象。
- call() 方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute() 方法来处理这些异常`
package com.yj.juc;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(500);
return "call 的返回值";
}
}
package com.yj.juc;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 启动线程执行任务
new Thread(futureTask).start();
// 需要同步阻塞获取到任务的结果
System.out.println(futureTask.get());
System.out.println("main1 方法执行结束");
}
}
package com.yj.juc;
import java.util.concurrent.*;
public class Main2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
5,
1000,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)
){
@Override
protected void afterExecute(Runnable r, Throwable t) {
// super.afterExecute(r, t);
System.out.println("call 执行结束"+t);
}
};
Future<String> future = threadPoolExecutor.submit(new MyCallable());
String result = future.get();
System.out.println(result);
threadPoolExecutor.shutdown();
}
}
// Main2 的打印结果:
call 的返回值
call 执行结束null
十、生产者与消费者模型
生产者、消费者是一个常见的多线程模型。
解释:
多个生产者线程往内存队列中存放数据;多个消费者线程从内存队列中取数据。
前提条件:
加锁
,内存队列本身要加锁,才能实现队列的安全。阻塞
,当内存队列满了,生产者放不进去数据,就会阻塞。当队列为空时,消费者取不到数据,就会被阻塞。唤醒通知
,消费者消费之后,要通知生产者生产新的数据。反之生产者生产消息之后,要通知消费者消费
如何阻塞?
办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait和notify
办法2:用一个阻塞队列,当娶不到或者放不进去数据的时候,入队、出队操作阻塞、
如何唤醒、通知?
办法1:使用wait和notify。必须在Synchronized中使用
办法2:Condition机制。Lock 锁
案例:
任务队列:
package com.yj.juc.demo01;
/**
* 任务队列
*/
public class MyQueue {
// 存放任务的队列
private String [] queue;
// 添加时元素的下标
private Integer putIndex = 0;
// 获取时元素的下标
private Integer getIndex = 0;
// 元素的个数
private Integer size = 0;
// 锁对象
private static final Object lock = new Object();
public MyQueue(Integer size){
this.queue = new String[size];
}
/**
* 当前方法存在的问题:
* 1、在单线程的情况下没有问题
* 2、多线程的情况可能会出现多个线程同时阻塞,然后同时被唤醒。那么添加的元素就有可能超过队列的长度。
* 解决方案:
* 1、唤醒之后重新获取锁,获取成功则添加元素
* @param element
*/
public synchronized void put(String element){
// 如果队列中的元素已经存满,则阻塞
if (size == queue.length){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue[putIndex++] = element;
++size;
putIndex = putIndex % queue.length;
notify();
}
public synchronized String get(){
if (size == 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String result = queue[getIndex++];
--size;
getIndex = getIndex % queue.length;
notify();
return result;
}
}
消费者:
package com.yj.juc.demo01;
import java.util.Random;
public class MyConsumer implements Runnable {
private MyQueue myQueue;
public MyConsumer(MyQueue myQueue){
this.myQueue = myQueue;
}
@Override
public void run() {
while (true){
try {
String result = myQueue.get();
System.out.println("消费者消费消息:" + result);
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者:
package com.yj.juc.demo01;
import java.util.Random;
public class MyProvider implements Runnable {
private MyQueue myQueue;
public MyProvider(MyQueue myQueue){
this.myQueue = myQueue;
}
@Override
public void run() {
int index = 0;
while (true){
try {
String temp = "生产者生产消息"+index++;
myQueue.put(temp);
System.out.println(temp);
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
当前存在的问题:在多线程情况下有可能会通知多个线程。
解决方案:
在通知唤醒之后,尝试获取锁,获取成功才能执行对应的操作。
package com.yj.juc.demo01;
public class MyQueue2 extends MyQueue {
// 存放任务的队列
private String [] queue;
// 添加时元素的下标
private Integer putIndex = 0;
// 获取时元素的下标
private Integer getIndex = 0;
// 元素的个数
private Integer size = 0;
// 锁对象
private static final Object lock = new Object();
public MyQueue2(Integer size){
super(size);
}
/**
* 当前方法存在的问题:
* 1、在单线程的情况下没有问题
* 2、多线程的情况可能会出现多个线程同时阻塞,然后同时被唤醒。那么添加的元素就有可能超过队列的长度。
* 解决方案:
* 1、唤醒之后重新获取锁,获取成功则添加元素
* @param element
*/
public synchronized void put(String element){
// 如果队列中的元素已经存满,则阻塞
if (size == queue.length){
try {
wait();
put(element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
put1(element);
notify();
}
}
private void put1(String element) {
queue[putIndex++] = element;
++size;
putIndex = putIndex % queue.length;
}
public synchronized String get(){
if (size == 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return get();
}else{
String result = get1();
notify();
return result;
}
}
private String get1() {
String result = queue[getIndex++];
--size;
getIndex = getIndex % queue.length;
return result;
}
}
当前代码还存在的问题:
notify()通知唤醒是随机唤醒的。
十一、interrupt() 和 interruptedException()
interrupt():用来打断正在睡眠的线程,如wait()、sleep()
interruptedException():只有在方法上声明了interruptException 异常,被打断的话就会抛出异常。
十二、轻量级阻塞和重量级阻塞
轻量级阻塞:能够被中断的阻塞称为轻量级阻塞。对应的线程状态为WAITING;
重量级阻塞:synchronized 不能被中断的阻塞称为重量级阻塞,对应的状态为Block;
十三、线程的interrupt、interrupted 、isInterrupted 区别?
总结:
1、interrupt:置线程的中断状态
2、isInterrupt:线程是否中断
3、interrupted:返回线程的上次的中断状态,并清除中断状态
十四、线程如何优雅关闭
14.1、stop、destory方法
这两个方法会强制杀死线程,比如线程运行到了一半被强制杀死?
问题:
这些方法不建议使用,因为强制杀死线程,则线程中所使用的资源,例如文件资源、网络连接等都无法关闭。
因此,一个线程一旦运行中,则应该尽量让他运行完,合理的释放资源,不要强行关闭。
如果是一个不断循环运行的线程,就需要用到线程通信机制,让主线程通知其退出。
14.2、使用守护线程
因为当用户线程执行完之后,守护线程也会结束。
14.3、设置关闭的标志位
通过标志位的方式,停止正在循环运行的线程
public class MyThread extends Thread{
private boolean flag = true;
@Override
public void run(){
while(flag){
// do ...
}
}
public void stop(){
this.flag = false;
}
}
但是当前代码存在一个问题:
如果Mythread 在while 循环里被阻塞了,例如里面调用了Object.wait(),则永远都无法执行到while(false),也就会一直无法退出。此时就可以用到interrupt()或interruptException()。