前言
接上一篇内容.......
5.指令重排序~(也是和编译器优化相关)
编译器会自动调整执行指令的顺序,以达到提高效率的效果
调整的前提是,保证指令的最终效果是不变的(如果当前的逻辑只是在单线程下运行.编译器判定顺序是否影响结果,就很容易.如果当前的逻辑可能在多线程下运行.编译器判定顺序是否影响结果.就可能出错)
(编译器优化,是一个非常智能的东西,哪怕程序猿代码写的很挫.但是编译器咔咔一顿优化,代码效率还是会挺高的)
最普适的办法,就是通过"原子性"这样的切入点来解决问题
把一些不是原子性的操作变成原子性的操作
synchronized 英文原义"同步"
这里理解为"互斥"更合适
这个单词一定要会拼写,会念
这里我们继续来看之前用到的一个案例:两个线程分别针对count自增5w次
public class TestDome7 { static class Counter{ public int count = 0; synchronized public void increase(){ count++; } } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t = new Thread(){ @Override public void run() { for (int i = 0; i < 50000; i++) { counter.increase(); } } }; Thread t2 = new Thread(){ @Override public void run() { for (int i = 0; i < 50000; i++) { counter.increase(); } } }; t.start(); t2.start(); t.join(); t2.join(); System.out.println(counter.count); } }
如果两个线程同时并发的方式调用这个synchronized修饰的方法
此时一个线程会先执行这个方法,另外一个线程会等待,等到第一个线程方法执行完了之后,第二个线程才会继续执行
就相当于"加锁"和"解锁"
进入synchronized修饰的方法,就相当于加锁
出了synchronized修饰的方法,就相当于解锁
如果当前是已经加锁的状态,其他的线程就无法执行这里的逻辑,就只能阻塞等待
synchronized的功能本质上就是把"并发"变成"串行"
适当的牺牲一下速度,但是换来的是结果更加准确
多次打印结果:
synchronized除了可以修饰方法之外,还可以修饰一个"代码块"
synchronized如果是修饰代码块的时候需要显示在 () 中指定一个要加锁的对象
如果是synchronized直接修饰的非静态方法,相当于加锁的对象是this
完整代码:
public class TestDome7 { static class Counter{ public int count = 0; public void increase(){ synchronized (this){ count++; } } } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t = new Thread(){ @Override public void run() { for (int i = 0; i < 50000; i++) { counter.increase(); } } }; Thread t2 = new Thread(){ @Override public void run() { for (int i = 0; i < 50000; i++) { counter.increase(); } } }; t.start(); t2.start(); t.join(); t2.join(); System.out.println(counter.count); } }
Java中,一个对象的内存布局大概是这样的:
Java中任意的对象,都可以作为"锁对象"
synchronized不光能够起到互斥的效果,还能够刷新内存~(解决内存可见性问题)
例如:
一个代码中进行循环++
每次自增都是
LOAD
ADD
SAVE
这三个操作
但是编译器会优化这里的效率,把中间的一些LOAD,SAVE操作省略掉
变成LOAD,ADD,ADD,ADD,ADD,SAVE
加上synchronized之后,就会禁止上面的优化,保证每次进行操作的时候,都会把数据真的从内存读,也真的写回内存中.
也是让程序跑的慢一点,但是能够算的准
大家要心中有数,一旦代码中使用了synchronized,此时咱们的程序很可能和"高性能"无缘了
synchronized允许一个线程针对一把锁,咔咔连续加锁两次~
进入increase方法,就加了一次锁
再进入代码块,又加了一次锁
这种操作对于synchronized来说是没问题
synchronized对这里进行了特殊处理
(如果是其他语言的锁操作,这里可能就会造成死锁)
第一次加锁,加锁成功
第二次再尝试针对这个线程加锁的时候,此时对象头的锁标记已经是true.按照咱们之前的理解,线程就要阻塞等待,等待这个锁标记被改成false,然后才重新竞争这个锁......
但是仔细一想,这个锁啥时候能释放?
但是synchronized为了防止程序猿犯蠢.(Java是属于对程序猿的智商评估的比较准的编程语言,而其他的语言,尤其是C++,高估了程序猿的智商)
于是就做了一个特殊处理:synchronized实现了可重入
synchronized内部记录了当前这个锁是哪个线程持有的
可重入:同一个线程连续两次针对同一个锁进行加锁操作,不会死锁
synchronized修饰普通方法的话,相当于是针对this进行加锁(这个时候如果两个线程并发的调用这个方法,此时是否会触发锁竞争就看实际的锁对象是否是同一个了)
synchronized修饰静态方法的话,相当于针对类对象进行加锁(由于类对象是单例的,两个线程并发调用该方法,一定会触发所竞争)
这就是一个类对象
反射的时候介绍过这个:
反射也是面向对象中的一个基本特性(和封装继承多态....是并列关系)
反射也叫"自省"
一个对象能够认清自己(程序运行时)
(这个对象里包含哪些属性,每个属性叫啥名字,是啥类型,public/private......
包含哪些方法,每个方法叫啥名字,参数列表是啥,public/private
这些信息来自于 .class文件(.Java被编译生成的二进制字节码)
会在JVM运行的时候加载到内存中~
就通过"类对象"来描述这个具体的.class文件的内容
类名.class就得到了这个类对象
特点:每个类的类对象都是单例的
这里的集合类,大部分是线程不安全的(不能在多线程环境下去并发修改同一个对象)
典型的:ArrayList/LinkedList/HaspMap/HashSet....都是线程不安全的
还有一些是线程安全的
Vector(在最新版本的Java16里好像已经删了,很早就已经被标记成"弃用"状态了)
这个是JDK早期内置的集合类,这里的设计不是特别科学
后续的编程中,一般不建议使用Vector,而是使用ArrayList来代替
Vector也是一个顺序表(动态数组),能自动扩容啥的.....
Vector中使用synchronized来保证了线程安全~
Vector中给这里的很多方法都加上了synchronized来修饰~
这种操作并不好!
大多数情况下并不需要在多线程中使用Vector,而如果我们加了太多的synchronized就会对单线程环境下的操作的效率造成负面影响
Stack(栈)
继承自Vector,躺枪了
和Vector类似,还有一个HashTable是一个哈希表结果.也是类似的做法,也是把很多方法都加了synchronized.结论也是不建议使用
ConcurretHashMap也是一个线程安全的哈希表
相比于HashTable来说,就设计的非常好了
StringBuffer也是
String认为也是线程安全的.但是又没有加锁
String是不可变对象~~
不可能存在两个线程并发的修改同一个String.
volatile 这个词也是需要大家会拼写,会读~计算机中一般理解成"可变的,容易改变的"
volatile的功能是保证内存可见性,但是不能保证原子性.
volatile使用案例:
public class TestDome8 { static class Counter{ public int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t = new Thread(){ @Override public void run() { while (counter.flag == 0){ //假设要执行一些操作 } System.out.println("循环结束"); } }; t.start(); Thread t1 = new Thread(){ @Override public void run() { //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值 Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数:"); counter.flag = scanner.nextInt(); } }; t1.start(); } }
为什么输入一个非0的整数后程序并没有结束
看分析:
加上volatile之后
public class TestDome8 { static class Counter{ //一旦给这个flag加上volatile之后,此时后续的针对flag的读写操作,就都能保证一定是操作内存了 volatile public int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t = new Thread(){ @Override public void run() { while (counter.flag == 0){ //假设要执行一些操作 } System.out.println("循环结束"); } }; t.start(); Thread t1 = new Thread(){ @Override public void run() { //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值 Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数:"); counter.flag = scanner.nextInt(); } }; t1.start(); } }
volatile的用法比较单一,只能修饰一个具体的属性
此时代码中针对这个属性的读写操作就一定会涉及到内存操作了
volatile不能保证原子性
1
2
使用synchronized也能读写内存
public class TestDome8 { static class Counter{ //一旦给这个flag加上volatile之后,此时后续的针对flag的读写1操作,就都能保证一定是操作内存了 public int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t = new Thread(){ @Override public void run() { //加上synchronized 之后,此时的针对flag的操作,也会读写内存 while (true){ synchronized (counter){ if(counter.flag != 0){ break; } } } System.out.println("循环结束"); } }; t.start(); Thread t1 = new Thread(){ @Override public void run() { //让用户输入一个整数,用这个用户输入的值来替换 counter.flag的值 Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数:"); counter.flag = scanner.nextInt(); } }; t1.start(); } }
打印结果:
volatile是和编译器优化密切相关的东西
编译器优化是一个相当复杂的事情
啥时候优化,啥时候不优化,优化的时候优化到啥程度.....
咱们把握不住
编译器优化这都是世界上最NB的程序猿经历了几十年的积累写出的程序....
有些时候,哪怕代码只是动了一小点,优化的方式可能就完全不同~~
一般来说,如果某个变量,在一个线程中读,一个线程中写,这个时候大概率需要使用volatile
volatile这里涉及到一个重要的知识点,JMM(java memory model)内存模型
未完待续........