这篇文章思维导图如下 :

什么是线程安全 ?? 什么又是线程不安全呢??
如果是在单线程环境下运行的结果与我们多个线程运行的结果一样,这就是线程安全的,但是由于多个线程并发执行就会导致与我们预期结果(单线程顺序执行的结果)不一样就会导致bug,这就是线程不安全.
- static class Counter{
- public int count = 0;
- public void increase() {
- count++;
- }
- }
-
- public static void main(String[] args) {
- Counter counter = new Counter();
- 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();
-
- try {
- t1.join();
- t2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter.count);
- }
输出结果如下 :

答案与我们预期的答案不一样,如果是在单线程的环境下,两个线程顺序执行答案就是10W,这里因为由于两个线程是并发执行的就导致bug-->让我们的线程不安全.
就如上述情况,多个线程并发执行,由于我们操作系统是随机调度的/抢占式执行,CPU以不确定的方式或者是随机的时间执行,就会导致线程不安全
对于上述案例 count++就分为三个指令
1.从内存中读取数据到CPU 2. 进行数据更新(在CPU进行运算) 3.将数据写回内存
这就导致比如我们完成1+1操作,由于多个线程随机调度这三个指令,就会出现有的没有累加成功,直接写回内存,就导致与我们预期结果不一致-->线程安全问题
如上述情景,两个线程共同修改同一个变量count,就会导致线程安全.
如果是其他情况如 : 多个线程修改不同变量 ,多个线程读同一变量,一个线程修改一个变量就不会出现线程安全问题.
什么是原子性 ???
原子性:原子性指的是一个操作或者多个操作,保证操作都执行或者都不执行,这就是原子性;
比如 = 赋值一次性操作一次就执行这就是原子性的.
如count++,这就是非原子性的,因为++包括三个指令 1.读取内存到CPU2.在CPU进行运算(修改操作) 3.将数据写回内存,包含3个指令,但是在多个线程之间并发执行就可能导致三个指令并不都执行(比如只执行两个操作1.读取内存2.进行++运算,但是没有写回内存,就导致其他线程在进行运算时拿的是旧的值进行计算,导致最终结果错误);
非原子性操作 : 是指一个操作被分成多个指令,由于操作系统的调度,导致有些指令并没有被执行,最终影响结果
什么是可见性???
可见性 : 可见性是指一个线程读一个线程写,当一个线程修改同一份共享变量时,另一个线程读取到的是最新值.
而如果一个线程修改同一个共享变量,而另外一个线程读取时感知不到这个变量的值已经被修改就会出现问题 这就是内存可见性问题.
这其实是编译器进行优化导致的,我们知道读取寄存器的速度要比读取CPU速度快的多,这就会导致由于编译器很频繁的读取就会导致读到的结果都是一样的,编译器就会对其进行一些优化操作,将读取内存全部优化成读取寄存器,这样就会导致一个线程修改,而另外一个线程在读取时感知不到,读取不到最新值.
总结:
内存可见性是编译器进行优化导致一个线程修改共享变量,而另外一个线程读取时读取不到最新值,导致结果错误
这也是一个重要的特性 有序性:
有序性就是指代码的执行顺序,按照先后顺序来执行
比如一个操作实例化对象 Test t = new Test();这个实例化操作其实包含3个方面
1.创建内存空间 2.往这个空间构造对象 3.将对象的引用赋值给该对象
如果是进行2,3这样的顺序,当一个线程读取这个对象为非null时这个对象是一个有效的对象
如果是进行3,2这样的顺序,当一个线程读取这个对象为非null时,由于这个对象并没有构造出对象,所以这个对象是一个无效的对象.
指令重排序这也是一个编译器进行优化导致的问题,对代码的顺序进行修改.
解决上述线程安全问题 可以使用两个关键字synchronized关键字(这个是给对象进行加锁),volatile关键字
synchronized可以理解为互斥当在方法或者代码块上加上synchronized关键字相当于给对象加锁,当一个线程执行时也就是使用这一份资源时相当于加锁,而其他线程要想使用这一份资源就要阻塞等待.
就如刚才那个示例加上synchronized关键字就会先让一个线程先执行,而另外一个线程阻塞等待,等第一个线程执行完之后(解锁),另外一个线程在执行.synchronized关键字保证原子性
- static class Counter{
- public int count = 0;
- public synchronized void increase() {
- count++;
- }
- }
- public static void main(String[] args) {
- Counter counter = new Counter();
- 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();
-
- try {
- t1.join();
- t2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter.count);
- }

对普通方法加锁相当于对当前对象this加锁,不同的对象对应着不同的锁,

对静态方法加锁相当于给类对象加锁-->所有对象调用静态方法都是互斥的
对普通方法加锁相当于对this对象加锁,对静态方法加锁相当于对类对象加锁.
对于普通方法,不同的对象对应着不同的锁,多个线程针对同一对象加锁才会发生锁竞争,多个线程对不同对象加上不会产生锁竞争
对于静态方法,由于类对象时全局的,只有一份,所以所有对象调用静态方法都会产生锁竞争(也就是一个线程使用资源,其他的线程需要阻塞等待)
- public void method3(){
- synchronized(this){
- //对当前对象加锁
- }
- }
- public void method4(){
- synchronized(TestDemo.class){
- //对类对象加锁
- }
- }
示例:
- public static Object locker = new Object();
- public static void main(String[] args) {
- //对相同对象加锁会产生锁竞争
- Thread t1 = new Thread(()->{
- synchronized(locker){
- System.out.println("t1 --> start");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("t1 --> end");
- }
- });
- t1.start();
- Thread t2 = new Thread(()->{
- synchronized(locker){
- System.out.println("t2 --> start");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("t2 --> end");
- }
- });
- t2.start();
-
- }

这就是当对一个对象加锁会产生所竞争,t1线程执行时,t2线程会阻塞等待,当t1执行完之后(释放锁),t2在执行.
- public static Object locker1 = new Object();
- public static Object locker2 = new Object();
- public static void main(String[] args) {
- //对不同对象加锁不会产生锁竞争
- Thread t1 = new Thread(()->{
- synchronized(locker1){
- System.out.println("t1 --> start");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("t1 --> end");
- }
- });
- t1.start();
- Thread t2 = new Thread(()->{
- synchronized(locker2){
- System.out.println("t2 --> start");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("t2 --> end");
- }
- });
- t2.start();
-
- }

- static class Counter{
- public int flag =0;
- }
-
- public static void main(String[] args) {
- Counter counter = new Counter();
- Thread t1 = new Thread(()->{
- System.out.println("t1 开始");
- while(counter.flag==0){
-
- }
- System.out.println("t1 结束");
- });
- t1.start();
- Thread t2 = new Thread(()->{
- Scanner scan = new Scanner(System.in);
- System.out.println("请输入一个值开始t2线程进行修改");
- scan.nextInt();
- counter.flag = 1;
- System.out.println("t2 结束 ");
- });
- t2.start();
- }
这就是典型的场景,一个线程在读,一个线程在写,由于要频繁读取,编译器就将读取内存操作优化为读取寄存器操作,导致一个线程在修改时,另一个线程在读取时没有感知到被修改,所以一直在读取旧值导致死循环.
但是当我们加上volatile关键字就可以很好地解决内存可见性问题

使用volatile关键字也能够禁止指令重排序,但是volatile不能解决原子性
synchronized关键字和volatile关键字的区别