哪几类
线程安全问题?上下文切换
?线程安全
《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果
不用考虑
这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步
,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果
,那这个对象是线程安全的”。
这句话的意思是:不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理
(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。
如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的。
运行速度降低,代码开发难度大。
2. 什么情况下会出现线程安全问题,怎么避免?
结果错误
:a++多线程下出现消失的请求现象/***
* 描述:第一种:执行结果出错。
* 演示计数不准确(减少),找出具体出错位置。
*
* */
public class MultiThreadError implements Runnable{
static MultiThreadError multiThreadError = new MultiThreadError();
int count = 0;
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("线程共执行 " + multiThreadError.count + "次");
}
}
运行结果
线程共执行 12932次
Process finished with exit code 0
运行结果分析
多次运行程序发现执行次数大不相同,使用这样的程序会使我们的系统很不稳定。
a++相当于是三个步骤:
i=1;//①
i+1;//②
i=2;//③
线程1执行完i+1后没来得及写入就切换到线程2;
线程2同样执行到i+1后尚未写入就切换回线程1;
此时线程1执行第③步,i的值为2,切换到线程2继续执行第三步导致两个线程执行的结果一样。
所以大部分情况下执行次数小于20000.
执行次数小于20000,是哪次发生了冲突,具体思路是:创建一个长度大于20000的boolean数组,每次count++之后,将count为boolean数组下标的值改为true,说明该值被标记,检查count++之后count为boolean数组下标的值为true,说明发生冲突。
public class MultiThreadError implements Runnable{
static MultiThreadError multiThreadError = new MultiThreadError();
private static AtomicInteger errorCount = new AtomicInteger();
private static AtomicInteger realCount = new AtomicInteger();
int count = 0;
final boolean [] marked = new boolean[1000000000];
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
count++;
realCount.incrementAndGet();
if(marked[count]){
System.out.println("已经被标记过,发生错误:" + count);
errorCount.incrementAndGet();
}
marked[count] = true;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("线程表面上共执行" + multiThreadError.count + "次");
System.out.println("冲突次数为" + errorCount.get());
System.out.println("正确执行的次数" + realCount.get());
}
}
运行结果
已经被标记过,发生错误:693
已经被标记过,发生错误:3572
已经被标记过,发生错误:4514
已经被标记过,发生错误:5538
已经被标记过,发生错误:6298
已经被标记过,发生错误:7031
已经被标记过,发生错误:7462
已经被标记过,发生错误:8493
已经被标记过,发生错误:8874
已经被标记过,发生错误:9760
已经被标记过,发生错误:10513
已经被标记过,发生错误:11048
已经被标记过,发生错误:11933
已经被标记过,发生错误:12728
已经被标记过,发生错误:13194
已经被标记过,发生错误:13718
已经被标记过,发生错误:14194
已经被标记过,发生错误:14909
已经被标记过,发生错误:15427
已经被标记过,发生错误:16069
已经被标记过,发生错误:16531
已经被标记过,发生错误:16977
已经被标记过,发生错误:17462
已经被标记过,发生错误:18006
已经被标记过,发生错误:18739
线程表面上共执行 19946次
冲突次数为25
正确执行的次数20000
Process finished with exit code 0
运行结果分析
很明显表面上的运行结果(19946)+冲突次数(25) != 线程正确执行的次数(20000),是哪里出现了问题?
两个线程同时执行if(marked[count])
,当线程1执行的时候marked[count]
为false
,
开始执行marked[count] = true
;但是还没有进行赋值操作,此时切换到线程2,线程2执行marked[count]
的时候同样为false
。所以虽然发生冲突,但是没有被统计到;
使用
synchronized
将赋值的代码保护起来instance
是唯一的,所以多个线程不能同时执行赋值代码块
public class MultiThreadError implements Runnable{
static MultiThreadError multiThreadError = new MultiThreadError();
private static AtomicInteger errorCount = new AtomicInteger();
private static AtomicInteger realCount = new AtomicInteger();
int count = 0;
final boolean [] marked = new boolean[1000000000];
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
count++;
realCount.incrementAndGet();
synchronized (multiThreadError){
if(marked[count]){
System.out.println("已经被标记过,发生错误:" + count);
errorCount.incrementAndGet();
}
marked[count] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("线程共执行 " + multiThreadError.count + "次");
System.out.println("冲突次数为" + errorCount.get());
System.out.println("正确执行的次数" + realCount.get());
}
}
运行结果
已经被标记过,发生错误:252
...
已经被标记过,发生错误:18389
线程共执行 20000次
冲突次数为247
正确执行的次数20000
运行结果分析
表面上的运行结果(20000)+冲突次数(247) != 线程正确执行的次数(20000);
index
为2的时候两个线程发生冲突,开始执行synchronized (multiThreadError)
,此时线程1获取到锁,开始执行同步代码块,将marked[2]
设置为true
,此时线程1继续执行for
循环,index
的值为3,此时切换为线程2,线程2开始执行marked[index]
,本来线程2开始执行marked[index]
的时候index的值为2,但是由于线程1又执行了一次index++
导致index
的值为3,此时marked[3]
为false
,因此少统计了一次冲突。
让两个线程同时开始执行
count++
,引入CyclicBarrier
public class MultiThreadError implements Runnable{
static MultiThreadError multiThreadError = new MultiThreadError();
private static AtomicInteger errorCount = new AtomicInteger();
private static AtomicInteger realCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
int count = 0;
final boolean [] marked = new boolean[1000000000];
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
count++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realCount.incrementAndGet();
synchronized (multiThreadError){
if(marked[count]){
System.out.println("已经被标记过,发生错误:" + count);
errorCount.incrementAndGet();
}
marked[count] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("线程共执行 " + multiThreadError.count + "次");
System.out.println("冲突次数为" + errorCount.get());
System.out.println("正确执行的次数" + realCount.get());
}
}
程序分析
在
count++
前引入cyclicBarrier1
是为了让两个线程同时执行,避免出现冲突没被统计到的情况;CPU切换到线程1开始执行count++
,count
值为1,一直执行到(但是尚未执行)marked[count] = true;
CPU切换到线程2开始执行,此时count
的值为1+count++
,count
值为3,此时CPU又切换到线程1执行marked[count] = true
此时count
值为3。线程1本来是想把marked[2]
置为true
,但是此时是marked[3] = true;
发生了错误,所以需要在count++
后面也引入cyclicBarrier2
。
运行结果
已经被标记过,发生错误:2
已经被标记过,发生错误:4
已经被标记过,发生错误:6
已经被标记过,发生错误:8
已经被标记过,发生错误:10
已经被标记过,发生错误:12
已经被标记过,发生错误:14
......
已经被标记过,发生错误:19964
已经被标记过,发生错误:19966
已经被标记过,发生错误:19968
已经被标记过,发生错误:19970
已经被标记过,发生错误:19972
已经被标记过,发生错误:19974
已经被标记过,发生错误:19976
已经被标记过,发生错误:19978
已经被标记过,发生错误:19980
已经被标记过,发生错误:19982
已经被标记过,发生错误:19984
已经被标记过,发生错误:19986
已经被标记过,发生错误:19988
已经被标记过,发生错误:19990
已经被标记过,发生错误:19992
已经被标记过,发生错误:19994
已经被标记过,发生错误:19996
已经被标记过,发生错误:19998
已经被标记过,发生错误:20000
线程共执行 20000次
冲突次数为10000
正确执行的次数20000
Process finished with exit code 0
运行结果分析
从输出结果看,发生错误的都是偶数值。是由于
synchronized
具有可见性导致的。假设两个线程同时执行到synchronized
且没有发生冲突,线程1的count值为2,线程2的值为3,此时CPU交由线程1执行同步代码块,由于synchronized
具有可见性,线程1可以读取到线程2的count值,此时线程1的count值为3,count等于2被忽略掉,此时marked[3]=true;marked[2]=false;
public class MultiThreadError implements Runnable{
static MultiThreadError multiThreadError = new MultiThreadError();
private static AtomicInteger errorCount = new AtomicInteger();
private static AtomicInteger realCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
int count = 0;
final boolean [] marked = new boolean[1000000000];
@Override
public void run() {
marked[0] = true;
for (int j = 0; j < 10000; j++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
count++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realCount.incrementAndGet();
synchronized (multiThreadError){
if(marked[count] && marked[count-1]){
System.out.println("已经被标记过,发生错误:" + count);
errorCount.incrementAndGet();
}
marked[count] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("线程共执行 " + multiThreadError.count + "次");
System.out.println("冲突次数为" + errorCount.get());
System.out.println("正确执行的次数" + realCount.get());
}
}
运行结果
已经被标记过,发生错误:18537
线程共执行 19999次
冲突次数为1
正确执行的次数20000
Process finished with exit code 0
活跃性问题
:(包括死锁、活锁、饥饿)死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
/**
* 演示死锁
* */
public class MultiThreadError implements Runnable{
static Object lock1 = new Object();
static Object lock2 = new Object();
int flag = 0;
public static void main(String[] args) {
MultiThreadError multiThreadError1 = new MultiThreadError();
MultiThreadError multiThreadError2 = new MultiThreadError();
multiThreadError1.flag=0;
multiThreadError2.flag=1;
new Thread(multiThreadError1).start();
new Thread(multiThreadError2).start();
}
@Override
public void run() {
if(flag == 0){
synchronized (lock1){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("进入lock1");
synchronized (lock2){
System.out.println("lock1 中 lock2获取到锁");
}
}
}else {
synchronized (lock2){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("进入lock2");
synchronized (lock1){
System.out.println("lock2中的lock1获取到锁");
}
}
}
}
}
运行结果
进入lock1
进入lock2
运行输出分析
从程序运行输出分析来看,并没有输出
lock1 中 lock2获取到锁
和lock2中的lock1获取到锁
。multiThreadError1
没有获取到lock2
的锁,所以无法输出lock1 中 lock2获取到锁
。multiThreadError2
没有获取到lock1
的锁,所以无法输出lock2中的lock1获取到锁
。
发布
和初始化
的时候安全问题/**
* 发布逸出
* */
public class MultiThreadsError3 {
private Map<String,String> properties;
public MultiThreadsError3(){
properties = new HashMap<>();
properties.put("dataSource.url", "jdbc:mysql://182.161.114.98:3306/product");
properties.put("dataSource.user", "CIMC_sys");
properties.put("dataSource.password", "%^Gjb@TY*YHF");
}
public Map<String,String> getProperties(){
return properties;
}
public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
Map<String, String> properties = multiThreadsError3.getProperties();
System.out.println(properties.get("dataSource.user"));
properties.remove("dataSource.user");
System.out.println(properties.get("dataSource.user"));
}
}
运行结果
CIMC_sys
null
Process finished with exit code 0
运行结果分析
properties
是私有成员变量,getProperties()
方法返回了该私有变量并改变了这个私有变量。项目中所有的数据库连接配置信息依赖properties
,而properties
的属性发生了改变导致程序发生错误。
public class MultiThreadError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
PointMaker pointMaker = new PointMaker();
pointMaker.start();
TimeUnit.MILLISECONDS.sleep(50);
if(point != null){
System.out.println(point);
}
}
}
class Point{
private int x,y;
public Point(int x, int y) {
this.x = x;
MultiThreadError4.point = this;
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class PointMaker extends Thread{
@Override
public void run() {
new Point(3,4);
}
}
运行结果
Point{x=3, y=0}
Process finished with exit code 0
public class MultiThreadError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
PointMaker pointMaker = new PointMaker();
pointMaker.start();
TimeUnit.MILLISECONDS.sleep(200);
if(point != null){
System.out.println(point);
}
}
}
class Point{
private int x,y;
public Point(int x, int y) {
this.x = x;
MultiThreadError4.point = this;
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class PointMaker extends Thread{
@Override
public void run() {
new Point(3,4);
}
}
运行结果
Point{x=3, y=4}
Process finished with exit code 0
运行结果分析
对比两次运行结果,
sleep
时间不同,程序输出结果不同。由于过早的发布Point
导致我们在不同的时间看到不同的运行结果,这存在很大的隐患。一旦Point
发布它就应该是固定的,在没有任何程序修改它之前它应该是一个完整的、没有瑕疵的对象,可是,如果仅仅以时间的不同就看到不同的运行结果这是一张线程安全问题。
/**
* 观察者模式
* */
public class MultiThreadError5 {
int count;
public MultiThreadError5(MySource mySource){
mySource.registerListener(e -> System.out.println("注册了监听器" + count));
for (int i = 0; i < 1000; i++) {
System.out.print(i);
}
count = 100;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}).start();
MultiThreadError5 multiThreadError5 = new MultiThreadError5(mySource);
}
static class MySource{
private EventListener listener;
void registerListener(EventListener listener){
this.listener = listener;
}
void eventCome(Event e){
if(listener != null){
listener.onEvent(e);
}else {
System.out.println("还没有初始化完毕");
}
}
}
interface EventListener{
void onEvent(Event e);
}
interface Event{
}
}
程序分析:
注册监听器之后在onEvent方法中就已经隐含的暴露了外部类的对象
new EventListener()
是个匿名内部类,而在这个匿名内部类中持有外部类的引用count
;如果构造函数对count
赋值还未完成,即便是像注册监听器这样安全的动作,实际上都有很大的安全隐患。
import java.util.HashMap;
import java.util.Map;
public class MultiThreadError6 {
private Map<String,String> properties;
public MultiThreadError6(){
new Thread(()->{
properties = new HashMap<>();
properties.put("dataSource.url", "jdbc:mysql://182.161.114.98:3306/product");
properties.put("dataSource.user", "CIMC_sys");
properties.put("dataSource.password", "%^Gjb@TY*YHF");
}).start();
}
public Map<String,String> getProperties(){
return properties;
}
public static void main(String[] args) {
MultiThreadError6 multiThreadsError6 = new MultiThreadError6();
Map<String, String> properties = multiThreadsError6.getProperties();
System.out.println(properties.get("dataSource.user"));
properties.remove("dataSource.user");
System.out.println(properties.get("dataSource.user"));
}
}
运行结果
Exception in thread "main" java.lang.NullPointerException
at com.whaleson.background.MultiThreadError6.main(MultiThreadError6.java:26)
Process finished with exit code 1
运行结果分析
在子线程的操作是有延时的。构造函数一旦执行完
start()
就认为对象已经创建好了,而不管子线程有没有执行完毕。
如何让解决逸出
副本
public class MultiThreadsError3 {
private Map<String,String> properties;
public MultiThreadsError3(){
properties = new HashMap<>();
properties.put("dataSource.url", "jdbc:mysql://182.161.114.98:3306/product");
properties.put("dataSource.user", "CIMC_sys");
properties.put("dataSource.password", "%^Gjb@TY*YHF");
}
public Map<String,String> getProperties(){
return properties;
}
public Map<String,String> getPropertiesImproved(){
return new HashMap<>(properties);
}
public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
System.out.println(multiThreadsError3.getPropertiesImproved().get("dataSource.user"));
multiThreadsError3.getPropertiesImproved().remove("dataSource.user");
System.out.println(multiThreadsError3.getPropertiesImproved().get("dataSource.user"));
}
}
运行结果
CIMC_sys
CIMC_sys
Process finished with exit code 0
/**
* 用工厂模式修复初始化问题
* */
public class MultiThreadsError7 {
int count;
private EventListener eventListener;
private MultiThreadsError7(){
eventListener = (e -> System.out.println("\n注册了监听器" + count));
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static MultiThreadsError7 getInstance(MySource mySource){
MultiThreadsError7 safeListener = new MultiThreadsError7();
mySource.registerListener(safeListener.eventListener);
return safeListener;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}).start();
MultiThreadsError7.getInstance(mySource);
}
static class MySource{
private EventListener listener;
void registerListener(EventListener listener){
this.listener = listener;
}
void eventCome(Event e){
if(listener != null){
listener.onEvent(e);
}else {
System.out.println("\n还没有初始化完毕");
}
}
}
interface EventListener{
void onEvent(Event e);
}
interface Event{
}
}
程序分析
一旦运行完
getInstance
函数,就代表listener
已经准备完成,可以正常处理事件。
尤其需要考虑线程安全的情况有哪些?
共享
的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等时序
的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act。捆绑
关系的时候(IP和PORT)双刃剑:多线程会导致的问题
体现
、什么是
性能问题为什么
多线程会带来性能问题调度:上下文切换
什么是
上下文:保存现场上下文切换可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行以下的活动 :(1)挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处;(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复;(3)跳转到程序计数器所指向的位置(即跳转到进程被中断的代码行),以恢复该进程。
缓存
开销:缓存失效何时
会导致密集
的上下文切换频繁地竞争锁,或者由于IO读写等原因导致频繁阻塞。
协作:内存同步
编译器和CPU都会优化我们的程序。指令重排序、JVM优化锁;
为了数据的正确性,同步往往会禁止使用编译器优化、使CPU的缓存失效。