多线程编程中,最让人头疼的问题莫过于线程安全,如果对存在线程安全问题的代码不加以处理,可能会带来严重的后果,例如用两个线程对同一个变量进行增加操作
class Counter {//这个 变量 是两个线程要去自增的变量public int count;public void increase() {count++;}
}public class Demo15 {private static Counter counter = new Counter();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0; i < 50000; ++i) {counter.increase();}});t1.start();Thread t2 = new Thread(()->{for(int i = 0; i < 50000; ++i) {counter.increase();}});t2.start();//等待t1和t2执行完,再打印count的结果t1.join();t2.join();//在main中打印一下两个线程自增完成后,得到的count结果System.out.println(counter.count);//如果不加锁,极端情况//所有的操作都是串行的,最终结果就是10w(可能出现,极小概率事件)//所有操作都是交错的(并行),最终结果就是5w(可能出现,极小概率事件)}
}
预期结果为10w,但实际结果却为67627,这就是线程安全问题导致的。
产生线程不安全的原因有很多:
上述介绍的这5种情况,都是产生线程不安全的原因
对此我们应该对increase()方法加锁或者count这个变量加锁,在C++中需要创建mutex变量,再加锁,然后再解锁,个人觉得有点麻烦(C++加锁的方式有很多)。在Java中,加锁的方式也有很多,最简单,最常用的方式就是在increase()方法最前面加上synchronized关键字,将整个方法锁住,这样就解决了线程安全问题
class Counter {//这个 变量 是两个线程要去自增的变量public int count;synchronized public void increase() {count++;}
}
synchronized 会自动加锁,本质是修改了Object对象中的"对象头"里面的一个标记
synchronized是个可重入锁,可重入锁内部会记录当前的锁被哪个线程占用,同时也会记录一个"加锁次数(引用计数)"。当锁的计数减到0后,就解锁,可重入锁的意义就是降低了使用成本,提高了开发效率,但是也带来了更大的开销(维护锁属于哪个线程,并增加了计数,降低了运行效率)
synchronized 的使用方法
1.直接修饰普通的方法
使用synchronized的时候,本质就是针对某个"对象"进行加锁,此时锁对象就是this
2.直接修饰代码块
需要显示指定哪个对象需要加锁(Java中的任何对象都可以作为锁对象)
3.修饰静态方法(更严谨的叫法应该是"类方法")
相当于针对当前类的类对象加锁
synchronized
synchronized几个典型的优化手段(只考虑JDK1.8)
1.锁膨胀/所升级
体现synchronized能够"自适应"这样的能力
代码还能执行到synchronized部分,此时处于无锁状态
当首个线程执行到了synchronized部分,此时就会进入偏向锁状态,偏向锁只是做了一个标记,并没有真的加锁,这样带来的好处就是后续如果没有线程竞争,就避免了加锁,解锁带来的开销
如果此时又有其他线程执行到了synchronized部分,产生锁竞争,此时进入**轻量级锁(自旋锁)状态
如果竞争进一步加剧,就会进入重量级锁(互斥锁)**状态
无锁——>偏向锁——>轻量级锁(自旋锁)——>重量级锁(互斥锁)
2.锁粗化/细化
此处的粗细是指"锁的粒度"
"锁的粒度"是指加锁的代码涉及到的范围
加锁的代码范围越大,认为锁的粒度越粗
加锁的代码范围越小,认为锁的粒度越细
到底锁的粒度是粗好,还是细好?各有各的好
如果锁的粒度比较细,多个线程之间的并发性就更高
如果锁的粒度比较粗,加锁解锁的开销就更小
Java编译器就会有一个优化,会自动判定(一般来说编译器优化后,效率会变高,但也有意外情况)
如果两次加锁之间的间隔较大(中间隔的代码多),会细化(一般不会进行这种优化)
如果两次加锁之间的间隔较小(中间隔的代码少),会粗化(很可能触发这个优化)
锁消除
有些代码,明明不用加锁,结果你给上锁了,编译器就会发现这个加锁操作好像没什么必要,就直接把锁给去掉了
例如给单线程进行加锁,这个时候编译器就会进行锁消除,
单线程中使用到了StringBuffer,Vector等,它们都是在标准库中进行的加锁操作,实际使用的时候可能存在锁消除