多线程及相关面试题总结

线程

线程cpu进行资源调度和分配的基本单位,线程间共享进程的内存,一个进程至少一个线程

线程的创建方式

继承Thread类

①创建于一个继承于Thread的子类
②重写Thred的run()方法
③创建Thread类的子类对象
④通过此类对象调用start()

class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i %2== 0) {
                System.out.println(i);

            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        MyThread t1=new MyThread();
        t1.start();//启动当前线程,调用当前线程的run()方法
        //如果直接调run方法,并没有启动另一个线程
        for (int i = 0; i <100 ; i++) {
            if (i%2 == 0) {
                System.out.println(i+"*******");
            }

        }
        //如果需要再启动一个线程,需要重新创建一个线程的对象
        MyThread t2=new MyThread();
        t2.start();
    }
}

实现Runnable接口

①、创建实现Runnable接口的类
②、重写Runnable接口的run()方法
③、创建类的实现对象
④、将此对象作为参数传到Thread类的构造器,创建Thread类的对象
⑤、通过Thread类的对象

class MThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i%2==0) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }

        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        MThread t=new MThread();
        Thread thread = new Thread(t);
        Thread thread2 = new Thread(t);
        thread.setName("线程1");
        thread2.setName("线程2");
        thread.start();
        thread2.start();
    }
}

一些注意的细节

区分run()和start()

  • start()方法:首先启动线程,再由jvm调用该线程的run()方法。

  • run()方法:仅仅封装被线程执行的代码。如果线程没有调用start()方法,直接调用它相当于是调用普通方法

java虚拟机启动时是单线程还是多线程的?

是多线程的,不只是启动main线程,至少还会启动垃圾回收线程。

推荐使用哪种方式创建线程?

一般情况下我们推荐使用实现Runnable接口。 可以避免java单继承带来的局限,增强代码健壮性

线程的优点

我们知道是进程是系统进行资源调度和分配的基本单位,也就是说进程可以进行资源分配和调度了,为什么还需要线程呢?
首先我们需要了解为了使程序并发执行,系统必须进行下列一系列操作:

  • 创建进程:为进程分配其所必需的除处理机以外的所有资源,如内存空间,IO设备以及进程相应的PCB.撤销进程,

  • 进程切换:对进程进行上下文切换时需要保留当前进程的cpu环境,设置新选中进程的Cpu环境,因而需要花费不少的处理机时间。

  • 撤销进程:系统在撤销进程时必须对其所占用的资源进行回收,然后再撤销PCB

线程的优点一下就显露出来!

  1. 创建一个新线程的代价要比新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务 比特科技6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  6. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

Thread类及其常用方法

  • Thread的常用构造方法
方法 说明
Thread 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
  • Thread常见属性
    在这里插入图片描述

ID: 是线程的唯一标识,不同线程不会重复 名称是各种调试工具用到
状态:表示线程当前所处的一个情况,
优先级高的线程理论上来说更容易被调度到
关于后台线程:,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活:,即简单的理解,为 run 方法是否运行结束了

在这里插入图片描述

  • currentThread(),获取当前正在执行线程的引用

  • yield()线程让步:会让别的线程先执行,但不确保真正让出cpu。
    Thread.yield();// 将当前线程由运行态—>就绪态

public class ThreadYield {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
        // 等待new Thread所有线程执行完毕,否则一直等待
        while(Thread.activeCount() > 1){//使用调试的方式运行
            Thread.yield();// 将当前线程由运行态--->就绪态
        }
        System.out.println(Thread.currentThread().getName());
    }

}

  • sleep(),休眠当前线程:因为线程的调度是不可控的,所以,这个方法只能保证休眠时间是大于等于休眠时间的。

        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(999999999L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
   
  • 中断一个线程:interrupt():并不是真正的中断,而是告诉线程需要中断,具体由线程自己决定。如果线程正在阻塞,则以异常方式通知,否则设置标志位。
    在这里插入图片描述
public static void test4()  {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.interrupted());//返回中断标志位,并重置标志位
                    
                }
            }
        });
        t.start();//t线程中的中断标志位=false
        t.interrupt();//t线程的中断标志位=true
    }
    //调用test4()方法打印:
    true  // 只有一开始是 true,后边都是 false,因为标志位被清
	false
	false
	false
	false
	false
	false
	false
	false
	false

 public static void test4()  {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //System.out.println(Thread.interrupted());//返回中断标志位,并重置标志位

                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t.start();//t线程中的中断标志位=false
        t.interrupt();//t线程的中断标志位=true
    }
    //运行结果:
    true//全是true,因为标志位没有被清
	true
	true
	true
	true
	true
	true
	true
	true
	true
  • 等待一个线程:join()

*有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class ThreadJoin {
    public static void without() throws InterruptedException {
        
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
        t.join();//等待t线程执行完毕,也就是说先让t执行,主线程等待
        System.out.println(Thread.currentThread().getName());
    }

    public static void withoutSleep() throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
        //t线程执行时间和2秒钟谁先到,就以这个时间点作为main线程等待的时间点,到了这个时间点就往下执行
        // t执行完(比2秒钟更快),就往下执行
        t.join(2000);//等待线程结束,最多等多少秒
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args) throws InterruptedException {
        //without();// // 打印顺序:thread-0--->main
        withoutSleep();// // 打印顺序:main--->thread-0
    }
}
  • 设置守护线程:setDeamon(boolean on)

什么是守护线程:守护线程是为其他线程服务的,比如垃圾回收线程。守护线程是–种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。用个比较通俗的比喻来解释–下“守护线程”:任何–个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。Daemon 的作用是为其他线程的运行提供便利

public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(999999999L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //至少有一个非守护线程没有被销毁,程序就不会退出
        t.setDaemon(true);//设置守护线程
        t.start();
    }
    //如果直接运行上述代码程序会正常结束。
    //如果t不是守护线程,也就是没有设置t为守护线程,代码进入死循环。

线程的几种状态

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state:Thread.State.values()
             ) {
            System.out.println(state);
        }
    }
}
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志

运行结果
在这里插入图片描述

理解线程的几种状态:

  • new:是指线程实例化后还没有执行start()方法的状态
  • Runnable状态是指线程进入运行时的状态,它包括运行中和就绪两个状态。(比如你今天去银行取钱,正在排队等服务,此时你属于就绪态,而正在让柜台人员办理业务的就是运行中。)
  • TERMINATED:线程被销毁时的状态(也就是你去办理业务已经办理完成)
  • TIMED_WAITING:指线程执行了Thread.sleep()方法呈等待状态。等待时间到达继续向下运行。
  • Blocked:线程阻塞,出现在某一个线程在等待锁的时候。
  • WAITING:指线程执行了Object.wait()等方法后所处的状态

线程安全

线程安全的概念:

我们先观察下列代码:创建了20个线程,每个线程打印10000次,预期结果因该是200000,而实际运行的结果却不是。这便牵扯了我们的线程安全问题。

在这里插入图片描述

public class UnsafeThread {
    static  int count;
    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {

                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        count++;
                    }
                }
            }).start();
        }
        while(Thread.activeCount()>1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的

线程不安全的原因:

原子性:

指一个操作是不可分割的。

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人,如果没有任何机制保证,A进入房间后没有出来,B也是可以进入房间的。这就是不具备原子性。
比如上面代码的count++操作就不具备完整性。 由三步操作构成:①从内存把数据读到cpu②进行数据更新(加一)③把数据写回cpu

不保证原子性会带来什么问题?

如果一个线程正在对一个变量操作,中途其他线程插进来了,如果这个操作被打断,结果就是错误的。

可见性

在这里插入图片描述

为了提高效率,JVM在执行过程中会近可能的将数据在工作内存中执行。这样会造成共享变量在多线程之间不能及时看到改变,这就是可见性问题

代码顺序性

先搞清楚代码(指令)重排序:

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递
    如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑 一次前台。这种叫做指令重排序

代码重排序会给多线程带来什么问题?

刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了代码就会是错误的

解决线程不安全

synchronized关键字—监视锁monitor lock

synchronized的底层是使用操作系统的mutex lock实现的。

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
    synchronized用的锁是存在Java对象头里的。

synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入

同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好

Java对于多线程的安全问题提供了专业的解决方式: 同步互斥机制

  1. 同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
  1. synchronized还可以放在方法声明中,表示整个方法为同步方法。
    例如:
public synchronized void show (String name){.
}

synchronized的锁是什么?

  •  任意对象都可以作为同步锁。 所有对象都自动含有单一的锁(监视器) 。
  •  同步方法的锁:静态方法(类名.class) 、 非静态方法(this)
  •  同步代码块:自己指定, 很多时候也是指定为this或类名.class

注意:

必须确保使用同一个资源的多个线程共用一把锁, 这个非常重要, 否则就无法保证共享资源的安全 
一个线程类中的所有静态方法共用同一把锁(类名.class) , 所有非静态方法共用同一把锁(this) , 同步代码块(指定需谨慎)

  1. 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
	public synchronized void methond() {
	} 
	public static void main(String[] args) {
		SynchronizedDemo demo = new SynchronizedDemo();
		demo.method(); // 进入方法会锁 demo 指向对象中的锁;出方法会释放 demo 指向的对象中的锁
	}
}
  1. 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
	public synchronized static void methond() {
	}
	public static void main(String[] args) {
		method(); // 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
	SynchronizedDemo.class 指向的对象中的锁
	}
}

什么时候释放锁?:

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、 该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception, 导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁
    注意线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁

volatile 关键字

是一种轻量级的同步机制。修饰的共享变量,可以保证(变量读取时的)可见性,部分保证顺序性,不能保证原子性。<提示线程每次从共享内存中读取变量而不是从私有内存中读取>

class ThraedDemo {
	private volatile int n;
}
  • 建立内存屏障
  • 禁止指令重排序

线程之间的通信

比如现在我们要实现两个线程打印1-100,线程1,线程2交替打印

public class ThreadCommucation implements Runnable {
    int i=1;
    @Override
    public void run() {
        while(true) {
            synchronized (this) {
                notify();
                if(i<=100) {
                    System.out.println(Thread.currentThread().getName()+":"+i++);
                }else {
                    break;
                }
                try {
                    wait();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }

}
class ThreadTest1 {
    public static void main(String[] args) {
        ThreadCommucation t=new ThreadCommucation();
        Thread thread = new Thread(t);
        Thread thread2 = new Thread(t);
        thread.setName("线程1");
        thread2.setName("线程2");
        thread.start();
        thread2.start();
    }
}

wait() 与 notify() 和 notifyAll()

  • wait():令当前线程挂起并放弃CPU、 同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行

  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待

  • notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

单例模式

立即加载-饿汉式

使用类的时候已经将对象创建完毕,常见的办法就是直接new实例化。

class Singleton1 {//饿汉式,getInstance方法没有同步,可能出现非线程安全问题
    private static Singleton1 instance=new Singleton1();
    private Singleton1() {

    }
    public static Singleton1 getInstance() {
        return instance;
    }
}

延迟加载-懒汉模式

在调用方法实例时才被创建

class Singleton2 {//懒汉式单线程版
    private static Singleton2 instance=null;
    private Singleton2() {}
    public static Singleton2 getInstance() {
        if (instance==null) {
            instance=new Singleton2();
        }
        return instance;
    }

}
class Singleton3 {//懒汉式多线程版,效率低下一个线程要想取得对象必须等上一个线程释放锁才可以继续执行。
    private static Singleton3 instance=null;
    private Singleton3() {}
    public static synchronized Singleton3 getInstance() {
        if(instance==null) {
            instance=new Singleton3();
        }
        return instance;
    }
}

双重校验锁(DCL)

针对某些重要的代码进行单独的同步,这样可以在运行中大大提高效率.

双重检查机制:并不是每次进入getInstance方法都需要同步,而是先不同步。先检查实例是否存在,如果不存在才进入同步代码块,这是第一重检查。进入同步代码块后再次检查实例是否存在,如果不存在,在同步的情况下创建一个实例,这是第二重检查。这样一来只需要同步一次了,减少了多次在同步情况下进行判断所浪费的时间。并且可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

class Singleton4 {//双重校验锁,性能佳线程安全
    private static volatile Singleton4 instance=null;
    private Singleton4() {}
    public static  Singleton4 getInstance() {
        if(instance==null) {//先检查实例是否存在,如果不存在再进入下面的同步块
        //模拟在创建对象时做一些准备性工作
            synchronized (Singleton4.class) {
                if (instance==null) {
                    //new对象分为三个操作
                    //分配内存
                    //调用构造方法初始化
                    //赋值
                    instance=new Singleton4();
                }
            }
        }
        return instance;
    }
}
//同步方法锁只允许一个线程执行方法,其他线程阻塞,
//而代码块不同,允许并发访问方法,只是在代码块这里只允许一个线程执行,
//这种加锁方式肯定效率高。

关于使用volatile修饰instance:
private static volatile Singleton4 instance=null;
我们知到new一个对象分为3部:故instance = new Singleton()这句,这并非是一个原子操作

  1. 分配内存
  2. 调用构造方法初始化
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

而jvm可能会对指令进行重排序的优化,也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

面试题

进程与线程的区别?线程死了进程会死吗?

进程:系统进行资源调度和分配的基本单位,进程间独享内存,一个系统至少一个进程;线程:cpu进行资源调度和分配的基本单位,线程间共享进程的内存,一个进程至少一个线程
会死

volatile和synchronized区别

1、 volatile不会进行加锁操作:
volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
2、volatile变量作用类似于同步变量读写操作:
从内存可见性的角度看,写入volatile变量相当于退出同步代码块, 而读取volatile变量相当于进入同步代码 块。
3、 volatile不如synchronized安全:
在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。-般来说,用同步机制会更安全 些。
4、volatile 无法同时保证内存可见性和原则性:
加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性, 原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用, 也就是说如下的表 达式都不是原子操作: "count+ +”、 "count = count+ 1”。

sleep和wait分别是那个类的方法,有什么区别

●sleep和wait

sleep是 Thread类的方法
wait是Object类的方法

●有什么区别

sleep()方法 (休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
wait()是Object类的方法, 调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行) ,进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lockpool),如果线程重新获得对象的锁就可以进入就绪状态。

持续更新中。。。

猜你喜欢

转载自blog.csdn.net/qq_41552331/article/details/105275403