多线程是每个互联网公司都会问的一个问题,这两天也是自己写了些demo推敲多线程相关的问题
但主要还是集中一下几个问题:
- 什么是线程安全
- 多个线程一个锁
- 多个线程多个锁
- 脏读以及如何避免脏读
什么是线程安全
其实这个问题个人觉得只有有一定的工作经验的才能理解到位,但只是理解到位,并不能运用自然,毕竟我们的日常工作也并非什么时候都要用到多线程。
个人理解线程安全:多个线程执行同一个对象,并得到正确的结果。
这是我个人理解的,不能代表所有,接下来我会用代码来说明这一点,当然如果有不正确的地方希望能指出。
多个线程一个锁
首先我们要理解为什么需要锁的概念,先以一个DEMO来演示下,有一个成员变量,run方法里主要就是对这个变量操作之后将当前线程的名称以及count打印。
public class ThreadTest implements Runnable{
private int count = 5;
@Override
public void run() {
count--;
System.out.println(Thread.currentThread().getName()+"--------count:"+count);
}
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
Thread t4 = new Thread(test);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
代码中起了4个线程,并发的去执行run方法,我们来看下执行的结果。
Thread-0--------count:3
Thread-3--------count:1
Thread-2--------count:3
Thread-1--------count:1
当然,这个结果并不是唯一的,你也有可能会的到你想要的结果例如
Thread-1--------count:4
Thread-2--------count:3
Thread-0--------count:2
Thread-3--------count:1
原因很简单,如果出现结果如果并不是我们预期的,其实是因为4个线程都在抢着执行这段代码,在这个案例中就是在抢count这个变量,并没有谁先谁后,那么你确定抢到的count的值还是你期望的值么?为了解决这个问题我们加入了锁。
@Override
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName()+"--------count:"+count);
}
我们看看加锁后的结果
Thread-0--------count:4
Thread-2--------count:3
Thread-1--------count:2
Thread-3--------count:1
其实这个名字很形象,拿这个例子来说,4个线程分别是1,2,3,4,当线程1得到锁之后去执行方法,而2,3,4三个线程则一直等着锁释放(这里其实是有锁竞争的问题),当线程1完成了方法,就会释放锁,这个锁又会由2,3,4三个线程去抢,以此类推
现在这个线程就是线程安全的了,这是多个线程1把锁的情况,也是普遍我们在自学的时候接触到的,容易理解,我们来看看多个线程多个锁的情况
多个线程多个锁
我们需要重新写一个DEMO更清晰的发现问题,我把代码贴的尽量全一些,大家也可以直接复制粘贴的形式验证,如果有问题可以一起探讨
public class ThreadTest{
private int count = 0;
public synchronized void printNum(String tag){
try {
if(tag.equals("a")){
count = 100;
System.out.println("tag a set count over");
}else{
count = 200;
System.out.println("tag b set count over");
}
System.out.println("tag:"+tag+"---------count:"+count);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadTest test1 = new ThreadTest();
ThreadTest test2 = new ThreadTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
test2.printNum("b");
}
});
t1.start();
t2.start();
}
}
不难发现pringNum方法上面是加了锁的,按理来说得到的结果顺序应该是这样的
tag a set count over
tag:a---------count:100
tag b set count over
tag:b---------count:200
但是我们来看看实际运行的结果
tag a set count over
tag b set count over
tag:a---------count:100
tag:b---------count:200
出现这种问题,证明锁并没有起到作用,我们在来看看代码,我用两个ThreadTest对象去执行printNum方法,不像上个例子是一个对象,我们在这里碰到的问题就是多个线程多个锁的问题,一个对象对应一把锁,这样两把锁互相不影响,所以并非线程安全,他们各自进各自的方法执行即可,那么如何解决呢?
public class ThreadTest{
private static int count = 0;
public static synchronized void printNum(String tag){
try {
if(tag.equals("a")){
count = 100;
System.out.println("tag a set count over");
}else{
count = 200;
System.out.println("tag b set count over");
}
System.out.println("tag:"+tag+"---------count:"+count);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadTest test1 = new ThreadTest();
ThreadTest test2 = new ThreadTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
test2.printNum("b");
}
});
t1.start();
t2.start();
}
}
只需要在执行的方法上用static修饰即可,当然方法中调用的成员变量也需要用static修饰,接下来我们看看结果
tag a set count over
tag:a---------count:100
tag b set count over
tag:b---------count:200
这就是我们心中期望的结果了,当你的锁在用static修饰的方法上,其实锁的是这个方法所在的类,在这个案例中锁的其实是ThreadTest,这就是多个线程多个锁
脏读以及如何避免脏读
接下来就是脏读的问题了,我们先要明白什么是脏读,
public class ThreadTest{
private String username = "Simon";
private String password = "123456";
public synchronized void setValue(String username,String password){
try{
this.username = username;
Thread.sleep(2000);
}catch (Exception e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果-----------username:"+username+"---password:"+password);
}
public void getValue(){
System.out.println("getValue最终结果-----------username:"+username+"---password:"+password);
}
public static void main(String[] args) throws InterruptedException {
ThreadTest test = new ThreadTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
test.setValue("lucy", "111111111111113");
}
});
t1.start();
Thread.sleep(1000);
test.getValue();
}
}
这个demo里执行的方法中,在setvalue设置了两秒的设置时间,getvalue就是将当前的属性取出来打印,线程中执行的是设置属性,并且在设置完之后一秒将数据打印出来,在运行方法之前我们再看下main方法,这里是有两个线程的,一个是main的主线程,另一个是我们创建的Thread线程,并且我在setvalue方法上加了锁,我们来运行下代码看看结果
getValue最终结果-----------username:lucy---password:123456
setValue最终结果-----------username:lucy---password:111111111111113
结果是不是和想的不太一样?这其实也是java中同步与异步的问题,如果不加锁就是异步,也就是说,在同步方法执行的同时,getValue也被执行了,那么就无法保证获得的变量的值是修改后的还是修改前的,其实解决方法也很简单,在getvalue方法上面也加上锁即可,我们看下加锁后的执行结果。
setValue最终结果-----------username:lucy---password:111111111111113
getValue最终结果-----------username:lucy---password:111111111111113
这样脏读的问题就可以解决了,可能很多人会有个疑问,我们是不是需要考虑性能方面的问题?同步的话会严重影响性能。其实这种问题往往出在并发量很高的情况下,而java对synchronized 也是不断的优化,可以说到现在java1.8的synchronized 性能已经非常好了,并且互联网行业的并发量也不是那么容易上去的,除非像电商,如果觉得这样性能不好的话后续也会继续研究。