多线程(3)
1volatile关键字:
1.1volatile的功能
volatile关键字能够保证内存可见性
当变量被volatile修饰后:
写操作--->会将寄存器内的值修改后会第一时间将新值写回内存(主内存),不会引起一个另外一个线程去读的时候还读个旧数据,导致出现bug,比如将0+1改为1后就应该立马写回内存,然后另外一个线程来读到1 然后+1得到2,两个线程分别++ 1次得结果2就没bug,但是第一个修改了不写回,另外一个线程又读个0,然后++变成1,这时候线程 1 2 再去写回,得到的结果就是1,出现bug,导致线程安全问题的出现.也就是没有保证内存可见性
读操作---->不会只是一味的读寄存器的旧值,每次读都是从主内存读到寄存器,然后从寄存器读到数据
确保每次读到数据都是最新的数据,不会因为读到旧数据而产生bug 比如:
static class Count{public static int flag=0;}public static void main(String[] args) {Count count=new Count();Thread t1=new Thread(()->{while (count.flag==0){//do noting}System.out.println("循环结束");});Thread t2=new Thread(()->{Scanner scanner=new Scanner(System.in);System.out.println("请输入值来改变flag");count.flag=scanner.nextInt();});t1.start();t2.start();}
当线程t2,将flag改为非0数后,循环结束这几个字迟迟没有打印,说明线程t1读到的flag一直就是寄存器内的旧值0,那么当然不会结束,与预期值不符合,出现bug,当在count前加上volatile就正常了,保证了内存可见性,保证了线程安全.
volatile不能保证原子性,synchronized能够保证原子性,但是volatile是保证内存可见性
2wait和notify方法
2.1wait和notify方法注意点:
1:必须搭配synchronized使用,且加锁对象和调用wait方法对象必须是同一个
2:wait方法是Object类方法,也就是所有对象的方法,所有的对象都能调用,但是要合理使用(搭配synchronized)
3:wait方法提供了三种,---不带参数的死等,必须等到有其他线程notify唤醒wait才会被唤醒----
带时间参数的:wait等待一定时间,时间已过如果还没被notify那么就自动唤醒
4:使用notify唤醒wait时候调用notify的对象必须和调用wait的对是同一个,否则wait不能被唤醒,总之就是那个对象调用notify,就唤醒那个对象调用的wait,这个对象是两个线程之间联系的桥梁.
5:notifyJava要求搭配synchronized使用,但是操作系统原生api不要求.因为notify并未涉及加锁解锁操作.
6:务必确保notify在wait之后,因为你都还没等呢,就去唤醒,唤醒个啥?当然notify通知没有等的对象也不会报错,如果先notify,再wait,后续wait就成死等了.
7:wait之前处于加锁状态,wait之中处于阻塞等待状态,wait被唤醒然后又获得锁之后处于加锁状态.
2.2wait做的事情:
1:将当前线程放入等待队列中进行阻塞等待
2:释放当前锁
3:被唤醒后尝试重新获取锁
2.3wait结束等待的条件:
1:其他线程同一锁对象调用notify唤醒
2:wait(时间)超出等待时间
3:其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException 异常.
2.4notify方法的介绍:
notify方法也必须在synchronized当中使用,该方法是去唤醒那些正在wait的线程,对其发出通知,将其唤醒,当有多个线程在wait时,notify只能通知同一锁对象等待中的其中一个,这是调度器随机调度的,notify通知后,并不意味着目前线程立马释放锁,其他线程立马就能去重新获取锁,而是要等待当前线程走完释放锁,就是释放锁和notify没有半毛钱关系.
代码案例:
public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{System.out.println("t1 wait之前");synchronized (locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程被唤醒");});Thread t2=new Thread(()->{System.out.println("t2 wait之前");synchronized (locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t2线程被唤醒");});Thread t3=new Thread(()->{System.out.println("输入任意之后唤醒一个线程线程");Scanner scanner=new Scanner(System.in);scanner.next();synchronized (locker){locker.notify();}});t1.start();t2.start();t3.start();}
2.5notifyAll方法:
该方法能够将所有等待的线程全都唤醒,但并不意味着全都能拿到锁,只是说他们都可以去竞争锁了,至于谁先加锁成功取决去调取器,是随机的,如果没有拿到锁即使被唤醒了也是继续阻塞等待.
代码案例
public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{synchronized (locker){System.out.println("t1 wait之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程被唤醒");});Thread t2=new Thread(()->{synchronized (locker){System.out.println("t2 wait之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t2线程被唤醒");});Thread t3=new Thread(()->{synchronized (locker){System.out.println("输入任意之后唤醒所有线程线程");Scanner scanner=new Scanner(System.in);scanner.next();//locker.notify();locker.notifyAll();}});t1.start();t2.start();t3.start();}
3单例模式
3.1啥是设计模式?
设计模式就好比我们物理数学当中的公式模版套路,在遇到符合某一特定情况的问题时,我们就采用这些固定的套路去实现代码,这些套路都是大佬总结出来的,在合理的情况下无脑套用是最佳选择,因为这些套路都是经验性的东西,初始者无法立马想出来用上,但是将其总结成公式记下来直接用即可.能短时间提升我们代码的质量.
单例模式的实现具体有很多.常见的有饿汉模式和懒汉模式
3.2什么是单例模式?
单例单例,就是单个例子,类只有一个实例化对象,并且这个对象在内内部直接实例化出来,那怎么样才能只有一个实例化对象,控制不出现第二个实例化对象呢? ----->对构造方法用private修饰--->单例模式的点睛之笔----这样在类外无法访问到这个构造方法,就谈不上在类外再去new 实例化对象了,类似String类的不可变性:如下就是一个单例模式下的类
class Singleton{private static Singleton instance=new Singleton();//静态成员的初始化在类加载阶段//属性啥的-----//private Singleton(int a){//带参数的构造方法}private Singleton(){//构造方法}public Singleton getInstance(){//获取这个唯一的独苗---实例化对象return instance;}}
3.3饿汉模式:
给人的感觉就是迫不及待,立即马上,就是立即把这个唯一的独苗----实例化对象new处来,也不管后续用不用的上,就是先new出来再说,有点像驾照,先不管买不买的起车,刚高考完满18就先去考了,迫不及待的感觉,不管用不用的上,先准备好.
代码:
class Singletonhungry{//饿汉模式----立马把对象new出来private static Singletonhungry instance=new Singletonhungry();//静态成员的初始化在类加载阶段////属性啥的-----//private Singletonhungry(int a){//带参数的构造方法}private Singletonhungry(){//构造方法}public Singletonhungry getInstance(){//获取这个唯一的独苗---实例化对象return instance;}}
3.4懒汉模式:
懒和饿是相对的,饿是尽可能早的new出实例,而懒是尽量晚的new出实例,如果后续没有要求要使用,甚至可能不创建了,就是一直延迟创建的感觉,边走边准备,就好像洗碗一样,中午吃完饭后不洗,一直拖到晚饭需要用碗的时候才临时去洗碗,因为在中午到晚上这段区间内没有需要用碗,既然不用那么就不去洗碗,这就是懒汉模式,需要的时候我再洗碗,我再new实例化对象.------也就是说从没人调用------->有人调用getInstance方法之前一直是没被实例出来的,只有被调用过一次然后再不急不慢的实例化对象出来,方便当下以及后续使用:
class Singletonlazy{//懒汉模式----不立马new出来private static Singletonlazy instance=null;//静态成员的初始化在类加载阶段////属性啥的-----//private Singletonlazy(int a){//带参数的构造方法}private Singletonlazy(){//构造方法}//当调用这个方法的时候,说明此时需要去获得这个单例类的实例化对象,这个时候可以确定一定要被用了,这个时候我//我们再去newpublic Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象if(instance==null){instance=new Singletonlazy();return instance;}return instance;}}
3.5懒汉模式意义:
懒在生活中可能是贬义词,但是懒汉模式在计算机当中是非常有意义的----->
假设有一个很大的文件(比如千万字的小说),当我们用编辑器打开的时候,如果用饿汉模式一下子全部读取出来到内存,那内存可能被占满,如果用懒汉模式--->看那一页就把这一页附近的先加载出来,前 后面好几百页没看的就暂时不读出来,因为读出来也没有,也不看
3.6懒汉模式在多线程环境下的使用:
public Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象if(instance==null){instance=new Singletonlazy();return instance;}return instance;}
这个方法在多线程环境下是否是安全的呢?
就是当第一次调用getInstance方法时候,会涉及到对象的实例化,涉及到写操作,而写操作不是原子的----存在线程安全问题----->多个线程同时调用getInstance方法导致多次new操作---->new操作是对象创建的过程,需要数据将类当中的成员初始化掉,这个数据需要从磁盘上读取,new每次读取耗时10分钟,原本只需要new一次即可,以后其他线程只涉及return读操作,现在new两次耗时20分钟---->这就导致bug出现----
因为你线程1已经new好了,我线程2来调用该方法应该就是直接一个简单的return完事了,但是由于多线程的安全问题导致花费了更多时间----
如何解决上述问题?----加锁
加锁得看加锁的位置----if()里面的条件判断操作是原子的,如果我们直接对new操作加锁---->确实保证了new操作的原子性,但是条件判断是原子的,new操作现在相当于是原子的,原子+原子--->可分--->分为两步---->我们讲的原子性是操作不可在分----你这没保证原子性啊---->所以不能解决问题
public Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象\Object locker=new Object();if(instance==null){synchronized (locker) {instance = new Singletonlazy();}}return instance;}
所以我们希望的是条件判断+new操作----->一起构成原子的---->变成一步操作一起执行完毕不可再分-正确代码-->
public Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象\Object locker=new Object();synchronized (locker) {if (instance == null) {instance = new Singletonlazy();}return instance;}}
也可以对方法加锁---我在执行这个方法时候你这个线程不能执行这个方法---我就是我在new对象的时候你不能来new对象----->当轮到下一个线程调用该方法时候已经new好了,不会再去new了从而避免了反复new的操作----保证了线程安全:
public synchronized static Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象\if (instance == null) {instance = new Singletonlazy();}return instance;}
3.7懒汉模式的优化
但是这里存在一个问题:但我们实例化第一次之后,后续的所有调用该方法操作都是直接return 读操作,并不涉及到new操作啊,也就是没有线程安全问题啊,我们讲synchronized要合理使用,该加锁的时候加锁,不该上锁的时候就不能无脑的上锁,因为会导致程序效率降低,就算你加上synchronized也是进去后然后立马不符合new的条件然后出来return ,那么为什么不一开始就不上锁直接return呢? 而且加锁后导致线程之间阻塞等待导致获取对象的效率变低----如何解决这个问题---->提高效率呢?
解决办法:双层if----这个锁肯定是不能去掉,虽然在后面这个锁没啥用,但是第一次调用该方法的时候还是要用的
public static Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象\Object locker=new Object();if (instance==null) {synchronized (locker) {if (instance == null) {instance = new Singletonlazy();}}}return instance;}
后续其他线程调用该方法的时候先进行第一层if判断然后不为null,也就是已经new过了,那么我们直接跳过里层的加锁操作了---直接return大大提高了代码效率
但是上述代码存在问题
问题就是你无法确保内存可见性---也就是内存里的instance已经不是null已经被改了,然而其他线程仍然读取寄存器内的值,不读取内存当中最新的值,导致重复new,出现bug
为了杜绝内存可见性问题我们应该还要给instance加上volatile
其实这里更严重的问题在于指令重排序问题---->就是编译器会保证原始代码逻辑不变的情况下去适当调整指令的执行顺序以提高程序的效率,在多线程环境下就可能导致bug出现,帮了倒忙,优化反而成了问题的导火索
new这个操作大概涉及三条指令:
1:申请内存空间
2:在内存上构造对象(实例化)---将属性啥的都初始化好
3:内存空间的首地址赋值给引用变量
正常来说应该是 1-->2-->3
编译器优化优化后可能变成1-->3-->2
那么当其他线程来读内存上的instance变量时,这个时候可能负者实例化线程正好执行完3,这个时候instance刚好被赋值了.刚好不为null,然后外层if进不去,直接return了,那么这个时候返回的对象还是个空壳,所有的属性都没被初始化,内存里面存着一个未初始化的对象,也就是这个对象压根就没实例化好,然后线程就认为已经实例化好了,然后就直接return了,这个时候就出bug了
就好像买房子后惯性的认为为---1拿到钥匙----2装修----3入住 三步
你拿到钥匙,然后立马就入住了,你的家人就会认为你已经装修好了,也想跟着搬进来一起住,但是发现是个没装修的毛坯房,大闹乌龙.
但是加volatile也能把这个问题解决--就是instance变量在被volatile修饰之后那么涉及到关于他的读取和修改操作都不会触发指令重排序.
volatile解决了两问题
1:保证内存可见性
2:避免指令重排序
总体的优化最佳代码:
class Singletonlazy{//懒汉模式----不立马new出来//加上volatile保证内存可见性+避免指令重排序private volatile static Singletonlazy instance=null;//静态成员的初始化在类加载阶段////属性啥的-----//private Singletonlazy(int a){//带参数的构造方法}private Singletonlazy(){//构造方法}//当调用这个方法的时候,说明此时需要去获得这个单例类的实例化对象,这个时候可以确定一定要被用了,这个时候我//我们再去newpublic static Singletonlazy getInstance(){//获取这个唯一的独苗---实例化对象\Object locker=new Object();if (instance==null) {synchronized (locker) {if (instance == null) {instance = new Singletonlazy();}}}return instance;}}