必须要深刻理解线程安全的相关内容.
通过一段代码了解线程不安全
static class Counter {
private static int count;
public static void increse(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
Counter.increse();
}
}
};
Thread t2 = new Thread() {
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
Counter.increse();
}
}
};
Counter counter = new Counter();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Counter.count);
}
正确的累加结果应该为100000
计算结果:
第一次输出: 100000
第二次输出: 68562
第三次输出: 87667
极端情况下,t1和t2每次++都是串行执行,那么结果就是100000
t1和t2每次++都是并发执行,那么结果就是50000
这样的结果出现的原因是
1.线程是抢占式执行的.(线程不安全的万恶之源)
2.自增操作不是原子的
原子性:原子是一个不可切分的单位,要么执行完,要么全都不执行
每次++都能拆分成三个步骤:
当CPU执行到上面三个步骤中的任何一步时,都可能会被调度器调度走
让给其他线程来执行.
如果两个线程是串行执行的,此时的计算结果是正确的
如果是并行执行,线程进行++一半的时候,线程2也在++
此时发现,明明自增了两次,但是结果还是1,这就产生了线程不安全的情况
3.多个线程尝试修改同一个变量
4.内存可见性导致线程安全问题
5.指令重排序(java编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,起到保证原有逻辑不变的情况下,提高程序的运行效率)
前三个原因更加重要,后两个原因也有影响,但是没有前三个影响范围那么广
如何解决线程不安全问题呢?
这里的锁和现实的锁类似
锁的特点:
1.加锁(获取锁)lock
2.解锁(释放锁)unlock
Java中使用锁,需要一个关键字synchronized (英文原意 同步)
加锁解锁都由一个关键字包办了,这样的好处就是避免出现忘记解锁的情况
尝试加锁的时候并不一定能马上成功,如果发现当前的锁已经被占用了,那么
要等到之前的线程释放锁,此时剩下的线程再重新竞争锁
synchronized public static void increse(){
count++;
}
在进入increase 方法之前,会尝试加锁,increase方法执行完后自动解锁
尝试加锁的时候不一定能立刻就成功,如果发现当前的锁已经被占用了,那么该代码就会阻塞等待.
一直等到之前的线程释放锁,才可能获取到这个锁.
有锁的优点:
哪怕线程1执行了一半被调度走了,线程2也想尝试进行++,也会因为线程1没有释放锁而阻塞,不会对线程1的修改操作产生任何影响.
这样的话线程1的自增操作就能一鼓作气的执行完,中间也不会受到干扰,也就相当于保证了++操作的原子性.
锁这个东西用起来也没那么容易:
1.使用的时候一定要按照正确的方式来使用,否则就很容易出现各种问题
2.一旦使用锁,那么这个代码基本就和高性能无缘了
锁的等待时间是不可等待的,可能会等很久
理解 synchronized具体使用:
用法可以灵活的指定某个对象来加锁,而不仅仅是把锁加到方法上
如果把synchronized写到方法前,相当于给当前对象(this)加锁,
(所谓的锁,其实是针对某个指定的对象来加锁)
对象 new出的对象会给这个对象申请一块内存空间
此处的synchronized就是针对couter这个对象来加锁,
进入到increase方法内部,就把加锁状态设为true
退出increase方法后,就把加锁状态设为false
synchronized几种常见用法:
1.加到普通方法前:表示锁this
2.加到静态方法前:表示锁当前类的对象
3.加到某个代码块之前,显示指定给某个对象加锁
public static void main(String[] args) {
//下面的代码意义是
//演示锁的相关特性
//如果线程1获取到锁以后,不进行输入操作的话,就会占着锁不放
//此时线程2是不能获取到锁的,等到线程1释放锁以后,线程2才能获取到锁
Object locker = new Object();
Thread t1 = new Thread() {
@Override
public void run(){
Scanner sc = new Scanner(System.in);
synchronized(locker){ //先尝试加锁.然后读取数据
System.out.println("线程1获取到锁");
System.out.println("请输入一个整数:");
int num = sc.nextInt(); //如果用户不输入,那么就会一直阻塞在nextInt,这个锁就会一直被占有
System.out.println(num);
}
}
};
Thread t2 = new Thread() {
@Override
public void run(){
while(true){
synchronized (locker){
System.out.println("线程2获取到锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t1.start();
t2.start();
}
一旦线程1获取到锁没有释放的话,那么线程2 就会在锁这里阻塞等待
利用jconsole查看线程的情况
↑这个就是线程1的调用栈,可以看到他阻塞在nextInt方法,在等待用户输入.
↑这是线程2的调用栈,并且 Java_0608.TestDemo3$2.run(TestDemo3.java:42) 说明阻塞在代码的第42行
执行到41行之后触发了阻塞,即将运行42行时被阻塞.
同时上面的BLOCKED也说明是等待锁而导致的阻塞
等待锁的线程会进入到BLOCKED这样的状态
当输入一个整数,让线程1释放锁之后,线程2才能继续执行.
如果把上面的代码改一下,对不同的对象进行加锁会如何
两个线程之间会独立运行,不会影响.
也就是说t1获取到锁以后,t2仍在运行,两个线程之间没有竞争.