多线程的三大特性包括原子性、可见性、有序性。下面分别解释这三大特性。当程序运行时,如果没有满足这三大特性,就有可能产生线程安全问题。
原子性其实就是保证数据一致、线程安全一部分,既一个或者多个操作时,要么全部执行完中途不会被打断,要么就不执行。
举例说明:
package com.jwb;
public class ThreadDemo3 {
// 这是一个全局变量
public static int count = 10;
public static void main(String[] args) {
// 一个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count = count - 1; // 对共享变量做写操作
System.out.println(Thread.currentThread().getName() + ":当前count = " + count);
}
}
});
// 另一个线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count = count - 1; // 对共享变量做写操作
System.out.println(Thread.currentThread().getName() + ":当前count = " + count);
}
}
});
t1.start();
t2.start();
}
}
以上代码中,两个线程都对同一个变量count进行count = count - 1
的操作,因为次操作本身并不是原子性的操作,这里的操作分为:1、读取count 2、将count-1的值重新写入count。
所以,在两个或者多个线程同时操作时,就会出现线程不安全的问题。因此,解决这样的线程安全问题,必须将count = count - 1
变为原子操作。比如以下的方式:
例子1:
synchronized(obj) {
count = count - 1;
System.out.println(Thread.currentThread().getName() + ":当前count = " + count);
}
例子2:
Lock lock = new ReentrantLock();
lock.lock();
try {
count = count - 1;
System.out.println(Thread.currentThread().getName() + ":当前count = " + count);
} catch (Exception e) {
} finally {
lock.unlock();
}
以上的两个例子,都可以实现将多个执行原子化。
有序性是:程序执行的顺序,是按照代码的顺序依次执行的,就被成为有序。
而计算机的执行顺序往往并不是有序的,但是计算机切却要保证最终的结果是和顺序执行的时候是一致的。比如同时对多个变量赋值:
int i = 1;
int j = 2;
int k = 3;
计算机在执行以上三行代码时,顺序是不一定的,很有可能执行的顺序与代码的编写顺序不一样,这是cpu的为了提高效率而进行的优化,但是结果并不会影响三个变量的赋值。
但是,如果是这样:
int i = 1;
int j = i + 1:
int k = j + 1;
int x = 4;
以上4行代码中,前三行必须是顺序执行,否则结果将会不如我们说期望的,就会出现问题,但是第四行代码却不受前三行代码的顺序影响。
在多线程的操作中,我们也会碰到,类似于需要有顺序执行的需求,就好比如,将前3行代码分别用三个线程进行操作,会有什么样的效果呢:
package com.jwb;
public class ThreadDemo3 {
static class Vars {
// 先生声明三个变量并初始化
public int i = 0, j = 0, k = 0;
}
public static void main(String[] args) {
Vars vars = new Vars(); // 实例化这个实力,共享给三个线程使用
// 第一个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
vars.i = 1; // 对i变量先赋值
System.out.println("i=" + vars.i);
}
});
// 第2个线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
vars.j = vars.i + 1; // 对i变量先赋值
System.out.println("j=" + vars.j);
}
});
// 第3个线程
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
vars.k = vars.j + 1; // 对i变量先赋值
System.out.println("k=" + vars.k);
}
});
// 安装顺序,启动三个线程
t1.start();
t2.start();
t3.start();
}
}
第一次执行结果:
k=1
j=2
i=1
第二次执行结果:
i=1
j=2
k=1
第三次执行结果:
k=1
i=1
j=2
以上的三次结果,可以看出,cpu执行三个线程时,并不是按照代码的顺序进行执行的,因此就会产生线程安全问题。那么应该如何解决呢,方法如下:
// 。。。。创建线程和其他部分代码没有改变,所以就不写出出来了,主要是开始线程这一段代码,详细请看join()方法
// 安装顺序,启动三个线程
t1.start();
t1.join(); // t1执行完,其他线程才能继续执行后面的代码
t2.start();
t2.join();// t2执行完,其他线程才能继续执行后面的代码
t3.start();
多次执行的结果都是一直的,如下:
i=1
j=2
k=3
使用了join()
方法,控制了执行的顺序,如此一来,就解决了由于执行顺序不可控导致的线程安全问题。
可见性:当多个线程共享同一个变量时,其中一个线程修改了变量,其他的线程必须立即得知并获取了最新的该变量的值,满足此条件就是线程的可见性,反之则会造成线程安全问题。
下面举个例子:
package com.jwb;
public class ThreadDemo4 {
static class MyThread extends Thread {
public static boolean flagRun = true;
public void run() {
System.out.println("线程开始了。");
while (flagRun) {
// ...
}
System.out.println("线程结束了。");
}
}
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
mt.start();
Thread.sleep(1000);
System.out.println("等待了1秒后。");
MyThread.flagRun = false;
System.out.println("将变量MyThread.flagRun的字变为false。");
System.out.println("main结束了。");
}
}
以上代码的设计需求是:在main线程中通过改变变量MyThread.flagRun
,控制子线程的循环结束。
打印结果如下:
打印结果看出,main线程中改变了MyThread.flagRun
变量的值,但是子线程并没有立即得知,也没有立即获取到MyThread.flagRun
最新的子,因此产生了线程安全的的问题。
解决办法:
// 其他部分的代码不变,就不写出来了,关键在于为此变量加上标识符`volatile`
public volatile static boolean flagRun = true;
java提供了关键字volatile
,此关键字是解决线程之间可见性的,当共享变量改变的时候,其他线程会立即能够得知且获取变量最新的值。
打印结果:
要解决线程安全问题,必须满足以上三大特性,缺一不可。任何一个特性不满足,都会产生线程安全问题。