关于java线程不安全问题的简述
什么是线程不安全及其具体解析
- 当我们执行一个Java.exe进程的时候,首先会初始化JVM参数,然后创建JVM虚拟机,再启动后台线程,最后执行就是执行我们代码行的main方法。
- 而在JVM运行的时候会将他管理的内存分为若干个区域,每一个线程都有其独有的程序计数器,java虚拟机栈和本地方法栈,以及线程共享的Java堆和方法区(包含运行时常量池)
- 当我们定义一个静态变量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.
这就是所谓的线程不安全
- 当我们定义一个静态变量,它被创建于方法区,而每一个线程想要去修改静态变量COUNT的时候,他必须从方法区去获取到COUNT的值,然后在自己的线程私有区域去进行修改也就是++操作,修改完成之后再将COUNT的值写入方法区。但是线程是并发并行的,当线程1去获取并且COUNT的值之时,线程2可能在线程1没有写入的时候去获取COUNT的值,这就出现了线程不安全问题
线程不安全出现的原因
- 原子性。上面的代码是不具备原子性的。原子性就是提供互斥访问,在同一段时间只能有一个线程对COUNT进行操作
- 可见性。上述代码不具备可见性。可见性就是在一个线程COUNT进行操作的时候其他线程可以看见这个线程对COUNT的操作。
- 代码有序性。这一般是编译期和运行期代码优化而产生的问题,比如老总给员工下达可一个命令,你先去楼下买一杯奶茶,然后去工作一个小时,然后再去楼下买是个包子。笨人就是会严格按照老总说的来做,但聪明人就会想我先工作一个小时,然后买包子和奶茶,更省时间。JVM相当于聪明人,这就会产生问题了。
使用synchronized关键字解决线程不安全问题
- 对某一段代码加锁,然后让这段代码满足上述的三个特性:原子性,可见性和有序性。
- 其原理就是让多个线程间同步互斥,在一段时间内只有一个线程在对某一个变量进行操作。
- 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);
}
多线程加锁操作是如何实现调度的
- 线程处于运行态的时候申请对象锁,当获取到对象锁之后就会去执行同步代码,执行完之后释放对象锁。
- 线程处于运行态的时候申请对象锁,若是没有获取到对象锁,就会转变成阻塞态,等待获取对象锁成功的线程执行完毕释放对象锁,当这个对象锁被释放后,JVM就会通知获取对象锁失败的线程去竞争再次获取对象锁。
- 竞争获取对象锁的时候,线程处于运行态,竞争失败的话就会转成阻塞态。
- 加锁操作是很耗费CPU性能的,当线程状态改变的时候是处于内核态,而改变完成之后线程就处于用户态。这个是比较消耗CPU性能的。