栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程之间的数据共享——线程通信(面试重点)、synchronized锁

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程之间的数据共享——线程通信(面试重点)、synchronized锁

线程通信
  • 前言
  • 一、线程通信
    • 1.1JVM下的内存区域划分
    • 1.2 代码中的存储理解
  • 二、线程安全
    • 2.1线程不安全举例
    • 2.2线程不安全原因(重点知识、面试重点)
      • 2.2.1站在开发者的角度
      • 2.2.2系统角度解释
        • 2.2.2.1原子性被破坏
        • 2.2.2.2 内存可见性,导致某些线程读取“脏数据”
        • 2.2.2.3 代码重排序
    • 2.3线程安全问题小结
    • 2.4常见类的线程安全问题
  • 三、保护线程安全的措施——锁(lock)
    • 3.1synchronized 锁 /同步锁/monitor锁(监视器锁)
      • 3.1.1语法
      • 3.1.2什么是锁
      • 3.1.3判断一下那些会互斥
      • 3.1.4 JVM中的锁
      • 3.1.5synchronized 的作用
        • 3.1.5.1保证临界区的原子性
        • 3.1.5.2内存可见性
        • 3.1.5.3增加约束代码的重排序
    • 3.2JUC下的锁
      • 3.2.1lock()
      • 3.2.2.lockInterruptibly()
      • 3.2.3try.lock()
      • 3.2.4tryLock(5, TimeUnit.SECONDS)
    • 3.3synchronized VS JUC下的锁
  • 四、线程的状态
    • 4.1阻塞状态
  • 五、volatile修饰变量
    • 5.1保护内存可见性
    • 5.2原子性
    • 5.3保证代码重排序
  • 六、单列模式
    • 6.1饿汉模式
    • 6.2懒汉模式
  • 七、生产-消费者模型、wait()和notify()
    • 7.1wait()和notify()
    • 7.2notify()唤醒
    • 7.3notify()和wait小结
    • 7.4与synchronized对应到lock也有相似的功能
  • 八、阻塞队列
    • 8.1阻塞队列实现
  • 总结


前言
一、线程通信

1.1JVM下的内存区域划分


1.2 代码中的存储理解






二、线程安全

2.1线程不安全举例
// 演示线程不安全的现象
public class Main {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 100;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();

        add.join();
        sub.join();

        // 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
        // 所以,结果应该是 0
        //但是每次输出结果都不同
        System.out.println(r);
    }
}
2.2线程不安全原因(重点知识、面试重点) 2.2.1站在开发者的角度

2.2.2系统角度解释

2.2.2.1原子性被破坏

2.2.2.2 内存可见性,导致某些线程读取“脏数据”



2.2.2.3 代码重排序


2.3线程安全问题小结

2.4常见类的线程安全问题


三、保护线程安全的措施——锁(lock) 3.1synchronized 锁 /同步锁/monitor锁(监视器锁) 3.1.1语法



一个是对当前对象加锁,一个是对当前类加锁

3.1.2什么是锁



3.1.3判断一下那些会互斥


3.1.4 JVM中的锁

3.1.5synchronized 的作用

加锁操作使得互斥(synchronized和我们一起配合(我们需要真正的使用synchronized))

3.1.5.1保证临界区的原子性



看4/24的开头和00:58~1:18

3.1.5.2内存可见性

3.1.5.3增加约束代码的重排序

3.2JUC下的锁

3.2.1lock()
public class Main1 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            lock.lock();
            System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
        }
    }

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

        MyThread t = new MyThread();
        t.start();

        t.join();
    }
}

上述代码中的永远加不到锁的理解

3.2.2.lockInterruptibly()

使用lock.lockInterruptibly
输出结果:收到停止信号,停止运行

public class Main3 {
    private static final Lock lock = new ReentrantLock();
    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lockInterruptibly();//允许停止的情况
                System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
            } catch (InterruptedException e) {
                System.out.println("收到停止信号,停止运行");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();
        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);
        t.interrupt();//直接使用尝试让子线程停下来,但实际会徒劳无功,所以用上面的捕获
        t.join();
    }
}

3.2.3try.lock()

使用lock.try.lock返回一个boolean,加锁失败后还可以有其他操作,更加灵活
输出:加锁失败

static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = lock.tryLock();
            if (b == true) {
                // 加锁成功了
                System.out.println("加锁成功");
                System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
            } else {
                System.out.println("加锁失败");
            }
        }
    }
3.2.4tryLock(5, TimeUnit.SECONDS)

使用lock.tryLock(5, TimeUnit.SECONDS),会等上5秒,更加灵活

public class Main5 {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = false;
            try {
                b = lock.tryLock(5, TimeUnit.SECONDS);//会等上5秒
                if (b == true) {
                    // 加锁成功了
                    System.out.println("加锁成功");
                    System.out.println("子线程进入临界区");
                } else {
                    System.out.println("5s 之后加锁失败");
                }
            } catch (InterruptedException e) {
                System.out.println("被人打断了");
            }
        }
    }

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

        MyThread t = new MyThread();
        t.start();

//        TimeUnit.SECONDS.sleep(2);
//        t.interrupt();      // 休息2 秒,然后输出:被人打断

//        TimeUnit.SECONDS.sleep(2);
//        lock.unlock();       //2秒之后把锁释放,然后可以正常取到锁,所以会,输出:加锁成功,子线程进入临界区

        TimeUnit.SECONDS.sleep(20);
        lock.unlock();  //由于休息了20秒,才解锁,但是上面只等待5秒,所以加锁失败输出:5秒之后加锁失败
       
        t.join();
    }
}

3.3synchronized VS JUC下的锁

四、线程的状态 4.1阻塞状态


阻塞状态转换图

public class Main2 {
    static Object lock = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (lock) {   // 永远请求不到锁 —— 死锁    BLOCKED
                System.out.println("永远到不了");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        synchronized (lock) {
            MyThread t = new MyThread();
            t.start();

            t.join();       // 等待的时候不会释放锁       WAITING
        }
    }
}
public class Main {
    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                TimeUnit.DAYS.sleep(1);     // TIMED_WAITING
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

//        t.join();       // WAITING
        t.join(1000 * 60);  // 一分钟    TIMED_WAITING
    }
}
五、volatile修饰变量

5.1保护内存可见性

没有volatile时,main即使改变quit,但是run()方法中的quit仍任是false,无法停止循环
有volatile时,main中改变quit,run()方法中quit改变,停止循环,输出结果
使得可以看到变量的更改

public class WithOutVolatile {
    volatile static boolean quit = false;

    static class MyThread extends Thread {
        @Override
        public void run() {
            long r = 0;
            while (quit == false) {
                r++;
            }
            System.out.println(r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(5);

        quit = true;
    }
}

5.2原子性

5.3保证代码重排序


六、单列模式

一开始就初始化(饿汉模式——等不及)
等用到的时候才初始化(懒汉模式)

6.1饿汉模式

天生就是线程安全的

public class StarvingMode {
    // 是线程安全的
    // 类加载的时候执行
    // JVM 保证了类加载的过程是线程安全的
    //所以只执行一次初始化
    private static StarvingMode instance = new StarvingMode();
    //谁要去调用这个对象,就要去调用下面这个静态方法
    public static StarvingMode getInstance() {
        return instance;//所以拿到的都是instance指向的对象,保证了整个应用过程中只有这一个对象
    }
    //为了保证没人可以用静态方法初始化其他对象
    //用private修饰只能在本类中只用,其他想调用也无法使用
    private StarvingMode() {}
}
6.2懒汉模式
public class LazyModeV1 {
    private static LazyModeV1 instance = null;//先不实例化对象

    public static LazyModeV1 getInstance() {
        // 第一次调用这个方法时,说明我们应该实例化对象了
        //之后则会直接返回instance,因此只初始化一次
        if (instance == null) {
            instance = new LazyModeV1();    // 只在第一次的时候执行
        }

        return instance;
    }
    private LazyModeV1() {}

由于if语句对之前instance既共享又有修改所以线程不安全,当原子性被破坏时多个线程可能进行多次初始化赋值

为了保证线程安全,可以对其进行加锁操作

public class LazyModeV2 {
    private static LazyModeV2 instance = null;

    public synchronized static LazyModeV2 getInstance() {
        // 第一次调用这个方法时,说明我们应该实例化对象了
        if (instance == null) {
            instance = new LazyModeV2();    // 只在第一次的时候执行
        }

        return instance;
    }

    private LazyModeV2() {}
}

但是该方法会对每一个线程进行加锁,所以需要优化支队第一个线程进行加锁
仔细阅读每一个注释并理解

public class LazyModeV3 {
    private volatile static LazyModeV3 instance = null;

    public static LazyModeV3 getInstance() {
        // 第一次调用这个方法时,说明我们应该实例化对象了
        if (instance == null) {
            // 只有 instance 还没有初始化时,才会走到这个分支
            // 这里没有锁保护,所以理论上可以有很多线程同时走到这个分支

            synchronized (LazyModeV3.class) {   // 通过上面的条件,
                // 让争抢锁的动作只在 instance
                // 实例化之前才可能发生。
                // 实例化之后就不再可能
                // 加锁之后才能执行
                // 第一个抢到锁的线程,看到的 instance 是 null
                // 其他抢到锁的线程,看到的 instance 不是 null
                // 保证了 instance 只会被实例化一次
                if (instance == null) {
                    instance = new LazyModeV3();    // 只在第一次的时候执行
                    // 当重排序成 1 -> 3 -> 2 的时候可能出问题
                    // 通过 volatile 修复
                }
            }
        }

        return instance;
    }

    private LazyModeV3() {}
}

假设第一次执行初始化的时候,A线程执行1->3还没有走到2被切走了,又接着进行下一个线程继续执行。呢么初始化已经出现错误,后续依然是错误的,因此需要用volatile进行修复

七、生产-消费者模型、wait()和notify() 7.1wait()和notify()


import java.util.concurrent.TimeUnit;

public class Demo1 {
    static class MyThread extends Thread {
        private Object o;

        MyThread(Object o) {
            this.o = o;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);//等待5秒钟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (o) {
                System.out.println("唤醒主线程");
                o.notify();
            }
        }
    }

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

        synchronized (o) {
            MyThread t = new MyThread(o);//保证主线程先执行,先唤醒再等待
            t.start();

            o.wait();   // 1. 释放 o 锁;2.等待 ......  3. 再次加锁
                        // wait() 过程中是不持有锁的,所以notify能加锁
            System.out.println("永远不会到达");//若只有wait没有唤醒则永远不会到达,只有先唤醒才能wait成功
        }
    }
}

注意:
1.若只有wait没有notify则 永远不会执行System.out.println(“永远不会到达”);只有执行notify才能wait成功进而执行 System.out.println(“永远不会到达”);
先执行主线程加锁然后让wait释放锁,子线程才能加到锁然后执行notify,然后wait再次加锁再解锁
2.为什么wait和notify都能进行加锁?
因为,wait()1. 释放 o 锁;2.等待 … 3. 再次加锁,等待 过程中不持有锁
3. MyThread t = new MyThread(o)放入锁中可以避免

public class Demo2 {
    static Object lock1 = new Object();
    static Object lock2 = new Object();
    static Object lock3 = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (lock3) {
                System.out.println("lock3 成功");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        synchronized (lock1) {
            synchronized (lock2) {
                synchronized (lock3) {
                    MyThread t = new MyThread();
                    t.start();

                    // 主线程持有 3 把锁
                    lock3.wait();   // 释放 lock3 锁
                }
            }
        }
    }
}

没有释放lock1、lock2

7.2notify()唤醒


1.随机唤醒

public class Demo3 {
    static Object o = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (o) {
                try {
                    o.wait();
                    System.out.println(getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            MyThread t = new MyThread();
            t.start();
        }

        // 保证了子线程们先 wait,主线程就先休眠一会儿
        TimeUnit.SECONDS.sleep(5);

        synchronized (o) {
//            o.notify();
            o.notifyAll();
        }
    }
}

o.notify();随机唤醒一个
o.notifyAll();唤醒全部wait但是顺序不定

2.先wait再notify是正常执行的,但是如果先notify再wait 是不行的,因为wait无法感知notify,则wait会一直等下去

public class Demo4 {
    static Object o = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (o) {
                System.out.println("notify");
                o.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);

        synchronized (o) {
            System.out.println("wait");
            o.wait();
        }
    }
}

7.3notify()和wait小结

7.4与synchronized对应到lock也有相似的功能

条件变量Condition
功能和wait和notify可以说“一模一样”

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();

        Condition condition = lock.newCondition();

        condition.await();  // 等价于 wait
        condition.signal(); //等价于notify
        condition.signalAll();

        lock.lock();//只有在lock锁住的情况下才能执行wait
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }
}
八、阻塞队列

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Main1 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new LinkedBlockingDeque<>();
        String poll = queue.poll();
        System.out.println(poll);
        String take = queue.take(); // 只要队列中取不到元素,就会一直阻塞
        System.out.println("永远到达不了");

        BlockingQueue e = new ArrayBlockingQueue<>(2);
        e.put("1");
        e.put("2");
        e.put("3");
    }
}
8.1阻塞队列实现
public class MyArrayBlockingQueue {
    private long[] array;
    private int frontIndex; // 永远在队列的第一个元素位置
    private int rearIndex;  // 永远在队列的最后一个的下一个位置
    private int size;

    public MyArrayBlockingQueue(int capacity) {
        array = new long[capacity];
        frontIndex = 0;
        rearIndex = 0;
        size = 0;
    }

    public synchronized void put(long e) throws InterruptedException {
        // 判断队列是否已经满了
        while (array.length == size) {
            this.wait();
        }

        // 预期:队列一定不是满的

        array[rearIndex] = e;
        rearIndex++;
        if (rearIndex == array.length) {
            rearIndex = 0;
        }

        size++;

        notifyAll();
    }

    public synchronized long take() throws InterruptedException {
        while (size == 0) {
            wait();
        }

        long e = array[frontIndex];
        frontIndex++;
        if (frontIndex == array.length) {
            frontIndex = 0;
        }
        size--;

        notifyAll();

        return e;
    }
}

import java.util.Scanner;

public class Main {
    static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            long e = scanner.nextLong();

            try {
                queue.put(e);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        long take = queue.take();
        System.out.println(take);
    }
}
import java.util.Scanner;

public class Main2 {
    static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            long e = scanner.nextLong();

            try {
                queue.take();
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);   // 阻塞
        System.out.println("4 被放入队列中");
    }
}

总结




转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/926923.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号