• 并发编程(上)


    并发编程简介

    什么是并发编程

    串行 : 串行化,按照步骤进行,一步一步来,不能越级(比如洗茶具,打水,烧水,等水开,冲茶)

    并行 : 多个任务一起执行(打水,烧水的时候洗茶具,等水开,冲茶)

    并行的好处是可以缩短整个流程的时间

    并发编程的目的 :

    • 更加充分的利用资源
    • 加快程序的响应速度(耗时任务,web服务器)
    • 简化异步事件的处理

    什么时候适合使用并发编程 :

    • 任务会阻塞线程,导致之后的代码不能执行
    • 任务执行时间过长,可以划分为分工明确的子任务,比如: 分段下载
    • 任务间断性执行,比如: 日志打印
    • 任务本身需要协作执行,比如: 生产者和消费者

    并发编程之频繁的上下文切换

    上下文切换是指: cpu为线程分配时间片,时间片非常短(毫秒),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行.

    上下文的频繁切换,会带来一定的性能开销.如何减少上下文切换的开销呢?

    • 无锁(free-lock)并发编程 : 多线程竞争锁,会引起上下文切换,所以可以使用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
    • CAS算法 : Java中的Atomic包使用CAS算法来更新数据,而不需要加锁.
    • 使用最少的线程 : 避免创建不需要的线程,比如任务很少,但是创建了很多线程处理,这样会造成大量线程处于等待状态.
    • 协程 : 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换. --GO

    并发编程之死锁

    死锁也就是多个线程互相占有对方想要的资源,造成了等待的问题,写个程序显示死锁的情况:

    public class DeadLockDemo {
    
        private static final Object HAIR_A = new Object();
        private static final Object HAIR_B = new Object();
    
        public static void main(String[] args) {
            new Thread(()->{
                synchronized (HAIR_A){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (HAIR_B){
                        System.out.println("HAIR_A在前");
                    }
                }
            }).start();
    
            new Thread(()->{
                synchronized (HAIR_B){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (HAIR_A){
                        System.out.println("HAIR_B在前");
                    }
                }
            }).start();
        }
    }
    

    上面的代码就会出现死锁,造成了程序永远不会结束,可以打开命令行,输入jconsole,打开工具,可以检测死锁的情况.

    针对于上面的这种情况,如何预防死锁呢? 就是不要嵌套死锁,这么使用很容易造成死锁.

    并发编程之线程安全

    线程不完全也就是多个线程共同运行,执行某个操作,但操作的结果与预期的结果不相同,代码演示:

    /**
     * @ClassName UnSafeThread
     * @Author wz157
     * @Date 2018/12/15 14:13
     * @Description 线程不安全操作实例代码
     */
    public class UnSafeThread {
    
        private static int count = 0;
    
        /**
         * 参数10表示有10个线程
         * */
        private static CountDownLatch countDownLatch = new CountDownLatch(10);
    
        public static void create(){
            count ++;
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    for (int i1 = 0; i1 < 100; i1++) {
                        create();
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    countDownLatch.countDown();
                }).start();
            }
            while(true){
                if(countDownLatch.getCount() == 0){
                    System.out.println(count);
                    break;
                }
            }
        }
    
    }
    

    这个时候可以尝试将睡眠10ms去掉,这样的话,实际结果与预期结果就会相同.

    产生线程不安全问题的原因 : num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快速切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全问题.

    并发编程之资源限制

    硬件资源
    	服务器: 1m
    	本机:2m
    
    	带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。
    
    软件资源
    	数据库连接 500个连接  1000个线程查询  并不会因此而加快
    	socket
    

    线程的基础

    进程与线程的区别

    进程 : 是系统分配和管理资源的基本单位

    线程 : 进程的一个执行单元,是进行内调度的实体,是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单元. 线程也被称为轻量级线程,线程是程序执行的最小基本单元.

    一个程序最少一个进程,一个进程至少一个线程.

    进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。

    而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

    线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。

    如何处理好同步与互斥是编写多线程程序的难点。

    多进程程序更健壮,进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,

    而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,所以可能一个线程出现问题,进而导致整个程序出现问题

    额外补充一下程序的概念: 程序就是静态的代码,也就是源码.

    线程的状态及其转换

    在jdk的Thread类中,有一个枚举类State,里面定义了线程的6种状态:

    初始(NEW) : 新创建一个线程对象,但还没调用start()方法
    
    运行(RUNNABLE) : 调用start方法,处于可运行状态,等待操作系统的调度或者其他资源.
    
    阻塞(BLOCKED) : 线程阻塞于synchronized锁,等待获取synchronized锁的状态.
    
    等待(WAITING) : Object.wait(),join(),LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定的动作(通知或中断).也就是唤醒等
    
    超时等待(TIME_WAITING) : Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,该状态不同于WAITING,它可以在指定的时间内自行返回.
    
    终止(TERMINATED) : 表明该线程已经执行完毕
    

    线程的状态切换 :

    thread_state

    创建线程的方式

    方式一 : 继承Thread类

    public class MyThread extends Thread {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.setName("线程1");
            myThread.start();
        }
    }
    

    方式二 : 实现Runnable接口

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.setName("MyRunnable");
            thread.start();
        }
    }
    

    一般会选择第二种,因为java只允许单继承,但会允许多实现,所以一般采用第二种,增加程序的健壮性,代码可以共享,代码跟数据相互独立.

    调用start方法和调用run方法的区别 : start()方式是启动一个线程,而调用run方法相当于是普通方法的调用,不会重新启动线程,相当于从头到尾都是一个线程.

    方式三 : 实现Callable接口

    public class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("call() : " + Thread.currentThread().getName());
            return "MyCallable";
        }
    
        public static void main(String[] args) {
            // 创建对象
            Callable<String> myCallable = new MyCallable();
            // 创建FutureTask,并传递Callable接口的实现类对象
            FutureTask<String> futureTask = new FutureTask<>(myCallable);
            // 创建线程池对象
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            // 直接执行线程
            executorService.submit(myCallable);
            // 执行线程,通关过FutureTask来执行线程
            executorService.execute(futureTask);
    
            try {
                System.out.println("futureTask.get() : " + futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    

    方式四 : 线程池的方式创建

    public class ThreadPool {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
    

    方式五 : 匿名内部类的方式创建

    public class MyInnerClass {
    
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            thread.setName("innerClass");
            thread.start();
        }
    }
    

    方式六 : lambda的方式创建

    public class MyLambda {
    
        public static void main(String[] args) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
            }).start();
        }
    }
    

    线程的挂起和恢复

    什么是线程的挂起?  线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行

    为什么挂起线程?  cpu分配的时间片非常短、同时也非常珍贵。避免资源的浪费。

    如何挂起线程?

    被废弃的方法
        thread.suspend()  和  thread.resume()
    
    可以使用的方法
    	wait() 暂停执行、放弃已经获得的锁、进入等待状态
    	notify() 随机唤醒一个在等待锁的线程
    	notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源
    

    什么时候适合使用挂起线程? 等待某些未就绪的资源,让线程进入挂起状态.当资源就绪,调用notify方法让线程进行运行状态.

    线程的中断操作

    stop()方法可以用来中断线程,但是不要使用,已经废弃,这个方法一旦使用,线程立刻停止,可能会引发相应的线程安全问题.

    interrupt()方法可以用来中断线程,看代码:

    public class InterruptDemo implements Runnable {
        @Override
        public void run() {
            // 判断状态,是否打上标记,如果为true,就会退出
            while (! Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName());
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new InterruptDemo());
    
            thread.start();
    
            Thread.sleep(2000L);
            // interrupt方法只是打上标记
            thread.interrupt();
        }
    }
    

    自行定义标志,用来进行判断,模仿interrupt()方法

    public class InterruptDemo2 implements Runnable {
    
        private static volatile boolean FLAG = true;
    
        @Override
        public void run() {
            // 判断状态,是否打上标记,如果为true,就会退出
            while (FLAG){
                System.out.println(Thread.currentThread().getName());
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new InterruptDemo2());
            thread.start();
            Thread.sleep(2000L);
            FLAG = false;
        }
    }
    

    线程的优先级

    线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级的那个线程。但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。

    线程的优先级,最大优先级是10,最小优先级是1,默认的优先级是5,优先级大于10或小于0,会抛出异常.

    代码案例:

    public class PriorityDemo {
    
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            thread.setPriority(Thread.MAX_PRIORITY);
            thread.setName("Thread-01 : " + thread.getPriority());
            thread.start();
    
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            // 设置优先级
            thread2.setPriority(Thread.MIN_PRIORITY);
            thread2.setName("Thread-02: " + thread2.getPriority());
            thread2.start();
        }
    }
    

    守护线程

    线程的分类: 用户线程和守护线程

    守护线程: 任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。当JVM实例中最后一个非守护线程结束时,守护线程也随JVM一起退出

    守护线程的用处 : jvm垃圾清理线程

    建议: 尽量少使用守护线程,因为其不可控. 不要在守护线程里去进行读写操作,执行计算逻辑.

    代码编写:

    public class DaemonThreadDemo implements Runnable {
        @Override
        public void run() {
            while (true){
                System.out.println(Thread.currentThread().getName());
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            // 用户线程
            Thread thread = new Thread(new DaemonThreadDemo());
            // 在start之前调用,否则无效,并会抛出异常
            thread.setDaemon(true);
            thread.start();
            // 当主线程退出,守护线程也结束
            Thread.sleep(2000);
    
        }
    }
    

    线程的安全性

    什么是线程安全性

    当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替运行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. -----并发编程实战一书

    什么是线程不安全? 多线程并发访问时,得不到正确的结果.

    原子性操作

    什么是原子性操作? 一个操作或多个操作,要么全部执行并且执行过程不会被任何因素打断,要么都不执行. 这的原子性与mysql事务中的原子性是一样的道理.

    如何将非原子性操作变成原子性?

    使用volatile关键字修饰变量,但是volatile仅仅保证可见性,并不保证原子性.
    synchronized关键字,是的操作具有原子性.也就是在方法上加上synchronized关键字
    

    深入理解synchronized

    内置锁 : 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁.线程进入同步代码块或方法的时候会自动获取该锁,在退出同步代码块或方法时会释放该锁. 获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法.

    互斥锁 : 内置锁就是互斥锁,这意味着最多只有一个线程能够获得该锁,当线程A尝试去获取线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁. 如果线程B不释放这个锁,那么线程A将永远等待下去.

    synchronized的使用场景:

    修饰普通的方法 : 锁住对象实例(普通方法属于对象)
    
    修饰静态方法 : 锁住整个类(静态方法属于类),尽量不要使用
    
    修饰代码块 : 锁住传入的对象(synchronized(lock))
    

    synchronized代码块是由一块monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元. jdk6之前,synchronized是使用操作系统的互斥锁实现的,需要进行用户态到内核态的奇幻,这个是无差别的重量级操作.在现在使用的jdk中,对synchronized进行了新的实现,也就是三种不同的锁: 偏斜锁、轻量级锁、重量级锁. 并且当JVM检测到不同的竞争状况时,会有锁的升级和降级,自动切换到适合的锁实现. 没有竞争出现,默认会使用偏斜锁,当有另外的线程试图锁定某个已经被偏斜过的对象,JVM会撤消偏斜锁,切换成轻量级锁,轻量级锁依赖CAS操作来试图获取锁,重试成功,使用普通的轻量级锁;否则,进一步升级为重量级锁.

    volatile关键字

    volatile关键字只能修饰变量,保证该对象的可见性. 禁止指令重排序(内存模型).

    A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说是可见的.

    使用场景 :

    作为线程开关
    
    单例、修饰对象实例,禁止指令重排序
    

    作为线程开关:

    public class VolatileDemo implements Runnable {
    
        private static volatile boolean flag = true;
        
        @Override
        public void run() {
            while(true){
                if(flag){
                    System.out.println(Thread.currentThread().getName());
                } else{
                    System.out.println("flag : " + flag);
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            new Thread(new VolatileDemo()).start();
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            VolatileDemo.flag = false;
        }
    }
    

    单例和线程安全

    饿汉式----本身是线程安全的, 在类加载的时候,就已经进行实例化,无论这个类会不会被使用. 如果该类比较占内存,之后有没有用到,就白白浪费资源.

    public class HungerSingleton {
    
        private static HungerSingleton ourInstance = new HungerSingleton();
    
        public static HungerSingleton getInstance() {
            return ourInstance;
        }
    
        /**
         * 构造器必须私有化
         */
        private HungerSingleton() {
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    HungerSingleton hungerSingleton =  HungerSingleton.getInstance();
                    System.out.println(hungerSingleton);
                }).start();
            }
        }
    }
    

    懒汉式 ---- 在需要的时候进行实例化

    public class LazySingleton {
        /**
         *  加上volatile关键字,防止指令重排序
         */
        private static volatile LazySingleton lazySingleton;
    
        private LazySingleton(){}
    
        public static LazySingleton getInstance(){
            // 如果不存在,直接new; 如果存在,直接返回
            if(null == lazySingleton){
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(LazySingleton.class){
                    if(null == lazySingleton) {
                        lazySingleton = new LazySingleton();
                    }
                }
            }
            return lazySingleton;
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    System.out.println(LazySingleton.getInstance());
                }).start();
            }
        }
    }
    

    如何避免线程安全性问题

    线程安全性问题成因 :

    多线程环境
    
    多个线程操作同一共享资源
    
    对该共享资源进行了非原子性操作 
    

    如何避免(打破成因中三点任意一点)

    将多线程环境改为单线程(必要的代码访问使用加锁访问)
    
    将共享资源变为不共享(ThreadLocal、不共享、操作无状态化、不可变)
    
    将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)
    

    锁的分类

    自旋锁 : 线程状态及上下文切换消耗资源,当访问共享资源的时间短,频繁上下文切换不值得. jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,执行几次空循环后,如果还没获得锁,则被挂起.

    阻塞锁 : 阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态.

    重入锁 : 支持线程再次进入的锁.比如说ReentrantLock.

    读写锁 : 两把锁,读锁和写锁,写写互斥、读写互斥、读读共享

    互斥锁 : 任一时刻,只有一个线程访问该对象

    悲观锁 : 总是假设最坏的情况,每次拿数据的时候都会认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞知道它拿到锁.

    乐观锁 : 每次那数据的时候都认为别人不会修改,所以不会上锁,但在更新的时候会判断在此期间别人有没有去更新这个数据. 一帮有两种实现方式: 版本号控制和CAS.

    公平锁 : 加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,也就是排队等待,先到先得.

    非公平锁 : 线程加锁时直接尝试获取锁,获取不到的话就自动到队尾等待.

    独占锁 : 独占锁模式下,每次只能有一个线程能持有锁.

    共享锁 : 允许多个线程同时获得锁,并发访问共享资源

    偏斜锁 : 偏斜锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏斜锁时,持有偏向锁的线程才会释放锁. 简单来说偏斜其实不算一种锁,一种假的锁.

    轻量级锁 : 当出现竞争条件是,jvm会撤销偏斜锁,尝试加轻量级锁,这个叫锁的升级. 使用自旋锁实现.

    重量级锁 : 尝试加轻量级锁失败,会转为重量级锁,也是锁的升级. 依靠操作系统来实现.

    Lock接口

    Lock的使用(使用ReentrantLock来实现) :

    public class UnSafeThread {
    
        private static int count = 0;
    
        /**
         * 参数10表示有10个线程
         * */
        private static CountDownLatch countDownLatch = new CountDownLatch(10);
    
        private static Lock lock = new ReentrantLock();
    
        public static void create(){
            lock.lock();
            count ++;
            lock.unlock();
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    for (int i1 = 0; i1 < 100; i1++) {
                        create();
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    countDownLatch.countDown();
                }).start();
            }
            while(true){
                if(countDownLatch.getCount() == 0){
                    System.out.println(count);
                    break;
                }
            }
        }
    }
    

    Lock与synchronized的区别(ReentrantLock与synchronized的区别) :

    Lock需要手动控制,也就是需要手动调用lock方法加锁,调用unlock方法释放锁.Lock是一种乐观锁,使用CAS机制
    
    synchronized在1.5之前依靠操作系统的互斥来实现,之后提供了三种实现: 偏斜锁、轻量级锁、重量级锁,不需要手动调用,只需要将同步代码代码synchronized代码块中就好
    

    实现了lock接口的锁 :

    lock

    内部的方法 :

    lockMethods

    实现自己的锁

    实现自己的锁需要实现Lock接口,模仿ReentrantLock,重写lock方法和unlock方法:

    public class MyLock implements Lock {
    
        /**
         * 定义一个标识,用来判断当前锁是否已被持有
         */
        private boolean isHoldLock = false;
    
        /**
         * 重入的线程
         */
        private Thread holdLockThread = null;
    
        /**
         * 重入次数
         */
        private int reentryCount = 0;
    
        /**
         * 同一时刻,能且仅能有一个线程获取到锁,其他线程只能等待该线程释放锁之后才能获取到锁
         */
        @Override
        public synchronized void lock() {
            if(isHoldLock && Thread.currentThread() != holdLockThread){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            holdLockThread = Thread.currentThread();
            isHoldLock = true;
            reentryCount ++ ;
        }
        /**
         *
         */
        @Override
        public synchronized void unlock() {
            // 判断当前线程是否是持有锁的线程,是, 重入次数减去1,不是就不进行操作
            if(Thread.currentThread() == holdLockThread){
                reentryCount --;
                // 重入次数为0的时候才允许释放锁
                if(reentryCount == 0) {
                    notify();
                    isHoldLock = false;
                }
            }
        }
        
        @Override
        public void lockInterruptibly() throws InterruptedException {
    
        }
    
        @Override
        public boolean tryLock() {
            return false;
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    
        @Override
        public Condition newCondition() {
            return null;
        }
    }
    

    使用可重入对象和可重入次数才实现可重入,如果不实现可重入,可能造成单条线程处于waiting状态.

    测试的代码,表名自己实现的锁可重入:

    public class ReentryDemo {
        public Lock lock = new MyLock();
        public void methodA(){
            lock.lock();
            System.out.println("进入方法A");
            methodB();
            lock.unlock();
        }
    
        public void methodB(){
            lock.lock();
            System.out.println("进入方法B");
            // methodA();
            lock.unlock();
        }
    
        public static void main(String[] args) {
            ReentryDemo reentryDemo = new ReentryDemo();
            reentryDemo.methodA();
        }
    }
    

    AbstractQueuedSynchronizer

    AbstractQueuedSynchronizer----为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等等)提供一个框架. 此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。 假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。

    此类支持默认的独占模式和共享模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的FIFO队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。

    此类通过支持独占模式的子类定义了一个嵌套的AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作Condition实现。isHeldExclusively()方法将报告同步对于当前线程是否是独占的;使用当前getState() 值调用release(int)方法则可以完全释放此对象;如果给定保存的状态值,那么acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject的行为当然取决于其同步器实现的语义。

    此类为内部队列提供了检查、检测和监视方法,还为condition对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer将这些方法导出到类中。

    此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject方法,该方法在反序列化时将此对象恢复到某个已知初始状态。

    主要方法:

    tryAcquire(int)
    tryRelease(int)
    tryAcquireShared(int)
    tryReleaseShared(int)
    isHeldExclusively()
    	Acquire:
         while (!tryAcquire(arg)) {
    	        enqueue thread if it is not already queued;
    	        possibly block current thread;
    	     }
    
    	Release:
    		   if ((arg))
    		        unblock the first queued thread;
    

    ReentrantLock

    阅读源码的方法:

    一段简单的代码
    看构造
    看类之间的关系,形成关系图
    看使用到的方法,并逐步理解,边看代码边看注释
    debug
    

    对于ReentrantLock的源码分析以后研究,还有StampedLock

    线程间的通信

    wait notify notifyAll

    何时使用: 在多线程环境下,有时候一个线程的执行,依赖于另外一个线程的某种状态的改变,这个时候,我们就可以使用wait与notify或者notifyAll. 空循环

    public class Demo {
    
        private static boolean flag = false;
    
        public static void main(String[] args) {
            // 线程空循环,会造成资源的浪费
            new Thread(()->{
               while(!flag){
                   try {
                       Thread.sleep(1000L);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println("flag is false");
               }
                System.out.println("flag is true");
            }).start();
    
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(()->{
                flag = true;
            }).start();
        }
    }
    

    wait和sleep的区别 : wait会释放持有的锁,而sleep不会,sleep是让线程在指定的时间内,不去抢占cpu的资源.

    注意点 : wait和notify必须放在同步代码块中,且必须拥有当前对象锁.也就是不能利用A对象的锁去调用B对象的wait, 那个对象的wait就得调用哪个对象的notify. 简单理解wait和notify是依赖与对象存在的.

    public class Demo1 {
    
        private static boolean flag = false;
    
        public static void main(String[] args) {
    
            Object obj = new Object();
    
            // 线程空循环,会造成资源的浪费
            new Thread(()->{
                synchronized (obj){
                    while(!flag){
                        System.out.println("flag is false");
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
    
                System.out.println("flag is true");
            }).start();
    
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(()->{
                synchronized (obj){
                    flag = true;
                    obj.notify();
                }
            }).start();
        }
    }
    

    notify与notifyAll的区别 : notify会随机唤醒一个等待的线程. notifyAll唤醒所有等待的线程.

    等待通知之生产消费

    生产者消费者模型一般包括三个角色 : 生产者 消费者 中间商

    代码进行展示:

    中间商的代码:
        public class Medium {
    
            /**
             * 库存
             */
            private int num = 0;
        
            /**
             * 库存最大容量
             */
            private static final int TOTAL = 20;
        
            /**
             * 生产
             */
            public synchronized void put(){
                // 判断当前库存是否已达最大容量
                if(num < TOTAL){
                    // 如果不是,生产完成之后通知消费者进行消费
                    System.out.println("生产者进行生产 : " + (++ num));
                    // 唤醒所有线程,让消费者进行消费
                    notifyAll();
                } else {
                    // 如果是,通知生产者暂停生产,进入等待,等待消费者进行消费
                    try {
                        System.out.println("库存已满,新增操作等待!!!" + num);
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        
            /**
             * 消费
             */
            public synchronized void take(){
                // 判断当前库存是否大于0
                if(num > 0){
                    // 是,进行消费,通知生产者进行生产
                    System.out.println("消费者进行消费,消费过后的剩余产品数 : " + (--num));
                    // 唤醒所有线程
                    notifyAll();
                } else{
                    // 不是,消费者等待,通知生产者进行生产
                    System.out.println("商品数量为0,等待生产者进行生产 : " + num);
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        
    生产者代码:
        public class Producer implements Runnable{
    
            private Medium medium;
        
            public Producer(Medium medium) {
                this.medium = medium;
            }
        
            @Override
            public void run() {
                while(true){
                    medium.put();
                }
        
            }
        }
        
    消费者代码:
        public class Consumer implements Runnable{
    
            private Medium medium;
        
            public Consumer(Medium medium) {
                this.medium = medium;
            }
        
            @Override
            public void run() {
                while(true) {
                    medium.take();
                }
            }
        }
        
    测试类代码:
        public class Main {
    
            public static void main(String[] args) {
        
                Medium medium = new Medium();
        
                for (int i = 0; i < 10; i++) {
                    new Thread(new Producer(medium)).start();
                }
        
                for (int i = 0; i < 10; i++) {
                    new Thread(new Consumer(medium)).start();
                }
        
            }
        }
    

    使用管道流进行通信

    以内存为媒介,用于线程之间的数据传输. 主要有面向字节(PipedOutputStream,PipedInputStream)和面向字符(PipedReader,PipedWriter)两种.

    管道的读操作:
        public class Reader implements Runnable{
    
            /**
             * 读字节管道流
             */
            private PipedInputStream pipedInputStream;
        
            public Reader(PipedInputStream pipedInputStream) {
                this.pipedInputStream = pipedInputStream;
            }
        
            @Override
            public void run() {
                if(pipedInputStream != null){
                    String collect = new BufferedReader(new InputStreamReader(pipedInputStream)).lines().collect(Collectors.joining("
    "));
                    System.out.println(Thread.currentThread().getName() + ":" +  collect);
                }
                try {
                    pipedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
    // 管道流通信的测试
        public class Main {
    
            public static void main(String[] args) throws IOException {
                PipedInputStream pipedInputStream = new PipedInputStream();
        
                PipedOutputStream pipedOutputStream = new PipedOutputStream();
        
                try {
                    // 管道连接
                    pipedOutputStream.connect(pipedInputStream);
                } catch (IOException e) {
                    e.printStackTrace();
                }
        
                // 创建线程
                new Thread(new Reader(pipedInputStream)).start();
        
                BufferedReader bufferedReader = null;
        
                try {
                    // 字符输入流
                    bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        
                    // 管道流进行写入
                    pipedOutputStream.write(bufferedReader.readLine().getBytes());
                } finally {
                    pipedOutputStream.close();
                    if(bufferedReader != null){
                        bufferedReader.close();
                    }
                }
            }
        }
    

    Thread.join通信

    使用场景 : 线程A执行到一半,需要一个数据,这个数据需要线程B去执行修改,只有B修改完成之后,A才能继续操作.

    public class Main {
    
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "开始运行");
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行结束");
            }, "线程1");
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "开始运行");
                thread.start();
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行结束");
            },"线程2").start();
        }
    }
    

    上面的执行结果是 线程2开始 线程1开始 线程1结束 线程2结束, 如果不使用thread.join的结果呢? 线程2开始 线程2结束 线程1开始 线程1结束,所以thread.join的目的就是将当前的时间片分给thread,等thread执行完继续前面的操作

    ThreadLocal的使用

    ThreadLock : 线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构. 为每个线程单独存放一份变量副本,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值. 只要线程处于活动状态并且ThreadLocal实例可访问,那么每个线程都拥有对其本地线程副本的隐式引用变量,一个线程消失后,它的所有副本线程局部实例受垃圾回收(除非存在其他对这些副本的引用)

    一般用的比较多的是:

    ThreadLocal.get : 获得ThreadLocal中当前线程共享变量的值
    ThreadLocal.set : 设置ThreadLocal中当前线程共享变量的值
    ThreadLocal.remove : 移除ThreadLocal中当前线程共享变量的值
    ThreadLocal.initialValue : ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值.
    

    代码展示:

    public class ThreadLocalDemo {
        /**
         * 为num赋值为0,ThreadLocal为每个对象存放一个单独的副本
         */
        ThreadLocal<Integer> num = ThreadLocal.withInitial(()->0);
    
        /**
         * 对num值进行自增
         */
        public void inCreate(){
            Integer myNum = num.get();
            myNum ++;
            System.out.println(Thread.currentThread().getName() + ":" + myNum);
            num.set(myNum);
        }
    
        public static void main(String[] args) {
    
            ThreadLocalDemo localDemo = new ThreadLocalDemo();
    
            for (int i = 0; i < 3; i++) {
                int finalI = i;
                new Thread(()->{
                    while(true) {
                        localDemo.inCreate();
                        try {
                            Thread.sleep(1000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        }
    }
    

    ThreadLocal是为每个对象存放一个单独的副本,对象与对象间互不影响

    Condition的使用

    Condition : 可以在一个锁里面,存在多种等待条件

    主要的方法 :

    await
    signal
    signalAll
    

    参考前面的生产者和消费者,修改中间的代码为下:

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @ClassName Medium
     * @Author wz157
     * @Date 2018/12/17 16:37
     * @Description 中间商
     */
    public class Medium {
    
        /**
         * 库存
         */
        private int num = 0;
    
        /**
         * 库存最大容量
         */
        private static final int TOTAL = 20;
    
        /**
         * 创建锁,ReentrantLock
         */
        private Lock lock = new ReentrantLock();
    
        /**
         * 声明变量,代表消费者的锁对象
         */
        private Condition consumerCondition = lock.newCondition();
    
        /**
         * 声明变量,代表生产者的锁对象
         */
        private Condition producerCondition = lock.newCondition();
    
        /**
         * 生产
         */
        public void put(){
            lock.lock();
            try {
                // 判断当前库存是否已达最大容量
                if(num < TOTAL){
                    // 如果不是,生产完成之后通知消费者进行消费
                    System.out.println("生产者进行生产 : " + (++ num));
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 唤醒消费者,让消费者进行消费
                    consumerCondition.signalAll();
                } else {
                    // 如果是,通知生产者暂停生产,进入等待,等待消费者进行消费
                    try {
                        System.out.println("库存已满,新增操作等待!!!" + num);
                        producerCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();
            }
        }
    
        /**
         * 消费
         */
        public void take(){
    
            lock.lock();
            try{
                // 判断当前库存是否大于0
                if(num > 0){
                    // 是,进行消费,通知生产者进行生产
                    System.out.println("消费者进行消费,消费过后的剩余产品数 : " + (--num));
                    try {
                        Thread.sleep(500L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 唤醒所生产者
                    producerCondition.signalAll();
                } else{
                    // 不是,消费者等待,通知生产者进行生产
                    System.out.println("商品数量为0,等待生产者进行生产 : " + num);
                    try {
                        consumerCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();
            }
        }
    }
  • 相关阅读:
    笔记:Oracle查询重复数据并删除,只保留一条记录
    64位系统安装ODBC驱动的方法
    批量Excel数据导入Oracle数据库
    Oracle自我补充之Decode()函数使用介绍
    解决nginx: [error] open() "/usr/local/nginx/logs/nginx.pid" failed错误
    Linux命令区
    Linux下安装PHP+Nginx+Msql
    Thinkphp时间转换与统计的问题
    phpStydy配置memcache扩展
    Thinkphp+Nginx(PHPstudy)下报的404错误,403错误解决
  • 原文地址:https://www.cnblogs.com/wadmwz/p/10140625.html
Copyright © 2020-2023  润新知