~~线程~~

一、线程

1.为什么要引入线程?

进程的切换开销比较大,线程比进程更轻量,所以,引入了线程。

2.线程和进程的区别?

a.线程是系统执行调度的基本单位,进程是系统分配资源的基本单位;
b.一个进程中最少包含一个线程;
c.多个线程共用一块进程资源。

3.Java中,如何创建线程

1.继承Thread类,重写run方法;
2.实现Runnable接口,重写run方法;
3.匿名内部类,继承Thread类,重写run方法;
4.匿名内部类,实现Runnable接口,重写run方法;
5.lambda表达式

//1.继承Thread类,重写run方法;
class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("任务");
    }
}
public class Test01 {
    public static void main(String[] args) {
        Thread thread=new MyThread();
        thread.start();

    }
}
//2.实现Runnable接口,重写run方法;
class MyThread02 implements Runnable{

    @Override
    public void run() {
        System.out.println("任务2");
    }
}
public class Test02 {
    public static void main(String[] args) {
        Thread t=new Thread(new MyThread02());
        t.start();
    }
}
//3.匿名内部类,继承Thread类,重写run方法;
public class Test03 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run(){
                System.out.println("任务3");
            }
        };
        t.start();
    }
}
//4.匿名内部类,实现Runnable接口,重写run方法;
public class Test04 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务4");
            }
        });
        t.start();
    }
}

//5.lambda表达式
public class Test05 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("任务5");
        });
        t.start();
    }
}

4.Java中,线程的几种状态?

1.New:新建状态,在用户态创建了线程对象Thread,内核态没有创建PCB;
2.Runnable:内核态也创建了PCB,线程处于就绪态或者是执行态;
3.Block:阻塞状态,一般加锁synchronized之后,会出现这种阻塞状态;
4.Waiting:等待态,一般调用了wait()、notify()之后会进入这种状态;
5.Timed_Waiting:等待态,一般调用sleep()之后会处于这种状态。
6.Terminated():线程中断,内核中的PCB被销毁了,但是用户态的Thread对象没有被销毁

5.创建线程的方法有Runnable,Runnable重写的run方法返回值是void类型的,想要获得线程中执行之后的结果,如果获得呢?

Java标准库提供了Callable接口,实现这个接口,重写里面的run方法,run方法是带有返回值的。run方法的返回值是什么类型的?取决于Callable的泛型参数是什么类型的。
下面,举例说明:获取线程执行从1+2+3+…+100的结果值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test06 {
    static class T6{
        int i;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        T6 t6=new T6();
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum=0;
                for(int i=1;i<=100;i++){
                    sum+=i;
                }
                t6.i=sum;
                return t6.i;
            }
        };
        //线程当中无法直接获得callable的参数,需要找一个中间媒介,FutureTask
        FutureTask futureTask=new FutureTask(callable);
        Thread t=new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }
}

6.start()和run()的区别

start()方法是会在内核态创建新的PCB,如果在main方法当中执行一个线程,采用的方法是start(),这样就是两个线程并发执行。
run()方法在内核态不创建新的PCB,如果在main方法当中执行一个线程,采用的方法是run(),是让代码串行执行。

二、线程安全

1.造成线程不安全的原因是什么?

a.多个线程针对一个变量进行更改操作;
b.多个线程并发执行,如果代码不能保证原子性,就会造成线程不安全;
c.由于处理器的缓存功能,造成多线程在读、写场景下,写的内容在内存中,读的操作是从缓存中取,就造成了线程不安全;
d.由于编译器的优化,在执行代码的时候,为了提高代码执行效率,会更改代码的执行顺序,在多线程场景下,这是线程不安全的。

2.如何保证线程安全?

造成线程不安全的原因主要有三点:内存可见性、指令重排序(执行顺序)、原子性。
如何解决内存可见性?让线程的读写操作直接操作内存,synchronize锁和volatile关键字可以做到这一点;
如何解决指令重排序?避免编译器的优化,synchronize锁和volatile关键字可以做到这一点;
如何解决原子性?执行代码加锁,synchronize锁可以做到这一点
synchronize锁:可以解决原子性、内存可见性、避免指令重排序;
volatile关键字:可以解决内存可见性、避免了指令重排序

3.synchronized锁的特性

1.具有互斥作用:synchronized锁具有互斥作用。
互斥是如何实现的?CPU提出了一些原子指令,操作系统针对这些原子指令进行封装,提出了一个mutex互斥量,在Java当中,提出了synchronized锁
2.具有内存刷新、解决了内存可见性;
3.避免了指令重排序;

三、线程池

1.为什么要引入线程池

虽然线程的创建销毁比进程更轻量,但是,会涉及到内核态的PCB的创建和销毁,频繁的创建和销毁线程,开销资源比较大,会频繁的切换用户态和内核态。所以,引入了线程池,创建线程、销毁线程都是在用户态完成,不需要用户态和内核态的转换。

2.Java标准库中提供了一个ThreadPoolExecutor类,这个类的构造方法中有几个参数,这些参数的含义是什么?

1.corePoolSize:核心线程数
2.maximumPoolSize:最大线程数,包括核心线程数和临时线程数
3.KeepAliveTime:临时线程可执行的时间是多少,unit:时间单位:ms,s,minute…
4.workQueue:阻塞队列,里面放要执行的任务
5.threadFactory:线程工厂,给不同的参数,创建不同的线程
6.handler:拒绝策略,当阻塞队列当中的线程已经满的时候,这个时候,来了一个新的任务,此时,要采取的策略是什么呢?是删除最新的任务?/删除任务队列中时间最久的任务?/还是抛出中断异常?根据具体情况来确定。

3.为什么要引入工厂方法?工厂模式是干什么用的?

创建实例的方法是:调用类的构造方法。创建不同的实例,就需要调用不同的构造方法,构造方法只能重载,重载的定义是:类名不变,参数个数和参数类型可变,这一点,带来了极大的局限性。举例说明:
如果想要在二维平面上定义一个点,有两种方式:一种是通过x,y坐标,一种是通过半径、角度来定义。
如果采用构造方法来定义:是区分不开的,因为类名相同,参数个数相同、参数类型也相同,这是区分不开的。这里就可以使用工厂方法。
工厂方法可以创建不同的实例。

4.Java标准库提出的ThreadPoolExecutor类中的构造方法的参数比较多,所以,引入了Executors类,这个类里面提供了四个方法。

Executors类可以创建不同类型的线程池。底层代码实际上是不同的方法调用ThreadPoolExecutor不同的构造方法。
1.newFixedThreadPool:有固定数量的线程池,当任务量大于线程数的时候,任务会在阻塞队列中等待。
2.newCachedThreadPool:数量可变的线程池,当任务量小于线程数的时候,会回收多余的线程,如果任务量大于线程数时,会创建线程。
3.newSignalThreadPool:只有一个线程的线程池。
4.newScheduleThreadPool:有固定数量的线程池,具有定时功能。

5.利用Executors类提供的方法,创建线程池。

//创建固定数量的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test16 {
    public static void main(String[] args) {
        ExecutorService executorService= Executors.newFixedThreadPool(10);
        for(int i=0;i<20;i++){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务");
                }
            });
        }
    }
}

6.自己实现一个线程池。

思路:用runnable定义任务,将任务放入阻塞队列当中;将线程创建出来之后,放在list当中;定义一个工作线程,工作线程主要是从阻塞队列当中取出任务,执行任务

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Test15 {
    //工作线程:从阻塞队列当中取出任务,执行任务
    static class WorkThread extends Thread{
        private BlockingQueue<Runnable> blockingQueue=null;
        public WorkThread(BlockingQueue<Runnable> blockingQueue){
            this.blockingQueue=blockingQueue;
        }
        @Override
        public void run(){
            while(true){
                try {
                    Runnable runnable=blockingQueue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //线程池:创建工作线程,把任务放在阻塞队列当中
    static class ThreadPool{
        private static final int MAX_COUNT=10;
        List<Thread> works=new ArrayList<>();
        BlockingQueue<Runnable> blockingQueue=new LinkedBlockingQueue();
        //这个方法里面主要负责创建工作线程,把线程放在list当中,往阻塞队列当中添加任务
        public void submit(Runnable runnable) throws InterruptedException {
            if(works.size()<MAX_COUNT){
                WorkThread workThread=new WorkThread(blockingQueue);
                workThread.start();
                works.add(workThread);
            }
            blockingQueue.put(runnable);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadPool t=new ThreadPool();
        for(int i=0;i<20;i++){
            t.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务");
                }
            });
        }
    }
}

四、生产者-消费者模型、阻塞队列

1.阻塞队列是什么?

阻塞队列是一种特殊的队列,遵循”先进先出“的原则。
阻塞队列是一种线程安全的数据结构,具有两个特性:
队列已满,阻塞,直到队列中被取出元素。
队列为空,阻塞,知道队列中被放入元素。

2.阻塞队列的作用是什么?

阻塞队列的作用是:降低耦合:减少服务器之间的交互,服务器执行任务之间从阻塞队列中获取;削峰填谷:在用户请求量大的时候,阻塞队列起缓冲作用,不会让大量的请求直接访问服务器,在用户请求量小的时候,不会让服务器空闲,服务器可以一直以一种均匀的速度从阻塞队列中拿任务,执行任务。

3.生产者-消费者模型–Java标准库中提供的BlockingQueue

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Test11 {
    public static void main(String[] args) {
        //BlockingQueue是一个接口
        //队列的实现方式有两种:链表和数组
        BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();
        //生产者
        Thread producer=new Thread(){
            @Override
            public void run(){
                for(int i=0;i<10;i++){
                    System.out.println("生产元素"+i);
                    try {
                        //BlockingQueue接口继承了Queue接口,所以,入队列方法包含add,offer方法,为什么要用put方法,因为put方法带有阻塞作用
                        blockingQueue.put(i);
                        //生产元素慢
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //消费者
        Thread consumer=new Thread(){
            @Override
            public void run(){
                while(true){
                    try {
                        System.out.println("消费元素"+blockingQueue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        consumer.start();
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果展示:

在这里插入图片描述

4.生产者-消费者模型-自己实现一个阻塞队列

public class Test12 {
    //用数组的形式实现一个阻塞队列
    static class BlockingQueue{
        private Object locker=new Object();
        int[] blockingQueue=new int[10];
        int head=0;
        int tail=0;
        int length=blockingQueue.length;
        int size=0;
        public void put(int value){
            //队列满,进入阻塞状态,阻塞利用wait、notify来实现
            //这里为什么用while循环?
            //在解锁之后需要重新判断队列是否为满
            while(size==length){
                synchronized (locker){
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            blockingQueue[tail]=value;
            tail++;
            size++;
            if(tail>=length){
                tail=0;
            }
            //解锁--队列为空
            synchronized (locker){
                locker.notify();
            }
        }
        public int take(){
            //队空阻塞
            //while循环的作用:在解锁之后,需要在判断一下队列是否真的不为空
            while(size==0){
                synchronized (locker){
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            int ret=blockingQueue[head];
            head++;
            size--;
            if(head>=length){
                head=0;
            }
            synchronized (locker){
                locker.notify();
            }
            return  ret;
        }
    }

    public static void main(String[] args) {
        BlockingQueue blockingQueue=new BlockingQueue();
        //生产者
        Thread producer=new Thread(){
            @Override
            public void run(){
                for(int i=0;i<20;i++){
                    System.out.println("生产元素"+i);
                    blockingQueue.put(i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //消费者
        Thread consumer=new Thread(){
            @Override
            public void run(){
                while(true){
                        System.out.println("消费元素"+blockingQueue.take());
                }
            }
        };
        producer.start();
        consumer.start();
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果展示:

阻塞队列大小设置为10,
需要生产20个元素,
当生产速度<消费速度的时候:当队列为空的时候,会进入阻塞状态,生产一个元素,消费一个元素。
当生产速度>消费速度的时候:当队列满的时候,会进入阻塞状态,消费一个元素,生产一个元素
第一种情况:当生产速度小于消费速度的时候:
在这里插入图片描述
第二种情况,当生产速度>消费速度的时候
在这里插入图片描述

五、定时器

1.定时器是什么?

可以设置任务执行的时间,时间到了,就执行任务

2.Java标准库当中提供了一个定时器类Timer

import java.util.Timer;
import java.util.TimerTask;

public class Test13 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        //任务1--3s后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },3000);
        //任务2--2s后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
        //任务3--1s后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务3");
            }
        },1000);
    }
}

代码结果:
在这里插入图片描述
3.自己实现一个定时器
思路:
写一个Task类,主要用于创建任务,初始化任务,初始化任务执行时间,任务执行。
写一个Timer类,主要用于往带有优先级的阻塞队列中添加任务,在构造方法中定义一个扫描线程,主要用于查看任务时间是否到了。
优先级阻塞队列主要作用是:根据执行时间调整任务执行顺序

import java.util.concurrent.PriorityBlockingQueue;

public class Test14 {
    static class Task implements Comparable<Task>{
        private  long time;
        private Runnable command;
        public Task(Runnable command,long time){
            this.command=command;
            this.time=System.currentTimeMillis()+time;
        }
        @Override
        public int compareTo(Task o) {
            return (int)(this.time-o.time);
        }
        public void run(){
            command.run();
        }
    }
    static class Timer{
        private Object locker=new Object();
        PriorityBlockingQueue<Task> priorityBlockingQueue=new PriorityBlockingQueue<>();
       //往阻塞队列当中添加要执行的任务
       public void schedule(Runnable runnable,long time){
           Task task = new Task(runnable,time);
           priorityBlockingQueue.put(task);
           //每增加一个任务的时候,就需要唤醒,根据阻塞队列当中时间最近的任务的执行时间到现在时间的时间差,作为wait的参数
           synchronized(locker){
               locker.notify();
           }
       }
       public Timer(){
           Thread t=new Thread(){
               Task task=null;
               @Override
               public void run(){
                   while (true){
                       try {
                           task=priorityBlockingQueue.take();
                           long time=System.currentTimeMillis();
                           if(task.time>time){
                               priorityBlockingQueue.put(task);
                               synchronized (locker){
                                   locker.wait(task.time-time);
                               }
                           }else{
                               task.run();
                           }
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
               }
           };
           t.start();
       }
    }
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3");
            }
        },1000);
    }
}

结果展示:

在这里插入图片描述

六、锁策略

1.乐观锁 VS 悲观锁

锁冲突的概率是否大。
乐观锁会认为锁冲突概率小,会直接操作数据,等到锁冲突的时候,才会处理冲突;
悲观锁会认为锁冲突概率大,会先加锁,然后对数据进行操作。
乐观锁当中有一个”版本号“,当前要操作的线程读取到的版本号与之前读到的版本号进行比较,相等就操作数据,不相等,操作失败。

2.读写锁RenntantLock

在多线程场景下:
多个线程读数据,读写锁不会产生互斥;
多个线程写数据,读写锁会产生互斥;
多个线程又读数据又写数据,读写锁会产生互斥。
产生互斥,影响程序执行效率

3.可重入锁 VS 不可重入锁

如何区分:对同一把锁进行连续的加锁,看是否产生死锁。
可重入锁:对同一把锁进行连续的加锁,不会产生死锁。
不可重入锁:对同一把锁进行连续的加锁,会产生死锁。

4.自旋锁 VS 挂起等待锁

如何区分:在未获取到锁的时候,是否会继续尝试获取锁。
自旋锁:在未获取到锁的时候,不会放弃CPU,会继续尝试获取锁。
挂起等待锁:在未获取到锁的时候,会进入阻塞状态,什么时候结束这种阻塞状态,取决于CPU的具体调度。
自旋锁和挂起等待锁的适用场景:
锁冲突比较小的时候,锁冲突时间比较短的时候,适合自旋锁;
对CPU资源比较敏感,不希望占用太多CPU资源的适合,适合挂起等待锁。
自旋锁的优点和缺点:
优点:一直在用户态,不涉及到用户态和内核态的切换;当其他线程释放锁之后,可以第一时间获取锁。
缺点:占用CPU资源。

5.公平锁和非公平锁

如果区分:看是否遵循先来后到的原则。
公平锁:遵循先来后到的原则。
非公平锁:不遵循先来后到的原则,具体是哪个线程先执行,具体看CPU的调度。

七、synchronized锁

1.结合锁策略来看,JDK1.8中synchronized锁的特性有哪些?

1.synchronized在刚开始的时候是乐观锁,锁冲突比较大的时候,会采用悲观锁。
2.synchronized起初是一个轻量级锁,当线程占用锁时间太久的时候,会变成一个重量级锁。
3.synchronized是轻量级锁的时候,大概率是自旋锁策略,后期变成重量级锁的时候,大概率是挂起等待锁策略。
4.synchronized是一个可重入锁。
5.synchronized不是读写锁。
6.synchronized是非公平锁。

2.CAS机制

1.CAS机制是如何理解的?
CAS机制是compare and swap:比较并且交换,是一个原子操作,同时做到”读取内存,比较是否相等,操作内存“。
2.CAS机制当中的ABA问题是什么?
ABA问题是:一个数据在操作的时候,会出现两种情况:第一种情况是:从头到尾,数据都没有发生过变化,第二种情况是:数据经过一系统的增加、删除操作之后,数据又恢复到原来的样子。
这个问题如果应用到实际场景中,会带来比较危险的操作。
场景如下:
kk出去吃饭,微信余额200元,吃饭消费100元,商家在扫码的时候,由于网络原因,商家未收到款,kk的微信余额也未发生改变,商家又扫了一次付款码。
在这个过程中,触发了两次付款的操作,
情况一:如果微信余额一直没有改变,线程1读取余额200元,和最初读取到的余额一致,于是就产生扣款的操作,线程2读取余额100元,和最初读取到的余额不一致,不产生扣款的操作。
情况二:如果微信余额在中途发生改变,线程1读取到余额200元,和最初读取的余额一致,产生扣款操作,在线程1执行完之后,账户在这个时间段内收到了100元,账户余额变成200元,线程2读取余额200元,发现和最初读取的一致,又扣款100元。这里就出现了两次扣款,是十分危险的。
如何避免这种情况呢?
引入了”版本号“这样的操作,最初读取账户余额200元为版本1,线程1读取余额为200元,版本号为1,和最初读取到的版本号一致,执行扣款100元,余额变为100元为版本2,在收到其他账户转款100元,账户余额变为200元,为版本3,线程2读取账户余额为200元是版本3,最初读取账户余额是版本1,版本不一致,不执行扣款操作。
3.CAS机制有哪些应用?
自旋锁+原子类

//CAS机制当中的原子类实现的例子
//借助AtomicInteger类,来完成两个线程并发执行对数据自增操作
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;

public class Test07 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger(10);
        Thread t=new Thread(){
            @Override
            public void run(){
                for (int i=0;i<100;i++){
                    atomicInteger.getAndIncrement();
                }
            }
        };
        t.start();
        Thread t2=new Thread(){
            @Override
            public void run(){
                for (int i=0;i<100;i++){
                    atomicInteger.getAndIncrement();
                }
            }
        };
        t2.start();
        try {
            t.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(atomicInteger.get());
    }
}

3.锁膨胀/锁升级

JVM将synchronized的状态分为:无锁->偏向锁->轻量级锁->重量级锁。synchronized具体采用哪种状态取决于synchronized的自适应性。
偏向锁:给对象头中添加一个"偏向锁的标记",如果不存在锁冲突,就不必加锁。如果产生锁冲突,取消偏向锁的状态,进入轻量级锁的状态。
轻量级锁:通过CAS机制中的自旋锁来实现。如果需要检查并更新一块内存,更新成功就认为获取锁成功,如果更新失败,就认为锁被占用,一直尝试再次获取锁。
重量级锁:用到了操作系统提供的mutex互斥量,会进行加锁,切到内核态,如果获取到锁,会再次切回用户态,如果没有获取到锁,会进入阻塞状态。

4.锁消除

因为JVM和编译器的优化,如果是单线程执行加锁代码,会将锁消除

5.锁的粗化和细化

锁的粗细取决于锁里面包含的代码量。
锁里面包含的代码量多,就证明锁粗化。
锁里面包含的代码量少,就证明锁细化。
锁粗化的优点:
可以将多个任务一起执行,避免了频繁的加锁解锁操作,提高了程序执行效率。
锁粗化的缺点:线程占用锁的时间过长,其他线程无法获取锁。

八、多线程模式–单例模式

1.单例模式是什么?

某个类在整个程序中,只存在一份实例。
单例模式分为饿汉模式和懒汉模式。
饿汉模式:在类加载的时候就会被创建出来。
懒汉模式:在使用的时候,会创建实例。

2.代码实现
a.饿汉模式:在类加载的时候就会被创建

//饿汉模式
class MyTest{
    //将构造方法设置为private,防止在其他类当中调用
    private MyTest(){

    }
    private static MyTest myTest=new MyTest();
    public static  MyTest getMyTest(){
        return myTest;
    }
    public void run(){
        System.out.println("MyTest当中的方法");
    }
}
public class Test08 {
    public static void main(String[] args) {
        MyTest myTest=MyTest.getMyTest();
        myTest.run();
    }
}

b.懒汉模式–单线程版本的

class MyTest02{
    private MyTest02(){

    }
    private static MyTest02 myTest02=null;
    public static MyTest02 getMyTest02(){
        if(myTest02==null){
            myTest02=new MyTest02();
        }
        return myTest02;
    }
    public void run(){
        System.out.println("MyTest02中的方法");
    }
}
public class Test09 {
    public static void main(String[] args) {
        MyTest02 myTest02=MyTest02.getMyTest02();
        myTest02.run();
    }
}

单线程版本的懒汉模式,放在多线程的场景下,会产生线程安全的问题,两个线程并发获取getMyTest02()方法,读到都是myTest02==null,就会创建两个实例,违背了单例模式。
如何解决这件事呢?
synchronized和volatile搭配使用
产生线程不安全的原因是:其一,if判断和创建实例代码不是原子操作;其二,多线程读取数据的时候,不会直接操作内存。
为了解决这两个问题,为了控制多线程读取数据是操作内存,修饰变量用volatile关键字,让if判断和创建实例是原子操作,采用synchronized锁

懒汉模式-多线程版本的

class MyTest03{
    //volatile关键字修饰变量:为了让多线程读直接操作内存
    private volatile static MyTest03 myTest03=null;
    private MyTest03(){

    }
    public static MyTest03 getMyTest03(){
        //这一层if判断是:判断是第一次创建实例还是已经创建过实例,避免每次都要进入锁之后才判断
        if(myTest03==null){
            //加锁目的:让if判断和创建实例变为一个原子操作
            synchronized (MyTest03.class){
                if(myTest03==null){
                    myTest03=new MyTest03();
                }
            }
        }
        return myTest03;
    }
    public void run(){
        System.out.println("MyTest03当中的方法");
    }
}
public class Test10 {
    public static void main(String[] args) {
        MyTest03 myTest03=MyTest03.getMyTest03();
        myTest03.run();
    }
}

`

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值