java 多线程笔记二 线程安全问题

高并发下可能回出现的超卖问题

运行一下代码,模拟超卖问题,运行后会发现会出现重复编号的票

public class Test {
    public static void main(String[] args) {
        /**
         * 模拟出售电影票
         */
        PiaoThread piaoThread = new PiaoThread();
        new Thread(piaoThread,"1号窗口").start();
        new Thread(piaoThread,"2号窗口").start();
        new Thread(piaoThread,"3号窗口").start();
        new Thread(piaoThread,"4号窗口").start();
    }
}

class PiaoThread implements Runnable {
    //多个窗口使用的是同一个对象,所以成员变量是共享的
    private int piao = 100;
    @Override
    public void run() {
        while (true){
            if (piao < 1){ break;}
            System.out.println(Thread.currentThread().getName()+"卖出了第"+piao+"编号的票");
            piao--;
        }
    }
}

同步代码块可以解决超卖问题

public class Test {
    public static void main(String[] args) {
        /**
         * 模拟出售电影票
         */
        PiaoThread piaoThread = new PiaoThread();
        new Thread(piaoThread,"1号窗口").start();
        new Thread(piaoThread,"2号窗口").start();
        new Thread(piaoThread,"3号窗口").start();
        new Thread(piaoThread,"4号窗口").start();
    }
}

class PiaoThread implements Runnable {
    //多个窗口使用的是同一个对象,所以成员变量是共享的
    private int piao = 100;
    @Override
    public void run() {
        while (true){
            //悲观锁
            // synchronized (锁对象),
            // 需要是同一个对象,这里都是使用的一个对象,所以可以使用this
           synchronized (this){
               if (piao < 1){ break;}
               System.out.println(Thread.currentThread().getName()+"卖出了第"+piao+"编号的票");
               piao--;
           }
        }

    }
}

同步方法,我们把需要同步的代码块抽取出来,idea抽取方法快捷键ctrl+alt+m

同步方法的锁对象:

如果是非静态同步方法,锁对象是this

如果是静态方法那么锁对象是该方法所在类的字节码对象(类名.class)

场景:a线程使用了同步代码块,b线程使用的同步方法,这时候ab线程需要同步那么就需要知道同步方法的锁对象是谁。

public class Test {
    public static void main(String[] args) {
        /**
         * 模拟出售电影票
         */
        PiaoThread piaoThread = new PiaoThread();
        new Thread(piaoThread,"1号窗口").start();
        new Thread(piaoThread,"2号窗口").start();
        new Thread(piaoThread,"3号窗口").start();
        new Thread(piaoThread,"4号窗口").start();
    }
}
class PiaoThread implements Runnable {
    //多个窗口使用的是同一个对象,所以成员变量是共享的
    private int piao = 100;
    @Override
    public void run() {
        while (true){
            if (extracted()) break;
        }
    }

    private synchronized boolean extracted() {
        if (piao < 1){
            return true;
        }
        System.out.println(Thread.currentThread().getName()+"卖出了第"+piao+"编号的票");
        piao--;
        return false;
    }
}

lock锁解决超卖问题

public class Test {
    public static void main(String[] args) {
        /**
         * 模拟出售电影票
         */
        PiaoThread piaoThread = new PiaoThread();
        new Thread(piaoThread,"1号窗口").start();
        new Thread(piaoThread,"2号窗口").start();
        new Thread(piaoThread,"3号窗口").start();
        new Thread(piaoThread,"4号窗口").start();
    }
}
class PiaoThread implements Runnable {
    //多个窗口使用的是同一个对象,所以成员变量是共享的
    private int piao = 100;
    //创建一个lock
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            lock.lock();//加锁
            if (piao < 1){
                lock.unlock();//释放锁
                break;
            }
            System.out.println(Thread.currentThread().getName()+"卖出了第"+piao+"编号的票");
            piao--;
            lock.unlock();//释放锁
        }

    }
}

多线程下的可见性问题

出现可见性问题的根本原因:java的内存模型(JMM),java所有共享变量都是存储在主内存中的,在线程执行的时候,线程有单独的工作内存,这个时候会把主内存中的共享变量拷贝一份到工作变量,并且对所有的变量操作都是在工作变量进行的。

复习可见性问题代码:一下代码我们的理想结果是子线程3秒后修改flag的值,然后主线程结束死循环,运行程序后发现结束不了死循环。下面主线程代码过于简单,运行速度很快,来不及去读主内存中的共享变量,所以一直读的工作内存变量。

public class Test2 {
    public static void main(String[] args) {
        /**
         * 可见性问题
         */
        MyThread myThread = new MyThread();
        new Thread(myThread).start();
        //主线程
        while (true){
            if (MyThread.flag){
                System.out.println("flag为true结束主线程死循环");
                break;
            }
        }
    }
}

class MyThread implements Runnable{
    static boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;

        System.out.println("修改了flag为true");
    }
}

 解决多线程安全问题之可见性问题方法:volatile关键字

public class Test2 {
    public static void main(String[] args) {
        /**
         * 可见性问题
         */
        MyThread myThread = new MyThread();
        new Thread(myThread).start();
        //主线程
        while (true){
            if (MyThread.flag){
                System.out.println("flag为true结束主线程死循环");
                break;
            }
        }
    }
}

class MyThread implements Runnable{
    //这里加上volatile关键字
    static volatile boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;

        System.out.println("修改了flag为true");
    }
}

多线程安全性问题之有序性问题

多线程情况下编译器对代码重排,会产生不同的结果,引发安全性问题。

volatile关键字也可以解决有序性问题


多线程安全性问题之原子性问题

原子性问题通过原子类来解决

复现原子性问题

public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        /**
         * 模拟原子性问题
         */
        MyThread3 myThread3 = new MyThread3();
        new Thread(myThread3).start();

        //主线程自增300000次
        for (int i = 0; i < 300000; i++) {
            MyThread3.i++;
        }
        //睡眠5秒保证两个线程300000都自增完毕
        Thread.sleep(5000);
        System.out.println("主线程自增300000次完毕");
        //打印,这里理想结果应该是600000,
        // 但是在没有处理原子性问题情况下,有时候并打不到600000次
        //因为i++是3步操作,并不是原子性的,第一步是读主内存值,第二步自增,第三步写
        System.out.println(MyThread3.i);
    }
}

class MyThread3 implements Runnable{
    static int i = 0;

    @Override
    public void run() {
        for (int i1 = 0; i1 < 300000; i1++) {
            i++;
        }
        System.out.println("子线程自增300000次完毕");
    }
}

解决原子性问题

public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        /**
         * 模拟原子性问题
         */
        MyThread3 myThread3 = new MyThread3();
        new Thread(myThread3).start();

        //主线程自增300000次
        for (int i = 0; i < 300000; i++) {
            //MyThread3.i++;
            MyThread3.atomicInteger.getAndAdd(1);
        }
        //睡眠5秒保证两个线程300000都自增完毕
        Thread.sleep(5000);
        System.out.println("主线程自增300000次完毕");
        //打印,这里理想结果应该是600000,
        // 但是在没有处理原子性问题情况下,有时候并打不到600000次
        //因为i++是3步操作,并不是原子性的,第一步是读主内存值,第二步自增,第三步写
        System.out.println(MyThread3.atomicInteger.getAndAdd(1));

    }
}

class MyThread3 implements Runnable{

    static AtomicInteger atomicInteger = new AtomicInteger();
    @Override
    public void run() {
        for (int i1 = 0; i1 < 300000; i1++) {
            //i++;
            atomicInteger.getAndAdd(1);
        }
        System.out.println("子线程自增300000次完毕");
    }
}

原子类工作原理:CAS机制(比较并交换)

参考CAS机制是什么?_IABQL的博客-CSDN博客_cas机制

并发包也是线程安全的,

  • java.util.concurrent.CopyOnWriteArrayList<E>
  • java.util.concurrent.CopyOnWriteArraySet<E>
  • java.util.concurrent.ConcurrentHashMap<K,V>

其他用的到的方法

  • java.util.concurrent.CountDownLatch允许一个或多个线程等待其他线程执行完后再执行
  • java.util.concurrent.Exchanger<V>用于【两个】线程之间的数据交换
  • java.util.concurrent.Semaphore主要作用是控制线程的并发数量
  • java.util.concurrent.CyclicBarrier让一组线程都达到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被拦截的线程才会继续执行
public class Test4 {
    /**
     * java.util.concurrent.CyclicBarrier
     * 让一组线程都达到一个屏障(同步点)时被阻塞,
     * 直到最后一个线程到达屏障时,屏障才会开门,
     * 所有被拦截的线程才会继续执行
     */
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                //5名员工达到会议室了,开始开会
                System.out.println("开会过程中.....");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("开完会");
            }
        });

        MyThread4 myThread4 = new MyThread4(cb);

        //5名员工线程
        new Thread(myThread4,"员工1").start();
        new Thread(myThread4,"员工2").start();
        new Thread(myThread4,"员工3").start();
        new Thread(myThread4,"员工4").start();
        new Thread(myThread4,"员工5").start();
    }
}

class MyThread4 implements Runnable{
    CyclicBarrier cb;
    public MyThread4() {}
    public MyThread4(CyclicBarrier cb) {
        this.cb = cb;
    }

    @Override
    public void run() {
        //打印某某员工到达会议室
        System.out.println(Thread.currentThread().getName()+"到达了会议室");
        //等待其他员工达到会议室
        try {
            cb.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        //会议开完离开会议室
        System.out.println(Thread.currentThread().getName()+"离开了会议室");
    }
}

 执行结果:

员工2到达了会议室
员工5到达了会议室
员工4到达了会议室
员工3到达了会议室
员工1到达了会议室
开会过程中.....
开完会
员工1离开了会议室
员工2离开了会议室
员工4离开了会议室
员工5离开了会议室
员工3离开了会议室


多线程安全性问题之死锁

死锁产生条件:

1,有多个线程

2,有多把锁

3,有同步代码块嵌套

参考什么是死锁?死锁产生的条件?_努力撸代码的小刑的博客-CSDN博客_死锁的条件


posted @ 2022-06-29 21:57  在线电影制作人  阅读(8)  评论(0)    收藏  举报  来源