多线程安全的解决
出现问题
为什么要有多线程,为了进行并发编程,更好利用多核CPU。 在多线程环境调度线程时,会经常出现bug,分析bug出现的原因:线程中断分析,进程调度线程是随机的。所以会存在顺序不一致的问题,线程执行存在两种调度方式“抢占式”和“非抢占式”。字面意思理解,两个线程在同一进程中,线程间的调度方式如果是“抢占性”一般都会带有优先级,优先级高的会将正在调度的线程“挤”下,进程选择优先级更高的线程进行调度,原来被调度的线程进入等待(阻塞)。
抢占式:
“非强占式”就相当于排队打饭,不管你优先级多高,都需要等待当前调度的线程执行完毕,才可以调用(使用资源)。
原子操作解决方案
我们知道,使用原子操作可以使得结果具有唯一性!那么解决多线程不安全问题是否也可以使用原子操作呢?答案当然是可以的,因为随机调度引发的问题,这里使用原子操作,将每个线程进行加锁🔒,将其行为变成不可拆分的最小单位,保障多线程安全。
1.使用synchronized
1.在public上添加synchronized关键字
class Add{
public int count;
//直接在方法上添加
synchronized public void add(){
count++;
}
public int getCount() {
return count;
}
}
2.在代码块上添加synchronized关键字
class Add{
public int count;
public void add(){
//在代码块上使用synchronized
//这里的this可以替换成任意一个object数据类型,不过该数据在此处无太大意义,只是一个标识作用
synchronized (this){
count++;
}
}
public int getCount() {
return count;
}
}
//同一main函数,代码如下
public class ThreadDome {
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000 ; i++) {
add.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000 ; i++) {
add.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(add.getCount());
}
}
这两种方法的结果一样,需要注意的是,对代码的执行效率不同。因为一个是对方法加锁,这就表明了当调用类中的这个方法时就开启加锁操作,需要等方法中所有代码执行完毕才会执行解锁操作。而对代码块进行加锁,只是单纯对代码加锁,只要代码块执行完毕就执行解锁操作!
入则锁出则解
2.使用lock和unlock
lock类需要创建一个锁对象,对不安全的代码或者方法进行加锁操作。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Count{
public int count;
synchronized public void add(){
count++;
}
public int getCount() {
return count;
}
}
public class ThreadLock {
public static void main(String[] args) throws InterruptedException {
Count count = new Count();
//定义锁
Lock l =new ReentrantLock();
Thread t1 =new Thread(()->{
for (int i = 0; i < 50000 ; i++) {
l.lock();//加锁
count.add();
l.unlock();//解锁
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i <50000 ; i++) {
l.lock();
count.add();
l.unlock();
}
});
//调用线程
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.getCount());
}
}
结果与synchronized一样,这个对象的使用与synchronized不同,Lock类方法需要直接进行解锁,如果你忘了解锁,系统不会帮助你解锁,可能会产生死锁问题,也就是bug。
3.使用volatile
说到volatile就必须提到==内存可见性==:在内存环境中,编译器对代码进行优化,可以保证结果不变,通过语句变换和一些其他操作,然后提高代码的运行效率。
在单线程中,编译器对程序结果不变的判断十分准确。在多线程环境下编译器对代码进行优化后,可能会产生误判,从而引发bug。
和原子性无关
public class ThreadVolier {
//volatile修饰的变量特性:
//每次读取该变量就会回到内存中读取,而不是简单读取
//可以保证变量修改时被察觉
volatile public static int flag;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (flag == 0){
}
System.out.println("循环结束!t1结束");
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数");
flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}
volatile的适用场景:一个线程读,一个线程写。
volatile的另一个效果:禁止指令重排序
**指令重排序:**调整代码执行顺序,让程序更加高效,保证逻辑不变即可,而且不改变程序运行结果。
锁竞争
多个线程尝试对同一个锁🔒对象进行加锁,这种情况就会产生锁竞争!如果多个线程对不同对象进行加锁操作,就不会有锁竞争!如果一个线程从锁中出来!其余线程之间如果发生锁竞争,必有一方会发生阻塞,不利于提高进程效率,但是提高了线程安全。
提高进程效率的方法,找出并发线程中的临界区(并发代码),在保证线程安全的情况下,合理分配线程资源,提高线程效率。
总结:线程不安全原因
1.抢占式调度,随机调度
2.多个线程修改同一个变量
3.修改变量操作不是 原子操作
4.内存可见性
5.指令重排序
要创建一个安全的线程应该检查以上五项,解决不安全问题的方法也很多,以上几种就可以解决相应的安全问题。
线程中的临界区(并发代码),在保证线程安全的情况下,合理分配线程资源,提高线程效率。
总结:线程不安全原因
1.抢占式调度,随机调度
2.多个线程修改同一个变量
3.修改变量操作不是 原子操作
4.内存可见性
5.指令重排序
要创建一个安全的线程应该检查以上五项,解决不安全问题的方法也很多,以上几种就可以解决相应的安全问题。