java多线程

目录

前言 

概念 

多线程的优势 

线程与进程的区别 

多线程的创建

多线程的基本创建方式

Thread类

Thread常见构造方法 

Thread常见属性

 启动一个线程-start()

线程的中断

 等待一个线程-join()

线程的状态

线程的所有状态

线程安全

线程不安全

如何解决线程不安全问题

死锁

线程不安全的原因-内存可见性问题

wait和notify

wait

notify

notifyAll

多线程案例

单例模式

 阻塞队列

阻塞队列的用处: 

阻塞队列的具体使用

定时器 

线程池

常见的锁策略

乐观锁和悲观锁

读写锁和普通互斥锁

重量级锁和轻量级锁

自旋锁和挂起等待锁

公平锁和非公平锁

可重入锁和不可重入锁

CAS

最常用的两个场景:

CAS的ABA问题

如何解决ABA问题?

Synchronized原理

锁升级/锁膨胀

锁消除

锁粗化

JUC

Callable接口

ReentrantLock

原子类

线程池

信号量Semaphore

CountDownLatch

线程安全的集合类-CopyOnWrite

多线程环境下使用哈希表

HashTable

ConcurrentHashMap

死锁

解决死锁


前言 

随着业务量和数据的增加,企业不可避免地会使用多线程的方式处理数据。在 Java 职位的面试中,多线程也是必考的高阶知识点之一。可以说,java多线程是衡量一名 Java 程序员是否资深的关键标准之一。

今天,我们就来学习一下 Java 多线程的概念吧!

概念 

线程是被包含在进程中的,一个进程会默认有一个线程,也可以有多个线程

每个线程都是一个“执行流”,可以单独的在CPU上进行调度。同一个进程中的这些线程,共用同一份系统资源(内存+文件) 

线程是轻量级的进程,创建线程的开销比创建进程小,销毁线程的开销比销毁进程小

多线程的优势 

1、能够充分利用多核CPU,提高效率

2、只需要在创建第一个线程时,申请资源,后续在创建新的线程,都是共用同一份资源(节省了申请资源的开销) 

线程与进程的区别 

1、进程包含线程;

2、线程比进程更轻量,创建更快,销毁也更快;

3、同一个进程之间的多个线程之间共用一份内存/文件资源,进程和进程之间,则是独立的内存/文件资源;

4、进程是资源分配的基本单位,线程是调度执行的基本单位

多线程的创建

多线程的基本创建方式

1、继承Thread,重写run

class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("hello thread!");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread t=new MyThread();
        t.start();
    }
}

创建一个MyThread实例,继承Thread父类,相当于对操作系统中的县城进行的封装。

run是父类中已经有的方法,run里边的逻辑,就是线程要执行的工作。

MyThread中重写run方法,相当于“安排任务”。

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

创建MyThread实例的过程中,并不会在系统中创建出一个线程,调用start方法时,才会创建出一个新的线程,新的线程就会执行run里边的逻辑,一直到run里边的代码执行完,新的线程就运行结束

在上边代码中,有一个进程,两个线程,main方法对应的主线程,和MyThread新线程,这两个线程是并发执行的关系

2、继承Runnable接口,重写run

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable=new MyRunnable();
        Thread t=new Thread(myRunnable);
        t.start();
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

把线程要干的活和线程本身分开,Runnable表示线程要完成的工作。

把任务提取出来,目的就是为了解耦合

使用这种方法,适合多个线程

Thread t=new Thread(myRunnable);
t.start();
Thread t1=new Thread(myRunnable);
t1.start();

3、使用匿名内部类,实现创建Thread子类的方式

public class Demo3 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }
}

创建Thread的子类,同时实例化出一个对象 

4、使用匿名内部类,实现实现Runnable接口的方式 

public class Demo4 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }
}

匿名内部类的实例,作为构造方法的参数 

5、lambda表达式

public class Demo5 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

多线程可以提高程序的效率?可以用代码来看一下

public class Demo6 {
    public static final long COUNT=10000000000L;

    public static void main(String[] args) {
        //serial();
        concurrency();
    }

    //串行执行任务
    public static void serial(){
        //记录ms级别的时间戳
        long begin= System.currentTimeMillis();
        long a=0;
        for (long i = 0; i < COUNT; i++) {
            a++;
        }
        a=0;
        for (long i = 0; i < COUNT; i++) {
            a++;
        }
        long end=System.currentTimeMillis();
        System.out.println("执行的时间间隔:"+(end-begin)+"ms");  //14355ms
    }

    //并行执行任务
    public static void concurrency(){
        long begin=System.currentTimeMillis();
        Thread t1=new Thread(()->{
            long a=0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });
        Thread t2=new Thread(()->{
            long a=0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        System.out.println("执行的时间间隔:"+(end-begin)+"ms");  //5164ms
    }
}

此处的两个join是两个线程都执行完,才继续执行计时操作。

Thread类

Thread常见构造方法 

可以在创建线程的时候,给线程起名字

Thread(String name)

Thread(Runnable target,String name)

public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"这是我的线程");
        t.start();
    }

Thread常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
public class Demo8 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"这是我的线程");
        t.start();
        System.out.println(t.getId());
        System.out.println(t.getName());
        System.out.println(t.getState());
        System.out.println(t.getPriority());
        System.out.println(t.isDaemon());
        System.out.println(t.isAlive());
        System.out.println(t.isInterrupted());
    }
}

 启动一个线程-start()

class MyThread2 extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo9 {
    public static void main(String[] args) {
        MyThread2 t=new MyThread2();
        t.start();
        //t.run();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

直接调用run并没有创建线程,只是在原来的线程中运行的代码

调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)

线程的中断

1、直接定义一个标志位,作为线程是否结束的标志

public class Demo10 {
    private static boolean isQuit=false;

    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (!isQuit){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t线程执行完了");
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        isQuit=true;
        System.out.println("设置让t线程结束");
    }
}

2、使用标准库自带的标志位

Thread.currentThread().isInterrupted() 判定标志位
public static void main(String[] args) {
            Thread t=new Thread(()->{
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                        break;
                    }
                }
                System.out.println("t线程执行完了");
            });
            t.start();

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            t.interrupt();
            System.out.println("设置让t线程结束");
        }

interrupt的方法行为有两种情况:

t线程在运行状态,会设置标志位为true;

t线程在阻塞状态,而是触发一个interruptedException,这个异常会提前把sleep唤醒,设置标志位但又给清除

 等待一个线程-join()

线程之间的调度顺序是不确定的,join就是控制线程之间的结束顺序

public static void main(String[] args) {
        Thread t=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("main线程 join之前");
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程 join之后");
    }

线程的状态

线程的所有状态

NEW:Thread对象创建出来了,但内核的PCB还没创建(还没有真正创建线程)

TERMINATED:内核的PCB销毁了,但是Thread对象还在

RUNNABLE:就绪状态(正在CPU运行+在就绪队列中排队)

TIMED_WAITING:按照一定的时间进行阻塞,sleep

WAITING:特殊的阻塞状态,调用wait

BLOCKED:等待锁的时候进入的阻塞状态

public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for (int i = 0; i < 100000000; i++) {

            }
        });
        //t开始之前,得到的就是NEW
        System.out.println(t.getState());
        t.start();

        //t正在工作,得到的是RUNNABLE
        Thread.sleep(1);
        System.out.println(t.getState());

        t.join();
        //t开始之后,得到的就是TERMINATED
        System.out.println(t.getState());
    }

NEW
RUNNABLE
TERMINATED

public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
//            for (int i = 0; i < 100000000; i++) {
//
//            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        //t开始之前,得到的就是NEW
        System.out.println(t.getState());
        t.start();

        //t正在工作,得到的是RUNNABLE
        Thread.sleep(50);
        System.out.println(t.getState());

        t.join();
        //t开始之后,得到的就是TERMINATED
        System.out.println(t.getState());
    }

NEW
TIMED_WAITING
TERMINATED

线程安全

线程不安全

class Counter{
    public int count=0;
    public void increase(){
        count++;
    }
}

public class Demo14 {
    private static Counter counter=new Counter();

    public static void main(String[] args) throws InterruptedException {
        //两个线程,每个线程都针对count进行5w的自增
        //预期结果10w
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("counter:"+counter.count);  //71457
    }
}

线程不安全的原因:

1、抢占式执行;

2、多个线程修改同一变量;

3、修改操作不是原子的(原子表示不可分割的最小单位)

4、内存可见性问题;

5、指令重排序

如何解决线程不安全问题

就上述代码而言,通过特殊手段,让count++变成原子的(加锁操作)

进行加锁,使用synchronized关键字

1、修饰方法

public synchronized void increase(){
    count++;
}

使用synchronized关键字来修饰一个普通方法,当进入方法时,就会加锁,方法执行完毕,自然解锁。

2、修饰代码块

把要进行加锁的逻辑放到synchronized修饰的代码块中,也能起到加锁的作用

3、修饰静态方法

锁对象相当于类对象(下边会介绍) 

写法一 

public void increase(){  //括号内是锁对象(被用来加锁的对象)
    synchronized (this){
        count++;
    }
}

写法二

public Object locker=new Object();
public void increase(){
    synchronized (locker){
        count++;
    }
}

写法三 

public static Locker locker=new Locker();
    public void increase(){
        synchronized (locker){
            count++;
        }
    }

java中的任何一个对象,都可以作为锁对象(成员变量、局部变量、静态变量、类对象……),而且这些不同形态的对象,作为锁对象是没有任何区别。锁对象只是用来控制线程之间的互斥的。针对同一对象加锁,就会产生互斥,针对不同对象加锁,就不会产生互斥。

        public void increase(){
                synchronized (this){
                    count++;
                }
            }

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });

此代码是针对不同对象进行加锁,不会出现锁竞争

        public Object locker=new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }        

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

此代码的对象是同一个,counter中的locker,则会出现锁竞争

        public Object locker=new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }        

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });

此代码的locker是两个对象,不涉及锁竞争

        static private Object locker=new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }        

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });

此时的locker是一个静态成员(类属性),类属性是唯一的(一个进程中,类对象只有一个,类属性也只有一份),因此,这两个locker实际上是一个locker,会产生锁竞争

        static private Object locker=new Object();
        public void increase(){
            synchronized (locker){
                count++;
            }
        }  

        public void increase2(){
            synchronized (this){
                count++;
            }
        }        

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase2();
            }
        });

 一个线程针对静态的locker对象加锁,一个则针对counter本身加锁,不同对象,则不会产生锁竞争

        public void increase(){
            synchronized (Counter.class){
                count++;
            }
        }  


        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter2.increase2();
            }
        });

类对象,在JVM进程中只有一个,针对同一个Counter.class加锁,也会存在锁竞争

死锁

    static class Counter{
        public synchronized void increase(){
            synchronized (this){

            }
        }
    }

    public static void main(String[] args) {
        threading.Counter counter=new threading.Counter();

        Thread t=new Thread(){
            @Override
            public void run() {
                counter.increase();
            }
        };
        t.start();
    }

针对上述情况,不会产生死锁的锁,叫“可重入锁”

可重入锁的底层实现:t线程尝试针对this来加锁,this这个锁里边就记录了是t线程持有它,第二次进行加锁的时候,如果还是t线程,直接通过,不会阻塞等待(就是让锁记录好是哪个线程持有的它)

        synchronized (this){
            synchronized (this){
                //
            }
        }

可重入锁的实现要点:

1、让锁里持有线程对象,记录是谁加了锁;

2、维护一个计数器,用来衡量撒时候是真加锁,撒时候是真解锁,撒时候是直接放行

线程不安全的原因-内存可见性问题

    static class Counter{
        public int count=0;
    }

    public static void main(String[] args) {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            while (counter.count==0){

            }
            System.out.println("t1执行结束");
        });
        t1.start();

        Thread t2=new Thread(()->{
            System.out.println("请输入一个int:");
            Scanner scanner=new Scanner(System.in);
            counter.count=scanner.nextInt();
        });
        t2.start();
    }

 t1读的是自己工作内存中的内容,当t2对变量count进行修改时,t1感知不到count的修改

这就是编译器优化在多线程环境下存在误判的问题 

volatile关键字能保证内存可见性,提醒编译器不要优化。此事被修饰的变量,编译器就不会做出“不读内存,只读寄存器”这样的优化。

volatile不保证原子性,只能针对一个线程读,一个线程修改这个场景

   static class Counter{
        volatile public int count=0;
        public void increase(){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }

而synchronized能保证原子性这个观点存疑。

wait和notify

wait

wait是Object的方法,是让当前线程进入等待状态

    public static void main(String[] args) {
        Object object=new Object();
        System.out.println("wait之前");
        try {
            object.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("wait之后");
    }

wait操作本质做了以下几件事:

1、释放当前锁;

2、进行等待通知;

3、满足一定条件时,被唤醒,然后尝试重新获取锁 

线程执行到wait,就会发生阻塞,直到另一个线程,调用notify把这个wait唤醒

wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常

    public static void main(String[] args) {
        Object object=new Object();
        System.out.println("wait之前");
        synchronized (object){
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("wait之后");
    }

在这里得保证加锁对象和调用wait对象是同一个,还要保证调用wait和notify的对象也是同一个

notify

notify方法是唤醒等待的线程

    public static void main(String[] args) {
        //准备一个对象,保证等待和调用是同一个
        Object object=new Object();

        //第一个线程
        Thread t1=new Thread(()->{
            while (true){
                synchronized (object){
                    System.out.println("wait之前");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait之后");
                }
            }
        });
        t1.start();

        //第二个线程,进行notify
        Thread t2=new Thread(()->{
            while (true){
                synchronized (object){
                    System.out.println("notify之前");
                    object.notify();
                    System.out.println("notify之后");
                }

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });
        t2.start();
    }

notifyAll

多个线程都在wait,notify是随机唤醒一个;notifyAll是全部唤醒

多线程案例

单例模式

单个实例instance(对象)。某个类,有且只有一个实例。

单例模式本质上就是借助编程语言自身的语法特性,强行限制某个类,不能创建多个实例。

单例模式创建:

static修饰的成员/属性,变成类成员/类属性,当属性变为类属性时,此时就是单例模式了。更具体地说,是类对象的属性,而类对象是通过JVM加载.class文件来的。此时类对象,在JVM中也是“单例”,换句话说,JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的成员(static修饰),也就只有一份。

//这个类,就作为一个”实例类“
//要求singleton只有一个实例
class Singleton{
    private static Singleton instance=new Singleton();

    public Singleton getInstance(){
        return instance;
    }

    //把构造方法设为private,此时在类的外边,就无法继续new实例了
    private Singleton(){

    }
}

public class Demo20 {
    public static void main(String[] args) {
        Singleton instance=Singleton.getInstance();
    }
}

使用这个唯一实例的方法就是通过getInstance方法,强制的保证了当前的Singleton是“单例”

上述代码中实现单例模式的方式叫做“饿汉模式”,是在类加载阶段创建的实例。

还有一种方式叫做“懒汉模式”

//懒汉模式来实现单例模式
class SingletonLazy{
    private static SingletonLazy instance=new SingletonLazy();

    public static SingletonLazy getInstance(){
        if (instance==null){
            instance=new SingletonLazy();
        }
        return instance;
    }
}

public class Demo21 {
    public static void main(String[] args) {
        SingletonLazy instance=SingletonLazy.getInstance();
    }
}

首次调用getInstance的时候才会触发创建实例的时机,后续调用getInstance,立即返回

由上述两个代码可知,懒汉模式是线程不安全的,getInstance方法既涉及了读,也涉及了修改。

class SingletonLazy{
    private volatile static SingletonLazy instance=new SingletonLazy();

    public static SingletonLazy getInstance(){
        if (instance==null){
            synchronized (SingletonLazy.class){
                if (instance==null){
                    instance=new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy(){
        
    }
}

public class Demo21 {
    public static void main(String[] args) {
        SingletonLazy instance=SingletonLazy.getInstance();
    }
}

此时线程就是安全的,并且是在实例没有创建之前(线程不安全的时候)需要加锁的时候加,在实例创建之后(线程安全的时候)不加。

 阻塞队列

1、线程安全的

2、带有阻塞功能

(a)如果队列满,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成

(b)如果队列空,继续出队列,出队列操作就会阻塞,直到队列不空,出队列才能完成

应用场景:生产者消费者模型,描述的是多线程协同工作的一种方式 

阻塞队列的用处: 

1、使用阻塞队列,有利于代码“解耦合”(耦合:两个模板之间的关联关系,关系越紧密,耦合越高,关系越不紧密,耦合越低)

2、削峰填谷

按照没有生产着消费者模型的写法,外面流量过来的压力,就会直接压到每个服务器上,如果某个服务器抗压能力不行,就容易挂。如果使用阻塞队列,当时当流量骤增的时候,队列承受了压力,其他的冲击力就不大

阻塞队列的具体使用

1、标准库的阻塞队列

    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque=new LinkedBlockingDeque<>(100);

        //带有阻塞功能的入队列
        blockingDeque.put(1);
        blockingDeque.put(2);
        blockingDeque.put(3);

        Integer ret=blockingDeque.take();
        System.out.println(ret);
        ret=blockingDeque.take();
        System.out.println(ret);
        ret=blockingDeque.take();
        System.out.println(ret);
    }

2、自己实现一个阻塞队列

先实现一个普通队列;加上线程安全;加上阻塞的实现

class MyBlockingQueue{
    private int[] items=new int[1000];
    private volatile int head=0;
    private volatile int tail=0;
    private volatile int size=0;

    //入队列
    public void put(int elem) throws InterruptedException {
        synchronized (this){
            //判定队列是否满了,满了则不能入队列
            while (size>=items.length){
                this.wait();
            }
            //进行插入操作,把elem放到items里,放到tail指向的位置
            items[tail]=elem;
            tail++;
            if (tail>=items.length){
                tail=0;
            }
            size++;
            this.notify();
        }
    }

    //出队列,返回删除元素的内容
    public Integer take(){
        synchronized (this){
            //判定队列是否空了,空了则不能出队列
            while (size==0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //进行取元素操作
            int ret=items[head];
            head++;
            if (head>=items.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}

public class Demo23 {
    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue();
        Thread producer=new Thread(()->{
            while (true){
                try {
                    int n=1;
                    queue.put(1);
                    System.out.println("生产元素"+n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread customer=new Thread(()->{
            while (true){
                try {
                    int n=queue.take();
                    System.out.println("消费元素"+n);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        customer.start();
    }
}

定时器 

1、标准库的定时器

public class Demo24 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到了,快起床");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到了,快起床1");
            }
        },4000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到了,快起床2");
            }
        },5000);
        System.out.println("开始计时");
    }
}

2、自己实现一个定时器

schedule第一个参数是一个任务,需要能够描述这个任务,任务包含两方面的信息,一个是要执行撒工作,一个是撒时候执行;

让MyTimer管理多个任务,使用BlockingQueue来实现;

任务已经被安排到优先级阻塞队列中,接下来需要从队列中取元素,创建一个单独的扫描线程,让这个线程不停的来检查队首元素,时间是否到了,如果时间到了,则执行该任务。

//这个类表示一个任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务
    private Runnable runnable;
    //什么时间来执行任务
    private long time;

    public MyTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time-o.time);
    }
}

class MyTimer{
    private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    private Object locker=new Object();

    public MyTimer(){
        //创建一个扫描线程
        Thread t=new Thread(()->{
            while (true){
                try {
                    synchronized (locker){
                        //取出队首元素
                        MyTask task=queue.take();
                        long curTime=System.currentTimeMillis();
                        if (curTime >= task.getTime()){
                            //到点了,该执行任务了
                            task.getRunnable().run();
                        }else {
                            //还没到点
                            queue.put(task);
                            locker.wait(task.getTime()-curTime);
                        }   
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable,long after) throws InterruptedException {
        MyTask myTask=new MyTask(runnable,after);
        queue.put(myTask);
        synchronized (locker){
            locker.notify();
        }
    }
}

public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer timer=new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到!");
            }
        },3000);
        System.out.println("开始计时");
    }
}

线程池

线程诞生的目的就是进程太重量了,导致进程的创建和销毁比较低效。

从线程池取线程,把线程放回线程池,这是纯用户态实现的逻辑;从系统这里创建线程,则是用户态->内核态共同完成的逻辑。 

这里可以使用线程池来进一步优化线程的速度,创建一个池子,创建很多线程,当需要执行任务的时候,不需要重新创建线程了,而是直接从池子里取一个现成的线程,直接使用,用完了,也不释放线程,而是还回到线程池里。

1、标准库实现线程池

此处创建线程池,是通过Executors类的静态方法newCachedThreadPool来完成-工厂模式 

public class Demo26 {
    public static void main(String[] args) {
        ExecutorService pool= Executors.newCachedThreadPool();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是任务");
            }
        });
    }
}

2、自己实现线程池

class MyThreadPool{
    private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int m){
        //在构造方法中,创建出M个线程,负责完成工作
        for (int i = 0; i < m; i++) {
            Thread t=new Thread(()->{
                while (true){
                    try {
                        Runnable runnable=queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

public class Demo27 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool=new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int taskId=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行当前任务:"+taskId+" 执行线程"+Thread.currentThread().getName());
                }
            });
        }
    }
}

常见的锁策略

乐观锁和悲观锁

乐观锁:预测锁冲突的概率不高,因此做的工作可以简单一点

悲观锁:预测锁冲突的概率较高,因此所做的工作就要复杂一点

读写锁和普通互斥锁

普通的互斥锁,就如同synchronized,当两个线程竞争同一把锁,就会产生等待

读写锁分为加读锁和加写锁

读锁和读锁之间,不会产生竞争;写锁和写锁之间,有竞争;读锁和写锁之间,有竞争

重量级锁和轻量级锁

重量级锁,加锁解锁开销比较大,典型的进入内核态的加锁逻辑

轻量级锁,加锁解锁开销比较小,典型的纯用户态的加锁逻辑

自旋锁和挂起等待锁

自旋锁,是一种轻量级锁的典型实现,自旋就类似于“忙等”,消耗大量的CPU,反复询问当前锁是否就绪

挂起等待锁,是重量级锁的一种典型实现

公平锁和非公平锁

公平锁:遵守“先来后到”

不公平锁:不遵守“先来后到”

操作系统内部的线程调度可以认为是随机的,如果不做任何额外的限制,锁就是非公平锁。如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序

可重入锁和不可重入锁

可重入锁:同一线程针对同一把锁,连续加锁两次,不会死锁

不可重入锁:同一线程针对同一把锁,连续加锁两次,会死锁

CAS

compare and swap

把内存中某个值,和CPU寄存器A中的值,进行比较。如果两个值相同,就把另一个寄存器B中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)

    boolean CAS(address,expectValue,swapValue){
        if (&address==expectedValue){
            &address=swapValue;
            return true;
        }
        return false;
    }

上述代码,是SS通过一个CPU指令完成的,是原子的

基于CAS实现一些逻辑的时候,不加锁,也能保证线程安全,但只能在特定场景使用。加锁适用面更广,加锁代码往往比CAS可读性更好

最常用的两个场景:

1、实现原子类

2、实现自旋锁

CAS的ABA问题

在CAS中,进行比较的时候,发现寄存器A和内存M的值相同,但无法判断M始终没变,还是M变了,又变回来了

如何解决ABA问题?

只要有一个记录,能够记录上内存中数据的变化,也就是保存M的修改次数或者是上次修改时间,这两个都是只增不减的

Synchronized原理

Synchronized具有以下特性:1、开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁;2、开始是轻量级锁实现,如果锁被持有的时间较长,就转换为重量级锁;3、实现轻量级锁的时候大概率用到的自旋锁策略;4、是一种不公平锁;5、是一种可重入锁;6、不是读写锁

锁升级/锁膨胀

synchronized效果是“加锁”,当两个线程针对同一个对象加锁的时候,就会出现锁竞争,后来尝试加锁的线程就得阻塞等待,直到前一个线程释放锁

synchronized加锁的具体过程:1、偏向锁;2、轻量级锁‘3、重量级锁

无竞争,偏向锁;有竞争,轻量级锁;竞争激烈,重量级锁

锁消除

JVM自动判定,如果发现这个地方的代码,不必加锁,如果你写了synchronized,就会自动的把锁干掉

锁粗化

synchronized对应代码块包含多少代码,包含的代码少,粒度细,包含的代码粗,粒度粗

锁粗化,就是把细粒度的加锁=》粗粒度的加锁

JUC

Callable接口

类似于Runnable,Runnable描述的任务,不带返回值;Callable描述的任务,带返回值

    //创建线程,计算1+2+……+1000
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Callable定义一个任务
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum=0;
                for (int i = 0; i < 1000; i++) {
                    sum+=i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask=new FutureTask<>(callable);

        //创建线程,执行任务
        //Thread的构造方法,不能直接传Callable,还需要一个中间的类
        Thread t=new Thread(futureTask);
        t.start();

        //获取线程的计算结果
        //get方法会阻塞,直到call方法计算完毕,get才会返回
        System.out.println(futureTask.get());
    }

ReentrantLock

也是可重入锁,ReentrantLock是对synchronized的一个补充

核心用法:1、lock()加锁;2、unlock()解锁

    public static void main(String[] args) {
        ReentrantLock locker=new ReentrantLock(true);
        try {
            locker.lock();
        }finally {
            locker.unlock();
        }
    }

优点:

1、tryLock试试看能不能加上锁,试成功了,就加锁成功,试失败了,就放弃,并且还可以指定加锁的等待超时时间;

2、ReentrantLock可以实现公平锁,默认是非公平的,构造的时候,传入一个参数,就成了公平锁

ReentrantLock locker=new ReentrantLock(true);

3、synchronized是搭配wait/notify实现等待通知机制的,唤醒操作是随机唤醒一个等待的线程

ReentrantLock搭配Condition类实现的,唤醒操作是可以指定唤醒哪的等待的线程的

原子类

在没有加锁的情况下,实现多线程的累加

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count=new AtomicInteger(0);
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //相当于count++
                count.getAndIncrement();
//                //相当于++count
//                count.incrementAndGet();
//                //相当于count--
//                count.getAndDecrement();
//                //相当于--count
//                count.decrementAndGet();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //相当于count++
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count.get());
    }

线程池

信号量Semaphore

信号量的基本操作:p操作,申请一个资源;v操作,释放一个资源

信号量本身是一个计数器,表示可用资源的个数,p操作申请一个资源,可用资源数就-1;v操作释放一个资源,可用资源数就+1,当计数器为0的时候,继续p操作,就会产生阻塞,阻塞等待到其它线程v操作了为止

信号量可以被视为一个更广义的锁

CountDownLatch

同时等待N个任务执行结束 

    public static void main(String[] args) throws InterruptedException {
        //有10个选手参加了比赛
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t=new Thread(()->{
                System.out.println("选手出发"+Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("选手到达"+Thread.currentThread().getName());
                //撞线
                countDownLatch.countDown();
            });
            t.start();
        }
        //await是进行阻塞等待,会等到所有的选手都撞线之后,才能解除阻塞
        countDownLatch.await();
        System.out.println("比赛结束");
    }

线程安全的集合类-CopyOnWrite

“双缓冲区”机制,写(修改)时拷贝(复制)

如果要修改,不直接修改,而是把原来的数据复制一份,修改的时候改这个新的数据

多线程环境下使用哈希表

HashTable

不推荐使用

ConcurrentHashMap

优化策略:

1、锁粒度的控制

HashTable直接在方法上加synchronized,相当于是对this加锁。相当于是针对哈希表对象加锁,一个哈希表只有一把锁。多个线程,无论这些线程,都是如何操作的这个哈希表,都会产生所冲突。

ConcurrentHashMap每个哈希桶有自己的锁,大大降低了锁冲突的概率,性能也就大大提高了。

2、ConcurrentHashMap做了一个激进的操作

只是给写操作加锁,读操作不加锁。两个线程同时修改,才会有锁冲突;两个线程同时读,不会有锁冲突;如果一个线程读,一个线程修改,也没有锁冲突。

3、充分的利用到了CAS的特性

像维护元素个数,都是通过CAS来实现的,而不是加锁,还有的地方直接使用CAS实现的轻量级锁来实现

ConcurrentHashMap思路就是能不加锁就不加

4、ConcurrentHashMap对于扩容操作,进行了特殊的优化,化整为零

HashTable的扩容机制:当put元素的时候,发现当前的负载因子已经超过阈值,就需要触发扩容,申请一个更大的数组,然后把之前旧的数据给搬运到新的数组上

ConcurrentHashMap在扩容的时候,不是一次性全部搬完,而是搬运一点。扩容的时候,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,此时在释放旧的空间

死锁

场景1:一个线程,一把锁,线程连续加锁两次,如果这个锁是不可重入锁,发生死锁

场景2:两个线程,两把锁

    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            System.out.println("t1尝试获取locker1");
            synchronized (locker1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1尝试获取locker2");
                synchronized (locker2){
                    System.out.println("t1获取两把锁成功");
                }
            }
        });
        Thread t2=new Thread(()->{
            System.out.println("t2尝试获取locker2");
            synchronized (locker2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2尝试获取locker1");
                synchronized (locker1){
                    System.out.println("t1获取两把锁成功");
                }
            }
        });
        t1.start();
        t2.start();
    }

场景3:多个线程多把锁

解决死锁

约定,加多个锁的时候,必须先加编号小的锁,后加编号大的锁,有效避免循环等待

    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            System.out.println("t1尝试获取locker1");
            synchronized (locker1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1尝试获取locker2");
                synchronized (locker2){
                    System.out.println("t1获取两把锁成功");
                }
            }
        });
        Thread t2=new Thread(()->{
            System.out.println("t2尝试获取locker1");
            synchronized (locker1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2尝试获取locker2");
                synchronized (locker2){
                    System.out.println("t1获取两把锁成功");
                }
            }
        });
        t1.start();
        t2.start();
    }

多线程的知识就到此为止了,大家也可以去搜些资料多扩充一下!

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值