临界资源
临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;
软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
竞态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步操作就可以避免竞态条件,如使用synchronized或者加锁机制。
线程安全
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。
线程安全出现问题的例子:
当多个线程同时操作一个变量时,可能会造成变量的脏读脏写(类似于mysql)
package com.company; public class Main { public static void main(String\[\] args) { Test test = new Test(); //创建20个线程 for (int i =1;i<=20;i++){ new Thread(()->{ for (int j =1;j<=1000;j++){ test.incA(); } },"测试"+i).start(); } while(Thread.activeCount() > 2){ //main, gc,说明还存在其他线程在执行 Thread.yield();//线程礼让 } System.out.println(Thread.currentThread().getName() + "\\t int类型的number最终值:" + test.a()); } } class Test{ public int a; public int a(){ return a; } public void incA(){ a++; } }
执行结果:
/Users/tioncico/Library/Java/JavaVirtualMachines/openjdk-14.0.1/Contents/Home/bin/java -javaagent:/Applications/IDE/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56786:/Applications/IDE/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/tioncico/IdeaProjects/test/out/production/untitled104 com.company.Main main int类型的number最终值:19893
可看到 本来是20个线程*1000次递增,但是实际值却小于20000,这个情况就属于非线程安全的一种
如何实现线程安全?
volatile关键字
通过volatile修饰属性,此属性将直接修改内存,不经过线程内部缓存和重排序
volatile关键字可以保证属性操作的可见性和有序性,但是不能保证原子性
可见性
指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改一个共享变量时,另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。
有序性
有序性是指在单线程环境中, 程序是按序依次执行的.
而在多线程环境中, 程序的执行可能因为指令重排而出现乱序
指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
原子性
子性是指一个操作是不可中断的. 即使是在多个线程一起执行的时候,
一个操作一旦开始,就不会被其它线程干扰.
volatile可见性案例:
package com.company; import java.util.concurrent.TimeUnit; public class Main { public static void main(String\[\] args) { Test test = new Test(); //创建1个线程 new Thread(()->{ System.out.println(Thread.currentThread().getName() + "\\t 正在执行"); try { TimeUnit.SECONDS.sleep(3);//留出时间使得主线程代码执行 test.setA(100); System.out.println(Thread.currentThread().getName() + "\\t int类型的值为:" + test.a()); } catch (InterruptedException e) { e.printStackTrace(); } },"演示").start(); while(test.a==0){//如果一直为0,则一直循环 } System.out.println(Thread.currentThread().getName() + "\\t int类型的number值为:" + test.a()); } } class Test{ public int a=0; public int a(){ return a; } public void incA(){ a++; } public void setA(int a){ this.a = a; } }
由于没有volatile关键字,主线程main一直获取到的值是0,所以循环不会中断
增加volatile关键字:
class Test{ public volatile int a=0; public int a(){ return a; } public void incA(){ a++; } public void setA(int a){ this.a = a; } }
volatile无法解决原子性问题:
主要原因为:
线程1拿到了a=0的值,并且0++变成了1
但是其实在同一时刻,线程1-20都拿到了a=0的值,都++变成了1,就会导致线程写入覆盖,最后就会导致值小于20000;
AtomicIntegrer原子类
虽然volatile无法实现原子性,但是可以通过java.util.concurrent.AtomicInteger 类
保存数据实现原子性操作:
class Test{ public AtomicInteger a = new AtomicInteger(); public int a(){ return a.get(); } public void incA(){ a.getAndIncrement(); } }
结果:
/Users/tioncico/Library/Java/JavaVirtualMachines/openjdk-14.0.1/Contents/Home/bin/java -javaagent:/Applications/IDE/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62725:/Applications/IDE/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/tioncico/IdeaProjects/test/out/production/untitled104 com.company.Main main int类型的number最终值:20000
synchronized关键字
synchronized关键字可对某个方法进行加锁,使得该方法同一时刻只能一个线程访问:
class Test { public int a; public int a() { return a; } public synchronized void incA() { a++; } }
运行结果:
本文参考:https://blog.csdn.net/weixin_41947378/article/details/112245369