Java多线程

Java多线程

      • Java多线程
        • 线程的创建
        • 线程常用方法
        • 线程的状态
        • 线程的优先级
        • 守护线程
        • 线程组
      • 线程安全问题
      • volatile关键字
      • Java中的锁
        • synchronized锁
        • synchronized使用场景
        • synchronized注意事项
        • Lock类的使用
        • Lock锁使用的注意事项
        • 公平锁、非公平锁
        • synchronzied 和 Lock 的区别
        • 死锁
        • 造成死锁的四个条件
        • 死锁的解决方案
      • 线程间通信
        • wait/notify机制的原理
        • notifyAll
        • wait()和sleep()的区别
        • LockSupport park()/unpark()
      • Java线程池
        • 线程池的优点
        • 线程池的6种创建方式
        • 线程池的第七种创建方式
        • 五种拒绝策略
        • ThreadPoolExecutor的执行方式
        • ThreadPoolExecutor的执行流程
        • 线程池的终止
        • 线程池的状态
        • 异步、同步
        • 线程工厂
      • SimpleDateFormat非线程安全问题
      • ThreadLocal
        • ThreadLocal的原理
        • ThreadLocal常用方法
        • ThreadLocal的初始化
        • InheritableThreadLocal的使用
      • 单例模式与多线程
        • 立即加载/饿汉模式
        • 延时加载/懒汉模式
        • 饿汉/懒汉对比
      • 阻塞队列的实现
      • 常见的锁策略
        • 乐观锁
        • CAS
        • CAS在java中的应用
        • CAS 的ABA问题
        • ABA 问题的解决
        • 悲观锁
        • 独占锁、共享锁、自旋锁、可重入锁
      • 详解synchronized锁的优化问题
      • Semaphore
      • CountDownLatch\CyclicBarrier
      • hashmap/ConcurrentHashMap
        • hashmap在JDK1.7中头插死循环问题
        • hashmap在JDK1.8中值覆盖问题
        • ConcurrentHashMap & HashTable

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的构造方法
Java多线程_第1张图片

线程常用方法

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

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());
    }
}

Java多线程_第2张图片

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());
    }
}

Java多线程_第3张图片

线程的等待
假设有一个坑位,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()之后:
Java多线程_第4张图片

线程的终止

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;

    }
}

Java多线程_第5张图片

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;

    }
}

Java多线程_第6张图片
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();

    }
}

Java多线程_第7张图片

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();

    }
}

Java多线程_第8张图片

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();
    }
}

Java多线程_第9张图片

线程的状态

Java多线程_第10张图片

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

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);
        }
    }
}

Java多线程_第11张图片

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多线程_第12张图片

线程的优先级

在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();
        }
    }
}

Java多线程_第13张图片
线程的优先级不是绝对的,只是给程序的建议。
线程之间的优先级具有继承的特性,如果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();
    }
}

Java多线程_第14张图片

再看

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多线程_第15张图片

守护线程

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()之前,否则就会报错。
Java多线程_第16张图片
在守护线程中创建的线程默认也是守护线程。

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());

    }
}

Java多线程_第17张图片
再看

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("所有选手到达了终点");
    }
}

运行结果:
Java多线程_第18张图片
不符合预期效果,就可以使用线程组来实现

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("所有选手到达了终点");
    }
}

Java多线程_第19张图片
线程组常用的方法
Java多线程_第20张图片

线程安全问题

来看单线程情况下让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());
    }
}

结果符合预期
Java多线程_第21张图片

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

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());
         */
    }
}

执行结果:
在这里插入图片描述
Java多线程_第22张图片
Java多线程_第23张图片

每次的执行结果是不一样的。这就是多线程的不安全问题
Java多线程_第24张图片
预期的结果是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());
         */
    }
}

这样就可以了:
Java多线程_第25张图片

内存不可见问题:看下面的代码,是不是到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();
    }

}

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

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

volatile关键字

可以使用volatile关键字,这个关键字可以解决指令重排和内存不可见的问题。
在这里插入图片描述
加上volatile关键字之后的运行结果
Java多线程_第28张图片
但是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多线程_第29张图片
Java多线程_第30张图片
那么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());
    }
}

结果是:
Java多线程_第31张图片

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());
    }
}

Java多线程_第32张图片

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--;
        }
    }
}

Java多线程_第33张图片

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多线程_第34张图片

公平锁、非公平锁

公平锁的调度:
一个线程释放锁。
主动唤醒“需要得到锁”的队列来得到锁。
非公平锁
当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁。
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();
    }
}

打印的结果是无序的
Java多线程_第35张图片
如果设置为公平锁:
在这里插入图片描述

Java多线程_第36张图片
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();
    }
}

这就造成了死锁
Java多线程_第37张图片

造成死锁的四个条件

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();
    }
}

Java多线程_第38张图片

线程间通信

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

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();

Java多线程_第39张图片
正确的使用之后

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();

        }

    }
}

Java多线程_第40张图片

注意:使用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()方法呢?
Java多线程_第41张图片
可以看到所有的线程都被唤醒了
Java多线程_第42张图片

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

wait()和sleep()的区别

也可以让wait()等待指定的时间,如果超过给定的时间,wait()不会无限期的等待下去.
Java多线程_第43张图片
没有被notify()唤醒,过了1000毫秒之后会自动停止。
Java多线程_第44张图片

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("主线程获取到了锁");
        }
    }
}

Java多线程_第45张图片

可以看到线程在sleep()的时候,线程是不会释放锁的。再来看看wait()方法:
Java多线程_第46张图片

Java多线程_第47张图片
线程使用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());
                }
            });
        }

    }
}

Java多线程_第48张图片
那么如果执行次数大于10次呢?
线程池不会创建新的线程,它会复用之前的线程。
Java多线程_第49张图片

Java多线程_第50张图片
那么如果只执行两个任务呢?它创建了是10个线程还是两个线程呢?
我们可以使用Jconsole来看一看:
Java多线程_第51张图片

Java多线程_第52张图片

结果是只有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());
                }
            });
        }
    }
}

Java多线程_第53张图片

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

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);
    }
}

Java多线程_第54张图片
执行任务的四个参数的意义:
参数1:延迟执行的任务
参数2:延迟一段时间后执行
参数3:定时任务执行的频率
参数4:配合前两个参数使用,是2、3参数的时间单位

还有两种执行的方法:
只会执行一次的方法:
Java多线程_第55张图片

Java多线程_第56张图片
第三种的执行方式:
Java多线程_第57张图片

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

方式一:
Java多线程_第59张图片

方式三:
Java多线程_第60张图片
结论很明显了:
第一种方式是以上一个任务的开始时间+定时的时间作为当前任务的开始时间

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

创建方式四:

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);
    }
}

Java多线程_第61张图片
Java多线程_第62张图片
创建方式五:
创建单个线程的线程池

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());
                }
            });
        }
    }
}

Java多线程_第63张图片
创建单个线程池的作用是什么?
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());
                }
            });
        }
    }
}

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

就可以输出结果了
Java多线程_第65张图片

线程池的第七种创建方式

前六种的创建方式有什么问题呢?
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());
                }
            });
        }
    }
}


Java多线程_第66张图片

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

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

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

Java多线程_第67张图片

五种拒绝策略

在这里插入图片描述
Java多线程_第68张图片

为什么拒绝策略可以舍弃最新的任务或者最旧的任务呢?
因为LinkedBlockingDeque时FIFO的。
第五种:自定义的拒绝策略
Java多线程_第69张图片

Java多线程_第70张图片

ThreadPoolExecutor的执行方式

Java多线程_第71张图片

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());
        
        

    }
}

无返回值的执行方式
Java多线程_第72张图片

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

ThreadPoolExecutor的执行流程

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

线程池的终止

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

线程池的状态

Java多线程_第74张图片

异步、同步

  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;
         }
     }

}



Java多线程_第75张图片

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));
    }

}

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

Java多线程_第76张图片
这是因为:
Java多线程_第77张图片
多线程的情况下:
Java多线程_第78张图片
线程1在时间片用完之后,线程2来setTime()那么线程1的得到了线程2的时间。

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

Java多线程_第79张图片

就不会有重复的时间了
Java多线程_第80张图片
但是虽然可以解决线程不安全的问题,但是排队等待锁,性能就会变得低

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

但是每次也都会创建新的私有变量
那么有没有一种方案既可以避免加锁排队执行,又不会每次创建任务的时候不会创建私有的变量呢?
那就是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常用方法

Java多线程_第82张图片

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();

    }
}

Java多线程_第83张图片
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();
    }
}

Java多线程_第84张图片
注意:
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();

    }
}

Java多线程_第85张图片
类ThreadLocal不能实现值的继承,那么就可以使用InheritableThreadLocal了

InheritableThreadLocal的使用

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

Java多线程_第86张图片
在来看运行的结果:
Java多线程_第87张图片

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

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);
        }

    }
}

Java多线程_第88张图片
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();
    }
}

Java多线程_第89张图片

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

单例模式与多线程

单例模式就是全局唯一但是所有程序都可以使用的对象
写单例模式步骤:
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);
    }

Java多线程_第91张图片

延时加载/懒汉模式

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

/**
 * 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);
    }

}

结果:
Java多线程_第92张图片

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

    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;
        }
    }

Java多线程_第94张图片
那么这样写就没有问题了吗?

不是的:有可能还会发生指令重排的问题
当有线程在进行第一次初始化的时候,就有可能发生问题
先来看初始化的过程
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();
    }
}

可以看到生产者每生产一个数据都会被取走:
Java多线程_第95张图片

常见的锁策略

乐观锁

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

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);
    }
}

Java多线程_第96张图片

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());
    }
}

Java多线程_第97张图片

注意:里面的旧值对比的是引用。
如果范围在-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();
                }
            }
        });


    }
}

Java多线程_第98张图片

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

Java多线程_第99张图片

Java多线程_第100张图片

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

详解synchronized锁的优化问题

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

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

Java多线程_第102张图片

当某一线程第一次获得锁的时候,虚拟机会把对象头中的锁标志位设置为“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。

Java多线程_第103张图片
2、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且锁对象的 Mark Word 中的锁标志位设置为 “00”,即表示此对象处于轻量级锁定状态,这时候虚拟机线程栈与堆中锁对象的对象头的状态如图所示。
Java多线程_第104张图片
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();
    }
}

Java多线程_第105张图片

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使用完毕后,它不能再次被使用。

Java多线程_第106张图片
模拟赛跑:当三个运动员都到达终点的时候宣布比赛结束

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();
    }
}

Java多线程_第107张图片
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();
    }
}

Java多线程_第108张图片
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;
            }
        }
    }

来看多线程情况下的问题:
Java多线程_第109张图片
这样就会造成死循环。

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;
    }


在多线程的情况下:
Java多线程_第110张图片
其中第六行代码是判断是否出现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,多线程,并发编程,java,线程池,线程安全)