Java并发编程之多线程会导致的问题

1. 线程安全问题

1.1 什么是线程安全?

  • 某权威作者定义:“当多个线程访问同一个对象时,如果不用考虑这些线程的调度,也不需要额外的同步,调用这个对象的行为都可以获得正确的结果,那么这个对象时线程安全的“。
  • 通俗易懂的定义:当多个线程访问同一个对象时,不需要做额外的任何处理,像单线程编程一样,程序可以正常运行,就可以称为线程安全。

1.2 线程安全问题有哪些?

  1. 运行结果错误:a++多线程下出现结果错误
  2. 活跃性问题:死锁、活锁、饥饿

1.2.1 运行结果错误

下面演示a++运行结果错误:

public class APlusPlus implements Runnable {

    static int a = 0;
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        APlusPlus aPlusPlus = new APlusPlus();
        Thread t1 = new Thread(aPlusPlus);
        Thread t2 = new Thread(aPlusPlus);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(a);
    }
}
11054

我们预计的结果是20000,最后结果比预计的少,因为a++,看上去是一个操作,实际上包含了三个动作:
1. 读取a
2. 将a加一
3. 将a的值写入到内存中
说明a++操作是不具备原子性的,因此需要对a++操作进行同步,保证同一时刻只有一个线程执行a++操作。

1.2.2 活跃性问题:死锁、活锁、饥饿

下面演示死锁问题:

public class DeadLock {
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "获取到lockA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在尝试获取lockB...");
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "获取到lockB");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取到lockB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在尝试获取lockA...");
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + "获取到lockA");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

由于t1获取到lockA,t2获取到lockB,t1和t2分别想获取lockB和lockA,由于双方都没有释放锁,所以t1和t2都进入到了死锁状态。

1.3 各种需要考虑线程安全的情况

  1. 访问共享变量,比如对象的属性、静态变量、共享缓存等;
  2. 所有依赖顺序的操作,比如:读改写(原子性)、读取-检查-操作(内存可见性);
  3. 数据之间存在绑定关系;
  4. 使用其他类的时候,比如使用HashMap就不是线程安全的;

2. 性能问题

为什么多线程会带来性能问题?一共分为以下两个原因:

  1. 上下文切换
  2. 内存同步

2.1 上下文切换

  • 什么是上下文切换:进行一次上下文切换时,先挂起当前线程,然后把当前线程状态存在某处,以便线程切换回来知道执行到哪里了,这个线程状态包含当前线程执行的指令和位置,这个状态就是上下文。
  • 由于上下文切换也会使得CPU自身的一些缓存失效,相对而言,也降低了执行速度。
  • 何时会密集地导致上下文切换:抢锁、IO

2.2 内存同步

  • 由于JMM规定了主内存和线程内存,而线程内存会缓存数据,可以增加计算速度。由于在多线程编程时使用了synchronized、volatile关键字等,禁止了线程缓存,也禁止了指令重排序,编译器CPU等优化手段,使得是能只用主内存了,相对而言就降低了性能
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章