提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


文章目录

  • 一、线程安全
  • 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修饰静态成员

锁对象相当于类对象(不是整个类)

多线程安全的容器Java 多线程实现线程安全_jvm

synchronized锁的以上内容都被称为锁对象,针对同一对象加锁就会有锁竞争,出现互斥


synchronized加锁用法总结

咱们写多线程代码的时候,不关心这个锁对象究竟是谁,哪种形态,只要关心,俩个线程是否锁同一个对象,锁相同对象就有竞争,锁不同就没竞争

几种加锁操作(锁对象)的演例:

多线程安全的容器Java 多线程实现线程安全_加锁_02


多线程安全的容器Java 多线程实现线程安全_开发语言_03

多线程安全的容器Java 多线程实现线程安全_java_04


多线程安全的容器Java 多线程实现线程安全_多线程安全的容器Java_05


多线程安全的容器Java 多线程实现线程安全_加锁_06


多线程安全的容器Java 多线程实现线程安全_开发语言_07


多线程安全的容器Java 多线程实现线程安全_java_08

结论

无论锁对象是啥样的形态,类型,核心原则都是,俩个线程争一个锁对象,就有竞争,不同锁对象,就没竞争。
有锁竞争的目的,是为了保证线程安全

4.synchronized实现可重入

引入:一个线程,连续针对一把锁,加锁俩次,就可能造成死锁

演示:

多线程安全的容器Java 多线程实现线程安全_开发语言_09

形如上面代码,一个线程针对一把锁,连续加锁俩次, 第一次加锁,能够加锁成功 第二次加锁,就会失败(锁已经被占用)
就会在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二次加锁才能成功
第一把锁解锁,则要求执行完synchronized代码块,也就是要求第二把锁能加锁成功

演示2:

多线程安全的容器Java 多线程实现线程安全_jvm_10


针对上述情况,不会产生死锁的话,这样的锁被叫做“可重入锁

针对上述情况,会产生死锁,这个锁叫做不可重入锁

而sychroized是可重入的:

上述代码:

多线程安全的容器Java 多线程实现线程安全_加锁_11


实际上:只要让锁里面记录好,是哪个线程持有的这把锁

t线程针对this来加锁,this这个锁里面就记录了是t线程持有了他,

锁一看,还是t线程,就直接通过了,不会阻塞等待

可重入锁的实现要点:
引入一个计数器,每次加锁,计数器++,
每次解锁计数器–
如果计数器为0,此时加锁操作才能真加锁
同样计数器为0,此时的解锁操作才真解锁

1.让锁里持有的线程对象,记录是谁加了锁
2.维护一个计数器,用来衡量是啥时候真枷锁,啥时候是真解锁,啥时候是指针放行

synchronized作为关键字原因:

加锁代码中出现异常,是不会死锁的
无论如何解锁代码都能执行到