导语
上面说到了多线程的简单实现,编写了几个入门的小例子,这里我们来研究一下关于实例变量和线程安全的问题。在自定义的线程类中的实例变量针对其他线程可以有共享和不共享之分,下多个线程之间进行交互的时候会产生线程安全的问题。下面就来看看会有什么技术点。
不共享数据的情况
如图在进行数据操作的时候,相互之间的数据不会产生影响,都在自己的一块内存中进行操作。
下面就通过一个小例子来看看不共享情况
自定义实现类
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name){
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while (count>0){
count--;
System.out.println("由 "+currentThread().getName()+" 计算,count="+count);
}
}
}
运行类
public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("a");
MyThread b = new MyThread("b");
MyThread c = new MyThread("c");
a.start();
b.start();
c.start();
}
}
运行结果
通过上面的例子可以看到,一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量值,这样所实现的变量就是不共享的,也就是说在JVM分配给线程独立的内存中进行运行。所以不会存在多个线程共同访问同一个变量的情况。
下面就来看看多个线程访问同一个变量的情况
共享数据情况
共享数据的情况就是多个线程可以访问同一个变量,例如在实现抢票软件功能的时候,多线程共同处理同一个火车上的火车票操作。
共享数据小例子
创建自定义线程
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println("由 "+currentThread().getName()+ " 计算, count = "+count);
}
}
测试类
public class Run {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread a = new Thread(myThread,"A");
Thread b = new Thread(myThread,"B");
Thread c = new Thread(myThread,"C");
Thread d = new Thread(myThread,"D");
Thread e = new Thread(myThread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
测试结果
从上面的结果中可以看到有些线程处理的数据居然是同一个数据,还有一个情况是每次运行的结果是不一样的。从这个角度上看对于count变量的访问并不是安全的在之前的之前的博客中分析过这种情况出现的原因。这里简单的说一下操作步骤
- 1、获取原有的值
- 2、计算i-1的值
- 3、对i进行赋值
在这个三个步骤任意的一个步骤都有可能出现问题,并且导致结果不同。有兴趣的读者可以研究一下或者看看博主的高并发系列。那么如果我们将自定义的Thread修改为如下,还会出现错误么!!
public class MyThread extends Thread {
private int count = 5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 "+currentThread().getName()+ " 计算, count = "+count);
}
}
运行结果
会看到结果是正确的。也就是说加入了synchronized关键字之后在线程进入run()方法之前会以排队的方式进行处理,当一个线程调用run方法之前首先判断run方法是不是被锁上了,如果被锁则说明有其他线程正在处理这个数据,那就要等到这个线程处理完成之后才会轮到排队线程进行处理。而这段被synchronized关键字标注的代码块被称为是互斥区或者是临界区。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去获取锁,如果能够拿到这把锁,那么这个线程就可以执行synchronize里面的代码,如果不能拿到这把锁,那么这个线程就会不断尝试获取这把锁,直到拿到为止,而且多个线程是同时争抢这把锁。
面试题
一个局部变量i=1,两个线程同时执行,是否是线程安全的?
情况一 属于自定义线程类中的变量
public class MyThread extends Thread {
private int i = 5;
@Override
public void run() {
i--;
System.out.println(i);
}
}
测试类
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
情况二 属于自定义线程类中的某个方法的局部变量
public class MyThread extends Thread {
public void print(){
int a = 5;
a--;
System.out.println(currentThread().getName() + " "+a);
}
@Override
public void run() {
print();
}
}
情况三 属于继承了Runnable接口的线程类变量
public class MyRunnable implements Runnable {
private int i = 5;
private String name ;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("name " + name +getClass().getName()+ " "+i--);
}
}
测试类
public class Run {
public static void main(String[] args) throws InterruptedException {
MyRunnable r1 = new MyRunnable("TE");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
TimeUnit.SECONDS.sleep(10);
t2.start();
}
}
有一个共享变量如何保证线程安全 ?
- 使用synchronize关键字
- 使用显式锁
- 使用单一对象模式
总结
这篇博客主要分析了两件事情,也就是结尾的时候所提供的两个面试题。怎样实现多线程线程安全是很多人在研究的一个课题,在分布式系统中,在高并发场景下,这些都是不可避免的问题。