精华内容
下载资源
问答
  • Java多线程

    万次阅读 多人点赞 2021-06-11 16:28:49
    Java多线程Java多线程线程的创建线程常见方法线程的状态线程的优先级守护线程线程组Java线程池线程池的创建线程池的参数线程池的使用线程不安全问题Java中的锁synchronized同步方法synchronized同步语句块...

    Java多线程

    线程的创建

    1.继承Thread
    2.实现Runnable
    3.实现Callable
    使用继承Thread类来开发多线程的应用程序在设计上是有局限性的,因为Java是单继承。
    继承Thread类

    public class ThreadDemo1 {
        // 继承Thread类 写法1
        static class MyThread extends Thread{
            @Override
            public void run() {
                //要实现的业务代码
            }
        }
    
        // 写法2
        Thread thread = new Thread(){
            @Override
            public void run() {
                //要实现的业务代码
            }
        };
    
    
    }
    

    实现Runnable接口

    //实现Runnable接口 写法1
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            //要实现的业务代码
        }
    }
    
    //实现Runnable接口 写法2 匿名内部类
    class MyRunnable2 {
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //要实现的业务代码
                }
            });
        }
    }
    

    实现Callable接口(Callable + FutureTask 创建带有返回值的线程)

    package ThreadDeom;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 17:34;
     */
    //创建有返回值的线程 Callable + Future
    public class ThreadDemo2 {
        static class MyCallable implements Callable<Integer>{
            @Override
            public Integer call() throws Exception {
                return 0;
            }
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建Callable子对象
            MyCallable myCallable = new MyCallable();
            //使用FutureTask 接受 Callable
            FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
            //创建线程并设置任务
            Thread thread = new Thread(futureTask);
            //启动线程
            thread.start();
            //得到线程的执行结果
            int num = futureTask.get();
        }
    }
    
    

    也可以使用lambda表达式

    class ThreadDemo21{
        //lambda表达式
        Thread thread = new Thread(()-> {
            //要实现的业务代码
        });
    }
    
    

    Thread的构造方法
    在这里插入图片描述

    线程常用方法

    获取当前线程的引用、线程的休眠

    class Main{
        public static void main(String[] args) throws InterruptedException {
            Thread.sleep(1000);
            //休眠1000毫秒之后打印
            System.out.println(Thread.currentThread());
            System.out.println(Thread.currentThread().getName());
        }
    }
    

    在这里插入图片描述

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 18:38;
     */
    public class ThreadDemo6 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    System.out.println("线程的ID:" + Thread.currentThread().getId());
                    System.out.println("线程的名称:" + Thread.currentThread().getName());
                    System.out.println("线程的状态:" + Thread.currentThread().getState());
    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"线程一");
    
            thread.start();
            Thread.sleep(100);
            //打印线程的状态
            System.out.println("线程的状态:"+thread.getState());
            System.out.println("线程的优先级:"+thread.getPriority());
            System.out.println("线程是否存活:"+thread.isAlive());
            System.out.println("线程是否是守护线程:"+thread.isDaemon());
            System.out.println("线程是否被打断:"+thread.isInterrupted());
        }
    }
    
    

    在这里插入图片描述

    线程的等待
    假设有一个坑位,thread1 和 thread2 都要上厕所。一次只能一个人上,thread2只能等待thread1使用完才能使用厕所。就可以使用join()方法,等待线程1执行完,thread2在去执行。👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 10:48;
     */
    public class ThreadDemo13 {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"🚾");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    System.out.println(Thread.currentThread().getName()+"出来了");
                }
            };
    
            Thread t1 = new Thread(runnable,"thread1");
            t1.start();
    
            //t1.join();
            Thread t2 = new Thread(runnable,"thread2");
            t2.start();
        }
    }
    
    

    在这里插入图片描述

    没有join()显然是不行的。加上join()之后:
    在这里插入图片描述

    线程的终止

    1.自定义实现线程的终止

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 9:59;
     */
    public class ThreadDemo11 {
        private static boolean flag = false;
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!flag){
                        System.out.println("我是 : " + Thread.currentThread().getName() + ",我还没有被interrupted呢");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("我是 "+Thread.currentThread().getName()+",我被interrupted了");
                }
    
            },"thread");
            thread.start();
    
            Thread.sleep(300);
            flag = true;
    
        }
    }
    
    

    在这里插入图片描述

    2.使用Thread的interrupted来中断

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 9:59;
     */
    public class ThreadDemo11 {
    //    private static boolean flag = false;
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!Thread.interrupted()){
                        System.out.println("我是 : " + Thread.currentThread().getName() + ",我还没有被interrupted呢");
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
    //                        e.printStackTrace();
                            break;
                        }
                    }
                    System.out.println("我是 "+Thread.currentThread().getName()+",我被interrupted了");
                }
    
            },"thread");
            thread.start();
    
            Thread.sleep(300);
            thread.interrupt();
    //        flag = true;
    
        }
    }
    
    

    在这里插入图片描述
    3.Thraed.interrupted()方法和Threaed.currentThread().interrupt()的区别
    Thread.interrupted()方法第一次接收到终止的状态后,之后会将状态复位,Thread.interrupted()是静态的,是全局的。Threaed.currentThread().interrupt()只是普通的方法。
    Thraed.interrupted()方法

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 10:32;
     */
    public class ThreadDemo12 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() ->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.interrupted());
                }
            });
    
            thread.start();
            thread.interrupt();
    
        }
    }
    
    

    在这里插入图片描述

    Threaed.currentThread().interrupt()

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 10:32;
     */
    public class ThreadDemo12 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() ->{
                for (int i = 0; i < 10; i++) {
    //                System.out.println(Thread.interrupted());
                    System.out.println(Thread.currentThread().isInterrupted());
                }
    
            });
    
            thread.start();
            thread.interrupt();
    
        }
    }
    
    

    在这里插入图片描述

    yield()方法
    让出CPU的执行权

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 11:47;
     */
    public class ThreadDemo15 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    Thread.yield();
                    System.out.println("thread1");
                }
            });
    
            thread1.start();
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    System.out.println("thread2");
                }
            });
    
            thread2.start();
        }
    }
    
    

    在这里插入图片描述

    线程的状态

    在这里插入图片描述

    打印出线程的所有的状态,所有的线程的状态都在枚举中。👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 11:06;
     */
    public class ThreadDemo14 {
        public static void main(String[] args) {
            for (Thread.State state: Thread.State.values()) {
                System.out.println(state);
            }
        }
    }
    
    

    在这里插入图片描述

    NEW 创建了线程但是还没有开始工作
    RUNNABLE 正在Java虚拟机中执行的线程
    BLOCKED 受到阻塞并且正在等待某个监视器的锁的时候所处的状态
    WAITTING 无限期的等待另一个线程执行某个特定操作的线程处于这个状态
    TIME_WAITTING 有具体等待时间的等待
    TERMINATED 已经退出的线程处于这种状态

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 11:06;
     */
    
    class TestThreadDemo{
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            System.out.println(thread.getState());
            thread.start();
            System.out.println(thread.getState());
    
            Thread.sleep(100);
    
            System.out.println(thread.getState());
    
            thread.join();
    
            System.out.println(thread.getState());
        }
    }
    

    在这里插入图片描述

    线程的优先级

    在Java中线程 的优先级分为1 ~ 10 一共十个等级

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 21:22;
     */
    public class ThreadDemo9 {
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                Thread t1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("t1");
                    }
                });
                //最大优先级
                t1.setPriority(10);
                t1.start();
                Thread t2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("t2");
                    }
                });
                //最小优先级
                t2.setPriority(1);
                t2.start();
                Thread t3 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("t3");
                    }
                });
                t3.setPriority(1);
                t3.start();
            }
        }
    }
    
    

    在这里插入图片描述
    线程的优先级不是绝对的,只是给程序的建议。
    线程之间的优先级具有继承的特性,如果A线程启动了B线程,那么B的线程的优先级与A是一样的。👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 20:46;
     */
    class ThreadA extends Thread{
        @Override
        public void run() {
            System.out.println("ThreadA优先级是:" + this.getPriority());
            ThreadB threadB = new ThreadB();
            threadB.start();
        }
    }
    
    class ThreadB extends ThreadA{
        @Override
        public void run() {
            System.out.println("ThreadB的优先级是:" + this.getPriority());
        }
    }
    public class ThreadDemo7 {
        public static void main(String[] args) {
    
            System.out.println("main线程开始的优先级是:" + Thread.currentThread().getPriority());
            
            System.out.println("main线程结束的优先级是:" + Thread.currentThread().getPriority());
    
            ThreadA threadA = new ThreadA();
            threadA.start();
        }
    }
    
    

    在这里插入图片描述

    再看👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 20:46;
     */
    class ThreadA extends Thread{
        @Override
        public void run() {
            System.out.println("ThreadA优先级是:" + this.getPriority());
            ThreadB threadB = new ThreadB();
            threadB.start();
        }
    }
    
    class ThreadB extends ThreadA{
        @Override
        public void run() {
            System.out.println("ThreadB的优先级是:" + this.getPriority());
        }
    }
    public class ThreadDemo7 {
        public static void main(String[] args) {
    
            System.out.println("main线程开始的优先级是:" + Thread.currentThread().getPriority());
            Thread.currentThread().setPriority(9);
            System.out.println("main线程结束的优先级是:" + Thread.currentThread().getPriority());
    
            ThreadA threadA = new ThreadA();
            threadA.start();
        }
    }
    
    

    结果为👇
    在这里插入图片描述

    守护线程

    Java中有两种线程:一种是用户线程,一种就是守护线程。
    什么是守护线程?守护线程是一种特殊的线程,当进程中不存在用户线程的时候,守护线程就会自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有了非守护线程,则垃圾回收线程也就没有存在的必要了。
    Daemon线程的作用就是为其他线程的运行提供便利的。👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 21:06;
     */
    
    public class ThreadDemo8 {
        static private int i = 0;
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        i++;
                        System.out.println(i);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            //设置守护线程
            thread.setDaemon(true);
            thread.start();
            Thread.sleep(5000);
            System.out.println("我是守护线程thread 当用户线程执行完成后 我也就销毁了😭哭了");
        }
    }
    
    

    在这里插入图片描述
    注意:守护线程的设置必须放在start()之前,否则就会报错。
    在这里插入图片描述
    在守护线程中创建的线程默认也是守护线程。

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 9:35;
     */
    public class ThreadDemo10 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(()->{
                Thread thread2 = new Thread(() -> {
                },"thread2");
                System.out.println("thread2是守护线程吗?:" + thread2.isDaemon());
    
            },"thread1");
    
            System.out.println("thread1是守护线程吗?:" + thread1.isDaemon());
    
            //thread1.setDaemon(true);
            thread1.start();
           // System.out.println("thread1是守护线程吗?:" + thread1.isDaemon());
    
        }
    }
    
    

    在这里插入图片描述
    再看👇

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 9:35;
     */
    public class ThreadDemo10 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(()->{
                Thread thread2 = new Thread(() -> {
                },"thread2");
                System.out.println("thread2是守护线程吗?:" + thread2.isDaemon());
    
            },"thread1");
    
            System.out.println("thread1是守护线程吗?:" + thread1.isDaemon());
    
            thread1.setDaemon(true);
            thread1.start();
            System.out.println("thread1是守护线程吗?:" + thread1.isDaemon());
    
        }
    }
    

    在这里插入图片描述

    线程组

    为了便于对某些具有相同功能的线程进行管理,可以把这些线程归属到同一个线程组中,线程组中既可以有线程对象,也可以有线程组,组中也可以有线程。
    使用线程模拟赛跑

    public class ThreadDemo5 {
        //线程模拟赛跑(未使用线程分组)
        public static void main(String[] args) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "到达了终点");
                }
            }, "选手一");
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "到达了终点");
                }
            }, "选手二");
    
            t1.start();
            t2.start();
    
    
            System.out.println("所有选手到达了终点");
        }
    }
    
    

    运行结果:
    在这里插入图片描述
    不符合预期效果,就可以使用线程组来实现

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-11;
     * time: 18:24;
     */
    class ThreadGroup1 {
        //线程分组模拟赛跑
        public static void main(String[] args) {
            ThreadGroup threadGroup = new ThreadGroup("Group");
            Thread t1 = new Thread(threadGroup, new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("选手一到达了终点");
                }
            });
    
            Thread t2 = new Thread(threadGroup, new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("选手二到达了终点");
                }
            });
            t2.start();
            t1.start();
    
            while (threadGroup.activeCount() != 0) {
    
            }
            System.out.println("所有选手到达了终点");
        }
    }
    
    

    在这里插入图片描述
    线程组常用的方法
    在这里插入图片描述

    线程安全问题

    来看单线程情况下让count分别自增和自减10000次

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 12:03;
     */
    class Counter {
        private static int count = 0;
        public void increase(){
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    
        public void decrease(){
            for (int i = 0; i < 10000; i++) {
                count--;
            }
        }
    
        public int getCount(){
            return count;
        }
    }
    public class ThreadDemo16 {
        public static void main(String[] args) {
            //单线程
            Counter counter = new Counter();
            counter.increase();
            counter.decrease();
            System.out.println(counter.getCount());
        }
    }
    
    

    结果符合预期
    在这里插入图片描述

    如果想使程序的执行速度快,就可以使用多线程的方式来执行。在来看多线程情况下的问题

    public class ThreadDemo16 {
        public static void main(String[] args) throws InterruptedException {
    
            //多线程情况下
            Counter counter = new Counter();
            Thread thread1 = new Thread(()->{
                counter.decrease();
            });
    
            Thread thread2 = new Thread(()->{
                counter.increase();
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
    
                    /*
            //单线程
            Counter counter = new Counter();
            counter.increase();
            counter.decrease();
            System.out.println(counter.getCount());
             */
        }
    }
    
    

    执行结果:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    每次的执行结果是不一样的。这就是多线程的不安全问题
    在这里插入图片描述
    预期的结果是0,但结果却不是。
    线程不安全问题的原因:
    1.CPU的抢占式执行
    2.多个线程共同操作一个变量
    3.内存可见性
    4.原子性问题
    5.编译器优化(指令重排)

    多个线程操作同一个变量
    如果多个线程操作的不是一个变量,就不会发生线程的不安全问题,可以将上面的代码修改如下:👇

    public class ThreadDemo16 {
        static int res1 = 0;
        static int res2 = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            Counter counter = new Counter();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    res1 = counter.getCount();
                }
            });
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    res2 = counter.getCount();
                }
            });
            System.out.println(res1 + res2);
    /*
            //多线程情况下
            Counter counter = new Counter();
            Thread thread1 = new Thread(()->{
                counter.decrease();
            });
    
            Thread thread2 = new Thread(()->{
                counter.increase();
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.getCount());
            */
    
    
                    /*
            //单线程
            Counter counter = new Counter();
            counter.increase();
            counter.decrease();
            System.out.println(counter.getCount());
             */
        }
    }
    
    

    这样就可以了:
    在这里插入图片描述

    内存不可见问题:看下面的代码,是不是到thread2执行的时候,就会改变num的值,从而终止了thread1呢?

    package ThreadDeom;
    
    import java.util.Scanner;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 13:03;
     */
    public class ThreadDemo17 {
    
        private static int num = 0;
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (num == 0){}
                }
            });
    
            thread1.start();
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Scanner scanner = new Scanner(System.in);
                    System.out.println("输入一个数字来终止线程thread1");
                    num = scanner.nextInt();
                }
            });
    
            thread2.start();
        }
    
    }
    
    

    结果是不能的:
    在这里插入图片描述
    输入一个数字后回车,并没有让thread1的循环结束。这就是内存不可见的问题。
    原子性的问题
    上面的++和–操作其实是分三步来执行的
    在这里插入图片描述
    假设在第二部的时候,有另外一个线程也来修改值,那么就会出现脏数据的问题了。
    所以就会发生线程的不安全问题

    编译器优化
    编译器的优化会打乱原本程序的执行顺序,就有可能导致线程的不安全问题发生。
    在单线程不会发生线程的不安全问题,在多线程就可能会不安全。

    volatile关键字

    可以使用volatile关键字,这个关键字可以解决指令重排和内存不可见的问题。
    在这里插入图片描述
    加上volatile关键字之后的运行结果
    在这里插入图片描述
    但是volatile关键字不能解决原子性的问题👇:

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 14:02;
     */
    
    class Counter1 {
        private static volatile int count = 0;
    
        public void increase() {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    
        public void decrease() {
            for (int i = 0; i < 10000; i++) {
                count--;
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    
    
    public class ThreadDemo18 {
        public static void main(String[] args) throws InterruptedException {
            Counter1 counter1 = new Counter1();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    counter1.decrease();
                }
            });
    
            Thread thread2 = new Thread(() -> {
                counter1.increase();
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(counter1.getCount());
        }
    }
    
    

    在这里插入图片描述
    在这里插入图片描述
    那么Java中如何解决原子性的问题呢👇

    Java中的锁

    Java中的加锁操作有两种:
    1.synchronized锁(jvm层的解决方案,也叫监视器锁)
    在操作系统的层面使用的是互斥锁(mutex lock)
    在Java中放在了对象头中。
    2.手动锁Lock
    操作锁的流程
    1.尝试获取锁
    2.使用锁
    3.释放锁

    synchronized锁

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 14:12;
     */
    class Counter2 {
        private static volatile int count = 0;
    
        public void increase() {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    
        public void decrease() {
            for (int i = 0; i < 10000; i++) {
                count--;
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    
    
    public class ThreadDemo19 {
        public static void main(String[] args) throws InterruptedException {
            //声明锁对象,任何的对象都可以作为锁
            Object lock = new Object();
            Counter2 counter2 = new Counter2();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //使用锁
                    synchronized (lock) {
                        counter2.decrease();
    
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock) {
                    counter2.increase();
                }
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(counter2.getCount());
        }
    }
    
    

    结果是:
    在这里插入图片描述

    synchronized使用场景

    1.使用synchronized来修饰代码块(可以给任意的对象进行加锁操作)

    public class ThreadDemo19 {
        public static void main(String[] args) throws InterruptedException {
            //声明锁对象,任何的对象都可以作为锁
            Object lock = new Object();
            Counter2 counter2 = new Counter2();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //使用锁
                    synchronized (lock) {
                        counter2.decrease();
    
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock) {
                    counter2.increase();
                }
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(counter2.getCount());
        }
    }
    
    

    在这里插入图片描述

    2.使用synchronized来修饰静态方法(对当前的类进行加锁的操作)

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 14:02;
     */
    
    class Counter1 {
        private static volatile int count = 0;
    
        public void increase() {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    
        public void decrease() {
            for (int i = 0; i < 10000; i++) {
                count--;
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    
    
    public class ThreadDemo18 {
        public static void main(String[] args) throws InterruptedException {
            Counter1 counter1 = new Counter1();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    counter1.decrease();
                }
            });
    
            Thread thread2 = new Thread(() -> {
                counter1.increase();
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(counter1.getCount());
        }
    }
    
    

    在这里插入图片描述

    3.使用synchronized来修饰普通的方法(对当前类的实例来进行加锁)

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 14:12;
     */
    public class ThreadDemo20 {
        private static int num = 0;
        private static final int maxSize = 100000;
    
        public static void main(String[] args) throws InterruptedException {
    
            ThreadDemo20 threadDemo20 = new ThreadDemo20();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    threadDemo20.increase();
                }
            });
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                   threadDemo20. decrease();
                }
            });
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(num);
        }
    
        //给静态的方法进行加锁,被加的锁是当前的对象。
    //    public synchronized static void increase(){
    
        //给普通的方法进行加锁的操作
        public synchronized void increase() {
    
            for (int i = 0; i < maxSize; i++) {
                num++;
            }
        }
    
        //    public synchronized static void decrease(){
        public synchronized void decrease() {
    
            for (int i = 0; i < maxSize; i++) {
                num--;
            }
        }
    }
    

    在这里插入图片描述

    synchronized注意事项

    1.加锁的时候一定要使用同一把锁对象

    Lock类的使用

    也叫手动锁

    package ThreadDeom;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 18:32;
     */
    public class ThreadDemo22 {
        private static int number = 0;
        private static final int maxSize = 100000;
    
        public static void main(String[] args) {
            //创建lock锁对象,lock是接口,不能实列化
            Lock lock = new ReentrantLock();
    
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < maxSize; i++) {
                    lock.lock();
                    try {
                        number++;
    
                    } finally {
                        lock.unlock();
                    }
                }
            });
    
    
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < maxSize; i++) {
                    lock.lock();
                    try {
                        number--;
    
                    } finally {
                        lock.unlock();
                    }
    
                }
            });
    
            System.out.println(number);
        }
    }
    
    

    在这里插入图片描述

    Lock锁使用的注意事项

    1.lock()操作一定要放在try外面
    如果放在try的里面:
    1.try中抛出了异常,还没有加锁就释放了finally中的锁的操作了
    2.如果放在了try,没加锁就释放了锁,就会抛出异常,就会将业务代码中的异常吞噬掉👇
    如果一定要放的话,将lock()放在try的第一行。

    package ThreadDeom;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 18:49;
     */
    public class ThreadDemo23 {
        public static void main(String[] args) {
            Lock lock = new ReentrantLock();
    
            try{
    
                System.out.println(1/0);
                lock.lock();
    
            } finally {
                lock.unlock();
            }
        }
    }
    
    

    在这里插入图片描述

    公平锁、非公平锁

    公平锁的调度:
    一个线程释放锁。
    主动唤醒“需要得到锁”的队列来得到锁。
    非公平锁
    当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁。
    Java中的所有锁默认都是非公平锁。
    非公平锁的性能更高。
    ReentrantLock可以设置非公平锁。
    公平锁

    package ThreadDeom;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 19:22;
     */
    public class ThreadDemo24 {
        public static void main(String[] args) throws InterruptedException {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    reentrantLock.lock();
                    try {
                        System.out.println("thread1");
    
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    reentrantLock.lock();
                    try {
                        System.out.println("thread2");
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            });
    
            Thread.sleep(100);
            thread1.start();
            thread2.start();
        }
    }
    
    

    打印的结果是无序的
    在这里插入图片描述
    如果设置为公平锁:👇
    在这里插入图片描述

    在这里插入图片描述
    thread1和thread2 交替输出

    synchronzied 和 Lock 的区别

    1.synchronzied可以自动的进行加锁和释放锁,而Lock需要手动的加锁、释放锁。
    2.Lock是Java层面的锁实现,而synchronzied 是JVM层面锁的实现
    3.synchronzed 即可以修饰代码块,又可以修饰普通方法和静态的方法,而Lock 只能修饰代码块
    4. synchronized 实现的是 非公平的锁,而Lock 可以实现公平锁。

    5.lock的灵活性更高

    死锁

    在两个或两个以上的线程运行中,因为资源的抢占而造成线程一直等待的问题。
    看👇:

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 19:48;
     */
    public class ThreadDemo25 {
        public static void main(String[] args) throws InterruptedException {
            Object lockA = new Object();
            Object lockB = new Object();
    
    
            Thread thread1 = new Thread(() -> {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "获取到lockA");
                    //让线程2获取lockB
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (lockB) {
    
                        System.out.println(Thread.currentThread().getName() + "获取到lockB");
                    }
                }
    
            });
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //线程2获取资源B
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "获取到lockB");
                        //让线程1先获取到锁lockA
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (lockA) {
                            System.out.println(Thread.currentThread().getName() + "获取到lockA");
    
                        }
    
                    }
    
                }
            });
            
            thread1.start();
    
            thread2.start();
        }
    }
    
    

    这就造成了死锁
    在这里插入图片描述

    造成死锁的四个条件

    1.互斥条件:
    当资源被一个线程拥有之后,就不能被其它的线程拥有了
    2.拥有请求条件:
    当一个线程拥有了一个资源之后,又试图请求另一个资源。
    3.不可剥夺条件:
    当一个线程拥有了一个资源之后,如果不是这个线程主动的释放资源,其他线程就不能拥有这个线程。
    4.环路等待条件:
    两个或两个以上的线程拥有了资源之后,试图获取对方的资源的时候形成了一个环路。

    死锁的解决方案

    解决请求拥有和环路等待。
    最有效的解决方案就是控制加锁的顺序。

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 20:25;
     */
    public class ThreadDemo26 {
        public static void main(String[] args) throws InterruptedException {
            Object lockA = new Object();
            Object lockB = new Object();
    
    
            Thread thread1 = new Thread(() -> {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "获取到lockA");
                    //让线程2获取lockB
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (lockB) {
    
                        System.out.println(Thread.currentThread().getName() + "获取到lockB");
                    }
                }
    
            });
    
            Thread thread2 = 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();
                        }
                        synchronized (lockB) {
                            System.out.println(Thread.currentThread().getName() + "获取到lockB");
    
                        }
    
                    }
    
                }
            });
    
            thread1.start();
    
            thread2.start();
        }
    }
    
    

    在这里插入图片描述

    线程间通信

    线程之间的通讯是指在一个线程中的操作可以影响另一个线程。

    wait/notify机制的原理

    拥有相同锁的线程之间才能使用wait/notify机制。
    wait()是Object()的方法,它的作用是是当前执行wait()方法的线程等待,在wati()所在的代码出停止执行,并释放锁,直到接到通知或者被中断为止。即在调用wait()的方法之前,线程必需先获取到对象级别的锁,也就是只能在同步方法或者同步块中使用wait()方法。
    如果在使用wait()方法之前线程没有获得相应的锁,那么程序在执行时就会抛出异常。
    notify()方法要在同步方法或者同步块中执行,即在调用notify()方法之前,线程必需要先获取到锁对象。如果线程没有持有锁对象的话,那么也会抛出异常。该方法用来通知可能在等待该锁的其它线程,如果有多个线程,那么则按照执行wait()方法的顺序来对处于wait()方法的线程发出通知,并使该线程重新获取锁。执行notify()方法之后,当前线程不会马上释放锁,处于wait()状态的线程也不会立马得到这个对象锁。而是要等notify的synchronized同步区域执行完成之后才会释放锁,处于wait()状态的线程才会得到锁对象。

    总结:wait()方法用于让线程停止运行,而notify()方法用于通知暂停的线程继续运行。
    在使用wait()或者notify()方法之前没有对象锁,就会报异常👇:

            lock.notify();
    
    

    在这里插入图片描述
    正确的使用之后

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-12;
     * time: 21:11;
     */
    public class ThreadDemo27 {
        //设置锁对象
    
        private static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
    
                @Override
    
                public void run() {
                    synchronized (lock) {
                        System.out.println("在wait()");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("被notify()唤醒之后");
                    }
                }
            });
            thread.start();
    
            Thread.sleep(1000);
    
            synchronized (lock) {
                lock.notify();
    
            }
    
        }
    }
    
    

    在这里插入图片描述

    注意:使用wait()方法的时候一定要和线程的锁对象是一个锁。

    notifyAll

    在多线程的情况下使用notify()方法只可以唤醒一个线程👇
    在这里插入图片描述

    package ThreadDeom;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 8:06;
     */
    public class ThreadDemo28 {
        //设置锁对象
    
        private static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new Runnable() {
    
                @Override
    
                public void run() {
                    synchronized (lock) {
                        System.out.println("thread1在wait()");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("thread1被notify()唤醒之后");
                    }
                }
            });
    
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("thread2在wait()");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread2被notify()唤醒之后");
                }
            });
    
            Thread thread3 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (lock) {
                        System.out.println("thread3在wait()");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("thread3被notify()唤醒之后");
                    }
    
                }
            });
    
    
            thread1.start();
            thread2.start();
            thread3.start();
            Thread.sleep(1000);
    
            synchronized (lock) {
    
                System.out.println("主线程调用notify()之后");
    
                lock.notify();
    
            }
    
        }
    }
    
    

    那么如果使用notifyAll()方法呢?
    在这里插入图片描述
    可以看到所有的线程都被唤醒了
    在这里插入图片描述

    那么使用notify()唤醒的线程有没有什么顺序呢?
    使用notify()唤醒线程的顺序是正序、倒序、还是随机的,这取决与JVM的具体实现,并不是所有的JVM在执行notify()时都是按照wait()的执行顺序进行唤醒的,也不是所有的notidyAll()都是按照wait()方法的倒序进行唤醒的,这取决于JVM的具体实现。
    wait()和notify()不能唤醒指定的线程。

    wait()和sleep()的区别

    也可以让wait()等待指定的时间,如果超过给定的时间,wait()不会无限期的等待下去.
    在这里插入图片描述
    没有被notify()唤醒,过了1000毫秒之后会自动停止。
    在这里插入图片描述

    wait()在不传入任何参数的时候,线程会进入waiting 的状态,而在wait()中加入一个大于0的参数的时候,线程会进入time_wating的状态。

    sleep()和wait()的区别 : 线程在sleep()的时候是不会释放锁的,而执行wait()的时候它就会释放锁。👇:

    package ThreadDeom;
    
    import jdk.nashorn.internal.ir.Block;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 8:45;
     */
    public class ThreadDemo29 {
        private static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (lock) {
                        try {
                            System.out.println("thread获取到了锁");
                            //如果sleep释放锁的话,会在thread获取到了锁和thread释放了锁之间打印
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
    
                    }
                    System.out.println("thread释放了锁");
    
                }
            });
            thread.start();
    
            //让thread 先获取到锁
            Thread.sleep(1000);
            synchronized (lock) {
                System.out.println("主线程获取到了锁");
            }
        }
    }
    
    

    在这里插入图片描述

    可以看到线程在sleep()的时候,线程是不会释放锁的。再来看看wait()方法👇:
    在这里插入图片描述

    在这里插入图片描述
    线程使用wait()的时候它就会释放掉锁。

    1.wait()和sleep()都是让线程进行休眠的
    2.wait()和sleep()方法都有可能在执行的过程接收到线程终止的通知
    3.wait()必须和synchronzied一起使用,而sleep()不用。
    4.wait()会释放锁,而sleep()不会释放锁。
    5.wait()时Object的方法,而sleep()时Thread的方法。
    6.默认情况下,wait()不传任何的参数的情况下,wait()会进入waiting的状态,如果传递了参数,wait()会进入time_waiting的状态。而sleep()进入的是time_waiting的状态。
    sleep(0) 和wait(0)的区别:
    1.sleep(0)表示0毫秒之后继续执行,而wait(0)表示线程会一直休眠下去wait(0)和wait()是一样的,wait()的源码就是调用了wait(0)方法。
    2.sleep(0)表示重新出发一次CPU的竞争。
    为什么wait()会释放锁,而sleep()不会释放锁?
    sleep()需要传递一个最大的等待时间,也就是说sleep()是可控的,而wait()是不可以传递参数的,从设计的层面来说,如果让wait()一直持有所得话,那么线程就可能一直阻塞。
    为什么wait()是Object的方法,而sleep()是线程的方法?
    wait()需要操作锁,而锁是属于对象级别的,所有的锁都是放在对象头中的,它不是线程级别的,一个线程可以有多把的锁,为了灵活,就将wait()放在Object中了。

    LockSupport park()/unpark()

    使用LockSupport可以解决wait()/notify()随机唤醒的问题。

    package ThreadDeom;
    
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 9:36;
     */
    public class ThreadDemo30 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //让线程休眠
                    LockSupport.park();
                    System.out.println("unPark()了thread1");
                }
            });
    
            Thread thread2 = new Thread(() -> {
                LockSupport.park();
                System.out.println("unPark()了thread2");
    
            });
    
    
            Thread thread3 = new Thread() {
                @Override
                public void run() {
                    LockSupport.park();
                    System.out.println("unPark()了thread3");
    
                }
            };
    
    
            thread1.start();
            thread2.start();
            thread3.start();
    
    
            LockSupport.unpark(thread1);
            LockSupport.unpark(thread2);
    
    
        }
    }
    
    

    在这里插入图片描述

    Java线程池

    线程的缺点:
    1.线程的创建它会开辟本地方法栈、JVM栈、程序计数器私有的内存,同时消耗的时候需要销毁以上三个区域,因此频繁的创建和销毁线程比较消耗系统的资源。
    2.在任务量远远大于线程可以处理的任务量的时候,不能很好的拒绝任务。
    所以就有了线程池:
    使用池化的而技术来管理和使用线程。

    线程池的优点

    1.可以避免频繁的创建和销毁线程
    2.可以更好的管理线程的个数和资源的个数。
    3.线程池拥有更多的功能,比如线程池可以进行定时任务的执行。
    4.线程池可以更友好的拒绝不能处理的任务。

    线程池的6种创建方式

    一共有7种创建方式
    创建方式一:
    创建固定个数的线程池:

    package ThreadPoolDemo;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 10:24;
     */
    public class ThreadPoolDemo1 {
        public static void main(String[] args) {
            //创建一个固定个数的线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            //执行任务
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("线程名" + Thread.currentThread().getName());
                    }
                });
            }
    
        }
    }
    
    

    在这里插入图片描述
    那么如果执行次数大于10次呢?
    线程池不会创建新的线程,它会复用之前的线程。
    在这里插入图片描述

    在这里插入图片描述
    那么如果只执行两个任务呢?它创建了是10个线程还是两个线程呢?
    我们可以使用Jconsole来看一看:
    在这里插入图片描述

    在这里插入图片描述

    结果是只有2个线程被创建。

    创建方式二:
    创建带有缓存的线程池:
    适用于短期有大量的任务的时候使用

    public class ThreadPoolDemo2 {
        public static void main(String[] args) {
            //创建带缓存的线程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 100; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName());
                    }
                });
            }
        }
    }
    
    

    在这里插入图片描述

    方式三:
    创建执行定时任务的线程池

    package ThreadPoolDemo;
    
    import java.util.Date;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 11:32;
     */
    public class ThreadPoolDemo3 {
        public static void main(String[] args) {
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
    
            System.out.println("执行定时任务前的时间:" + new Date());
            scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务的时间:" + new Date());
                }
            },1,2, TimeUnit.SECONDS);
        }
    }
    
    

    在这里插入图片描述
    执行任务的四个参数的意义:
    参数1:延迟执行的任务
    参数2:延迟一段时间后执行
    参数3:定时任务执行的频率
    参数4:配合前两个参数使用,是2、3参数的时间单位

    还有两种执行的方法:
    只会执行一次的方法:
    在这里插入图片描述

    在这里插入图片描述
    第三种的执行方式:
    在这里插入图片描述

    在这里插入图片描述
    那么这种的执行方式和第一种的执行方式有什么区别呢?
    当在两种执行的方式中分别加上sleep()之后:
    在这里插入图片描述

    方式一:
    在这里插入图片描述

    方式三:
    在这里插入图片描述
    结论很明显了:
    第一种方式是以上一个任务的开始时间+定时的时间作为当前任务的开始时间

    第三种方式是以上一个任务的结束时间来作为当前任务的开始时间。

    创建方式四:

    package ThreadPoolDemo;
    
    import java.util.Date;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 12:38;
     */
    public class ThreadPoolDemo4 {
        public static void main(String[] args) {
            //创建单个执行任务的线程池
            ScheduledExecutorService scheduledExecutorService
                    = Executors.newSingleThreadScheduledExecutor();
            System.out.println("执行任务之前" + new Date());
            scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
                @Override
                public void run() {
                    System.out.println("我是SingleThreadSchedule"+ new Date());
                }
            },3,1, TimeUnit.SECONDS);
        }
    }
    
    

    在这里插入图片描述
    在这里插入图片描述
    创建方式五:
    创建单个线程的线程池

    package ThreadPoolDemo;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 12:55;
     */
    public class ThreadPoolDemo5 {
        public static void main(String[] args) {
            //创建单个线程的线程池
    
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 20; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("线程名 " +  Thread.currentThread().getName());
                    }
                });
            }
        }
    }
    
    

    在这里插入图片描述
    创建单个线程池的作用是什么?
    1.可以避免频繁创建和销毁线程所带来的性能的开销
    2.它有任务队列,可以存储多余的任务
    3.可以更好的管理任务
    4.当有大量的任务不能处理的时候,可以友好的执行拒绝策略
    创建方式六:
    创建异步线程池根据当前CPU来创建对应个数的线程池

    package ThreadPoolDemo;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 13:12;
     */
    public class ThreadPoolDemo6 {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newWorkStealingPool();
    
            for (int i = 0; i < 10; i++) { 
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("线程名" + Thread.currentThread().getName());
                    }
                });
            }
        }
    }
    
    

    在这里插入图片描述
    运行结果为什么什么都没有呢?
    看下面的异步与同步的区别就知道了。
    加上这个
    在这里插入图片描述

    就可以输出结果了
    在这里插入图片描述

    线程池的第七种创建方式

    前六种的创建方式有什么问题呢?
    1.线程的数量不可控(比如带缓存的线程池)
    2.工作任务量不可控(默认的任务队列的大小时Integer.MAX_VALUE),任务比较大肯会导致内存的溢出。
    所以就可以使用下面的创建线程池的方式了:

    package ThreadPoolDemo;
    
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 15:05;
     */
    public class ThreadPoolDemo7 {
        private static int threadId = 0;
    
        public static void main(String[] args) {
            ThreadFactory threadFactory = new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setName("我是threadPool-" + ++threadId);
                    return thread;
                }
            };
    
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 100,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(12),
                    threadFactory, new ThreadPoolExecutor.AbortPolicy());
    
            for (int i = 0; i < 15; i++) {
                threadPoolExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName());
                    }
                });
            }
        }
    }
    
    
    

    在这里插入图片描述

    参数说明:
    在这里插入图片描述

    参数一:核心线程数|线程池正常情况下的线程 数量
    参数二:最大线程数|当有大量的任务的时候可以创建的最多的线程数
    参数三:最大线程的存活时间
    参数四:配合参数三一起使用的表示参数三的时间单位
    参数五:任务队列
    参数六:线程工厂
    参数七:决绝策略

    注意事项:最大的线程数要大于等于核心的线程数
    在这里插入图片描述

    在这里插入图片描述

    五种拒绝策略

    在这里插入图片描述
    在这里插入图片描述

    为什么拒绝策略可以舍弃最新的任务或者最旧的任务呢?
    因为LinkedBlockingDeque时FIFO的。
    第五种:自定义的拒绝策略
    在这里插入图片描述

    在这里插入图片描述

    ThreadPoolExecutor的执行方式

    在这里插入图片描述

    package ThreadPoolDemo;
    
    import java.util.concurrent.*;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 16:58;
     */
    public class ThreadPoolDemo9 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 4, 100,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10), new ThreadPoolExecutor.DiscardOldestPolicy());
    
    
            //线程池的执行方式一
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("使用了execute()执行了线程池");
                }
            });
    
            //线程池的执行方式二
    
            Future<String> futureTask =
                    threadPoolExecutor.submit(new Callable<String>() {
                        @Override
                        public String call() throws Exception {
                            return "使用submit(new Callable<>())执行了线程池";
                        }
                    });
    
            System.out.println(futureTask.get());
            
            
    
        }
    }
    
    

    无返回值的执行方式
    在这里插入图片描述

    有返回值的执行方式
    在这里插入图片描述

    ThreadPoolExecutor的执行流程

    当任务量小于核心线程数的时候,ThreadPoolExecutor会创建线程来执行任务
    当任务量大于核心的线程数的时候,并且没有空闲的线程时候,且当线程池的线程数小于最大线程数的时候,此时会将任务存放到任务队列中
    如果任务队列也被存满了,且最大线程数大于线程池的线程数的时候,会创建新的线程来执行任务。
    如果线程池的线程数等于最大的线程数,并且任务队列也已经满了,就会执行拒绝策略。👇
    在这里插入图片描述

    线程池的终止

    shutdown()
    线程池的任务会执行完
    shutdownNow()
    立即终止线程池,线程池的任务不会执行完

    线程池的状态

    在这里插入图片描述

    异步、同步

    1. Java 线程 同步与异步
      多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局资源造成的,有时为了解决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制

    同步
    A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去

    异步
    A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
    同步的方式:
    1.发送请求
    2.等待执行完成
    3.有结果的返回

    异步的方式
    1.发请求
    2.执行完成
    3.另一个线程异步处理
    4.处理完成之后返回回调结果

    显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,使用异步的机制,性能会有所提升

    线程工厂

    设想这样一种场景,我们需要一个线程池,并且对于线程池中的线程对象,赋予统一的线程优先级、统一的名称、甚至进行统一的业务处理或和业务方面的初始化工作,这时工厂方法就是最好用的方法了

    package ThreadPoolDemo;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadFactory;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 11:12;
     */
    public class ThreadFactoryDemo {
        public static void main(String[] args) {
            MyThreadFactory myThreadFactory = new MyThreadFactory();
            ExecutorService executorService =  Executors.newFixedThreadPool(10,myThreadFactory);
    
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("使用线程工厂设置的线程名:"+ Thread.currentThread().getName() +
                                " 使用线程工厂设置的线程的优先级" + Thread.currentThread().getPriority());
                    }
                });
            }
    
    
    
        }
    
        private static int count = 0;
         static class MyThreadFactory implements ThreadFactory{
             @Override
             public Thread newThread(Runnable r) {
                 Thread thread = new Thread(r);
                 thread.setPriority(8);
                 thread.setName("thread--" + count++);
                 return thread;
             }
         }
    
    }
    
    
    
    

    在这里插入图片描述

    SimpleDateFormat非线程安全问题

    实现1000个线程的时间格式化

    package SimpleDateFormat;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 17:30;
     */
    public class SimpleDateFormat1 {
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    
        public static void main(String[] args) {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,10,100,
                    TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1000),new ThreadPoolExecutor.DiscardPolicy());
    
    
            for (int i = 0; i < 1001; i++) {
                int finalI = i;
                threadPoolExecutor.submit(new Runnable() {
                    @Override
                    public void run() {
                        Date date = new Date(finalI * 1000);
                        myFormatTime(date);
                    }
                });
            }
    
            threadPoolExecutor.shutdown();
        }
    
        private static void myFormatTime(Date date){
            System.out.println(simpleDateFormat.format(date));
        }
    
    }
    
    

    产生了线程不安全的问题👇:

    在这里插入图片描述
    这是因为:
    在这里插入图片描述
    多线程的情况下:
    在这里插入图片描述
    线程1在时间片用完之后,线程2来setTime()那么线程1的得到了线程2的时间。

    所以可以使用加锁的操作:

    在这里插入图片描述

    就不会有重复的时间了
    在这里插入图片描述
    但是虽然可以解决线程不安全的问题,但是排队等待锁,性能就会变得低

    所以可以使用局部变量:
    在这里插入图片描述
    也解决了线程不安全的问题:
    在这里插入图片描述

    但是每次也都会创建新的私有变量
    那么有没有一种方案既可以避免加锁排队执行,又不会每次创建任务的时候不会创建私有的变量呢?
    那就是ThreadLocal👇:

    ThreadLocal

    ThreadLocal的作用就是让每一个线程都拥有自己的变量。
    那么选择锁还是ThreadLocal?
    看创建实列对象的复用率,如果复用率比较高的话,就使用ThreadLocal。

    ThreadLocal的原理

    类ThreadLocal的主要作用就是将数据放到当前对象的Map中,这个Map时thread类的实列变量。类ThreadLocal自己不管理、不存储任何的数据,它只是数据和Map之间的桥梁。
    执行的流程:数据—>ThreadLocal—>currentThread()—>Map。
    执行后每个Map存有自己的数据,Map中的key中存储的就是ThreadLocal对象,value就是存储的值。每个Thread的Map值只对当前的线程可见,其它的线程不可以访问当前线程对象中Map的值。当前的线程被销毁,Map也随之被销毁,Map中的数据如果没有被引用、没有被使用,则随时GC回收。

    ThreadLocal常用方法

    在这里插入图片描述

    set(T):将内容存储到ThreadLocal
    get():从线程去私有的变量
    remove():从线程中移除私有变量

    package ThreadLocalDemo;
    
    import java.text.SimpleDateFormat;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 18:37;
     */
    public class ThreadLocalDemo1 {
        private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            //设置私有变量
            threadLocal.set(new SimpleDateFormat("mm:ss"));
    
            //得到ThreadLocal
            SimpleDateFormat simpleDateFormat = threadLocal.get();
            
            //移除
            threadLocal.remove();
        }
    }
    
    

    ThreadLocal的初始化

    ThreadLocal提供了两种初始化的方法
    initialValue()和
    initialValue()初始化:

    package ThreadLocalDemo;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 19:07;
     */
    public class ThreadLocalDemo2 {
        //创建并初始化ThreadLocal
    
        private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal() {
            @Override
            protected SimpleDateFormat initialValue() {
                System.out.println(Thread.currentThread().getName() + "执行了自己的threadLocal中的初始化方法initialValue()");
                return new SimpleDateFormat("mm:ss");
            }
        };
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                Date date = new Date(5000);
                System.out.println("thread0格式化时间之后得结果时:" + threadLocal.get().format(date));
            });
            thread1.setName("thread0");
            thread1.start();
    
    
            Thread thread2 = new Thread(() -> {
                Date date = new Date(6000);
                System.out.println("thread1格式化时间之后得结果时:" + threadLocal.get().format(date));
            });
            thread2.setName("thread1");
    
            thread2.start();
    
        }
    }
    
    

    在这里插入图片描述
    withInitial方法初始化:

    package ThreadLocalDemo;
    
    import java.util.function.Supplier;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 17:23;
     */
    public class ThreadLocalDemo3 {
        private static ThreadLocal<String> stringThreadLocal =
                ThreadLocal.withInitial(new Supplier<String>() {
                    @Override
                    public String get() {
                        System.out.println("执行了withInitial()方法");
                        return "我是" + Thread.currentThread().getName() + "的ThreadLocal";
                    }
                });
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                System.out.println(stringThreadLocal.get());
            });
    
            thread1.start();
    
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(stringThreadLocal.get());
                }
            });
    
            thread2.start();
        }
    }
    
    

    在这里插入图片描述
    注意:
    ThreadLocal如果使用了set()方法的话,那么它的初始化方法就不会起作用了。
    来看:👇

    package ThreadLocalDemo;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 18:43;
     */
    
    class Tools {
        public static ThreadLocal t1 = new ThreadLocal();
    }
    
    class ThreadA extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("在ThreadA中取值:" + Tools.t1.get());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class ThreadLocalDemo4 {
        public static void main(String[] args) throws InterruptedException {
            //main是ThreadA 的 父线程 让main线程set,ThreadA,是get不到的
    
            if (Tools.t1.get() == null) {
                Tools.t1.set("main父线程的set");
            }
    
            System.out.println("main get 到了: " + Tools.t1.get());
    
    
            Thread.sleep(1000);
            ThreadA a = new ThreadA();
            a.start();
    
        }
    }
    
    

    在这里插入图片描述
    类ThreadLocal不能实现值的继承,那么就可以使用InheritableThreadLocal了👇

    InheritableThreadLocal的使用

    使用InheritableThreadLocal可以使子线程继承父线程的值

    在这里插入图片描述
    在来看运行的结果:
    在这里插入图片描述

    子线程有最新的值,父线程依旧是旧的值

    package ThreadLocalDemo;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 19:07;
     */
    class ThreadB extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("在ThreadB中取值:" + Tools.t1.get());
                if (i == 5){
                    Tools.t1.set("我是ThreadB中新set()");
                }
                try {
                    Thread.sleep(100);
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class ThreadLocalDemo5 {
        public static void main(String[] args) throws InterruptedException {
            if (Tools.t1.get() == null) {
                Tools.t1.set("main父线程的set");
            }
    
            System.out.println("main get 到了: " + Tools.t1.get());
    
    
            Thread.sleep(1000);
            ThreadA a = new ThreadA();
            a.start();
            Thread.sleep(5000);
    
            for (int i = 0; i < 10; i++) {
                System.out.println("main的get是:" + Tools.t1.get());
                Thread.sleep(100);
            }
    
        }
    }
    
    

    在这里插入图片描述
    ThreadLocal的脏读问题
    来看👇

    package ThreadLocalDemo;
    
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 19:49;
     */
    
    public class ThreadLocalDemo6 {
        private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        private static class MyThread extends Thread {
            private static boolean flag = false;
    
            @Override
            public void run() {
                String name = this.getName();
    
                if (!flag) {
                    threadLocal.set(name);
                    System.out.println(name + "设置了" + name);
                    flag = true;
                }
    
                System.out.println(name + "得到了" + threadLocal.get());
            }
        }
    
        public static void main(String[] args) {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10));
    
    
            for (int i = 0; i < 2; i++) {
                threadPoolExecutor.execute(new MyThread());
            }
    
            threadPoolExecutor.shutdown();
        }
    }
    
    

    在这里插入图片描述

    发生了脏读:
    线程池复用了线程,也复用了这个线程相关的静态属性,就导致了脏读
    那么如何避免脏读呢?
    去掉static 之后:
    在这里插入图片描述
    在这里插入图片描述

    单例模式与多线程

    单例模式就是全局唯一但是所有程序都可以使用的对象
    写单例模式步骤:
    1.将构造函数设置为私有的
    2.创建一个静态的类变量
    3.提供获取单例的方法

    立即加载/饿汉模式

    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 21:02;
     */
    //饿汉方式实现单例模式
    public class Singleton {
        //1.将构造函数设置为私有的,不然外部可以创建
        private Singleton(){
        }
        
        //2.创建静态的类变量(让第三步的方法进行返回)
        private static Singleton singleton = new Singleton();
        
        //给外部接口提供的获取单例的方法
        public static Singleton getInstance(){
            return singleton;
        }
        
    }
    
    

    测试饿汉的单例模式

        //测试饿汉方式实现的单例模式,创建两个线程,看是不是得到了一个实列对象,如果为true就说明饿汉的单例模式没有问题
    
        static Singleton singleton1 = null;
        static Singleton singleton2 = null;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(() -> {
                singleton1 = Singleton.getInstance();
            });
            Thread thread2 = new Thread(() -> {
                singleton2 = Singleton.getInstance();
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(singleton1 == singleton2);
        }
    
    

    在这里插入图片描述

    延时加载/懒汉模式

    不会随着程序的启动而启动,而是等到有人调用它的时候,它才会初始化

    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 21:22;
     */
    //懒汉方式实现单例模式
    public class Singleton2 {
    
        static class Singleton {
            //1.设置私有的构造函数
            private Singleton() {
            }
    
            //2.提供一个私有的静态变量
            private static Singleton singleton = null;
    
            //3.提供给外部调用,返回一个单例对象给外部
    
            public static Singleton getInstance() {
                if (singleton == null) {
                    singleton = new Singleton();
                }
    
                return singleton;
            }
        }
    }
    
    

    那么这样写有什么问题呢?
    我们来看看多线程情况下的懒汉方式实现单例模式:

    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 21:22;
     */
    //懒汉方式实现单例模式
    public class Singleton2 {
    
        static class Singleton {
            //1.设置私有的构造函数
            private Singleton() {
            }
    
            //2.提供一个私有的静态变量
            private static Singleton singleton = null;
    
            //3.提供给外部调用,返回一个单例对象给外部
    
            public static Singleton getInstance() throws InterruptedException {
                if (singleton == null) {
                    Thread.sleep(100);
                    singleton = new Singleton();
                }
    
                return singleton;
            }
        }
    
        static Singleton singleton1 = null;
        static Singleton singleton2 = null;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(() -> {
                try {
                    singleton1 = Singleton.getInstance();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            Thread thread2 = new Thread(() -> {
                try {
                    singleton2 = Singleton.getInstance();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println(singleton1 == singleton2);
        }
    
    }
    
    

    结果:
    在这里插入图片描述

    所以发生了线程不安全的问题
    那么要如何更改呢?
    加锁:👇
    在这里插入图片描述
    结果就是true了:
    在这里插入图片描述
    给方法加锁可以实现线程安全,但是所锁的粒度太大。
    使用双重校验锁优化后:

        static class Singleton {
            //1.设置私有的构造函数
            private Singleton() {
            }
    
            //2.提供一个私有的静态变量
            private static Singleton singleton = null;
    
            //3.提供给外部调用,返回一个单例对象给外部
    
            public static Singleton getInstance() {
                if (singleton == null) {
                    synchronized (Singleton.class) {
                        if (singleton == null) {
                            singleton = new Singleton();
                        }
                    }
                }
    
                return singleton;
            }
        }
    
    

    在这里插入图片描述
    那么这样写就没有问题了吗?

    不是的:有可能还会发生指令重排的问题
    当有线程在进行第一次初始化的时候,就有可能发生问题👇
    先来看初始化的过程
    1。先分配内存空间
    2.初始化
    3.将singleton指向内存

    有可能指令重排序之后:
    线程1执行的顺序变成了 1 --> 3 --> 2
    在线程1执行完1、3之后时间片使用完了
    线程2再来执行,线程2得到了未初始化的singleton,也就是的到了一个空的对象
    也就发生了线程不安全的问题

    那么要如何解决指令重排序的问题呢?
    那就是使用volatile关键字👇:

    /**
     * user:ypc;
     * date:2021-06-13;
     * time: 21:22;
     */
    //懒汉方式实现单例模式
    public class Singleton2 {
    
        static class Singleton {
            //1.设置私有的构造函数
            private Singleton() {
            }
    
            //2.提供一个私有的静态变量
            private static volatile Singleton singleton = null;
    
            //3.提供给外部调用,返回一个单例对象给外部
    
            public static Singleton getInstance() {
                if (singleton == null) {
                    synchronized (Singleton.class) {
                        if (singleton == null) {
                            singleton = new Singleton();
                        }
                    }
                }
    
                return singleton;
            }
        }
    
    
    

    这样就没有问题了

    饿汉/懒汉对比

    饿汉方式:
    优点:实现简单,不存在线程安全的问题,因为饿汉的方式是随着程序的启动而初始化的,因为类加载是线程安全的,所以它是线程安全的。
    缺点:随着程序的启动而启动,有可能在整个程序的运行周期都没有用到,这样就带来了不必要的开销。

    阻塞队列的实现

    import java.util.Random;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 8:57;
     */
    public class MyBlockingQueue {
        private int[] values;
        private int first;
        private int last;
        private int size;
    
        MyBlockingQueue(int maxSize) {
            this.values = new int[maxSize];
            this.first = 0;
            this.last = 0;
            this.size = 0;
        }
    
        public void offer(int val) throws InterruptedException {
    
            synchronized (this) {
                if (this.size == values.length) {
                    this.wait();
                }
                this.values[last++] = val;
                size++;
    
                //变为循环队列
                if (this.last == values.length) {
                    this.last = 0;
                }
    
                //唤醒消费者
                this.notify();
            }
    
        }
    
        public int poll() throws InterruptedException {
            int result = 0;
            synchronized (this) {
                if (size == 0) {
                    this.wait();
                }
                result = this.values[first++];
                this.size--;
                if (first == this.values.length) {
                    this.first = 0;
                }
                //唤醒生产者开生产数据
                this.notify();
    
            }
            return result;
        }
    
        public static void main(String[] args) {
            MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);
            //生产者
            Thread thread1 = new Thread(() -> {
    
                while (true) {
                    try {
                        int num = new Random().nextInt(100);
                        myBlockingQueue.offer(num);
                        System.out.println("生产者生产数据:" + num);
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            });
    
    
            //消费者
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            int res = myBlockingQueue.poll();
    
                            System.out.println("消费者消费数据:" + res);
                        }
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            thread1.start();
            thread2.start();
        }
    }
    
    

    可以看到生产者每生产一个数据都会被取走:
    在这里插入图片描述

    常见的锁策略

    乐观锁

    它认为程序在一般的情况下不会发生问题,所以他在使用的时候不会加锁,只有在数据修改的时候才会判断有没有锁竞争,如果没有就会直接修改数据,如果有就会返回失败信息给用户自行处理。

    CAS

    乐观锁的经典实现
    Compare and Swap
    CAS 实现的三个重要的属性:
    (V,A,B)

    V:内存中的值
    A:预期的旧值
    B:新值
    V == A? V -> B : 修改失败

    修改失之后:
    自旋对比和替换
    CAS 的底层实现:
    CAS在Java中是通过unsafe来实现的,unsafe时本地类和本地方法,它是c/c++实现的原生方法,通过调用操作系统Atomic:: cmpxchg原子指令来实现的

    CAS在java中的应用

    i++、i–问题
    可以使用加锁、ThreadLocal 解决问题
    也可以使用atomic.AtomicInteger来解决问题,底层也使用了乐观锁。

    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 10:12;
     */
    public class ThreadDemo1 {
        private static AtomicInteger count  = new AtomicInteger(0);
        private static final int MaxSize = 100000;
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < MaxSize; i++) {
                        count.getAndIncrement();//i++
                    }
                }
            });
    
            thread1.start();
    
            Thread thread2 = new Thread(()->{
                for (int i = 0; i < MaxSize; i++) {
                 count.getAndDecrement();//i--
                }
            });
    
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(count);
        }
    }
    
    

    在这里插入图片描述

    CAS 的ABA问题

    当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。
    来看:

    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 10:43;
     */
    public class ThreadDemo2 {
        //线程操作资源,原子类ai的初始值为4
        static AtomicInteger ai = new AtomicInteger(4);
        public static void main(String[] args) {
            new Thread(() -> {
                //利用CAS将ai的值改成5
                boolean b = ai.compareAndSet(4, 5);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为5:"+b);
                //休眠一秒
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
                //利用CAS将ai的值改回4
                b = ai.compareAndSet(5,4);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为4:"+b);
            },"A").start();
            new Thread(() -> {
                //模拟此线程执行较慢的情况
                try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}
                //利用CAS将ai的值从4改为10
                boolean b = ai.compareAndSet(4, 10);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为10:"+b);
            },"B").start();
    
            //等待其他线程完成,为什么是2,因为一个是main线程,一个是后台的GC线程
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
    
            System.out.println("ai最终的值为:"+ai.get());
        }
    }
    
    

    上面例子模拟的是A、B两个线程操作一个资源ai,A的执行速度比B的快,在B执行前,A就已经将ai的值改为5之后马上又把ai的值改回为4,但是B不感知,所以最后B就修改成功了。

    那么会造成会有什么问题呢?
    假设A现在有100元,要给B转账100元,点击了两次转账按钮,第一次B只会得到100元,A现在剩余0元。第二次A是0元,预期的旧值是100,不相等,就不会执行转账操作。
    如果点击第二次按钮之前,A又得到了100元,B不能感知的到,此时A得到了转账100元,预期的旧值就是100,又会转给B100元。

    那么如何解决这个问题呢?👇

    ABA 问题的解决

    我们可以给操作加上版本号,每次修改的时候判断版本号和预期的旧值,如果不一样就不会执行操作了。
    即是预期的旧值和V值相等,但是版本号不一样,也不会执行操作。
    在Java中的实现:

    
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 11:05;
     */
    public class ThreadDemo3 {
        static AtomicStampedReference<Integer> ai = new AtomicStampedReference<>(4,0);
        public static void main(String[] args) {
            new Thread(() -> {
                //四个参数分别是预估内存值,更新值,预估版本号,初始版本号
                //只有当预估内存值==实际内存值相等并且预估版本号==实际版本号,才会进行修改
                boolean b = ai.compareAndSet(4, 5,0,1);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为5:"+b);
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
                b = ai.compareAndSet(5,4,1,2);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为4:"+b);
            },"A").start();
            new Thread(() -> {
                try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}
                boolean b = ai.compareAndSet(4, 10,0,1);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为10:"+b);
            },"B").start();
    
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
    
            System.out.println("ai最终的值为:"+ai.getReference());
        }
    }
    
    

    在这里插入图片描述

    注意:里面的旧值对比的是引用。
    如果范围在-128 - 127 里,会使用缓存的值,如果超过了这个范围,就会重新来new对象
    可以将Integer 的高速缓存的值的边界调整

    悲观锁

    悲观锁认为只要执行多线程的任务,就会发生线程不安全的问题,所以正在进入方法之后会直接加锁。
    直接使用synchronzied关键字给方法加锁就可以了

    独占锁、共享锁、自旋锁、可重入锁

    独占锁:指的是这一把锁只能被一个线程所拥有
    比如:synchronzied、Lock
    共享锁: 指的是一把锁可以被多个线程同时拥有
    ReadWriterLock读写锁就是共享锁
    读锁就是共享的,将锁的粒度更加的细化

    import java.util.Date;
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 11:42;
     */
    public class ThreadDemo4 {
        //创建读写锁
        public static void main(String[] args) {
            ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
            //读锁
    
            ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
            //写锁
            ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 1000,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.DiscardPolicy());
    
    
            //任务一:读锁演示
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    readLock.lock();
    
                    try {
                        System.out.println(Thread.currentThread().getName() + "进入了读锁,时间:" + new Date());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        readLock.unlock();
                    }
                }
            });
    
            //任务二:读锁演示
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    readLock.lock();
    
                    try {
                        System.out.println(Thread.currentThread().getName() + "进入了读锁,时间:" + new Date());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        readLock.unlock();
                    }
                }
            });
    
            //任务三:写锁
    
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    writeLock.lock();
    
                    try {
                        System.out.println(Thread.currentThread().getName() + "进入了写锁,时间:" + new Date());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        writeLock.unlock();
                    }
                }
            });
            //任务四:写锁
    
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    writeLock.lock();
    
                    try {
                        System.out.println(Thread.currentThread().getName() + "进入了写锁,时间:" + new Date());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        writeLock.unlock();
                    }
                }
            });
    
    
        }
    }
    
    

    在这里插入图片描述

    可重入锁:
    当一个线程拥有了锁之后,可以重复的进入,就叫可重入锁。
    synchronzied就是典型的可重入锁的代表
    读锁的时间在一秒内,所以两个线程读到的锁是一把锁,即读锁是共享锁
    而写锁的时间刚好是一秒,所以写锁是独占锁。

    在这里插入图片描述

    在这里插入图片描述

    自旋锁:相当于死循环,一直尝试获取锁

    详解synchronized锁的优化问题

    synchroized加锁的整个过程,都是依赖于Monitor(监视器锁)实现的,监视器锁在虚拟机中又是根据操作系统的Metux Lock(互斥量)来实现的,这就导致在加锁的过程中需要频繁的在操作系统的内核态和和JVM级别的用户态进行切换,并且涉及到线程上下文的切换,是比较消耗性能的。所以后来有一位大佬Doug Lea基于java实现了一个AQS的框架,提供了Lock锁,性能远远高于synchroized。这就导致Oracle公司很没有面子,因此他们在JDK1.6对synchroized做了优化,引入了偏向锁和轻量级锁。存在一个从无锁-》偏向锁–》轻量级锁–》重量级锁的升级过程,优化后性能就可以和Lock锁的方式持平了。
    对象头
    HotSpot虚拟机中,对象在内存中分为三块区域:对象头、实例数据和对齐填充。
    在这里插入图片描述

    对象头包括两部分:Mark Word 和 类型指针。类型指针是指向该对象所属类对象的指针,我们不关注。mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。在32位系统上mark word长度为32bit,64位系统上长度为64bit。他不是一个固定的数据结构,是和对象的状态紧密相关,有一个对应关系的,具体如下表所示:

    在这里插入图片描述

    当某一线程第一次获得锁的时候,虚拟机会把对象头中的锁标志位设置为“01”,把偏向模式设置为“1”,表示进入偏向锁模式。同时使用CAS操作将获取到这个锁的线程的ID记录在对象的Mark Word中。如果CAS操作成功,持有偏向锁的线程每次进入这个锁的相关的同步块的时候。虚拟机都可以不在进行任何的同步操作。

    当其他线程进入同步块时,发现已经有偏向的线程了,偏向模式马上结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向,也就是将偏向模式设置为“0”,撤销后标志位恢复到“01”,也就是未锁定的状态或者轻量级锁定,标志位为“00”的状态,后续的同步操作就按照下面的轻量级锁那样去执行
    1、在线程进入同步块的时候,如果同步对象状态为无锁状态(锁标志为 01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用来存储锁对象目前的 Mark Word 的拷贝。拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向锁对象的 Mark Word。如果更新成功,则执行 2,否则执行 3。

    在这里插入图片描述
    2、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且锁对象的 Mark Word 中的锁标志位设置为 “00”,即表示此对象处于轻量级锁定状态,这时候虚拟机线程栈与堆中锁对象的对象头的状态如图所示。
    在这里插入图片描述
    3、如果这个更新操作失败了,虚拟机首先会检查锁对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重要量级锁,锁标志的状态值变为 “10”,Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁。自旋失败后膨胀为重量级锁,被阻塞。

    Semaphore

    Semaphore的作用:

    在java中,使用了synchronized关键字和Lock锁实现了资源的并发访问控制,在同一时间只允许唯一了线程进入临界区访问资源(读锁除外),这样子控制的主要目的是为了解决多个线程并发同一资源造成的数据不一致的问题。也就是做限流的作用

    Semaphore实现原理:

    Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

    如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

    就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空侧数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问。

    Semaphore的使用:

    Semaphore使用时需要先构建一个参数来指定共享资源的数量,Semaphore构造完成后即是获取Semaphore、共享资源使用完毕后释放Semaphore。

    使用Semaphore 来模拟有四辆车同时到达了停车场的门口,但是停车位只有两个,也就是只能停两辆车,这就可以使用信号量来实现。👇:

    
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 14:00;
     */
    public class ThreadDemo6 {
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(2);
    
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.DiscardPolicy());
    
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "到达了停车场");
    
                    try {
                        Thread.sleep(1000);
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "进入了停车场");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
    
                    try {
                        Thread.sleep(1000);
    
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    System.out.println(Thread.currentThread().getName() + "出了了停车场");
    
                    semaphore.release();
    
                }
            });
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "到达了停车场");
    
                    try {
                        Thread.sleep(1000);
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "进入了停车场");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(2000);
    
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "出了了停车场");
    
                    semaphore.release();
    
    
                }
            });
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "到达了停车场");
    
                    try {
                        Thread.sleep(1000);
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "进入了停车场");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
    
                    try {
                        Thread.sleep(500);
    
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "出了了停车场");
    
                    semaphore.release();
    
                }
            });
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "到达了停车场");
    
                    try {
                        Thread.sleep(1000);
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "进入了停车场");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
    
                    try {
                        Thread.sleep(1500);
    
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    System.out.println(Thread.currentThread().getName() + "出了了停车场");
    
                    semaphore.release();
    
                }
            });
    
            threadPoolExecutor.shutdown();
        }
    }
    
    

    在这里插入图片描述

    CountDownLatch\CyclicBarrier

    CountDownLatch
    一个可以用来协调多个线程之间的同步,或者说起到线程之间的通信作用的工具类。

    它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。

    CountDownLatch的用法

    某一线程在开始运行前等待n个线程执行完毕。
    将CountDownLatch的计数器初始化为n:new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1, countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

    实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

    CountDownLatch的不足
    CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

    在这里插入图片描述
    模拟赛跑:当三个运动员都到达终点的时候宣布比赛结束

    import java.util.Random;
    import java.util.concurrent.*;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 14:27;
     */
    public class ThreadDemo7 {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(3);
    
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100));
    
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开跑");
    
                    int num = new Random().nextInt(4);
                    num += 1;
    
                    try {
                        Thread.sleep(1000*num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "到达了终点");
    
                    countDownLatch.countDown();
                }
            });
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开跑");
    
                    int num = new Random().nextInt(4);
                    num += 1;
    
                    try {
                        Thread.sleep(1000*num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "到达了终点");
    
                    countDownLatch.countDown();
                }
            });
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开跑");
    
                    int num = new Random().nextInt(4);
                    num += 1;
    
                    try {
                        Thread.sleep(1000*num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "到达了终点");
    
                    countDownLatch.countDown();
                }
            });
            countDownLatch.await();
            System.out.println("所有的选手都到达了终点");
            threadPoolExecutor.shutdown();
        }
    }
    

    在这里插入图片描述
    CyclicBarrier

    CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。

    CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

    import java.util.concurrent.*;
    
    /**
     * user:ypc;
     * date:2021-06-14;
     * time: 15:03;
     */
    public class ThreadDemo8 {
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
                @Override
                public void run() {
                    System.out.println("到达了循环屏障");
                }
            });
    
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100));
    
            for (int i = 0; i < 10; i++) {
    
                int finalI = i;
                threadPoolExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
    
                        try {
                            Thread.sleep(finalI * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "进入了任务");
    
                        try {
    
                            cyclicBarrier.await();
    
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "退出了任务");
    
                    }
                });
            }
    
    
            threadPoolExecutor.shutdown();
        }
    }
    
    

    在这里插入图片描述
    CyclicBarrier原理
    每当线程执行await,内部变量count减1,如果count!= 0,说明有线程还未到屏障处,则在锁条件变量trip上等待。
    当count == 0时,说明所有线程都已经到屏障处,执行条件变量的signalAll方法唤醒等待的线程。
    其中 nextGeneration方法可以实现屏障的循环使用:
    重新生成Generation对象
    恢复count值
    CyclicBarrier可以循环的使用。

    hashmap/ConcurrentHashMap

    hashmap在JDK1.7中头插死循环问题

    来看👇JDK1.7 hashMap transfer的源码

    void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }
    

    来看多线程情况下的问题:
    在这里插入图片描述
    这样就会造成死循环。

    hashmap在JDK1.8中值覆盖问题

    在JDK1.8的时候使用的是尾插法
    来看👇:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    
    

    在多线程的情况下:
    在这里插入图片描述
    其中第六行代码是判断是否出现hash碰撞,假设两个线程1、2都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程1执行完第六行代码后由于时间片耗尽导致被挂起,而线程2得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程2插入的数据被线程1覆盖了,从而线程不安全。

    除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程1、2,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程1执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程2快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程1再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程1、2都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

    ConcurrentHashMap & HashTable

    来看这个🤣

    展开全文
  • java 多线程 出现数据重复调用问题 用id来判断下是否重复可以啊? 求大神给个实列
  • Java多线程面试题

    万次阅读 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话); wait 方法: 是 Object 的方法...

    1、sleep( ) 和 wait( ) 的区别?

    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话);

     

    wait 方法: 是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。

    2、synchronized 关键字?

    底层实现:

    进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1;

    当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。

     

    含义:(monitor 机制)

    Synchronized 是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。

    该关键字是一个几种锁的封装。

    3、volatile 关键字?

    该关键字可以保证可见性不保证原子性。

    功能:

    · 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;

    · 禁止 JVM 进行的指令重排序。

    解析:关于指令重排序的问题,可以查阅 DCL 双检锁失效相关资料。

    4、volatile 能使得一个非原子操作变成原子操作吗?

    能。

    一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。

     

    面试官:volatile 修饰符的有过什么实践?

     

    一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。

    volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

    5、ThreadLocal(线程局部变量)关键字?

    当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。

    ThreadLocal 内部实现机制:

    每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;

    Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;

    Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。

    7.程序,进程和线程的区别

    1)程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

    2)进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个程序从创建、运行到消亡的过程。简单地说,一个进程就是一个执行中地程序,它在计算机中一个指令接着一个指令地执行,同时,每个进程还占有某些系统资源,如CPU时间、内存空间、文件、输入输出设备地使用权等等。

    3)线程:其实与进程相似,也是一个执行中地程序,但是线程是一个比进程更小地执行单位。一个进程在执行过程中可以产生多个线程,形成多条执行执行路径。但是与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换的工作时,负担要比进程小得多,也正因为如此,也正因为如此,线程也被称为轻量级进程。

    8.线程有哪几种状态?

    新建状态、就绪状态、运行状态、阻塞状态、消亡状态这五种状态

    9.线程的互斥与同步的区别

    互斥是指两个或多个线程不能同时运行,而同步则是两个或多个线程的运行有先后次序的约束。

    10.线程的同步与共享数据的区别?

    共享是指线程之间对内存数据的共享,因为线程共同拥有对内存空间中数据的处理权力,这样会导致因为多个线程同时处理数据而使数据出现不一致,所以提出同步解决此问题,即同步是在共享的基础上,是针对多个线程共享会导致数据不一致而提出来的。

    同步指的是处理数据的线程不能处理其他线程当前还没处理完的数据,但是可以处理其他数据。

    11.线程同步与异步区别

    线程同步是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低;线程同步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制。

    1、并发编程三要素?

    (1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

     

    (2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

     

    (3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    (1)发挥多核 CPU 的优势

    多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

     

    (2)防止阻塞

    从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

     

    (3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    (1)继承 Thread 类创建线程类

     

    (2)通过 Runnable 接口创建线程类

     

    (3)通过 Callable 和 Future 创建线程

     

    (4)通过线程池创建

    5、创建线程的三种方式的对比?

    (1)采用实现 Runnable、Callable 接口的方式创建多线程。

    优势是:

     

    线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

     

    劣势是:

     

    编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。

     

    (2)使用继承 Thread 类的方式创建多线程

    优势是:

     

    编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。

     

    劣势是:

     

    线程类已经继承了 Thread 类,所以不能再继承其他父类。

     

    (3)Runnable 和 Callable 的区别

    1、Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。

     

    2、Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。

     

    3、Call 方法可以抛出异常,run 方法不可以。

     

    4、运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    image.png

    7、Java 线程具有五中基本状态

    (1)新建状态(New):

    当线程对象对创建后,即进入了新建状态,如:Thread t= new MyThread();

     

    (2)就绪状态(Runnable):

    当调用线程对象的 start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说执行了t.start()此线程立即就会执行;

     

    (3)运行状态(Running):

    当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

     

    (4)阻塞状态(Blocked):

    处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

     

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

     

    1)等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;

     

    2)同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),

     

    它会进入同步阻塞状态;

     

    3)其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

     

    (5)死亡状态(Dead):

    线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

     

    java 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。

    9、四种线程池的创建:

    (1)newCachedThreadPool 创建一个可缓存线程池

     

    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

     

    (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

     

    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    (1)重用存在的线程,减少对象创建销毁的开销。

     

    (2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

     

    (3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    (1)CountDownLatch

     

    (2)CyclicBarrier

     

    (3)Semaphore

     

    (4)Exchanger

    12、CyclicBarrier 和 CountDownLatch 的区别

    (1)CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用 countDown()方法发出通知后,当前线程才可以继续执行。

     

    (2)cyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await()方法之后,所有线程同时开始执行!

     

    (3)CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用 reset() 方法重置。所以 CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

     

    (4)CyclicBarrier 还提供其他有用的方法,比如 getNumberWaiting 方法可以获得 CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回 true,否则返回 false。

    13、synchronized 的作用?

    在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 既可以加在一段代码上,也可以加在方法上。

    14、volatile 关键字的作用

    对于可见性,Java 提供了 volatile 关键字来保证可见性。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

    15、什么是 CAS

    CAS 是 compare and swap 的缩写,即我们所说的比较交换。

     

    cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

     

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

     

    java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS 的问题

    (1)CAS 容易造成 ABA 问题

    一个线程 a 将数值改成了 b,接着又改成了 a,此时 CAS 认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。

     

    (2)不能保证代码块的原子性

    CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 synchronized 了。

     

    (3)CAS 造成 CPU 利用率增加

    之前说过了 CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是 Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承 thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。通过实现 Callback 接口,并用 Future 可以来接收多线程的执行结果。

     

    Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback 以便在任务执行成功或失败后作出相应的操作。

    18、什么是 AQS

    AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。

     

    AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于AQS 的。

    19、AQS 支持两种同步方式:

    (1)独占式

    (2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如Semaphore,CountDownLatch,组 合 式 的 如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock 是什么

    首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask 是什么

    这个其实前面有提到过,FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

    22、synchronized 和 ReentrantLock 的区别

    synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

     

    (1)ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁

     

    (2)ReentrantLock 可以获取各种锁的信息

     

    (3)ReentrantLock 可以灵活地实现多路通知

     

    另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word,这点我不能确定。

    23、什么是乐观锁和悲观锁

    (1)乐观锁:

    就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

     

    (2)悲观锁:

    还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。

    24、线程 B 怎么知道线程 A 修改了变量

    (1)volatile 修饰变量

     

    (2)synchronized 修饰修改变量的方法

     

    (3)wait/notify

     

    (4)while 轮询

    25、synchronized、volatile、CAS 比较

    (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

     

    (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

     

    (3)CAS 是基于冲突检测的乐观锁(非阻塞)

    26、sleep 方法和 wait 方法有什么区别?

    这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器

    27、ThreadLocal 是什么?有什么用?

    ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

    28、为什么 wait()方法和 notify()/notifyAll()方法要在同步块中被调用

    这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁

    29、多线程同步有哪几种方法?

    Synchronized 关键字,Lock 锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

     

    (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利

     

    (2)线程体中调用了 sleep 方法使线程进入睡眠状态

     

    (3)线程由于 IO 操作受到阻塞

     

    (4)另外一个更高优先级线程出现

     

    (5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap 的并发度是什么

    ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有 16 条线程操作 ConcurrentHashMap,这也是ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 能同时有两条线程获取 Hashtable 中的数据吗?

    32、Linux 环境下如何查找哪个线程使用 CPU 最长

    (1)获取项目的 pid,jps 或者 ps -ef | grep java

     

    (2)top -H -p pid,顺序不能改变

    33、Java 死锁以及如何避免?

    Java 中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java 死锁情况出现至少两个线程和两个或更多资源。

     

    Java 发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    (1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁 A 并且没有释放的情况下去申请锁 B,这时,另一个线程已经获得了锁 B,在释放锁 B 之前又要先获得锁 A,因此闭环发生,陷入死锁循环。

     

    (2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了 wait()、sleep()或 者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

    多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

    38、如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

     

    (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

     

    (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

    39、Java 中用到的线程调度算法是什么

    抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    40、什么是线程调度器(Thread Scheduler)和时间分片(TimeSlicing)?

    线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    41、什么是自旋

    很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

    42、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

    Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

     

    它的优势有:

     

    (1)可以使锁更公平

     

    (2)可以使线程在等待锁的时候响应中断

     

    (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

     

    (4)可以在不同的范围,以不同的顺序获取和释放锁

    43、单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

     

    (1)饿汉式单例模式的写法:线程安全

     

    (2)懒汉式单例模式的写法:非线程安全

     

    (3)双检锁单例模式的写法:线程安全

    44、Semaphore 有什么作用

    Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

    45、Executors 类是什么?

    Executors 为 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。

     

    如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:

     

    (1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的

     

    (2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java 线程数过多会造成什么异常?

    (1)线程的生命周期开销非常高

     

    (2)消耗过多的 CPU 资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

     

    (3)降低稳定性

    JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

     1、Synchronized用过吗,其原理是什么?

    这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程 序的开发者会没听说或者没接触过Synchronizedo Synchronized是由JVM 实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过 的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在 编译前后被编译器生成了 monitorenter和monitorexit两个字节码指令。这两 个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试 获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对 象的锁,把锁的计数器+ 1 ;当执行monitorexit指令时将锁计数器-1 ;当计 数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞 等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对 象头设置标记,达到了获取锁和释放锁的目的。

    2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的 锁?

    "锁"的本质其实是monitorenter和monitorexit字节码指令的一个Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以 修饰不同的对象,因此,对应的对象锁可以这么确定。

     

    1. 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、 Synchronized(this)等,说明加解锁对象为该对象。

     

    2. 如果没有明确指定:

     

    若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象

     

    若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。

     

    注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。

    3、什么是可重入性,为什么说Synchronized是可重入锁?

    可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。 比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如 Synchronized不支持重入,进入method2方法时当前线程获得锁, method2方法里面执行method1时当前线程又要去尝试获取锁,这时如 果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。 对Synchronized来说,可重入性是显而易见的,刚才提到,在执行 monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了 这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1, 其实本质上就通过这种方式实现了可重入性。

    4、JVM对Java的原生锁做了哪些优化?

    在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现, 也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。

    由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个 线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内 核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做 了大量的优化。一种优化是使用自旋锁,即在把线程进行阻塞操作之前先 让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无 需再让线程执行阻塞操作,避免了用户态到内核态的切换。

    现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:

     

    •偏向锁(Biased Locking)

     

    •轻量级锁

     

    •重量级锁

     

    这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同 的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。

     

    •当没有竞争出现时,默认会使用偏向锁。

    JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表 示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应 用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可 以降低无竞争开销。

     

    •如果有另一线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切 换到轻量级锁实现。

     

    •轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就 使用普通的轻量级锁;否则,进一步升级为重量级锁。

    5、为什么说Synchronized是非公平锁?

    非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待 线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样 做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

    6、什么是锁消除和锁粗化?

    •锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检 测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。

    程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程 序员自己加入的。

     

    •锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操 作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进 行互斥同步操作也会导致不必要的性能损耗。

    锁粗化就是增大锁的作用域。

    7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?

    Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否 会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁 计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的 发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没 有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲 突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要 线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预 期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。 这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否 则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。 CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调 用Native方法调用由C++编写的硬件级别指令,JDK中提供了 Unsafe 类执行这些操作。

    8、乐观锁一定就是好的吗?

    乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

     

    1. 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐 观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗 粒度大小。

     

    2. 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会 给CPU带来很大的开销。

     

    3. ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后 来被一条线程改为B,最后又被改成了 A,则CAS认为此内存值并没有发 生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景 的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本 号加一。

    9、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什 么不同?

    其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某 种标记。

    Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原 生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现 类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有 对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

    10、那么请谈谈AQS框架是怎么回事儿?

    AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架, 各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其 他如 Semaphorex CountDownLatch,甚至是早期的 FutureTask 等,都是 基于AQS来构建。

     

    1. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调 用lock方法时,如果state=O,说明没有任何线程占有共享资源的 锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使 用共享变量,其他线程必须加入同步队列进行等待。

     

    2. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线 程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。

     

    • Node类是对要访问同步代码的线程的封装,包含了线程本身及其状 态叫

    waitStatus(有五种不同取值,分别表駅是否被阻塞,是否等待唤醒,是 否已经被取消等),每个Node结点关联其prev结点和next结点,方便 线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。

     

    • Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和 独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时 间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等 待(如 ReentranLock)。

     

    3. AQS通过内部类ConditionObject构建等待队列(可有多个),当 Condition调用wait。方法后,线程将会加入等待队列中,而当 Condition调用signal。方法后,线程将从等待队列转移动同步队列中进 行锁竞争。

     

    4. AQS和Condition各自维护了不同的队列,在使用Lock和Condition 的时候,其实就是两个队列的互相移动。

    11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。

    ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度, ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一 样使用),甚至实现Synchronized没有的高级功能,如:

     

    •等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以 选择放弃等待,对处理执行时间非常长的同步块很有用。

     

    •带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无 法获取则返回。

     

    •可以判断是否有线程在排队等待获取锁。

     

    •可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断

    时,能够响应中断,中断异常将会被抛出,同时锁会被释放。

     

    •可以实现公平锁。

     

    从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些 监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证 锁定一定会被释放,就必须将unLock。放到finally。中。

    从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大 多数场景性能都相差较大。

     

    但是在Java 6中对其进行了非常多的改进,在竞争不激烈时, Synchronized的性能要优于ReetrantLock;在高竞争情况下, Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常 态。

    12、ReentrantLock是如何实现可重入性的?

    ReentrantLock内部自定义了同步器Sync(Sync既实现了 AQS,又实现了 AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下 当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入 了。

    13、除了 ReetrantLock,你还接触过JUC中的哪些并发工具?

    通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:

     

    •提供了 CountDownLatch、CyclicBarrier、Semaphore等,比 Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。

     

    •提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通 过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线

    程安全的容器。

     

    •提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue等,各种并发队列实现。

     

    •强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行 等。

    14、请谈谈 ReadWriteLock 和 StampedLock。

    虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限 性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写 操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提 供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图 锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作 结束,这样就可以自动保证不会读取到有争议的数据。

     

    ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构, 当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势

    读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其 表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后 期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先 试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。

    15、如何让Java的线程彼此同步?

    你了解过哪些同步器?请分别介绍下。 JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。

     

    CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看 几个场景:

     

    •跑步比赛,裁判需要等到所有的运动员(“其他线程")都跑到终点(达到目 标),才能去算排名和颁奖。

     

    •模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们 能同时并发,而不是一个一个的去执行。

     

    CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部 同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使 用。CyclicBarrier的典型应用场景是用来等待并发线程结束。CyclicBarrier 的主要方法是await(),await。每被调用一次,计数便会减少1,并阻塞住 当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞 的线程开始运行。

     

    在这之后,如果再次调用await。,计数就又会变成N-1,新一轮重新开 始,这便是Cyclic的含义所在。CyclicBarrier.await。带有返回值,用来表 示当前线程是第几个到达这个Barrier的线程。

     

    Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来 达到限制通用资源访问的目的,其原理是通过acquire。获取一个许可,如 果没有就等待,而release。释放一个许可。

     

    如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比 如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类 似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

    16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?

    它们的行为有一定相似度,区别主要在于:

     

    • CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这 种限制,可以重用。

     

    • CountDownLatch的基本操作组合是countDown/await,调用await的线 程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可。CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。

    CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后, 自己再去做某个事(通过CyclicBarrier的第二个构造方法public

     

    CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达 到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有 的都达到某个状态,然后这N个线程再继续执行各自后续(通过 CountDownLatch在某些场合也能完成类似的效果)。

    17、Java中的线程池是如何实现的?

    •在Java中,所谓的线程池中的“线程",其实是被抽象为了一个静态内部 类Worker,它基于AQS实现,存放在线程池的

    HashSet workers成员变量中;

     

    •而需要执行的任务则存放在成员变量workQueue(BlockingQueue workQueue)中。

    这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执 行的任务,放在Workers中进行处理。

    18、创建线程池的几个核心构造参数?

    Java中的线程池的创建其实非常灵活,我们可以通过配置不同的参数, 创建出行为不同的线程池,这几个参数包括:

     

    • corePoolSize:线程池的核心线程数。

     

    • maximumPoolSize:线程池允许的最大线程数。

     

    • keepAliveTime:超过核心线程数时闲置线程的存活时间。

     

    • workQueue :任务执行前保存任务的队列,保存由execute方法提交的 Runnable 任务。

    19、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好 的吗?

    显然不是的。线程池默认初始化后不启动Worker,等待有请求时才启 动。

     

    每当我们调用execute()方法添加一个任务时,线程池会做如下判断: •如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务

     

    •如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放 入队列;

     

    •如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

     

    •如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException o 当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程 无事可做,超过一定的时间(keepAliveTime )时,线程池会判断。

    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以 线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

    20、既然提到可以通过配置不同参数创建出不同的线程池,那么Java中 默认实现好的线程池又有哪些呢?请比较它们的异同。

    1. SingleThreadExecutor 线程池

    这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有 任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代 它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    • corePoolSize:'只有一个核心线程在工作。

    • maximumPoolSize: 1。

    • keepAliveTime: 0L。

    • workQueue:new LinkedBlockingQueue<Runnable>(),其缓冲队列 是无界的。

     

    2. FixedThreadPool 线程池

    FixedThreadPool是固定大小的线程池,只有核心线程。每次提交一个任 务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦 达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程 池会补充一个新线程。

    FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服

    务器。

    • corePoolSize: nThreads

    • maximumPoolSize: nThreads

    • keepAliveTime: 0L

    • workQueue:new LinkedBlockingQueue<Runnable>(),其缓冲队列 是无界的。

     

    3. CachedThreadPool 线程池

    CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所 需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增 加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依 赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为1的阻塞队列。缓存型池子通常用于执行一些生存期很 短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不 多。但对于生存期短的异步任务,它是Executor的首选。

    • corePoolSize: 0

    • maximumPoolSize: Integer.MAX_VALUE

    • keepAliveTime: 60L

    • workQueue:new SynchronousQueue<Runnable>(), —个是缓冲区为 1的阻塞队列。

     

    4. ScheduledThreadPool 线程池

    ScheduledThreadPool :核心线程池固定,大小无限的线程池。此线程池 支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程 池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内 回收。

    • corePoolSize: corePoolSize

    • maximumPoolSize: Integer.MAX_VALUE

    • keepAliveTime: DEFAULT_KEEPALIVE_MILLIS

    • workQueue:new DelayedWorkQueue()

    21、如何在Java线程池中提交线程?

    线程池最常用的提交任务的方法有两种:

    1. execute。: ExecutorService.execute 方法接收一个例,它用来执行一个任务

     

    2. submit。: ExecutorService.submit。方法返回的是 Future 对象。可以用 isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果, 可以调用get。来获取结果。也可以不用isDone。进行检查就直接调用 get,在这种情况下,get将阻塞,直至结果准备就绪。

    22、什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的 变量的?

    Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变 量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字 段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数, 因为这些是线程私有的,不会被共享,所以不存在竞争问题。

     

    Java中各个线程是怎么彼此看到对方的变量的呢?Java中定义了主内存与 工作内存的概念:

     

    所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该 线程使用到的变量的主内存副本拷贝。

     

    线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接 读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变 量,线程间变量值的传递需要通过主内存。

    23、请谈谈volatile有什么特点,为什么它能保证变量对所有线程的可见 性?

    关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被 定义成volatile之后,具备两种特性:

     

    1. 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新 值对于其他线程是可以立即得知的。而普通变量做不到这一点。

     

    2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到 正确结果,但是不保证程序代码的执行顺序。

     

    Java的内存模型定义了 8种内存间操作:

     

    lock 和 unlock

     

    •把一个变量标识为一条线程独占的状态。

    •把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程 锁定。

     

    read 和 write

     

    •把一个变量值从主内存传输到线程的工作内存,以便load。

    •把store操作从工作内存得到的变量的值,放入主内存的变量中。

     

    load 和 store

     

    •把read操作从主内存得到的变量值放入工作内存的变量副本中。•把工 作内存的变量值传送到主内存,以便write。

     

    use 和 assgin

     

    •把工作内存变量值传递给执行引擎。

    •将执行引擎值传递给工作内存变量值。

     

    volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变 量的修改,一定会被另一个线程看见,即保证了可见性。

    24、既然volatile能够保证线程间的变量可见性,是不是就意味着基于 volatile变量的运算就是并发安全的?

    显然不是的。基于volatile变量的运算在并发下不一定是安全的。volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中 volatile变量,每次使用前都要刷新到主内存)。

     

    但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一 样是不安全的。

    25、请对比下volatile对比Synchronized的异同。

    Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可 见性,无法保证原子性。

     

    ThreadLoca l和Synchonized都用于解决多线程并发访问,防止任务在共 享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。 Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一 时该只能被一个线程访问,是一种“以时间换空间''的方式。

     

    而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一 时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间 换时间”的方式。

    26、请谈谈ThreadLocal是怎么解决并发安全的?

    ThreadLocal这是Java提供的一种保存线程私有信息的机制,因为其在 整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模 块之间传递信息,比如事务ID、Cookie等上下文相关信息。

     

    ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制 在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用 于存储每一个线程的变量的副本。

    27、很多人都说要慎用ThreadLocal,谈谈你的理解,使用ThreadLocal需要注意些什么?

    使用 ThreadLocal 要注意 remove!

     

    ThreadLocal的实现是基于一个所谓的ThreadLocalMap,在 ThreadLocalMap中,它的key是一个弱引用。

     

    通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例 外,它并没有这么做。

     

    这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束, 进而回收相应ThreadLocalMap!这就是很多00M的来源,所以通常都会 建议,应用一定要自己负责remove,并且不要和线程池配合,因为 worker线程往往是不会退出的。

    11.进程和线程的区别是什么?

    进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。

    12.创建线程有几种不同的方式?你喜欢哪一种?为什么?

    有三种方式可以用来创建线程:

    ●继承Thread类

    ●实现Runnable接口

    ●应用程序可以使用Executor框架来创建线程池

    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。

    13.概括的解释下线程的几种可用状态。

    线程在执行过程中,可以处于下面几种状态:

    ●就绪(Runnable):线程准备运行,不一定立马就能开始执行。

    ●运行中(Running):进程正在执行线程的代码。

    ●等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。

    ●睡眠中(Sleeping):线程被强制睡眠。

    ●I/O阻塞(Blocked on I/O):等待I/O操作完成。

    ●同步阻塞(Blocked on Synchronization):等待获取锁。

    ●死亡(Dead):线程完成了执行。

    14.同步方法和同步代码块的区别是什么?

    在Java语言中,每一个对象有一把锁。线程可以使用synchronized关键字来获取对象上的锁。synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。

    15.在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

    监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

    16.什么是死锁(deadlock)?

    两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。

    17.如何确保N个线程可以访问N个资源同时又不导致死锁?

    使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

    3、start()方法和run()方法的区别

    只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

    4、Runnable接口和Callable接口的区别

    Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

     

    这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

    5、CyclicBarrier和CountDownLatch的区别

    两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

     

    1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

     

    2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

     

    3) CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

    6、volatile关键字的作用

    一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个:

     

    1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。

     

    2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger

    7、什么是线程安全

    如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

     

    这个问题有值得一提的地方,就是线程安全也是有几个级别的:

     

    1)不可变

    像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

     

    2)绝对线程安全

    不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

     

    3)相对线程安全

    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

     

    4)线程非安全

    这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类,点击这里了解为什么不安全。

    8、Java中如何获取到线程dump文件

    死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

     

    1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

     

    2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

     

    另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。

    9、一个线程如果出现了运行时异常会怎么样

    如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

    10、如何在两个线程之间共享数据

    通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

    11、sleep方法和wait方法有什么区别 

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    12、生产者消费者模型的作用是什么

    这个问题很理论,但是很重要:

    1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

    2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

    13、ThreadLocal有什么用

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

    14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

    wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

    16、为什么要使用线程池

    避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。点击这里学习线程池详解。

    17、怎么检测一个线程是否持有对象监视器

    我也是在网上看到一道多线程面试题才知道有方法可以判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。

    18、synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

     

    (1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

     

    (2)ReentrantLock可以获取各种锁的信息

     

    (3)ReentrantLock可以灵活地实现多路通知

     

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    19、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

     

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、Linux环境下如何查找哪个线程使用CPU最长

    这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:

     

    (1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

     

    (2)top -H -p pid,顺序不能改变

     

    这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下。

     

    使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。

     

    最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。

    23、Java编程写一个会导致死锁的程序

    1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

     

    2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁

     

    3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的

     

    这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。

    24、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    25、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    26、什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    27、如果你提交任务时,线程池队列已满,这时会发生什么

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

     

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

    28、Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    29、Thread.sleep(0)的作用是什么

    这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

    30、什么是自旋

    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

    31、什么是Java内存模型

    Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:

     

    1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去

     

    2)定义了几个原子操作,用于操作主内存和工作内存中的变量

     

    3)定义了volatile变量的使用规则

     

    4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

    32、什么是CAS

    CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。更多CAS详情请点击这里学习。

    33、什么是乐观锁和悲观锁

    1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

     

    2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    35、单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

     

    1)饿汉式单例模式的写法:线程安全

     

    2)懒汉式单例模式的写法:非线程安全

     

    3)双检锁单例模式的写法:线程安全

    36、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    37、Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?

    这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

    关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:

     

    1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

     

    2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

    38、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

     

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

     

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

     

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    39、同步方法和同步块,哪个是更好的选择

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

     

    借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。

    40、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

    关于这个问题,个人看法是:

     

    1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

     

    2)并发不高、任务执行时间长的业务要区分开看:

    a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

    b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

    c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

     

    展开全文
  • Java多线程(三)- 线程的唯一标识是什么? 数据库里头,我们建表的时候通常唯一标识叫什么名字?没错,叫ID,Thread对象也有ID,可以通过getId来获取。这个ID是自增长的,我们可以查看Thread源代码,下面是我截取...

    结论

    threadSeqNumber是线程的ID,可以通过线程对象的getId方法来获取。

    分析

    数据库里头,我们建表的时候通常唯一标识叫ID,Thread对象也有ID,可以通过getId来获取。这个ID是自增长的,我们可以查看Thread源代码,下面是我截取出来的代码片段。

    public class Thread implements Runnable {
        /* For generating thread ID */
        private static long threadSeqNumber;
        
        private static int threadInitNumber;
    
        private static synchronized long nextThreadID() {
            return ++threadSeqNumber;
        }
    
        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        }
    }
    
    

    从代码中可以看到有两种ID,分别是threadSeqNumberthreadInitNumberthreadSeqNumber是线程的ID,可以看到这个ID是同步递增的,我们可以在Thread的init方法中看到,每个线程都会分配一个这样的ID。

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
          
         // ... (省略其它源码内容)
         
         /* Set thread ID */
         tid = nextThreadID();
    }
    

    而threadInitNumber是当我们没有给线程起名字的时候,默认会采用Thread-N的格式给线程起名,这里的N就是threadInitNumber。从Thread源码中我们可以看到下面的构造函数,这里只列出来两个与之相关的构造。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    public Thread(String name) {
        init(null, null, name, 0);
    }
    

    很明显,我们可以发现,如果我们采用这里无参构造,会产生一个格式为Thread-N的默认名称,此时threadInitNumber也会随之递增,如果我们采用有参的构造,threadInitNumber就不会递增。所以我们不要愚蠢的看到默认名称后面的数字,就认为我们当前主线程下开启了这么多个线程,更不要愚蠢的认为这个数字就是线程的ID。这个数字跟ID和线程数没半毛钱关系。
    当我们拿到线程的ID以后,就可以根据ID获取到这个线程对象。
    如下:

    public static void main(String[] args) throws Exception {
    		
    	// 构造一个线程并启动,取名为"my-thread"
    	Thread myThread = new Thread(new Runnable() {
    		public void run() {
    			try {
    				Thread.sleep(100);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}, "my-thread");
    	myThread.start();
    	long myThreadId = myThread.getId();
    	System.out.println("my-thread线程ID为:" + myThreadId);
    	
    	// 拿到当前线程下所有子线程,找出名称为"my-thread"的线程并输出该线程的ID
    	// 这串代码用到了activeCount和enumerate方法,在API的介绍中有详细说明
    	Thread[] ts = new Thread[Thread.activeCount()];
    	Thread.enumerate(ts);
    	for(Thread t: ts) {
    		if(t.getId() == myThread.getId()) {
    			System.out.println("从主线程中找到名为my-thread的线程,线程名称为:" + t.getName() + ", 状态为: " + t.getState());
    		}
    	}
    }
    

    找遍了Thread的方法,没有找到可以直接通过ID或者NAME去获取thread对象的方法,最终采用了这种拙劣的方式。无论这种方式是否最佳,我们根据ID拿到了子线程对象进而可以操作这个子线程。

    注意,Thread.enumerate只能拿到非NEW和非TERMINATED状态的子线程。所以这段代码可能没有任何输出。

    (完)

    展开全文
  • JAVA多线程并发

    千次阅读 多人点赞 2019-09-18 12:14:29
    JAVA多线程并发1 JAVA并发知识库2 JAVA 线程实现/创建方式2.1 继承 Thread 类2.2 实现 Runnable 接口2.3 Callable 、Future 、ExecutorService 有返回值线程2.4 基于线程池的方式2.4.1 4种线程池2.4.1.1 ...

    JAVA多线程并发

    1 JAVA并发知识库

    在这里插入图片描述

    2 JAVA 线程实现/创建方式

    2.1 继承 Thread 类

    Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

    public class MyThread extends Thread {
    	public void run() {
    		System.out.println("MyThread.run()");
    	}
    }
    MyThread myThread1 = new MyThread();
    myThread1.start();
    

    2.2 实现 Runnable 接口

    如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口。

    public class MyThread extends OtherClass implements Runnable {
    	public void run() {
    		System.out.println("MyThread.run()");
    	}
    }
    
    //启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
    MyThread myThread = new MyThread();
    Thread thread = new Thread(myThread);
    thread.start();
    
    //事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用target.run()
    public void run() {
    	if (target != null) {
    		target.run();
    	}
    }
    

    2.3 Callable 、Future 、ExecutorService 有返回值线程

    有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    //创建一个线程池
    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    // 创建多个有返回值的任务
    List<Future> list = new ArrayList<Future>();
    for (int i = 0; i < taskSize; i++) {
    	Callable c = new MyCallable(i + " ");
    	// 执行任务并获取 Future 对象
    	Future f = pool.submit(c);
    	list.add(f);
    }
    // 关闭线程池
    pool.shutdown();
    // 获取所有并发任务的运行结果
    for (Future f : list) {
    	// 从 Future 对象上获取任务的返回值,并输出到控制台
    	System.out.println("res:" + f.get().toString());
    }
    

    2.4 基于线程池的方式

    线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {
    	threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
    		@Override
    		public void run() {
    			System.out.println(Thread.currentThread().getName() + " is running ..");
    			try {
    				Thread.sleep(3000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	});
    }
    

    2.4.1 4种线程池

    Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。

    2.4.1.1 newCachedThreadPool

    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

    2.4.1.2 newFixedThreadPool

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

    2.4.1.3 newScheduledThreadPool

    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
    scheduledThreadPool.schedule(new Runnable(){
    	@Override
    	public void run() {
    		System.out.println("延迟三秒");
    	}
    }, 3, TimeUnit.SECONDS);
    scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
    	@Override
    	public void run() {
    		System.out.println("延迟 1 秒后每三秒执行一次");
    	}
    },1,3,TimeUnit.SECONDS);
    

    2.4.1.4 newSingleThreadExecutor

    Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

    3 线程生命周期(状态)

    在这里插入图片描述
    当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

    3.1 新建状态(NEW)

    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。

    3.2 就绪状态(RUNNABLE)

    当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

    3.3 运行状态(RUNNING)

    如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

    3.4 阻塞状态(BLOCKED)

    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。

    阻塞的情况分三种:

    • 等待阻塞( o.wait -> 等待对列 ):运行(running)的线程执行 o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞 ( lock -> 锁池 ) :运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM
      会把该线程放入锁池(lock pool)中。
    • 其他阻塞 ( sleep/join ) :运行(running)的线程执行 Thread.sleep(long ms)或
      t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当
      sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪(runnable)状态。

    3.5 线程死亡(DEAD)

    线程会以下面三种方式结束,结束后就是死亡状态。

    • 正常结束:run()或 call()方法执行完成,线程正常结束。
    • 异常结束:线程抛出一个未捕获的 Exception 或 Error。
    • 调用 stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

    4 终止线程的4种方式

    4.1 正常运行结束

    程序运行结束,线程自动结束。

    4.2 使用退出标志退出线程

    一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出,代码示例:

    public class ThreadSafe extends Thread {
    	public volatile boolean exit = false;
    	public void run() {
    		while (!exit){
    			//do something
    		}
    	}
    }
    

    定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

    4.3 Interrupt 方法结束线程

    使用 interrupt()方法来中断线程有两种情况:

    • 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
    • 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
    class ThreadSafe extends Thread {
    	public void run() {
    		while (!isInterrupted()) { // 非阻塞过程中通过判断中断标志来退出
    			try {
    				Thread.sleep(5 * 1000);// 阻塞过程捕获中断异常来退出
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    				break;// 捕获到异常之后,执行 break 跳出循环
    			}
    		}
    	}
    }
    

    4.4 stop方法终止线程

    程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

    5 相关知识

    5.1 sleep 与 wait 区别

    • 对于sleep()方法,我们首先要知道该方法是属于Thread 类中的。而wait()方法,则是属于Object 类中的。
    • sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复就绪状态。
    • 在调用sleep()方法的过程中,线程不会释放对象锁。
    • 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入就绪状态。

    5.2 start 与 run 区别

    • start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
    • 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
    • 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

    5.3 JAVA后台线程

    • 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
    • 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
    • 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是调用线程对象的 setDaemon 方法(thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程)。
    • 在 Daemon 线程中产生的新线程也是 Daemon 的。
    • 线程是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。正是因为这个很隐晦的问题,所以很多有经验的开发者不太赞成在Web应用中私自启动线程。
    • example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
    • 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

    6 JAVA锁

    6.1 乐观锁

    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    6.2 悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

    6.3 自旋锁

    • 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
    • 线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。
    • 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    6.3.1 自旋锁的优缺点

    • 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
    • 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

    6.3.2 自旋锁时间阈值(1.6 引入了适应性自旋锁)

    • 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
    • JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

    6.3.3 自旋锁的开启

    JDK1.6 中 -XX:+UseSpinning 开启;
    -XX:PreBlockSpin=10 为自旋次数;
    JDK1.7 后,去掉此参数,由 jvm 控制;

    6.4 Synchronized同步锁

    synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

    6.4.1 Synchronized 作用范围

    1. 作用于方法时,锁住的是对象的实例(this);
    2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
    3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    6.4.2 Synchronized 核心组件

    1. Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
    2. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    3. Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck;
    5. Owner:当前已经获取到所资源的线程被称为 Owner;
    6. !Owner:当前释放锁的线程。

    6.4.3 Synchronized 实现

    在这里插入图片描述

    1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
    2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
    3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
    4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
    5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
    6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
    7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
    8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
    9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
    10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
    11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

    6.5 ReentrantLock

    ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

    6.5.1 Lock 接口的主要方法

    1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
    2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
    3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
    4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
    5. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。
    6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
    7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
    8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
    9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
    10. hasQueuedThreads():是否有线程等待此锁
    11. isFair():该锁是否公平锁
    12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
    13. isLock():此锁是否有任意线程占用
    14. lockInterruptibly():如果当前线程未被中断,获取锁
    15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
    16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

    6.5.2 非公平锁

    JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。

    6.5.3 公平锁

    公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。

    6.5.4 ReentrantLock 与 synchronized

    1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
    2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。

    6.5.5 ReentrantLock 实现

    public class MyService {
    	private Lock lock = new ReentrantLock();
    	//Lock lock = new ReentrantLock(true);//公平锁
    	//Lock lock = new ReentrantLock(false);//非公平锁
    	private Condition condition = lock.newCondition();// 创建 Condition
    
    	public void testMethod() {
    		try {
    			lock.lock();// lock 加锁
    			//1:wait 方法等待:
    			//System.out.println("开始 wait");
    			condition.await();
    			//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
    			//:2:signal 方法唤醒
    			condition.signal();// condition 对象的 signal 方法可以唤醒 wait 线程
    			for (int i = 0; i < 5; i++) {
    				System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    }
    

    6.5.6 Condition 类和 Object 类锁方法区别

    1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
    2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
    3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
    4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

    6.5.7 tryLock 和 lock 和 lockInterruptibly 的区别

    1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。
    2. lock 能获得锁就返回 true,不能的话一直等待获得锁。
    3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

    6.6 Semaphore 信号量

    Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。

    6.6.1 实现互斥锁(计数器为 1 )

    我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

    // 创建一个计数阈值为 5 的信号量对象
    // 只能 5 个线程同时访问
    Semaphore semp = new Semaphore(5);
    try { // 申请许可
    	semp.acquire();
    	try {
    		// 业务逻辑
    	} catch (Exception e) {
    	
    	} finally {
    		// 释放许可
    		semp.release();
    	}
    } catch (InterruptedException e) {
    
    }
    

    6.6.2 Semaphore 与 ReentrantLock

    • Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
    • 此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
    • Semaphore的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。

    6.7 AtomicInteger

    • 首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference将一个对象的所有操作转化成原子操作。
    • 我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。 通常我们会使用 synchronized将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger 的性能是 ReentantLock 的好几倍。

    6.8 可重入锁(递归锁)

    这里讲的是广义上的可重入锁,而不是单指JAVA下的 ReentrantLock。可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

    6.9 公平锁与非公平锁

    6.9.1 公平锁( Fair )

    加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

    6.9.2 非公平锁( Nonfair )

    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

    1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
    2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

    6.10 ReadWriteLock 读写锁

    为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
    Java 中读写有个接口 java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock。

    6.10.1 读锁

    如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。

    6.10.2 写锁

    如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    6.11 共享锁和独占锁

    java 并发包提供的加锁模式分为独占锁和共享锁。

    6.11.1 独占锁

    独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

    6.11.2 共享锁

    共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

    1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
    2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

    6.12 锁的状态

    锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

    锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

    6.12.1 重量级锁( Mutex Lock )

    Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

    6.12.2 轻量级锁

    “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀
    为重量级锁。

    6.12.3 偏向锁

    Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

    6.13 分段锁

    分段锁也并非一种实际的锁,而是一种思想。ConcurrentHashMap 是学习分段锁的最好实践。

    6.14 锁优化

    6.14.1 减少锁持有时间

    只用在有线程安全要求的程序上加锁.

    6.14.2 减小锁粒度

    将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

    6.14.3 锁分离

    最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]JDK 并发包 1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据

    6.14.4 锁粗化

    通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

    6.14.5 锁消除

    锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。

    7 线程基本方法

    线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。
    在这里插入图片描述

    7.1 线程等待( wait )

    调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

    7.2 线程睡眠( sleep )

    sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。

    7.3 线程让步( yield )

    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

    7.4 线程中断( interrupt )

    中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
      程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
    2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
      InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
    3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
    4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

    7.5 Join 等待其他线程终止

    join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

    为什么要用 join() 方法 ?很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

    System.out.println(Thread.currentThread().getName() + "线程运行开始!");
    Thread6 thread1 = new Thread6();
    thread1.setName("线程 B");
    thread1.join();
    System.out.println("这时 thread1 执行完毕之后才能执行主线程");
    

    7.6 线程唤醒( notify )

    Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
    争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

    7.7 其他方法

    1. sleep():强迫一个线程睡眠N毫秒。
    2. isAlive(): 判断一个线程是否存活。
    3. join(): 等待线程终止。
    4. activeCount(): 程序中活跃的线程数。
    5. enumerate(): 枚举程序中的线程。
    6. currentThread(): 得到当前线程。
    7. isDaemon(): 一个线程是否为守护线程。
    8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    9. setName(): 为线程设置一个名称。
    10. wait(): 强迫一个线程等待。
    11. notify(): 通知一个线程继续运行。
    12. setPriority(): 设置一个线程的优先级。
    13. getPriority()::获得一个线程的优先级。

    8 线程上下文切换

    巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
    在这里插入图片描述

    8.1 进程

    (有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

    8.2 上下文

    是指某一时间点 CPU 寄存器和程序计数器的内容。

    8.3 寄存器

    是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

    8.4 程序计数器

    是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

    8.5 PCB-“切换桢”

    上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。

    8.6 上下文切换的活动

    1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
    2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
    3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。

    8.7 引起线程上下文切换

    1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
    2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
    3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
    4. 用户代码挂起当前任务,让出 CPU 时间;
    5. 硬件中断;

    9 同步锁与死锁

    9.1 同步锁

    当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

    9.2 死锁

    何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

    10 线程池原理

    线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

    10.1 线程复用

    每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

    10.2 线程池的组成

    一般的线程池主要分为以下 4 个组成部分:

    1. 线程池管理器:用于创建并管理线程池
    2. 工作线程:线程池中的线程
    3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
    4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

    Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
    在这里插入图片描述
    ThreadPoolExecutor 的构造方法如下:

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, 
    		TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    		Executors.defaultThreadFactory(), defaultHandler);
    }
    
    1. corePoolSize:指定了线程池中的线程数量。
    2. maximumPoolSize:指定了线程池中的最大线程数量。
    3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
    4. unit:keepAliveTime 的单位。
    5. workQueue:任务队列,被提交但尚未被执行的任务。
    6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
    7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

    10.3 拒绝策略

    线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

    JDK 内置的拒绝策略如下:

    1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
    2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
    3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
    4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

    以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

    10.4 Java 线程池工作过程

    • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
    • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
      a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
      b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
      c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
      d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
    • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
      在这里插入图片描述

    11 JAVA阻塞队列原理

    阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

    • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
      在这里插入图片描述
    • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
      在这里插入图片描述

    11.1 阻塞队列的主要方法

    在这里插入图片描述

    • 抛出异常:抛出一个异常;
    • 特殊值:返回一个特殊值(null 或 false,视情况而定);
    • 阻塞:在成功操作之前,一直阻塞线程;
    • 超时:放弃前只在最大的时间内阻塞;

    11.1.1 插入操作

    • public abstract boolean add(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。如果该元素是 NULL,则会抛出 NullPointerException 异常。
    • public abstract boolean offer(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。
    • public abstract void put(E paramE) throws InterruptedException: 将指定元素插入此队列中,将等待可用的空间(如果有必要)。
    public void put(E paramE) throws InterruptedException {
    	checkNotNull(paramE);
    	ReentrantLock localReentrantLock = this.lock;
    	localReentrantLock.lockInterruptibly();
    	try {
    		while (this.count == this.items.length)
    			this.notFull.await();//如果队列满了,则线程阻塞等待
    		enqueue(paramE);
    		localReentrantLock.unlock();
    	} finally {
    	localReentrantLock.unlock();
    	}
    }
    
    • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入 BlockingQueue,则返回失败。

    11.1.2 获取数据操作

    • poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null;
    • poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
    • take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
    • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

    11.2 Java 中的阻塞队列

    • ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
    • DelayQueue:使用优先级队列实现的无界阻塞队列。
    • SynchronousQueue:不存储元素的阻塞队列。
    • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:由链表结构组成的双向阻塞队列
      在这里插入图片描述

    11.2.1 ArrayBlockingQueue(公平、非公平)

    用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
    ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

    11.2.2 LinkedBlockingQueue(两个独立锁提高并发)

    基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)。

    11.2.3 PriorityBlockingQueue(compareTo 排序实现优先)

    是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

    11.2.4 DelayQueue(缓存失效、定时任务 )

    是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:

    • 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
    • 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。

    11.2.5 SynchronousQueue(不存储数据、可用于传递数据)

    是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和ArrayBlockingQueue。

    11.2.6 LinkedTransferQueue

    是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 ,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

    • transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
    • tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。

    11.2.7 LinkedBlockingDeque

    是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

    12 CyclicBarrier 、CountDownLatch 、Semaphore 的用法

    • CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不同;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行;而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;另外,CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。
    • Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

    12.1 CountDownLatch(线程计数器)

    CountDownLatch类位于java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。

    final CountDownLatch latch = new CountDownLatch(2);
    new Thread() {
    	public void run() {
    		System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
    		try {
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
    		latch.countDown();
    	};
    }.start();
    new Thread() {
    	public void run() {
    		System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
    		try {
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
    		latch.countDown();
    	};
    }.start();
    System.out.println("等待 2 个子线程执行完毕...");
    latch.await();
    System.out.println("2 个子线程已经执行完毕");
    System.out.println("继续执行主线程");
    

    12.2 CyclicBarrier(回环栅栏-等待至 barrier 状态再全部同时执行)

    CountDownLatch类位于java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做 barrier,当调用 await()方法之后,线程就处于 barrier 了。

    CyclicBarrier 中最重要的方法就是 await 方法,它有 2 个重载版本:

    1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务;
    2. public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。

    具体使用如下,另外 CyclicBarrier 是可以重用的。

    public class Test {
    	public static void main(String[] args) {
    		int N = 4;
    		CyclicBarrier barrier = new CyclicBarrier(N);
    		for (int i = 0; i < N; i++)
    			new Writer(barrier).start();
    	}
    
    	static class Writer extends Thread {
    		private CyclicBarrier cyclicBarrier;
    
    		public Writer(CyclicBarrier cyclicBarrier) {
    			this.cyclicBarrier = cyclicBarrier;
    		}
    
    		@Override
    		public void run() {
    			try {
    				Thread.sleep(5000); // 以睡眠来模拟线程需要预定写入数据操作
    				System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕");
    				cyclicBarrier.await();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			} catch (BrokenBarrierException e) {
    				e.printStackTrace();
    			}
    			System.out.println("所有线程写入完毕,继续处理其他任务,比如数据操作");
    		}
    	}
    }
    

    12.3 Semaphore(信号量-控制同时访问的线程个数)

    S emaphore 翻译成字面意思为 信号量,Semaphore 可以控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Semaphore 类中比较重要的几个方法:

    1. public void acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    2. public void acquire(int permits):获取 permits 个许可
    3. public void release() { } :释放许可。注意,在释放许可之前,必须先获获得许可。
    4. public void release(int permits) { }:释放 permits 个许可

    上面 4 个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

    1. public boolean tryAcquire():尝试获取一个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
    2. public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取一个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
    3. public boolean tryAcquire(int permits):尝试获取 permits 个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
    4. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
    5. 还可以通过 availablePermits()方法得到可用的许可数目。例子:若一个工厂有5 台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过 Semaphore 来实现:
    public class Test {
    	public static void main(String[] args) {
    		int N = 8; // 工人数
    		Semaphore semaphore = new Semaphore(5); // 机器数目
    		for (int i = 0; i < N; i++)
    			new Worker(i, semaphore).start();
    	}
    
    	static class Worker extends Thread {
    		private int num;
    		private Semaphore semaphore;
    
    		public Worker(int num, Semaphore semaphore) {
    			this.num = num;
    			this.semaphore = semaphore;
    		}
    
    		@Override
    		public void run() {
    			try {
    				semaphore.acquire();
    				System.out.println("工人" + this.num + "占用一个机器在生产...");
    				Thread.sleep(2000);
    				System.out.println("工人" + this.num + "释放出机器");
    				semaphore.release();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    13 volatile 关键字的作用 (变量可见性、禁止重排序)

    Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

    13.1 变量可见性

    其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

    13.2 禁止重排序

    volatile 禁止了指令重排。

    13.3 比 sychronized 更轻量级的同步锁

    在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
    在这里插入图片描述
    当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。

    13.4 适用场景

    值得说明的是对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
    (1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
    (2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

    14 如何在两个线程之间共享数据

    Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性原子性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:

    14.1 将数据抽象成一个类,并将数据的操作作为这个类的方法

    将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“

    class MyData {
    	private int j = 0;
    
    	public synchronized void add() {
    		j++;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public synchronized void dec() {
    		j--;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public int getData() {
    		return j;
    	}
    }
    
    class AddRunnable implements Runnable {
    	MyData data;
    
    	public AddRunnable(MyData data) {
    		this.data = data;
    	}
    
    	public void run() {
    		data.add();
    	}
    }
    
    class DecRunnable implements Runnable {
    	MyData data;
    
    	public DecRunnable(MyData data) {
    		this.data = data;
    	}
    
    	public void run() {
    		data.dec();
    	}
    
    }
    
    public class mwjtest {
    	public static void main(String[] args) {
    		MyData data = new MyData();
    		Runnable add = new AddRunnable(data);
    		Runnable dec = new DecRunnable(data);
    		for (int i = 0; i < 2; i++) {
    			new Thread(add).start();
    			new Thread(dec).start();
    		}
    	}
    }
    

    14.2 Runnable 对象作为一个类的内部类

    将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个 Runnable 对象调用外部类的这些方法。

    class MyData {
    	private int j = 0;
    
    	public synchronized void add() {
    		j++;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public synchronized void dec() {
    		j--;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public int getData() {
    		return j;
    	}
    }
    
    public class mwjtest {
    	public static void main(String[] args) {
    		final MyData data = new MyData();
    		for (int i = 0; i < 2; i++) {
    			new Thread(new Runnable() {
    				public void run() {
    					data.add();
    				}
    			}).start();
    			new Thread(new Runnable() {
    				public void run() {
    					data.dec();
    				}
    			}).start();
    		}
    	}
    }
    

    15 ThreadLocal 作用( 线程本地存储 )

    ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

    15.1 ThreadLocalMap (线程的一个属性)

    1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
    2. 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
    3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义ThreadLocal.ThreadLocalMap threadLocals = null;
      在这里插入图片描述

    15.2 使用场景

    最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。

    private static final ThreadLocal threadSession = new ThreadLocal();
    public static Session getSession() throws InfrastructureException {
    	Session s = (Session) threadSession.get();
    	try {
    		if (s == null) {
    			s = getSessionFactory().openSession();
    			threadSession.set(s);
    		}
    	} catch (HibernateException ex) {
    		throw new InfrastructureException(ex);
    	}
    	return s;
    }
    

    16 synchronized 和 ReentrantLock 的区别

    16.1 两者的共同点

    1. 都是用来协调多线程对共享对象、变量的访问
    2. 都是可重入锁,同一线程可以多次获得同一个锁
    3. 都保证了可见性和互斥性

    16.2 两者的不同点

    1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
    2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
    3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
    4. ReentrantLock 可以实现公平锁
    5. ReentrantLock 通过 Condition 可以绑定多个条件
    6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
    7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
    8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
    9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
    11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

    17 ConcurrentHashMap 并发

    17.1 减小锁粒度

    减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的 HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为 ConcurrentHashMap 的并发度。

    17.2 ConcurrentHashMap 分段锁

    ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

    17.3 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

    ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
    在这里插入图片描述

    18 Java 中用到的线程调度

    18.1 抢占式调度

    抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

    18.2 协同式调度

    协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
    在这里插入图片描述

    18.3 JVM 的线程调度实现(抢占式调度)

    java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

    18.4 线程让出 cpu 的情况

    1. 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
    2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
    3. 当前运行线程结束,即运行完 run()方法里面的任务。

    19 进程调度算法

    19.1 优先调度算法

    1. 先来先服务调度算法(FCFS)
      当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用 FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较
      简单,可以实现基本上的公平。
    2. 短作业(进程)优先调度算法
      短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。该算法未照顾紧迫型作业。

    19.2 高优先权优先调度算法

    为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。

    1. 非抢占式优先权算法
      在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
    2. 抢占式优先权调度算法
      在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
    3. 高响应比优先调度算法
      在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:
      在这里插入图片描述
      (1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
      (2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
      (3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

    19.3 基于时间片的轮转调度算法

    1. 时间片轮转法
      在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。
    2. 多级反馈队列调度算法
      (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第 i+1 个队列的时间片要比第 i 个队列的时间片长
      一倍。
      (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按 FCFS 原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。
      (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第 1~(i-1)队列均空时,才会调度第 i 队列中的进程运行。如果处理机正在第 i 队列中为某进程服务时,又有新进程进入优先权较高的队列(第 1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第 i 队列的末尾,把处理机分配给新到的高优先权进程。在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。

    20 什么是 CAS( 比较并交换-乐观锁机制-锁自旋 )

    20.1 概念及特性

    CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。

    CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

    20.2 原子包 java.util.concurrent.atomic(锁自旋)

    JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。相对于对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。由于一般 CPU 切换时间比 CPU 指令集操作更加长, 所以 J.U.C 在性能上有了很大的提升。如下代码:

    public class AtomicInteger extends Number implements java.io.Serializable {
    	private volatile int value;
    	public final int get() {
    		return value;
    	}
    	public final int getAndIncrement() {
    		for (;;) { //CAS 自旋,一直尝试,直达成功
    			int current = get();
    			int next = current + 1;
    			if (compareAndSet(current, next))
    			return current;
    		}
    	}
    	public final boolean compareAndSet(int expect, int update) {
    		return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    	}
    }
    

    getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。
    在这里插入图片描述

    20.3 ABA 问题

    CAS 会导致“ABA 问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

    21 什么是 AQS( 抽象的队列同步器 )

    AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
    在这里插入图片描述
    它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的访问方式有三种:
    getState()
    setState()
    compareAndSetState()
    AQS 定义两种资源共享方式:

    1. Exclusive 独占资源 -ReentrantLock
      Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
    2. Share 共享资源 -Semaphore/CountDownLatch
      Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。

    AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
    1. isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
    2. tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
    3. tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
    4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回 false。

    21.1 同步器 的实现是 ABS 核心(state 资源状态计数)

    同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

    21.2 ReentrantReadWriteLock 实现独占和共享两种

    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。

    注:以上内容来源网络收集和归纳,如有错误请不吝赐教。

    展开全文
  • Java 多线程通过 Mybatis 获取 Oracle 序列值重复问题处理 在同一个事务内开多线程访问序列值时,会取出同一个值。 select mySequence.nextval from dual 解决方法是将查询语句放在一个新事务中,同时增加同步...
  • Java多线程和线程池

    万次阅读 2016-05-12 21:28:53
    一、Java自带线程池先看看Java自带线程池的例子,开启5个线程打印字符串List:package com.luo.test;import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import ...
  • 本文介绍使用javasynchronized同步锁来实现对相同userId进行加锁 众所周知synchronized只能锁对象地址,而对于如下加锁是完全没有用的 public void test(Long userId) { synchronized (userId) {//除了-127-128...
  • Java 多线程 并发编程

    万次阅读 多人点赞 2013-08-28 01:42:48
    一、多线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • java多线程学习总结

    万次阅读 2020-12-24 12:38:58
    一个进程由一个或线程组成,线程是一个进程中代码的不同执行路线 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信 号等),某...
  • 多线程特别重要,虽然内容偏多,但是需要熟练掌握。面试也会在此章节有考验的!请大家耐心学习! 目录 一、什么是线程 ...思维导图参考: 【十三】Java多线程思维导图 【知识扩展】 你知道synchronize...
  • Java多线程-7】阅尽Java千般锁

    千次阅读 2020-03-31 15:40:46
    前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。 例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。 public ...
  • ExecutorService实现java多线程

    千次阅读 2014-11-06 11:38:55
    Java5之后,并发线程这块发生了根本的变化,最重要的莫过于新的启动、调度、管理线程的一大堆API了。在Java5以后,通过Executor来启动线程比用Thread的start()更好。在新特征中,可以很容易控制线程的启动、执行和...
  • JAVA-多线程

    千次阅读 2021-06-12 22:08:44
    JAVA-多线程一、什么是多线程?二、多线程有什么用?三、多线程常见的问题?四、Java多线程怎么用?1.创建线程的三种方式1.11.21.3五、保证Java中的线程安全 一、什么是多线程? 粗略的讲,多线程通常是指在程序...
  • java多线程并发机制

    千次阅读 2017-04-27 10:59:33
    一、多线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • JAVA多线程与队列

    千次阅读 2018-07-07 10:14:20
    JAVA 已经给我们提供了比较好的队列实现Queue,继承于Collection。 本次我使用的是BlockingQueue,继承于Queue。 在
  • Java多线程导论及延申

    千次阅读 2021-03-16 16:52:46
    多线程导论 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看...
  • Java多线程』基础之基础

    千次阅读 2020-11-10 21:50:31
    Java基础篇~多线程 线程概念 程序(Program) ​ 程序是指令、数据及其组织形式的描述,也就是存储在磁盘或者其他存储设备中含有指令和数据的文件,是一段静态代码 进程(Process) ​ 进程是受操作系统管理的基本...
  • java多线程编程

    千次阅读 2007-10-29 16:25:00
    Java自1995年面世以来得到了广泛得一个运用,但是对多线程编程的支持Java很长时间一直停留在初级阶段。在Java 5.0之前Java里的多线程编程主要是通过Thread类,Runnable接口,Object对象中的wait()、 notify()、 ...
  • Java 多线程 并发 锁 Java线程面试题

    千次阅读 2018-05-12 00:02:12
    1) 什么是线程?线程是操作系统能够进行运算调度的最小单位,它被...Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。2) 线程和进程有什么区别?线程是进程的子集,一个进程可以有很多线程,每条线...
  • Java多线程实现接口调用

    千次阅读 2019-11-23 19:37:24
    前言 有一张客户信息表,数据量很大。需要把这些记录调用某个接口(这个接口一次只能查询10个客户...id cust_id status remark input_time update_tiem 1 20191111000001 2019-11-23 10:45:04 2019-11-23 10...
  • java多线程的6种实现方式详解

    万次阅读 多人点赞 2017-10-12 12:41:57
    多线程的形式上实现方式主要有两种,一种是继承Thread类,一种是实现Runnable接口。本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run方法)。这里所说的6种,实际上都是在...
  • Java 多线程 并发编程 整理

    千次阅读 2018-04-09 11:42:20
    一、多线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • Java多线程-6】synchronized同步锁

    千次阅读 2020-03-31 15:35:49
    前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。 例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。 public ...
  • Java 多线程的详解

    千次阅读 2016-07-22 18:35:31
    多线程:就是在一个进程中多个执行路径同时执行。 图上的一键优化与垃圾清除同时在运行,在一个进程中同时在执行了多个任务。 假象: 电脑上的程序同时在运行。“多任务”操作系统能同时运行多个进程(程序)...
  • Java多线程常用面试题(含答案,精心总结整理)

    万次阅读 多人点赞 2017-11-23 15:42:48
    Java并发编程问题是面试过程中很容易遇到的问题,提前准备是解决问题的最好办法,将试题总结起来,时常查看会有奇效。 ...这个多线程问题比较简单,可以用join方法实现。 核心: thread.Jo
  • Java多线程之进阶篇(一)

    万次阅读 多人点赞 2018-07-30 14:50:03
    在学习完Java多线程之基础篇(一)和Java多线程之基础篇(二)后接下来开始学习Java多线程之进阶篇的内容。 Java 5 添加了一个新的包到Java平台,这个包是java.util.concurrent包(简称JUC)。这个包包含了有一系列...
  • java多线程与并发编程详解

    千次阅读 2018-03-17 11:35:28
    一、多线程1、操作系统有两个容易混淆的概念,进程和线程。进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源...
  • java多线程面试题TOP50

    千次阅读 2014-10-14 10:01:46
    大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。 在典型的Java面试中, 面试官会从线程的基本概念问起, 如:为什么...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 106,332
精华内容 42,532
关键字:

java多线程id重复

java 订阅