多线程和并发编程之并发编程三大特性,引发问题:多线程环境
多线程和并发编程之并发编程三大特性,引发问题:多线程环境
并发编程的三大特性:原子性、可见性、有序性,只有掌握这三大特性才能说是真正踏入并发编程的门槛,而这三大特性也会将贯穿我们学习并发编程的所有历程!
一、原子性:
1、定义:操作要么全部执行完成,要么全部不执行,不会被线程调度打断。
引发问题:多线程环境下,非原子操作可能会被其他线程中断,导致数据错误。
public class AtomicityDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建100个线程
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
count++;
}
});
threads[i].start();
}
//阻塞主线程防止主线程在子线程还没执行完之前执行
for (Thread thread : threads) {
thread.join();
}
//正常的结果应该是10000,但由于多线程并发执行,最终结果只能是<=10000
System.out.println("最终结果------" + count);
}
}
2、解决方法
①使用原子类 AtomicInteger(除此之外还有AtomicBoolean、AtomicLong等)
/**
* 原子性demo
*/
public class AtomicityDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
//创建100个线程
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
//原子操作这个方法是先添加在获取值
count.incrementAndGet();
}
});
threads[i].start();
}
//阻塞主线程防止主线程在子线程还没执行完之前执行
for (Thread thread : threads) {
thread.join();
}
//正常的结果应该是10000,但由于多线程并发执行,最终结果只能是<=10000
System.out.println("最终结果------" + count.get());
}
}
②使用Sychronized关键字施行同步机制确保每次只有一个线程执行count++的操作
public class AtomicityDemo {
private static Integer count = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
//创建100个线程
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
synchronized (lock) { //同步块 提供互斥锁
count++;
}
}
});
threads[i].start();
}
//阻塞主线程防止主线程在子线程还没执行完之前执行
for (Thread thread : threads) {
thread.join();
}
//正常的结果应该是10000,但由于多线程并发执行,最终结果只能是<=10000
System.out.println("最终结果------" + count);
}
}
③使用显示锁ReentrantLock
public class AtomicityDemo {
private static Integer count = 0;
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//创建100个线程
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
//加锁
lock.lock();
try {
count++;
} finally {
//一定不要忘记释放锁,防止死锁
lock.unlock();
}
}
});
threads[i].start();
}
//阻塞主线程防止主线程在子线程还没执行完之前执行
for (Thread thread : threads) {
thread.join();
}
//正常的结果应该是10000,但由于多线程并发执行,最终结果只能是<=10000
System.out.println("最终结果------" + count);
}
}
二、可见性
1、定义:一个线程修改共享的变量后,其他线程能立即看到修改后的值
举个例子:食堂今天做了红烧鸡腿,贴了一份公告在学校公告牌上,但是你没立即知道这个消息,就可能会导致你就会吃不到鸡腿。
2、问题根源:CPU缓存的一致性,可能导致线程直接读取本地缓存中的旧值。指令重排序,编译器/处理器优化可能导致写入顺序不可见。
public class VisibilityDemo {
private static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag) {
//直到获取共享变量变化为false才退出循环
}
System.out.println("哇!食堂出鸡腿了!");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("食堂出大鸡腿咯");
});
t1.start();
t2.start();
}
}
①、使用volatile关键字
public class VisibilityDemo {
private static volatile boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag) {
//直到获取共享变量变化为false才退出循环
}
System.out.println("哇!食堂出鸡腿了!");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("食堂出大鸡腿咯");
});
t1.start();
t2.start();
}
}
在这里volatile保证变量的可见性,防止指令重排序。 volatile后续会单独出一期重要关键词的作用及其底层详细讲解。
②sychronized同步代码块
public class VisibilityDemo {
private static boolean flag = true;
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag) {
synchronized (lock) {
if(!flag) break;
}
}
System.out.println("哇!食堂出鸡腿了!");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("食堂出大鸡腿咯");
});
t1.start();
t2.start();
}
}
原理:进入代码块前清空工作内存,退出时刷新主内存
③使用原子类AtomicBoolean
public class VisibilityDemo {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag.get()) {
}
System.out.println("哇!食堂出鸡腿了!");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(false);
System.out.println("食堂出大鸡腿咯");
});
t1.start();
t2.start();
}
}
原理:原子类内部是volatile + CAS的操作
三、有序性
1、定义:程序执行的顺序复合代码先后顺序(避免指令重排序带来的意外结果)
举个例子:就好像你点外卖,餐厅都还没有开始做餐呢,就告诉你已经做好了。
2、问题根源:编译器/处理器为提高性能可能对指令重排序(单线程正确,多线程异常)
public class OrderingDemo {
// 外卖状态
private static boolean foodStatus = false;
private static boolean drinkStatus = false;
public static void main(String[] args) throws InterruptedException {
// 顾客线程 - 模拟等外卖
Thread customer= new Thread(() -> {
while(!foodStatus || !drinkStatus) {
}
System.out.println("饭和饮料都到位了,可以开动了!");
});
//餐厅线程1 - 准备主食
Thread cook1 = new Thread(() -> {
System.out.println("准备主食...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
foodStatus = true;
System.out.println("饭准备完毕!");
});
Thread cook2 = new Thread(() -> {
System.out.println("准备饮料...");
drinkStatus = true;
System.out.println("饮料准备完毕!");
});
customer.start();
cook1.start();
cook2.start();
customer.join();
cook1.join();
cook2.join();
}
}
①、使用volatile关键字
public class OrderingDemo {
// 外卖状态
private static volatile boolean foodStatus = false;
private static volatile boolean drinkStatus = false;
public static void main(String[] args) throws InterruptedException {
// 顾客线程 - 模拟等外卖
Thread customer= new Thread(() -> {
while(!foodStatus || !drinkStatus) {
}
System.out.println("饭和饮料都到位了,可以开动了!");
});
//餐厅线程1 - 准备主食
Thread cook1 = new Thread(() -> {
System.out.println("准备主食...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
foodStatus = true;
System.out.println("饭准备完毕!");
});
Thread cook2 = new Thread(() -> {
System.out.println("准备饮料...");
drinkStatus = true;
System.out.println("饮料准备完毕!");
});
customer.start();
cook1.start();
cook2.start();
customer.join();
cook1.join();
cook2.join();
}
}
上面说到volatile除了可见性操作还可以防止指令重排序 至于为什么后期会出一期单独说一下volatile
正常顺序: 分配内存空间 -> 初始化对象 -> 引用指向内存地址
重排序后:分配内存空间 -> 引用指向内存地址(此时 instance != null) ->初始化对象(未完成)
这样就会导致:线程A执行到步骤3(未初始化),线程B判断 instance != null 返回未初始化对象
用户点评