java面试必问:多线程的实现和同步机制,一文帮你搞定多线程编程

时光毁灭记忆、已成空白 提交于 2020-08-06 08:51:56

前言

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。
线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。

多线程的实现

继承Thread类

  1. 创建一个类,这个类需要继承Thread类
  2. 重写Thread类的run方法(run方法中是业务代码)
  3. 实例化此线程类
  4. 调用实例化对象的start方法启动线程
package com.test;

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

class ThreadDemo extends Thread{
    @Override
    public void run() {
        System.out.println("运行了run方法");
    }
}

在多线程编程中,代码的执行结果与代码的执行顺序或者调用顺序是无关的线程是一个子任务,CPU以不确定的方式或者是以随机的时间来调用线程中的run方法这体现了线程运行的随机性

package com.test;

public class Demo2 {
    public static void main(String[] args) {
        Demo2Thread demo2Thread = new Demo2Thread();
        /*
    *demo2Thread.start方法才是启动线程
    *demo2Thread.run方法只是由main主线程来调用run方法
    */
        demo2Thread.start();
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("运行了main方法");
                Thread.sleep(100);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Demo2Thread extends Thread{
    @Override
    public void run() {
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("运行了run方法");
                Thread.sleep(100);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

start的执行顺序和线程的启动顺序是不一致的
1,2,3,4,5的输出顺序是随机的

package com.test;

public class Demo3 {
    public static void main(String[] args) {
        Demo3Thread demo3Thread1 = new Demo3Thread(1);
        Demo3Thread demo3Thread2 = new Demo3Thread(2);
        Demo3Thread demo3Thread3 = new Demo3Thread(3);
        Demo3Thread demo3Thread4 = new Demo3Thread(4);
        Demo3Thread demo3Thread5 = new Demo3Thread(5);

        demo3Thread1.start();
        demo3Thread2.start();
        demo3Thread3.start();
        demo3Thread4.start();
        demo3Thread5.start();
    }
}

class Demo3Thread extends Thread{
    private int i;

    public Demo3Thread(int i){
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println("i=" + i);
    }
}

实现Runnable接口

1)创建一个类,整个类需要实现Runnable接口
2)重写Runnable接口的run方法
3)实例化创建的这个类
4)实例化一个Thread类,把第3步实例化创建的对象通过Thread类的构造方法传递给Thread类
5)调用Thread类的run方法



package com.test;

public class Demo4 {
    public static void main(String[] args) {
        Demo4Thread thread = new Demo4Thread();
        Thread t = new Thread(thread);
        t.start();
        System.out.println("运行了main方法");
    }
}

class Demo4Thread implements Runnable{
    @Override
    public void run() {        
    System.out.println("运行了run方法");
    }
}

使用继承Thread类的方式开发多线程应用程序是有局限的,因为Java是单继承,继承了Thread类就无法继承其他类,所以为了改变这种局限,用实现Runnable接口的方式来实现多线程

成员变量与线程安全

自定义线程类中的成员变量对于其他线程可以是共享或者不共享的,这对于多线程的交互很重要

  1. 不共享数据时
package com.test;

public class Demo5 {
    public static void main(String[] args) {
        Thread t1 = new Demo5Thread();
        Thread t2 = new Demo5Thread();
        Thread t3 = new Demo5Thread();
        t1.start();
        t2.start();
        t3.start();    
    }
}

class Demo5Thread extends Thread{
    private int i = 5;
    @Override
    public void run() {
        while(i > 0){
            i--;
            System.out.println(Thread.currentThread().getName() + " i = " + i);
        }
    }
}                    

每个线程都有各自的i变量,i变量的值相互之间不影响

  1. 共享数据时
package com.test;

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo6Thread();
            /*
     为什么能将Thread类的对象传递给Thread类?
         因为Thread类本身就实现了Runnable接口
            */
        Thread thread1 = new Thread(t);
        Thread thread2 = new Thread(t);
        Thread thread3 = new Thread(t);
        Thread thread4 = new Thread(t);
        Thread thread5 = new Thread(t);

    thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
   }
}

class Demo6Thread extends Thread{
    private int i = 5;
    @Override
    public void run() {
            i--;
            System.out.println(Thread.currentThread().getName() + " i = " + i);        
    }
}            

共享数据时,将数据所在类的对象传递给多个Thread类即可
共享数据有概率出现不同线程产生相同的i的值,这就是非线程安全

线程常用API

  1. currentThread方法
    返回代码被哪个线程调用的详细信息
package com.test;

public class Demo7 {
    public static void main(String[] args) {
        //main线程调用Demo7Thread的构造方法
        Thread thread = new Demo7Thread();
        //Thread-0线程调用run方法
        thread.start();
        System.out.println("main方法" + Thread.currentThread().getName());
    }
}

class Demo7Thread extends Thread{
    public Demo7Thread(){
        System.out.println(Thread.currentThread().getName() + "的构造方法");
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的run方法");
    }
}

输出结果为:
main的构造方法
main方法main
Thread-0的run方法


main方法会被名称为main的线程调用,在新建线程类的对象时,线程类的构造方法会被main线程调用;
线程类对象的start方法会调用run方法,此时线程类默认的名称为Thread-0

  1. isAlive方法
    判断当前的线程是否处于活动的状态,活动状态就是线程已经启动并且没有结束运行的状态
package com.test;

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Demo8Thread();
        System.out.println("线程启动前:" + t.isAlive());
        t.start();
        System.out.println("线程启动后:" + t.isAlive());
    }
}

class Demo8Thread extends Thread{
    @Override
    public void run() {
        System.out.println("run方法的运行状态" + this.isAlive());
    }
}

输出结果为:
线程启动前:false
线程启动后:true
run方法的运行状态true


true表示线程正处于活动状态,false则表示线程正处于非活动状态

  1. sleep方法
    使当前正在执行的线程在指定的毫秒数内暂停执行
package com.test;

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Demo8Thread();
        System.out.println("线程启动前时间:" + System.currentTimeMillis());
        t.start();
        System.out.println("线程启动后时间:" + System.currentTimeMillis());
    }
}

class Demo8Thread extends Thread{
    @Override
    public void run() {
        System.out.println("线程sleep前的时间:" + System.currentTimeMillis());
        try {
        Thread.sleep(300);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("线程sleep后的时间:" + System.currentTimeMillis());
    }
}
  1. getId方法
    获取当前线程的唯一标识
package com.test;

public class Demo9 {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName() + ", " + t.getId());
        Thread thread = new Thread();
        System.out.println(thread.getName() + ", " + thread.getId());
    }
}
  1. 停止线程
    停止一个线程,即线程在完成任务之前,就结束当前正在执行的操作
    1)使用退出标志,使线程正常停止,即run方法运行完后线程终止

package com.test;

public class Demo10 {
    public static void main(String[] args) {
        Demo10Thread thread = new Demo10Thread();
        thread.start();
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        /*
        stopThread方法,将flag变为false,为什么能够传递到当前线程中?
        我觉得是因为当前线程是一直在运行的,while()中的条件一直成立
        所以当调用了stopThread方法,将flag变为false,while循环就结束了,run方法中的代码也结束了
        所以线程停止了
         */
        thread.stopThread();
    }
}

class Demo10Thread extends Thread{
    private Boolean flag = true;
    @Override
    public void run() {
        try {
            while (flag){
                System.out.println("线程正在运行");
                Thread.sleep(1000);
            }
            System.out.println("线程结束运行");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void stopThread(){
        flag = false;
    }
}

2)stop方法强制结束线程

package com.test;

public class Demo11 {
    public static void main(String[] args) {
        Demo11Thread thread = new Demo11Thread();
        thread.start();
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        //stop方法中的斜杠表示方法已经被作废,不建议使用此方法
        thread.stop();
    }
}

class Demo11Thread extends Thread{
    private Boolean flag = true;
    @Override
    public void run() {
        try {
            while (flag){
                System.out.println("线程正在运行~~~");
                Thread.sleep(1000);
            }
            System.out.println("线程结束运行~~~");
        }catch (Exception e){
            e.printStackTrace();
        }catch (ThreadDeath e){//捕获线程终止的异常
            System.out.println("进入catch块");
            e.printStackTrace();
        }
    }
}

stop强制停止线程可能使一些清理性的工作得不到完成;还会对锁定的对象进行解锁,使数据得不到同步的处理,导致数据不一致

3)interrupt方法中断线程

package com.test;
public class Demo12 {
    public static void main(String[] args) {
        Demo12Thread thread = new Demo12Thread();
        thread.start();
        thread.interrupt();
        System.out.println("thread线程是否已经停止?" + thread.isInterrupted() + ", " + thread.getName());
        System.out.println("当前线程是否已经停止?" + Thread.interrupted() + ", " + Thread.currentThread().getName());
    }
}

class Demo12Thread extends Thread{  
    @Override
    public void run() {
    for(int i = 0; i < 5; i++){
            System.out.println(i);
        }
    }
}

调用interrupt方法不会真正的结束线程,而是给当前线程打上一个停止的标记
Thread类提供了interrupt方法测试当前线程是否已经中断,isInterrupted方法测试线程是否已经中断

执行结果为:

thread线程是否已经停止?true, Thread-0
0
1
2
3
4
当前线程是否已经停止?false, main

thread.isInterrupted方法检查线程类是否被打上停止的标记,Thread.interrupted方法检查主线程是否被打上停止的标记

暂停线程

暂停线程使用suspend方法,重启暂停线程使用resume方法
suspend方法暂停线程后,i的值就不会继续增加。两次"第一次suspend"输出的结果一致
resume方法重启暂停线程后,i的值会继续增加,再使用suspend方法暂停线程,两次"resume后第二次suspend:"输出的结果一致

package com.test;

public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
    Demo13Thread thread = new Demo13Thread();
    thread.start();
    Thread.sleep(100);
    thread.suspend();
    System.out.println("第一次suspend:" + thread.getI());
        Thread.sleep(100);
        System.out.println("第一次suspend:" + thread.getI());
        thread.resume();
        Thread.sleep(100);
        thread.suspend();
        System.out.println("resume后第二次suspend:" + thread.getI());
        Thread.sleep(100);
        System.out.println("resume后第二次suspend:" + thread.getI());
    }
}

class Demo13Thread extends Thread{
    private long i;
    public long getI() {
        return i;
    }        
    public void setI(long i) {
        this.i = i;
    }
    @Override
    public void run() {
        while (true){
            i++;
        }
    }
}

suspend方法会使线程独占公共的同步对象,使其他线程无法访问公共的同步对象
suspend方法还可能会造成共享对象的数据不同步

yield方法

yield方法是使当前线程放弃CPU资源,将资源让给其他的线程,但是放弃的时间不确定,可能刚刚放弃,马上又获取CPU时间片

package com.test;

public class Demo15 {
    public static void main(String[] args) {
        Demo15Thread thread = new Demo15Thread();
        thread.start(); 
    }
}

class Demo15Thread extends Thread{
    @Override
    public void run() {
        long start = System.currentTimeMillis();
        int count = 0;
        for(int i = 0; i < 50000; i++){
            Thread.yield();//使当前线程放弃CPU资源,但是放弃的时间不确定
            count = count + i;
        }    
        long end = System.currentTimeMillis();
        System.out.println("花费时间:" + (end - start));
    }
}

线程的优先级

在操作系统中,线程是可以划分优先级的,优先级较高的线程能够得到更多的CPU资源,即CPU会优先执行优先级较高的线程对象中的任务。设置线程优先级有助于帮助"线程调度器"确定下一次选择哪个线程优先执行
设置线程的优先级使用setPriority方法,优先级分为1~10级,如果设置的优先级小于1或者大于10,JVM会抛出IllegalArgumentException异常,JDK默认设置了3个优先级常量,MIN_PRIORITY=1(最小值),NORM_PRIORITY=5(中间值,也是默认值),MAX_PRIORITY=10(最大值)
获取线程的优先级使用getPriority方法

package com.test;

public class Demo16 {
    public static void main(String[] args) {
        System.out.println("主线程的运行优先级是:" + Thread.currentThread().getPriority());
        System.out.println("设置主线程的运行优先级");
        Thread.currentThread().setPriority(8);
        System.out.println("主线程的运行优先级是:" + Thread.currentThread().getPriority());
    /*
        线程的优先级具有继承性,本来默认的线程的优先级为5
        但是将主线程的优先级设置为8,此子线程也会继承主线程的优先级8
         */
        Thread t = new Demo16Thread();
        t.start();
    }
}

class Demo16Thread extends Thread{
    @Override
    public void run() {
     System.out.println("线程的优先级是:" + this.getPriority());
    }
}

优先级较高的线程,先执行的概率较大

线程的同步机制

Java多线程中的同步,指的是如何在Java语言中开发出线程安全的程序,或者如何在Java语言中解决线程不安全时所带来的问题
"线程安全"与"非线程安全"是多线程技术中的经典问题。"非线程安全"就是当多个线程访问同一个对象的成员变量时,读取到的数据可能是被其他线程修改过的(脏读)。"线程安全"就是获取的成员变量的值是经过同步处理的,不会有脏读的现象

synchronized同步方法

局部变量是线程安全的
局部变量不存在线程安全的问题,永远都是线程安全的,这是由局部变量是私有的特性造成的

package com.test.chap2;

public class Demo1 {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadDemo1 t1 = new ThreadDemo1(service);
        t1.start();
        ThreadDemo2 t2 = new ThreadDemo2(service);
        t2.start();
    }
}

class Service {
    public void add(String name){
        int number = 0;//number是方法内的局部变量 
        if("a".equals(name)){
            number = 100;
            System.out.println("传入的参数为a,修改number的值为:" + number);
            try {
            //这里使线程休眠是为了等待其他线程修改number的值
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            number = 200;
            System.out.println("传入的参数不为a,修改number的值为:" + number);
        }   
   }
}

class ThreadDemo1 extends Thread{
    private Service service;
    public ThreadDemo1(Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("a");
    }
}

class ThreadDemo2 extends Thread{
    private Service service;
    public ThreadDemo2(Service service){
        this.service = service;
    }               
    @Override
    public void run() {
        service.add("b");
    }
}

成员变量不是线程安全的
如果有两个线程,都要操作业务对象中的成员变量,可能会产生"非线程安全"的问题,此时需要在方法前使用synchronized关键字进行修饰
number是Demo2Service类的成员变量,Demo2Service类的add方法中,当传入的参数为a时,会进入if条件,休眠1s,并将number的值改为100,当传入的参数不为a时,不会休眠,将number的值改为200
t3线程,传入的参数为a;t4线程,传入的参数为b,所以在线程start之后,t3线程会休眠1s,t4线程不会休眠,所以t4线程会先将number的值改为200并输出,但是当t3线程结束休眠后,输出的number的值也是200,这就产生了线程安全的问题
为了解决此线程不安全的问题,可以在方法前,加上synchronized关键字进行修饰,此时调用此方法的线程需要执行完,方法才会被另一个线程所调用



package com.test.chap2;

public class Demo2 {
    public static void main(String[] args) {
        Demo2Service service = new Demo2Service();
        ThreadDemo3 t3 = new ThreadDemo3(service);
        t3.start();
        ThreadDemo4 t4 = new ThreadDemo4(service);
        t4.start();
    }
}

class Demo2Service{
    private int number = 0;
    public void add(String name){
        if("a".equals(name)){
            number = 100;
            try {
                //这里使线程休眠是为了等待其他线程修改number的值
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("传入的参数为a,修改number的值为:" + number);
        }else {
            number = 200;
            System.out.println("传入的参数不为a,修改number的值为:" + number);
        }
   }
}

class ThreadDemo3 extends Thread{
    private Demo2Service service = new Demo2Service();
    public ThreadDemo3(Demo2Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("a");
    }
}

class ThreadDemo4 extends Thread{
    private Demo2Service service;
    public ThreadDemo4(Demo2Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.add("b");
    }
}                        

多个对象使用多个对象锁
synchronized设置的锁都是对象锁,而不是将代码或者方法作为锁
当多个线程访问同一个对象时,哪个线程先执行此对象带有synchronized关键字修饰的方法,其他线程就只能处于等待状态,直到此线程执行完毕,释放了对象锁,其他线程才能继续执行
如果多个线程分别访问多个对象,JVM会创建出多个对象锁,此时每个线程之间都不会互相干扰


锁的自动释放
当一个线程执行的代码出现了异常,其持有的锁会自动释放

synchronized同步语句块

synchronized关键字修饰的方法的不足之处
假如线程A和线程B都访问被synchronized关键字修饰的get方法,线程B就必须等线程A执行完后,才能执行,这样运行的效率低

synchronized同步代码块的使用
同步代码块的作用与在方法上添加synchronized关键字修饰的作用是一样的

t1和t2两个线程同时访问Demo10Service的synTest方法,synTest方法中部分代码加上了同步代码块,从输出结果可以发现,t1和t2线程会同时访问synTest方法并同时执行非同步代码块的逻辑,但是同步代码块的部分,t1线程先访问的话,t2线程就必须等到t1线程执行完毕后,才能继续执行
假如在synTest方法上加上synchronized关键字修饰,t1线程先访问synTest方法的话,t2线程就必须等到t1线程执行完毕后,才会访问synTest方法并执行,总的来说,同步代码块可以锁住部分需要同步执行的代码,而方法中没有锁住的其他代码可以异步执行

package com.test.chap2;

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Demo10Service service = new Demo10Service();
        Thread t1 = new Demo10Thread(service);
        t1.setName("A");
        Thread t2 = new Demo10Thread(service);
        t2.setName("B");
        t1.start();
        t2.start();
    }
}

class Demo10Service{
    public void synTest(){
        System.out.println(Thread.currentThread().getName() + "线程访问synTest方法");        
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + "线程开始~~~");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "线程结束~~~");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Demo10Thread extends Thread{
    Demo10Service service;
    public Demo10Thread(Demo10Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.synTest();
    }
}    

volatile关键字

volatile关键字的主要作用是使变量在多个线程之间可见
当线程启动后,如果flag变量前没有volatile关键字修饰,线程会一直卡在run方法中的while循环中,修改flag的值不会生效,而加了volatile关键字修饰后,修改flag的值会生效,线程会退出while循环

在启动线程时,flag变量存在于公共堆栈及线程的私有堆栈中。JVM为了线程的运行效率,一直从私有堆栈中取flag的值,当执行service.flag = false语句时,虽然修改了flag的值,但是修改的却是公共堆栈的flag值,线程还是从私有堆栈中取flag的值,所以并不会退出while循环。使用volatile关键字修饰成员变量后,会强制JVM从公共堆栈中获取变量的值,所以能够退出while循环

package com.test.chap2;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        DemoService service = new DemoService();
        Thread t1 = new Thread(service);
        t1.start();
        Thread.sleep(100);
        System.out.println("准备修改flag的值");
        service.flag = false;
        System.out.println(service.flag);
    }
}

class DemoService extends Thread{
    //没有volatile关键字的话,线程会一致处于while循环中
    volatile public boolean flag = true;        
    @Override
    public void run() {
        System.out.println("开始运行run方法");
        while (flag){

        }
        System.out.println("结束运行run方法");
    }
}

synchronized和volatile的区别:
1、volatile是线程同步的轻量级实现,所以volatile的性能要比synchronized要好,但是volatile只能修饰变量。而synchronized可以修饰方法以及代码块。随着JDK的版本更新,synchronized在执行效率上也有很大的提升,使用率还是较高
2、多线程访问volatile不会阻塞,而访问synchronized会出现阻塞
3、volatile能保证数据的可见性,但是不能保证原子性,可能会出现脏读;而synchronized能够保证原子性,也能间接保证可见性,因为其能将私有内存和公共内存中的数据做同步
4、volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性



最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!