JavaEE初阶-线程2


一、多线程安全问题

代码示例如下:

public class Demo20 {
    
    
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread t1 = new Thread(() -> {
    
    

            for (int i = 0; i < 50000; i++) {
    
    
                count++;

            }

        });

        Thread t2= new Thread(() -> {
    
    

            for (int i = 0; i < 50000; i++) {
    
    
                count++;

            }

        });

        //串行执行
//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();

        t1.start();

        t2.start();

        t1.join();
        t2.join();
        System.out.println("count:"+count);

    }
}

这段代码每次执行的结果都不一样,都没有达到预期的结果即100000。主要的原因在于:
(1)count++这个操作在cpu指令的角度上看其实是三个指令

  • load:把内存中的数据加载到寄存器。
  • add:把寄存器中的值+1。
  • 把寄存器中的值写回内存。

(2)两个线程并发进行count++,多线程的执行是随即调度,抢占式的执行模式。

综合以上两点,实际并发执行时,两个线程的指令执行的相对顺序就存在多种可能,不同执行顺序下得到的结果就会有差异,例如:
在这里插入图片描述

光这一种情况,得到的结果就不可能时100000。

1.1 线程安全问题的原因

(1)线程在系统中是随机调度的,抢占式执行。
这个无法改变,是内核设计者的考虑。
(2)多个线程修改同一个变量。
一个线程修改一个变量=>没事
多个线程读取一个变量=>没事
多个线程修改不同的变量=>没事
(3)线程针对变量的修改操作,不是"原子"的。
(4)内存可见性。
(5)指令重排。

1.2 如何解决线程安全问题

从原因入手:
(1)原因1:无法干预。
(2)原因2:是一个切入点,但是在java中这种做法不是很普适,只针对一些特定场景是可以做到的。
(3)原因3:这是解决线程安全问题最普适的方案。可以通过一些操作,将非原子的操作打包成一个原子的操作。(加锁)

二、加锁

加锁就是针对原因3解决线程安全问题的操作。
锁的几个特点:
(1)锁涉及的几个操作:加锁和解锁。
(2)锁的主要特性:互斥。
一个线程获取到一个锁之后,如果其它线程也想要获取该锁,会进行阻塞等待。
(3)代码中可以创建多个锁。
只有多个线程竞争同一把锁才会互斥,针对不同的锁则不会。

2.1 synchronized

synchronized后面带上()里面写的就是"锁对象"。
注意:锁对象的用途有且仅有一个,就是用来区分两个线程是否对同一个对象加锁。如果是就会互斥,引起阻塞等待。如果不是,就不会出现锁竞争,也不会阻塞等待
synchronized后面还跟着{},进了代码块就是对()内的锁对象进行了加锁,出了代码块就是对()的锁对象进行了解锁。
用加锁解决线程安全问题代码示例如下:

public class Demo23 {
    
    
    public static int count=0;

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

        Thread t1=new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                synchronized (object) {
    
    
                    count++;
                }
            }
        });

        Thread t2=new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                synchronized (object) {
    
    
                    count++;
                }
            }
        });

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

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

在这里插入图片描述
每次执行count++会因为锁竞争,从而强制变为串行执行,但是执行for循环的条件以及i++都是并发执行的。和join等待操作相比,join操作时是等一个线程执行完了 再让join返回,第二个线程才能继续执行。
上述代码t1释放锁之后,下一次拿到锁的是t1还是t2仍然是概率性问题。

2.2 synchronized的几种使用方式

在示例方法内加锁及给类方法加锁:

class Counter {
    
    
    public int count = 0;

    synchronized public void add() {
    
    
//        synchronized (this) {
    
    
//
//        }
        count++;
    }

    synchronized public static void func() {
    
    
//        synchronized (Counter.class) {
    
    
//
//        }
    }

    public int get() {
    
    
        return count;
    }
}


public class Demo24 {
    
    


    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter = new Counter();
        Counter counter1 = new Counter();
        Thread t1 = new Thread(() -> {
    
    
//            synchronized (counter) {
    
    
//                counter.add();
//            }
            for (int i = 0; i < 5000; i++) {
    
    
                //counter.func();
                counter.add();
            }

        });

        Thread t2 = new Thread(() -> {
    
    
//           synchronized (counter) {
    
    
//               counter.add();
//           }

            //counter.add();
            for (int i = 0; i < 5000; i++) {
    
    
                //counter1.func();
                counter.add();
            }
        });


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

        System.out.println(counter.get());
    }

}

2.3 synchronized的可重入性

class Counter1 {
    
    
    private int count = 0;

    synchronized public void add() {
    
    
        count++;
    }

    public int get() {
    
    
        return count;
    }

}

public class Demo25 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter1 counter=new Counter1();
        Thread t1=new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
               //相当于
//                synchronized (counter) {
    
    
//                    synchronized (counter) {
    
    
//                        count++;
//                    }
//                }
                //按理说这样写应该会阻塞,因为外括号已经在counter上加了锁,内括号再加就会堵塞
                //内等待外执行完但是外又在等内执行完,于是进入死锁
                //但是java中不会这样,因为如果在java中内外是一个锁对象则直接进入
                //理论上上这是线程死锁的情况一
                synchronized (counter) {
    
    
                    counter.add();
                }

            }

        });

        Thread t2=new Thread(()->{
    
    

            for (int i = 0; i < 50000; i++) {
    
    
                counter.add();
            }
        });


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

        System.out.println(counter.get());
    }

}

上述代码中的t1按理应该进入死锁,但是因为java中的synchronized具有可重入性所以避免了死锁,但是在其它语言中都是会死锁的。

三、死锁

死锁的三种典型场景:

  • 锁的不可重入性,一个线程获取了一个锁,如果再对这个锁加锁就会产生死锁。不过引入可重入锁就可以解决,并且java中的用来加锁的synchronized具有可重入性所以无需考虑这种情况。
  • 两个线程两把锁。例如线程1加锁a,线程2加锁b,线程a又请求锁b,线程2又请求锁a此时就会死锁。
  • 多个线程多把锁。例子就是经典的哲学家就餐问题。

3.1 死锁的必要条件

(1)锁具有互斥性,一个线程获取锁a,如果另一个线程也想获取锁a就得阻塞等待。
(2)锁具有不可剥夺性:一个线程获取锁a,除非它主动释放,其它线程无法夺取线程上的锁a。
(3)请求和保持:一个线程获取锁a,在不释放该锁的前提下,去获取其它的锁。
(4)循环等待:多个线程获取多个锁的过程中出现循环等待。
这四个必要条件缺一不可,任何死锁的情况都必须具备以上四点否则无法构成死锁。
如果想避免死锁,可以打破上述的四个必要条件。条件一二如果是自己实现的锁那么可以打破,但是在java的synchronized中这两条是无法打破的,但是可以从后面两条来进行打破。
打破条件三是要从代码结构上进行改变,因为条件三会造成锁的嵌套,只要让自己建立的锁不嵌套即可,当然有的时候会不得不去嵌套锁。
打破条件四只需要规定好加锁的顺序即可。
代码示例如下:

public class Demo26 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
    
    
            synchronized (locker1) {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
    
    
                    System.out.println("t1 获取了两把锁");
                }
            }

        });

        Thread t2 = new Thread(() -> {
    
    
            synchronized (locker1) {
    
    

                synchronized (locker2) {
    
    
                    System.out.println("t2 获取了两把锁");
                }
            }
        });
        //以上代码会发生线程死锁的情况二
        //可以通过修改代码结构来避免死锁
        //避免循环等待:约定加锁的顺序
        //Thread t1 = new Thread(() -> {
    
    
        //            synchronized (locker1) {
    
    
        //                try {
    
    
        //                    Thread.sleep(1000);
        //                } catch (InterruptedException e) {
    
    
        //                    throw new RuntimeException(e);
        //                }
        //                synchronized (locker2) {
    
    
        //                    System.out.println("t1 获取了两把锁");
        //                }
        //            }
        //
        //        });
        //
        //        Thread t2 = new Thread(() -> {
    
    
        //            synchronized (locker1) {
    
    
        //
        //                synchronized (locker2) {
    
    
        //                    System.out.println("t2 获取了两把锁");
        //                }
        //            }
        //        });

        //避免请求和保持:
        //Thread t1 = new Thread(() -> {
    
    
        //            synchronized (locker1) {
    
    
        //                try {
    
    
        //                    Thread.sleep(1000);
        //                } catch (InterruptedException e) {
    
    
        //                    throw new RuntimeException(e);
        //                }
        //
        //            }
    //               synchronized (locker2) {
    
    
//                         System.out.println("t1 获取了两把锁");
        //            }
        //        });
        //
        //        Thread t2 = new Thread(() -> {
    
    
        //            synchronized (locker2) {
    
    
        //
        //
        //            }
        //        synchronized (locker1) {
    
    
        //                 System.out.println("t2 获取了两把锁");
        //         }
        //        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();


    }
}

猜你喜欢

转载自blog.csdn.net/qq_45965652/article/details/137225640
今日推荐