线程安全的本质:多个线程同时竞争性的访问同一个数据的时候(同时读写同一个数据的时候),可能会出现安全问题;只要有多线程出现的情况就一定存在线程安全问题。
不会出现线程安全的情况
每个线程执行自己的任务,没有线程安全问题
举个栗子:
现在有一个任务类MyTask
public class MyTask implements Runnable{
private final int Id;
public MyTask(int id) {
Id = id;
}
@Override
public void run() {
int n = 0;
for (int i = 0; i < 100; i++) {
n++;
n++;
}
IO.println("第"+ Id +"号任务:"+n);
}
}
测试类:
public class Test01 {
static void main() {
for (int i = 0; i < 5; i++) {
MyTask task = new MyTask(i);
Thread t1 = new Thread(task);
t1.start();
}
IO.println("Main线程结束");
}
}
运行后不会出现线程安全问题,因为每个线程执行的都是自己的任务
所以:即便在多线程环境下,也不一定出现线程安全问题
多个线程执行同一个任务,也不存在线程安全问题
保持MyTask类不变,将测试类中的new MyTask对象放到for循环外面:
public class Test01 {
static void main() {
MyTask task = new MyTask(100);
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread(task);
t1.start();
}
IO.println("Main线程结束");
}
}
运行结果:
Main线程结束
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
每个线程执行run方法时都和别的线程没有任何关系,每个线程拥有自己的变量,变量存在一个独立的栈中,与其他线程互不影响,即局部变量永远不会出现竞争问题
出现线程安全问题的情况
将MyTask类中for循环中定义的n删除,将n定义为类的一个属性
public class MyTask implements Runnable{
private final int Id;
private int n;
public MyTask(int id) {
Id = id;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
n++;
n++;
}
IO.println("第"+ Id +"号任务:"+n);
}
}
此时n是存放在堆里面的,不是放在栈里,不是局部变量。
测试类:
public class Test01 {
static void main() {
MyTask task = new MyTask(100);
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread(task);
t1.start();
}
IO.println("Main线程结束");
}
}
此时,十个线程共同对n进行操作,执行结果:
Main线程结束
第100号任务:800
第100号任务:1200
第100号任务:1400
第100号任务:1600
第100号任务:2000
第100号任务:1000
第100号任务:1800
第100号任务:600
第100号任务:200
第100号任务:400
演示线程安全问题
修改MyTask类,添加一个本不应该出现的条件:
public class MyTask implements Runnable{
private final int Id;
private int n;
public MyTask(int id) {
Id = id;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
n++;
n++;
if(n%2 == 1){
IO.println("这应该是不可能发生的");
}
}
IO.println("第"+ Id +"号任务:"+n);
}
}
for循环内每次循环n都自增2,理论上if条件永远不可能成立。
测试类和上文保持不变,运行后有一定的概率会出现如下结果:
Main线程结束
这应该是不可能发生的
第100号任务:994
第100号任务:1594
第100号任务:1794
第100号任务:1194
第100号任务:200
第100号任务:794
第100号任务:604
第100号任务:1394
第100号任务:1994
第100号任务:400
在此时出现了本不应该出现的结果,此结果完全随机、不可预料、不可干预。
如何避免线程安全问题
最基本的方法是不要出现共享资源,但在实际生产环境中难以做到。
解决线程安全可以加锁(同步锁)。
同步锁
同步锁不是Java所独有的,是所有编程语言共同有的特性;
加锁是把安全放在第一位的,而不是把效率放在第一位的,加锁的方式效率并不高。
加锁使用的是synchronized关键字,
需要注意的是,在Java中几乎所有的对象都可以当锁,但是必须保证每个线程都能访问到这把锁。
示例1:以静态变量为锁,所有线程共⽤⼀把锁。
一般使用0个元素的byte数组当做锁:
private final byte[] lock = new byte[0];
//同步锁语法:同步块
synchronized(lock){
}
所以,修改上文中提到的MyTask类,为会产成线程冲突的代码块加锁:
public class MyTask implements Runnable{
private final int Id;
private int n;//所有线程共享读写
private final byte[] lock = new byte[0];//锁:所有线程可见
public MyTask(int id) {
Id = id;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//同步锁语法:同步块
synchronized(lock){
n++;
n++;
if(n%2 == 1){
IO.println("这应该是不可能发生的");
}
}
}
IO.println("第"+ Id +"号任务:"+n);
}
}
一旦加锁,当其中一个线程在执行加锁的代码块儿时,其他线程就只能阻塞等待。
THE END