关于java线程不安全问题的简述

什么是线程不安全及其具体解析
  1. 当我们执行一个Java.exe进程的时候,首先会初始化JVM参数,然后创建JVM虚拟机,再启动后台线程,最后执行就是执行我们代码行的main方法。
  2. 而在JVM运行的时候会将他管理的内存分为若干个区域,每一个线程都有其独有的程序计数器,java虚拟机栈和本地方法栈,以及线程共享的Java堆和方法区(包含运行时常量池)
  3. 当我们定义一个静态变量COUNT,它在被编译的时候创建于方法区。当我们创建多个线程去给COUNT执行++的操作的时候,我们最终所得到的COUNT值是不符合我们所期望的。
private static int COUNT = 0;
    // 有一个变量COUNT=0;同时启动10个线程,每个线程循环1000次
    // 每次循环COUNT++
    // 每一个线程执行完毕之后打印COUNT
    public static void main(String[] args) throws InterruptedException {
        // 尽量同时启动
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                            COUNT++;
                    }
                }
            });
        }

        for (Thread t:
             threads) {
            t.start();
        }

        for (Thread t:
             threads) {
            t.join();
        }
        System.out.println(COUNT);
    }

当我们执行完这个代码的时候,它的结果往往小于10000.

这就是所谓的线程不安全

  1. 当我们定义一个静态变量,它被创建于方法区,而每一个线程想要去修改静态变量COUNT的时候,他必须从方法区去获取到COUNT的值,然后在自己的线程私有区域去进行修改也就是++操作,修改完成之后再将COUNT的值写入方法区。但是线程是并发并行的,当线程1去获取并且COUNT的值之时,线程2可能在线程1没有写入的时候去获取COUNT的值,这就出现了线程不安全问题
线程不安全出现的原因
  1. 原子性。上面的代码是不具备原子性的。原子性就是提供互斥访问,在同一段时间只能有一个线程对COUNT进行操作
  2. 可见性。上述代码不具备可见性。可见性就是在一个线程COUNT进行操作的时候其他线程可以看见这个线程对COUNT的操作。
  3. 代码有序性。这一般是编译期和运行期代码优化而产生的问题,比如老总给员工下达可一个命令,你先去楼下买一杯奶茶,然后去工作一个小时,然后再去楼下买是个包子。笨人就是会严格按照老总说的来做,但聪明人就会想我先工作一个小时,然后买包子和奶茶,更省时间。JVM相当于聪明人,这就会产生问题了。
使用synchronized关键字解决线程不安全问题
  1. 对某一段代码加锁,然后让这段代码满足上述的三个特性:原子性,可见性和有序性。
  2. 其原理就是让多个线程间同步互斥,在一段时间内只有一个线程在对某一个变量进行操作。
  3. synchronized关键字加锁的对象一定是同一个对象。
private static int COUNT = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000 ; i++) {
                    synchronized (SynchronizedTest.class) {
                        COUNT++;
                    }
                }
            }
        });
        t1.start();


        Thread t2 = new Thread((new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    synchronized (SynchronizedTest.class) {
                        COUNT++;
                    }
                }
            }
        }));

        t2.start();

        t1.join();
        t2.join();

        System.out.println(COUNT);

    }
多线程加锁操作是如何实现调度的
  1. 线程处于运行态的时候申请对象锁,当获取到对象锁之后就会去执行同步代码,执行完之后释放对象锁。
  2. 线程处于运行态的时候申请对象锁,若是没有获取到对象锁,就会转变成阻塞态,等待获取对象锁成功的线程执行完毕释放对象锁,当这个对象锁被释放后,JVM就会通知获取对象锁失败的线程去竞争再次获取对象锁。
  3. 竞争获取对象锁的时候,线程处于运行态,竞争失败的话就会转成阻塞态。
  4. 加锁操作是很耗费CPU性能的,当线程状态改变的时候是处于内核态,而改变完成之后线程就处于用户态。这个是比较消耗CPU性能的。