5.多线程学习

时间:2023-03-31 15:59:37

作者:爱塔居

专栏:JavaEE

作者简介:大三学生,喜欢总结与分享~

5.多线程学习


章节回顾

线程安全

1.一个线程不安全的案例(两个线程各自自增5w次,结果不是10w)

2.线程不安全的原因

(1)抢占式执行,随机调度。线程中的代码执行到任意一行,都随意可能被切换出去。

(2)多个线程同时修改同一个变量。

(3)修改操作不是原子的。

(4)内存可见性(volatile)编译器可能会对我们的代码进行优化。

一个线程频繁读,一个线程修改

(5)指令重排序。

除了这些原因,还有其他原因。

3.解决方案  加锁

在线程1加锁的过程中,线程2无法把自己的指令插入到线程1的修改过程中。

synchronized指定一个“锁对象”

4.volatile

关于volatile和内存可见性补充

内存可见性:

t1频繁读取主内存,效率比较低,就被优化成直接读自己的工作内存。

t2修改了主内存的结果,由于t1没有读主内存,导致修改不能被识别到。

工作内存=>CPU寄存器

主内存=>内存

工作内存和主内存都是由英文work memory和main memory翻译来的。所以,工作内存不一定非要是内存,可以是记忆,存储区,不一定是特指“内存条”。

这一套说法,也称为JMM(java memory model)

java是跨平台的。

1.兼容多种操作系统

2.兼容多种硬件设备

不同的硬件,其实差别很大。cpu和cpu之间,差别就会比较大。

像以前的cpu,上面只有寄存器。现在的cpu上面还有缓存。

而且有的cpu缓存还有好几个,L1,L2,L3,(现在常见的cpu都是3级缓存)

工作内存准确来说,代表cpu寄存器+缓存(CPU内部存储数据的空间)

5.多线程学习

 cpu读储存器速度比读内存快3-4个数量级。

 缓存读取速度介于寄存器和内存之间。

L1最快,空间最小(仍然比寄存器慢)

L3最快,空间最大(仍然比内存快很多)

实际上cpu尝试读一个内存数据:

1.先看寄存器里有没有

2.没有,看L1有没有

3.没有,看L2有没有

4.没有,看L3有没有

5.没有,看内存有没有

具体缓存的大小,对于程序效率的影响,也看实际的应用场景。


一、wait 和notify

线程的调度是无序的,随机的。但是,也是有一定的需求场景的,希望线程有序执行。

join是一种控制顺序的方式,但是功效有限。

wait就是让某个线程先暂停下来,等一等。

wait主要做三件事:

1.解锁

2.阻塞等待

3.当收到通知的时候,就唤醒,同时尝试重新获取锁。

notify就是把该线程唤醒,能够继续执行。

wait和notify是Object的方法

只要你是个类对象(不是内置类型/基本数据类型),都是可以使用wait和notify。

public class test {
        static int i;
        public static void main(String argv[]) throws InterruptedException{
           Object locker=new Object();
        Thread t1=new Thread(()->{
            while (true){
                try {
                    System.out.println("wait 开始");
                    synchronized (locker){
                        locker.wait();
                    }
                    System.out.println("wait 结束");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        Thread.sleep(1000);
        Thread t2=new Thread(()->{
            synchronized (locker){
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        t2.start();

    }
}

 使用外套,阻塞等待会让线程进入WAITING状态。wait也提供了一个带参数的版本,参数指定的是最大等待时间。不带参数的wait是死等,带参数的wait就会等最大时间之后,还没有人通知,就自己唤醒自己。

wait会导致阻塞,竞争锁也会导致阻塞,两种不同的进入阻塞的方式。wait的初心就是为了实现阻塞的效果。

join只能是让t2的线程先执行完,再继续执行t1,一定是串行的

wait、notify,可以让t2执行完一部分,再让t1执行一部分,再让t2去执行,再……

唤醒操作,还有一个notifyAll。可以有多个线程,等待同一个对象。

比如在t1,t2,t3中都调用object.wait。此时在main中调用了object.notify 会随机唤醒上述的一个线程。(另外两个仍然会是waiting状态)

如果是调用了object.notifyAll,此时就会把上述三个线程都唤醒。伺候这三个线程就会重新竞争锁,然后依次执行。

总结:

1.wait需要搭配synchronized使用,sleep不需要。

2.wait是Object的方法,sleep是Thread的静态方法。

wait和sleep都是可以提前唤醒的。

他们最大的区别在于初心不同。

wait解决的是线程之间的顺序控制

sleep单纯是让当前线程休眠一会。

二、设计模式

设计模式,就是软件开发中的棋谱。大佬们针对一些常见场景,总结出来的代码的编写套路。设计模式有很多种。

在校招阶段,主要考察两个设计模式。

1.单例模式

2.工厂模式

设计模式需要大家有一定的开发经验的积累,才好理解。

2.1 单例模式

单例指的是单个实例(instance)对象。类的实例,就是对象。Java中的单例模式,借助java语法,保证某个类,只能够创建出一个实例,而不能new多次。

有些场景,本身就是要求某个概念是单例的。

//把这个类设定为单例
class Singleton{
    //唯一的实例的实体
private static Singleton instance=new Singleton();
//被static修饰,该属性是类的属性。JVM中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份了。
//获取到实例的方法
    public static Singleton getInstance(){
        return instance;
    }
    //禁止外部new实例
    private Singleton(){};
}
public class test {
    public static void main(String[] args) {
    Singleton s1=Singleton.getInstance();
    Singleton s2=Singleton.getInstance();
    }

    }

java中实现单例模式是有多种写法的。

主要说两种:

1.饿汉模式(急迫)

2.懒汉模式(从容)

A吃完饭立马洗碗(饿汉行为)

B吃完饭,不洗碗,等下一顿要用碗的时候,再洗碗。(懒汉行为)

通常认为,懒汉模式更好,效率更高。(非必要,不洗碗)

举一个计算机的例子:

打开一个硬盘上的文件,读取文件内容,并显示出来。

饿汉:把文件所有内容都读到内存中,并显示出来

懒汉:只把文件读一小部分,把当前的屏幕填充上,如果用户翻页,再读其他文件内容。

当文件特别大的时候,就可以看出懒汉模式的优势了。

饿汉模式:

//把这个类设定为单例,饿汉
class Singleton{
    //唯一的实例的实体
private static Singleton instance=new Singleton();
//被static修饰,该属性是类的属性。JVM中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份了。
//获取到实例的方法
    public static Singleton getInstance(){
        return instance;
    }
    //禁止外部new实例
    private Singleton(){};
}
public class test {
    public static void main(String[] args) {
    Singleton s1=Singleton.getInstance();
    Singleton s2=Singleton.getInstance();
    }

    }

懒汉模式:

//把这个类设定为单例模式中的懒汉模式。
class SingletonLazy{
private static SingletonLazy instance=null;
    public static SingletonLazy getInstance(){
      if(instance==null){
          instance=new SingletonLazy();
      }
      return instance;
    }
    private SingletonLazy(){};
}
public class test {
    public static void main(String[] args) {
    SingletonLazy s1=SingletonLazy.getInstance();
    SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);
    }
    }

 饿汉模式一开始就把实例创建好了,而懒汉模式是非必要不创建实例。

上述两个代码,是否是线程安全的?多个线程下调用getInstance,是否会出现问题?

饿汉模式,认为是线程安全的,只是读数据。

而在多线程下,懒汉模式可能无法保证创建对象的唯一性。

比如以下情况:

5.多线程学习

 如何解决上诉线程安全问题?

进行加锁,保证判定和new操作是原子性的。

//把这个类设定为单例模式中的懒汉模式。
class SingletonLazy{
private static SingletonLazy instance=null;
    synchronized public static SingletonLazy getInstance(){
      if(instance==null){
          instance=new SingletonLazy();
      }
      return instance;
    }
    private SingletonLazy(){};
}
public class test {
    public static void main(String[] args) {
    SingletonLazy s1=SingletonLazy.getInstance();
    SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);
    }
    }