Java EE——线程不安全原因和synchronized用法详解

上一篇博客主要介绍了Thread类的各种方法,并且阐述了线程不安全的缘由,这篇博客就来讨论一下如何解决线程不安全的问题

线程不安全的原因

抢占式执行

为了程序执行的效率,我们的线程采用抢占式执行,也就是谁抢到资源谁就可以完成任务,因此多个线程执行任务时线程的调度是随机的,因此我们很难找到规律

多个线程修改同一个变量

这个原因我们在上一篇博客讲过了。需要注意的是,一个线程修改一个变量,多个线程修改不同的变量,多个线程读不同的变量都是不存在线程安全问题的,只有多个变量修改同一个变量才会出现问题

修改不是原子的

上一条出现问题的原因就是因为修改并不是原子的——即有可能在修改的读操作时另一个线程就把资源占用了,因此,我们把修改操作锁起来,使得修改时别的线程无法对这个变量进行操作,就能够解决线程安全问题。

需要注意的是,我们不能看代码是一行,修改操作就是原子性的,而是应该看代码背后的cpu操作逻辑

内存可见性问题

由于机器会优化我们的代码,即如果进行100次a++操作,本身应该cpu从内存读取a变量的值,++后再放回内存,这个操作进行100次
但是由于代码自动的优化,就有可能变成了cpu从内存读取a变量的值,++100次后再放回内存。
因此,虽然代码跑的效率更高了,但是在多线程操作时可能产生意想不到的后果

指令重排序

也是代码的优化,代码将指令的顺序重新排序,使得执行的逻辑不变,效率提升,但是在多线程操作下可能产生问题

线程不安全解决方案

我们通过将修改操作变成原子的来解决线程不安全问题

加锁

当一个线程访问变量时,先对变量加锁,完成任务后再对变量进行解锁,当别的线程访问这个已经被加锁的变量,那么就会触发阻塞等待的状态

synchronized

我们的java使用synchronized关键字来加锁,在一个方法前用这个关键字来修饰,那么就可以使这个方法变成原子性的

public synchronized void 方法名(){

}

需要注意的是,由于加锁使得原来多线程的并发执行,变成了串行执行,因此效率会下降,因此我们在确保必要的情况下再进行加锁操作

synchronized还可以修饰代码块

synchronized (对象) {

}

我们要对哪个对象加锁,就在括号中填哪个对象,如果填的是this,那么谁调用这个代码块外面的方法,那么谁就是this。

  1. synchronized直接修饰方法,就相当于锁的对象是this
  2. 两个线程在锁同一个对象时会触发锁的阻塞等待,在执行不同对象时就不存在竞争

因此通过加锁操作,我们可以让之前两个线程同时对一个变量++操作的代码进行优化

public class sumByThread {
    final static int SUM = 100;
    static long a = 0;
    static Count c = new Count();

    static class Count {
        public synchronized void count(){
            a++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < SUM; i++) {
                c.count();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < SUM; i++) {
                c.count();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(a);
    }

}

下面演示一下不同情况下的加锁状态

demo1
public class demo1 {
    static int count = 0;
    public static class Counter{
        public void increase(){
            synchronized (this){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });
    }
}

我们的synchronized的加锁对象是this,也就是谁调用increase方法,谁就加锁。我们的t1和t2都是通过counter来调用increase方法的,因此会产生锁冲突

demo2
public class demo2 {
    static int count = 0;
    public static class Counter{
        public void increase(){
            synchronized (this){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter2.increase();
            }
        });
    }
}

上面的代码同样是对this进行加锁,但是t1和t2是通过不同的对象调用increase方法,因此两个线程不会出现锁竞争

demo3
public class demo3 {
    static int count = 0;
    public static class Counter{
        public Object locker = new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });
    }
}

这次我们在Counter类中加入了一个专门用来加锁的对象locker,我们synchronized修饰了这个对象,那么如果我们访问的是同一个locker对象,就会发生锁竞争,由于t1和t2访问的是同一个counter对象,而同一个counter对象中的locker对象就是相同的,因此会发生竞争。

和demo1不同的是,我们专门创建了一个locker对象来对increase方法进行加锁,以后如果还有increase2方法的话,我们可以创建locker2对象来对increase2方法进行加锁,从而使这两个方法互相没有影响

demo4
public class demo4 {
    static int count = 0;
    public static class Counter{
        public Object locker = new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter2.increase();
            }
        });
    }
}

和demo3不同的是,demo4中t1和t2访问的是不同的counter对象,因此其内部的locker对象也是不同的,因此不构成锁冲突

demo5
public class demo5 {
    static int count = 0;
    public static class Counter{
        public static Object locker = new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter2.increase();
            }
        });
    }
}

和demo4不同的是,demo5中的locker对象是static修饰的,也就是说其是一个静态成员,也就是类属性,而一个进程中类对象只有一个,类属性也只有一个,因此t1和t2虽然是通过不同的counter对象调用increase方法,但是这两个实例中的locker对象是同一个,因此还是会发生锁冲突

demo6
public class demo6 {
    static int count = 0;
    public static class Counter{
        public static Object locker = new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }
        public void increase2(){
            synchronized (this){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increase2();
            }
        });
    }
}

demo6中increase1针对locker加锁,而increase2针对调用该方法的当前对象加锁,因此t1针对静态的locker加锁,而t2针对counter对象加锁,二者并不是访问同一个对象,因此不构成锁冲突

demo7
public class demo7 {
    static int count = 0;
    public static class Counter{
        public void increase(){
            synchronized (Counter.class){
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter2.increase();
            }
        });
    }
}

demo7中我们针对类对象进行加锁。类对象在之前的反射中讲过,来自于.class文件,在jvm进程中只有一个,因此多个进程针对类对象加锁会产生锁竞争

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值