面试腾讯,字节跳动首先要掌握的Java多线程,一次帮你全掌握!

2,119 阅读12分钟

一、程序,进程,线程联系和区别

其实程序是一段静态的代码,它是应用程序执行的脚本。进程就是程序动态的执行过程,它具有动态性,并发性,独立性。线程是进程调度和执行的单位。

进程:每个进程都有独立的代码和数据空间(进程上下文),一个进程包含一个或者多个线程,同时线程是资源分配的最小单位。

线程:同一类线程共享代码和数据空间,并且每个线程有独立运行栈和程序计数器,同时线程是调度的最小单位。

那什么是多进程呢? ,常见的是打开我们自己电脑任务管理器里面就还有多个进程,其实指的是我们的操作系统能同时运行多个任务(微信,QQ等)。

多线程其实就是一个进程有多条路径在运行。

二、多线程实现的方式

在Java中实现多线程有三种方式,第一种方式是继承Thread类,第二种方式是实现Runnable接口, 第三种是实现Callable,结合Future使用(了解),并发编程中使用,这里不详细说:

第一种实现多线程的方式,继承Thread类,代码实现如下:

public class RabbitDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("兔子跑了:"+i+"步");
        }
    }
}
public class TortoiseDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("乌龟跑了:"+i+"步");
        }
    }
}
public class ClientThreadTest {
    public static void main(String[] args) {
        RabbitDemo rabbitDemo = new RabbitDemo();
        TortoiseDemo tortoiseDemo = new TortoiseDemo();
        rabbitDemo.start();
        tortoiseDemo.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main==>" + i);
        }
    }
}


第二个实例实现多个线程同时共享一个成员变量线程的运行状态。

public class Qp12306 implements Runnable {
    private int num=50;
    @Override
    public void run() {
        while (true){
            if (num<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"抢到票"+num--);
        }
    }
}
public class ClientQpTest {
    public static void main(String[] args) {
        Qp12306 qp12306 = new Qp12306();
        Thread thread = new Thread(qp12306, "张三");
        Thread thread1 = new Thread(qp12306, "李四");
        Thread thread2 = new Thread(qp12306, "王五");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

第二种方式:实现 Runnable接口,重写run方法,这种方式我们实现多线程常用的方式(避免Java单继承的限制)并且该方式采用了静态代理设计模式(参考: 静态代理),代码实例如下 :

public class RunnableDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Runnable:" + i);
        }
    }
}
public class ClientRunableTest {
    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();
        Thread thread = new Thread(runnableDemo);
        thread.start();
    }
}

第三方式:实现Callable接口,结合Future实现多线程,代码实例如下:

/**
 * 使用Callable创建线程
 */
public class Race implements Callable<Integer> {
    private String name;
    private long time; //延时时间
    private boolean flag = true;
    private int step = 0; //步

    @Override
    public Integer call() throws Exception {
        while (flag) {
            Thread.sleep(time);
            step++;
        }
        return step;
    }

    public Race(String name, long time) {
        this.name = name;
        this.time = time;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
public class CallableTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Race race = new Race("张三", 200);
        Race race1 = new Race("李四", 500);
        Future<Integer> result = executorService.submit(race);
        Future<Integer> result1 = executorService.submit(race1);
        Thread.sleep(3000);
        race.setFlag(false);
        race1.setFlag(false);
        int num1 = result.get();
        int num2 = result1.get();
        System.out.println("张三-->" + num1 + "步");
        System.out.println("李四-->" + num2 + "步");
        //停止服务
        executorService.shutdownNow();
    }
}

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1、适合多个相同的程序代码的线程去处理同一个资源

2、可以避免java中的单继承的限制

3、增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4、线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM就是在操作系统中启动了一个进程。

三、线程的状态和方法

线程从创建,运行到结束总是处于五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。 线程共包括一下5种状态:

1.新建状态 :线程对象被创建后就进入了新建状态,Thread thread = new Thread();

2.就绪状态(Runnable):也被称之为“可执行状态”,当线程被new出来后,其他的线程调用了该对象的start()方法,即thread.start(),此时线程位于“可运行线程池”中,只等待获取CPU的使用权,随时可以被CPU调用。进入就绪状态的进程除CPU之外,其他运行所需的资源都已经全部获得。

3.运行状态(Running):线程获取CPU权限开始执行。注意:线程只能从就绪状态进入到运行状态。

4.阻塞状态(Bloacked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行,知道线程进入就绪状态后才能有机会转到运行状态。

阻塞的情况分三种:

(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池中”。进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒。 (2)、同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其他线程占用,则JVM会吧该线程放入“锁池”中。 (3)、其他阻塞:通过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新回到就绪状态 5. 死亡(Dead):线程执行完了或因异常退出了run()方法,则该线程结束生命周期。

阻塞线程方法的说明:

wait(), notify(),notifyAll()这三个方法是结合使用的,都属于Object中的方法,wait的作用是当当前线程释放它所持有的锁进入等待状态(释放对象锁),而notify和notifyAll则是唤醒当前对象上的等待线程。 wait() —— 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。

sleep() 和 yield()方法是属于Thread类中的sleep()的作用是让当前线程休眠(正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务),即当前线程会从“运行状态”进入到阻塞状态”,但仍然保持对象锁,仍然占有该锁。当延时时间过后该线程重新阻塞状态变成就绪状态,从而等待cpu的调度执行。 sleep()睡眠时,保持对象锁,仍然占有该锁。 yield()的作用是让步,它能够让当前线程从运行状态进入到就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权,也有可能是当前线程又回到“运行状态”继续运行。

wait () , sleep()的区别:

1、每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 2、 wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 3、 sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 4、 sleep()睡眠时,保持对象锁,仍然占有该锁,而wait()释放对象锁;

四、线程的基本信息和优先级

涉及到线程的方法:Thread.currentThread() 当前线程,setName():设置名称,getName():获取名称,isAlive():判断状态 优先级:Java线程有优先级,优先级高的线程能获取更多的运行机会。 Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量: static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。 static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。 static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。

下面代码实现: 基本信息

public static void main(String[] args) throws InterruptedException {
        Rundemo rundemo = new Rundemo();
        Thread thread = new Thread(rundemo);
        thread.setName("线程"); //设置线程的名称
        System.out.println(thread.getName()); //获取当前线性的名称
        System.out.println(Thread.currentThread().getName()); //main线程
        thread.start();
        System.out.println("线程启动后的状态:" + thread.isAlive());
        Thread.sleep(10);
        rundemo.stop();
        Thread.sleep(1000); //停止后休眠一下,再看线程的状态
        System.out.println("线程停止后的状态:" + thread.isAlive());
        test();

    }

优先级

public static void test() throws InterruptedException {
        Rundemo rundemo1 = new Rundemo();
        Thread thread1 = new Thread(rundemo1);
        thread1.setName("线程thread1");
        Rundemo rundemo2 = new Rundemo();
        Thread thread2 = new Thread(rundemo2);
        thread2.setName("线程thread2");
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        rundemo1.stop();
        rundemo2.stop();

    }

五、线程的同步和死锁问题

同步方法:synchronized 同步代码块:synchronized(引用类型|this|类.class){ } 线程同步是为了多个线程同时访问一份资源确保数据的正确,不会造成数据的桩读,死锁是线程间相互等待锁锁造成的.

下面代码实现:多个线程同时访问一份资源使用synchronized

public class Qp12306 implements Runnable {
    private int num = 10;  
    private boolean flag = true;

    @Override
    public synchronized void run() {
        while (true) {
            test();
        }
    }

    public synchronized void test() {
        if (num <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "抢到票" + num--);
    }
}

public class ClientQpTest {
    public static void main(String[] args) {
        Qp12306 qp12306 = new Qp12306();
        Thread thread = new Thread(qp12306, "张三");
        Thread thread1 = new Thread(qp12306, "李四");
        Thread thread2 = new Thread(qp12306, "王五");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

运行结果

synchronized 静态代码块

public class Client {
    public static void main(String[] args) throws InterruptedException {
        JvmThread thread1 = new JvmThread(100);
        JvmThread thread2 = new JvmThread(500);
        thread1.start();
        thread2.start();


    }

}

class JvmThread extends Thread{
    private long time;
    public JvmThread() {
    }
    public JvmThread(long time) {
        this.time =time;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->创建:"+Jvm.getInstance(time));
    }
}


/**
 * 单例设计模式
 * 确保一个类只有一个对象
 * 懒汉式  
 * 1、构造器私有化,避免外部直接创建对象
 * 2、声明一个私有的静态变量
 * 3、创建一个对外的公共的静态方法 访问该变量,如果变量没有对象,创建该对象
 */
class Jvm {
    //声明一个私有的静态变量
    private static Jvm instance =null;
    //构造器私有化,避免外部直接创建对象
    private Jvm(){

    }
    //创建一个对外的公共的静态方法 访问该变量,如果变量没有对象,创建该对象
    public static Jvm getInstance(long time){
        if(null==instance){
            synchronized(Jvm.class){
                if(null==instance ){
                    try {
                        Thread.sleep(time); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance =new Jvm();
                }
            }
        }
        return instance;
    }


    public static Jvm getInstance3(long time){
        synchronized(Jvm.class){
            if(null==instance ){
                try {
                    Thread.sleep(time); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance =new Jvm();
            }
            return instance;
        }
    }
    
}

六、生产者消费者模式

生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,如下图所示,生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况: 存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品。互相等待,从而发生死锁.

具体实现代码如下:

/**
 * 生产者
 */
public class ProduceRu implements Runnable {
    private Movie movie;

    public ProduceRu(Movie movie) {
        this.movie = movie;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (0 == i % 2) {
                movie.proTest("向往的生活");
            } else {
                movie.proTest("好声音");
            }
        }
    }
}

/**
 *消费者
 */
public class ConsumeRu implements Runnable {
    private Movie movie;

    public ConsumeRu(Movie movie) {
        this.movie = movie;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            movie.conTest();
        }
    }
}

/**
 * 生产者消费者模式共同访问同一份资源
 * wait() 等等,释放锁,sleep()不释放锁
 * notify()/notifyAll():唤醒
 */
public class Movie {

    private String pic;
    //flag--->true 生产者生产,消费者等待,生产完后通知消费
    //flag--->false 消费者消费,生产者等待,消费完通知生产
    private boolean flag = true;

    /**
     * 生产者生产,消费者等待,生产完后通知消费
     * @param pic
     */
    public synchronized void proTest(String pic) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产了:" + pic);
        this.pic = pic;
        this.notify();
        this.flag = false;

    }

    /**
     * 消费者消费,生产者等待,消费完通知生产
     */
    public synchronized void conTest() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者:" + pic);
        this.notifyAll();
        this.flag = true;
    }
}

public class ClientTest {
    public static void main(String[] args) {
        Movie movie=new Movie();
        new Thread(new ProduceRu(movie)).start();
        new Thread(new ConsumeRu(movie)).start();
    }
}

七、任务调度

1.Thread实现方法 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下:

public class ThreadTest {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("任务开始了");
                    while (true) {
                        Thread.sleep(1000);
                        System.out.println("hello");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

2.TimeTask实现方法 Thread方法优点就是简单,但缺少了灵活性,TimeTask实现方法最主要的两个优点是:可以控制启动和取消任务的时间、第一次执行任务时可以指定想要delay的时间。 实现的过程中Timer用于调度任务,TimeTask用户具体的实现,是线程安全的,代码如下:

public class TimeTest {

    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务运行开始.......");
            }
        },new Date(),1000);
    }
}

3.ScheduledExecutorService实现方法 ScheduledExecutorService是从Java java.util.concurrent里 相比于上两个方法,它有以下好处: 相比于Timer的单线程,它是通过线程池的方式来执行任务的可以很灵活的去设定第一次执行任务 delay时间 具体代码如下:

方法说明: ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 创建并执行在给定的初始延迟之后首先启用的定期动作,随后在一个执行的终止和下一个执行的开始之间给定的延迟。 如果任务的执行遇到异常,则后续的执行被抑制。 否则,任务将仅通过取消或终止执行人终止。 参数 command - 要执行的任务 initialDelay - 延迟第一次执行的时间 (就是第一次指定定时延时多久),代码里我延时10秒 delay - 一个执行终止与下一个执行的开始之间的延迟 unit - initialDelay和delay参数的时间单位


public class ScheduledExecutorServiceTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务!!!!");
            }
        },10000,1000, TimeUnit.MILLISECONDS);
    }
}

总结:看完有什么不懂的欢迎在下方留言评论!