是
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 一、线程安全
- 1.线程不安全的原因
- 2.如何解决线程安全问题
- 原子性角度:
- 加锁
- 怎么进行加锁:
- synchronized关键字:
- 1.synchronized关键字修饰一个普通方法
- 2.sychronized修饰代码块
- 3.synchronized修饰静态成员
- synchronized加锁用法总结
- 几种加锁操作(锁对象)的演例:
- 结论
- 4.synchronized实现可重入
- synchronized作为关键字原因:
一、线程安全
有些代码在多线程环境下执行会出现bug,这样的问题成为线程不安全
1.线程不安全的原因
1.抢占式执行(线程不安全的万恶之源,罪魁祸首)
多个线程的调度执行过程,可以视为是全随机的(没有规律的),在写多线程代码的时候,就需要考虑到说,任何一种调度的情况下,都能够运行出正确结果
2.多个线程修改同一变量
3.修改操作不是原子的
解决线程安全问题,最常见的办法,就是从这里入手,把多个操作通过特殊手段,打包成一个原子操作
Count++命令,本质上是三个cpu指令load add save
cpu执行指令,都是以一个指令为一个单位进行执行,一个指令相当于cpu的最小单位,不能说指令执行一般就把线程调度走了
4.内存可见性问题
jvm的代码优化过程中引入的bug
5.指令重排序
2.如何解决线程安全问题
原子性角度:
加锁
解决上述线程不安全问题,在count++之前先加锁,在count++之后,再解锁
在加锁和解锁之前进行修改,这个时候别的线程想要修改,就不能了(别的线程只能阻塞等待线程的状态,blocked状态)
怎么进行加锁:
synchronized关键字:
在java代码中进行加锁使用 synchronized关键字,
1.synchronized关键字修饰一个普通方法
public synchronized void increase(){
count++;
}
当进入方法的时候,就会加锁,方法执行完毕,自然解锁。
锁具有独占特性,当前锁没人来加,加锁操作就能成功。
如果当前锁已经被人给加上了,加锁操作就会阻塞等待
加锁你要考虑好锁那端代码,锁的代码范围不一样,对代码执行效果由很大影响
正确进行加锁案例
package Threading;
//演示一下线程安全问题:
class Locker{
}
class Counter {
public int count = 0;
synchronized public void increase() {
count++;
}
}
public class demo14 {
private static Counter counter=new Counter();
private static Counter counter2=new Counter();
public static void main(String[] args) throws InterruptedException{
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
counter.increase();
//counter.increase2
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter: "+counter.count);
}
}
不正确加锁姿势案例:
不正确的加锁姿势,不一定线程安全
如果只给一个线程加锁,这个是没啥用的,一个线程加锁,不涉及到锁竞争,也就不会阻塞等待,也就不会并发修改–》串行修改
一个线程加锁,一个线程不加锁
package Threading;
//演示一下线程安全问题:
class Counter {
public int count = 0;
synchronized public void increase() {
count++;
}
public void increase2(){
count++;
}
}
public class demo14 {
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();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter: "+counter.count);
}
}
此时只给一个线程加锁,线程不安全。
线程安全,不是加了锁就一定安全,而是通过加锁,让并发修改同一个变量–》串行修改同一个变量才是安全的
2.sychronized修饰代码块
把要进行加锁的逻辑放到synchornized修饰的代码块之中,也能起到加锁的作用
synchronized(){
count++;
}
如果一个方法中有些代码需要加锁,有些不需要,就可以使用这种修饰代码块的方式进行了
()里是针对哪个对象进行加锁(被用来加锁的对象成为锁对象) ,任意对象都可以作为锁对象
例如:针对当前对象加锁,谁调用synchronized,谁就是this
public void increase(){
synchronized(this){
count++;
}
}
例如:counter内部的另外一个对象locker;
> class Locker{
}
class Counter {
public static Locker locker=new Locker();
public void increase(){
synchronized(locker){
count++;
}
}
}
3.synchronized修饰静态成员
锁对象相当于类对象(不是整个类)
synchronized锁的以上内容都被称为锁对象,针对同一对象加锁就会有锁竞争,出现互斥
synchronized加锁用法总结
咱们写多线程代码的时候,不关心这个锁对象究竟是谁,哪种形态,只要关心,俩个线程是否锁同一个对象,锁相同对象就有竞争,锁不同就没竞争
几种加锁操作(锁对象)的演例:
结论
无论锁对象是啥样的形态,类型,核心原则都是,俩个线程争一个锁对象,就有竞争,不同锁对象,就没竞争。
有锁竞争的目的,是为了保证线程安全
4.synchronized实现可重入
引入:一个线程,连续针对一把锁,加锁俩次,就可能造成死锁
演示:
形如上面代码,一个线程针对一把锁,连续加锁俩次, 第一次加锁,能够加锁成功 第二次加锁,就会失败(锁已经被占用)
就会在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二次加锁才能成功
第一把锁解锁,则要求执行完synchronized代码块,也就是要求第二把锁能加锁成功
演示2:
针对上述情况,不会产生死锁的话,这样的锁被叫做“可重入锁
针对上述情况,会产生死锁,这个锁叫做不可重入锁
而sychroized是可重入的:
上述代码:
实际上:只要让锁里面记录好,是哪个线程持有的这把锁
t线程针对this来加锁,this这个锁里面就记录了是t线程持有了他,
锁一看,还是t线程,就直接通过了,不会阻塞等待
可重入锁的实现要点:
引入一个计数器,每次加锁,计数器++,
每次解锁计数器–
如果计数器为0,此时加锁操作才能真加锁
同样计数器为0,此时的解锁操作才真解锁
1.让锁里持有的线程对象,记录是谁加了锁
2.维护一个计数器,用来衡量是啥时候真枷锁,啥时候是真解锁,啥时候是指针放行
synchronized作为关键字原因:
加锁代码中出现异常,是不会死锁的
无论如何解锁代码都能执行到