10. 线程安全问题 / synchronized 关键字(重点)

Java
399
0
0
2022-11-28
标签   Java多线程

1. 线程不安全

线程不安全代码:

public class ThreadDemo13 {
    static class Counter{
        public static int count = 0;

        public void increase(){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++){
                    counter.increase();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++){
                    counter.increase();
                }
            }
        };

        t1.start();
        t2.start();

        t1.join();//当执行这里的时候,线程就阻塞了,一直到t1结束,才会往下执行
        t2.join();

        System.out.println(Counter.count);
    }
}

以上代码有两个新的线程,此时最终的运行结果不唯一

2. 为什么会出现线程不安全的情况呢?

1.线程是抢占式执行的(根本原因)

2.自增操作不是原子的,每次++,都能分为以下三个步骤 a)把内存中的数据读取到CPU (load) b)把CPU中的数据+1 (incr) c)把计算结束的数据写回到内存 (save) 当CPU执行到任意一步骤的时候,调度器随时都有可能调度走,来让其他线程来执行

3.多个线程尝试修改同一个变量

4.内存可见性导致的线程安全问题

5.指令重排序(在编译器编译代码时,会对指针进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下提高程序的运行效率)

3. 加锁 synchronized

1)实现了原子性 性能比较低

加上 synchronized 之后不一定立即就能成功,如果发现当前的锁已经被占用,该代码就会阻塞等待,一直等到之前的线程释放,才可能会获取到这个锁

以下代码假设线程一先获取到锁,那么线程二再尝试获取锁就会阻塞等待,线程一的运行不会受到影响。当线程一释放锁之后,线程二才有可能获取到锁。如果此时线程一中出现死锁,一旦锁死,就解不开了

public class ThreadDemo13 {
    static class Counter{
        public static int count = 0;

        //表示进入此方法之前会尝试加锁 
        //increase方法执行完毕后会自动解锁
  
        //这里就相当于是针对counter这个对象进行加锁 
        //进入方法内部,把加锁状态设为true,执行完成这个方法之后,就把加锁状态设为false 
        synchronized public void increase(){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(){
            @Override 
            public void run() {
                for (int i = 0; i < 50000; i++){
                    counter.increase();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override 
            public void run() {
                for (int i = 0; i < 50000; i++){
                    counter.increase();
                }
            }
        };

        t1.start();
        t2.start();

        t1.join();//当执行这里的时候,线程就阻塞了,一直到t1结束,才会往下执行
        t2.join();

        System.out.println(Counter.count);
    }
}

此时运行结果就唯一了

img

2)以下是加锁和非加锁的对照: StringBuffer(加锁了) / StringBuilder(没加锁) Vector(加锁了) / ArrayList(没加锁) HashTable (加锁了)/ HashMap(没加锁)

3)理解synchronized具体使用 synchronized是可以灵活加锁的

  1. 加到普通方法前:表示锁this 如果synchronized关键字写到方法前面,那就相当于是给当前对象来加锁

img

  1. 加到静态方法前:表示锁当前类的类对象——反射
  2. 加到某一代码块之前:显示指定给某个对象加锁
public class ThreadDemo14 {
    static class Test{
        public void method(){
            //这里括号为this,就相当于是给当前创建的对象加锁(t)
            synchronized (this){
                System.out.println("hh");
            }
        }
    }

    public static void main(String[] args) {
        Test t = new Test();
        t.method();
    }
}